145 lines
4.9 KiB
JavaScript
145 lines
4.9 KiB
JavaScript
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('attendees.list calls correct endpoint', async () => {
|
|
const f = mockFetch({ attendees: [] })
|
|
await api.attendees.list({ search: 'test' })
|
|
expect(f.mock.calls[0][0]).toBe('/api/attendees?search=test')
|
|
})
|
|
|
|
it('attendees.delete uses DELETE method', async () => {
|
|
const f = mockFetch({}, 204)
|
|
await api.attendees.delete(5)
|
|
expect(f.mock.calls[0][0]).toBe('/api/attendees/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 })
|
|
})
|
|
})
|