import { describe, it, expect, beforeEach, vi } from 'vitest' import { db, saveSession, clearSession } from './db.js' // Must import api after fake-indexeddb is initialized (via test-setup.js) const { apiFetch, apiJSON, api } = await import('./api.js') beforeEach(async () => { await Promise.all(db.tables.map(t => t.clear())) vi.restoreAllMocks() }) function mockFetch(body = {}, status = 200) { const fn = vi.fn(() => Promise.resolve({ ok: status >= 200 && status < 300, status, statusText: 'OK', json: () => Promise.resolve(body), text: () => Promise.resolve(JSON.stringify(body)), }) ) globalThis.fetch = fn return fn } describe('apiFetch', () => { it('adds Authorization header when session exists', async () => { await saveSession('mytoken', { id: 1 }) const f = mockFetch() await apiFetch('/api/test') expect(f).toHaveBeenCalledTimes(1) const [, opts] = f.mock.calls[0] expect(opts.headers['Authorization']).toBe('Bearer mytoken') }) it('omits Authorization when no session', async () => { const f = mockFetch() await apiFetch('/api/test') const [, opts] = f.mock.calls[0] expect(opts.headers['Authorization']).toBeUndefined() }) it('clears session on 401', async () => { await saveSession('expired', { id: 1 }) mockFetch({}, 401) await expect(apiFetch('/api/test')).rejects.toThrow('unauthorized') expect(await db.session.get(1)).toBeUndefined() }) }) describe('apiJSON', () => { it('parses JSON response', async () => { mockFetch({ name: 'Titania' }) const result = await apiJSON('/api/test') expect(result.name).toBe('Titania') }) it('throws on non-OK response', async () => { mockFetch({ error: 'not found' }, 404) await expect(apiJSON('/api/test')).rejects.toThrow('not found') }) }) describe('api methods', () => { it('login calls correct endpoint', async () => { const f = mockFetch({ token: 'tok', user: { id: 1 } }) await api.login('admin', 'pass') const [url, opts] = f.mock.calls[0] expect(url).toBe('/api/login') expect(opts.method).toBe('POST') expect(JSON.parse(opts.body)).toEqual({ username: 'admin', password: 'pass' }) }) it('participants.list calls correct endpoint', async () => { const f = mockFetch({ participants: [] }) await api.participants.list({ search: 'test' }) expect(f.mock.calls[0][0]).toBe('/api/participants?search=test') }) it('participants.delete uses DELETE method', async () => { const f = mockFetch({}, 204) await api.participants.delete(5) expect(f.mock.calls[0][0]).toBe('/api/participants/5') expect(f.mock.calls[0][1].method).toBe('DELETE') }) it('sync.pull passes since param', async () => { const f = mockFetch({ server_time: '2026-01-01', attendees: [] }) await api.sync.pull('2026-01-01T00:00:00Z') expect(f.mock.calls[0][0]).toContain('since=') }) it('sync.pull omits since when empty', async () => { const f = mockFetch({ server_time: '2026-01-01', attendees: [] }) await api.sync.pull('') expect(f.mock.calls[0][0]).toBe('/api/sync/pull') }) }) describe('signup methods', () => { it('signup.config fetches config without auth', async () => { const f = mockFetch({ departments: [], volunteer_note_label: 'Note' }) await api.signup.config() const [url, opts] = f.mock.calls[0] expect(url).toBe('/api/public/signup-config') expect(opts.headers['Authorization']).toBeUndefined() }) it('signup.submit posts form data without auth', async () => { const f = mockFetch({ ok: true }) await api.signup.submit({ preferred_name: 'Titania', email: 'titania@example.com' }) const [url, opts] = f.mock.calls[0] expect(url).toBe('/api/public/signup') expect(opts.method).toBe('POST') expect(JSON.parse(opts.body)).toEqual({ preferred_name: 'Titania', email: 'titania@example.com' }) expect(opts.headers['Authorization']).toBeUndefined() }) it('signup.confirm posts token without auth', async () => { const f = mockFetch({ status: 'confirmed' }) await api.signup.confirm('abc123') const [url, opts] = f.mock.calls[0] expect(url).toBe('/api/public/confirm') expect(opts.method).toBe('POST') expect(JSON.parse(opts.body)).toEqual({ token: 'abc123' }) expect(opts.headers['Authorization']).toBeUndefined() }) it('signup.submit throws on 400', async () => { mockFetch({ error: 'preferred name and email are required' }, 400) await expect(api.signup.submit({})).rejects.toThrow('preferred name and email are required') }) }) describe('settings shift signups', () => { it('toggleShiftSignups posts open flag', async () => { await saveSession('tok', { id: 1 }) const f = mockFetch({ shift_signups_open: true }) await api.settings.toggleShiftSignups(true) const [url, opts] = f.mock.calls[0] expect(url).toBe('/api/settings/shift-signups') expect(opts.method).toBe('POST') expect(JSON.parse(opts.body)).toEqual({ open: true }) }) })