ten99timecard/client/src/__tests__/appStore.test.ts
2026-03-04 21:21:59 -05:00

151 lines
5.7 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { useAppStore } from '@/store/appStore';
describe('appStore — CRUD', () => {
beforeEach(async () => {
// Reset to logged-in fresh state
useAppStore.setState({
data: {
workEntries: [],
payments: [],
expenses: [],
taxInputs: {},
dashboard: { charts: [], widgets: [] },
settings: { theme: 'standard', mode: 'dark', storageMode: 'cookie', defaultRate: 50 },
version: 1,
},
localAuth: { unlocked: true, username: 'test' },
cloudAuth: { token: null, email: null, provider: null },
vault: null, // no persistence in tests
});
});
it('addWorkEntry assigns id and timestamps', () => {
const e = useAppStore.getState().addWorkEntry({
date: '2024-01-01', description: 'code review', amount: 150,
});
expect(e.id).toBeTruthy();
expect(e.createdAt).toBeGreaterThan(0);
expect(useAppStore.getState().data.workEntries).toHaveLength(1);
});
it('updateWorkEntry patches fields', () => {
const e = useAppStore.getState().addWorkEntry({
date: '2024-01-01', description: 'old', amount: 100,
});
useAppStore.getState().updateWorkEntry(e.id, { description: 'new' });
const updated = useAppStore.getState().data.workEntries[0];
expect(updated.description).toBe('new');
expect(updated.amount).toBe(100); // unchanged
expect(updated.updatedAt).toBeGreaterThanOrEqual(e.createdAt);
});
it('deleteWorkEntry removes it', () => {
const e = useAppStore.getState().addWorkEntry({
date: '2024-01-01', description: 'x', amount: 1,
});
useAppStore.getState().deleteWorkEntry(e.id);
expect(useAppStore.getState().data.workEntries).toHaveLength(0);
});
it('addPayment / addExpense follow same pattern', () => {
const p = useAppStore.getState().addPayment({
date: '2024-01-01', amount: 5000, payer: 'Acme',
});
const ex = useAppStore.getState().addExpense({
date: '2024-01-01', amount: 200, description: 'laptop', deductible: true,
});
expect(p.id).toBeTruthy();
expect(ex.id).toBeTruthy();
expect(useAppStore.getState().data.payments).toHaveLength(1);
expect(useAppStore.getState().data.expenses).toHaveLength(1);
});
it('mutations bump version counter', () => {
const v0 = useAppStore.getState().data.version;
useAppStore.getState().addWorkEntry({ date: '2024-01-01', description: 'x' });
const v1 = useAppStore.getState().data.version;
expect(v1).toBe(v0 + 1);
});
it('setTaxInputs merges per-year', () => {
useAppStore.getState().setTaxInputs(2024, { priorYearAGI: 50000 });
useAppStore.getState().setTaxInputs(2024, { priorYearTax: 6000 });
const ti = useAppStore.getState().data.taxInputs[2024];
expect(ti.priorYearAGI).toBe(50000);
expect(ti.priorYearTax).toBe(6000);
expect(ti.filingStatus).toBe('single');
});
it('chart add/update/remove', () => {
useAppStore.getState().addChart({ title: 'test chart' });
const charts = useAppStore.getState().data.dashboard.charts;
expect(charts).toHaveLength(1);
const id = charts[0].id;
useAppStore.getState().updateChart(id, { type: 'bar' });
expect(useAppStore.getState().data.dashboard.charts[0].type).toBe('bar');
useAppStore.getState().removeChart(id);
expect(useAppStore.getState().data.dashboard.charts).toHaveLength(0);
});
it('setTheme updates both theme and mode', () => {
useAppStore.getState().setTheme('cyberpunk', 'light');
expect(useAppStore.getState().data.settings.theme).toBe('cyberpunk');
expect(useAppStore.getState().data.settings.mode).toBe('light');
});
it('setDefaultRate updates settings', () => {
useAppStore.getState().setDefaultRate(85);
expect(useAppStore.getState().data.settings.defaultRate).toBe(85);
});
});
describe('appStore — auth', () => {
beforeEach(() => {
useAppStore.getState().logout();
});
it('register creates encrypted vault and unlocks', async () => {
await useAppStore.getState().register('alice', 'my-strong-password');
expect(useAppStore.getState().localAuth.unlocked).toBe(true);
expect(useAppStore.getState().localAuth.username).toBe('alice');
expect(document.cookie).toContain('t99_alice');
});
it('register fails if user exists', async () => {
await useAppStore.getState().register('bob', 'password123');
useAppStore.getState().logout();
await expect(
useAppStore.getState().register('bob', 'different')
).rejects.toThrow();
});
it('login succeeds with correct password', async () => {
await useAppStore.getState().register('carol', 'secret-pass-123');
useAppStore.getState().addWorkEntry({ date: '2024-01-01', description: 'marker', amount: 999 });
await useAppStore.getState().persist();
useAppStore.getState().logout();
await useAppStore.getState().login('carol', 'secret-pass-123');
expect(useAppStore.getState().localAuth.unlocked).toBe(true);
expect(useAppStore.getState().data.workEntries[0].description).toBe('marker');
});
it('login fails with wrong password', async () => {
await useAppStore.getState().register('dave', 'correct-pass');
useAppStore.getState().logout();
await expect(
useAppStore.getState().login('dave', 'wrong-pass')
).rejects.toThrow(/Wrong password/);
});
it('logout locks and clears data', async () => {
await useAppStore.getState().register('eve', 'password123');
useAppStore.getState().addWorkEntry({ date: '2024-01-01', description: 'secret' });
useAppStore.getState().logout();
expect(useAppStore.getState().localAuth.unlocked).toBe(false);
expect(useAppStore.getState().data.workEntries).toHaveLength(0);
});
});