From e5072ff663fbd50b41ecb9739d17c104f6dd4129 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 10 Jun 2026 11:26:59 -0400 Subject: [PATCH] test(06-05): add failing SessionExpiredError detection tests --- apps/pwa/src/api/client.test.ts | 147 ++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/apps/pwa/src/api/client.test.ts b/apps/pwa/src/api/client.test.ts index 8b578e7..02a1937 100644 --- a/apps/pwa/src/api/client.test.ts +++ b/apps/pwa/src/api/client.test.ts @@ -10,10 +10,157 @@ * - eventFormMode: 'create'|'edit', defaults 'create' * - eventFormUid: string|null, defaults null * - setEventForm: setter for all three + * + * Plan 06-05: SessionExpiredError detection tests (D-11) + * - fetchEvents / createEvent / updateEvent / deleteEvent with opaqueredirect → SessionExpiredError + * - fetchEvents / createEvent / updateEvent / deleteEvent with 401 → SessionExpiredError + * - 500 response does NOT produce SessionExpiredError + * - fetchMe's existing opaqueredirect/401 path now throws SessionExpiredError (unified) */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +// ── Plan 06-05: SessionExpiredError detection tests ─────────────────────────── + +describe('SessionExpiredError — fetchEvents (06-05)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('throws SessionExpiredError on opaqueredirect', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + type: 'opaqueredirect', + status: 0, + } as unknown as Response) + + const { fetchEvents, SessionExpiredError } = await import('./client.js') + await expect(fetchEvents('2026-06-01', '2026-06-30')).rejects.toBeInstanceOf(SessionExpiredError) + }) + + it('throws SessionExpiredError on 401', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + type: 'basic', + status: 401, + } as unknown as Response) + + const { fetchEvents, SessionExpiredError } = await import('./client.js') + await expect(fetchEvents('2026-06-01', '2026-06-30')).rejects.toBeInstanceOf(SessionExpiredError) + }) + + it('throws a generic Error (not SessionExpiredError) on 500', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + type: 'basic', + status: 500, + } as unknown as Response) + + const { fetchEvents, SessionExpiredError } = await import('./client.js') + await expect(fetchEvents('2026-06-01', '2026-06-30')).rejects.toSatisfy( + (e: unknown) => e instanceof Error && !(e instanceof SessionExpiredError), + ) + }) +}) + +describe('SessionExpiredError — createEvent (06-05)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('throws SessionExpiredError on 401', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + type: 'basic', + status: 401, + } as unknown as Response) + + const { createEvent, SessionExpiredError } = await import('./client.js') + await expect( + createEvent({ title: 'Test', allDay: false, start: '2026-06-15T10:00:00', end: '2026-06-15T11:00:00' }), + ).rejects.toBeInstanceOf(SessionExpiredError) + }) +}) + +describe('SessionExpiredError — updateEvent (06-05)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('throws SessionExpiredError on 401', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + type: 'basic', + status: 401, + } as unknown as Response) + + const { updateEvent, SessionExpiredError } = await import('./client.js') + await expect( + updateEvent('uid-123', { title: 'Test', allDay: false, start: '2026-06-15T10:00:00', end: '2026-06-15T11:00:00' }), + ).rejects.toBeInstanceOf(SessionExpiredError) + }) +}) + +describe('SessionExpiredError — deleteEvent (06-05)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('throws SessionExpiredError on 401', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + type: 'basic', + status: 401, + } as unknown as Response) + + const { deleteEvent, SessionExpiredError } = await import('./client.js') + await expect(deleteEvent('uid-to-delete')).rejects.toBeInstanceOf(SessionExpiredError) + }) +}) + +describe('SessionExpiredError — fetchMe unified (06-05)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('throws SessionExpiredError (instanceof) on opaqueredirect — unified with other wrappers', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + type: 'opaqueredirect', + status: 0, + } as unknown as Response) + + const { fetchMe, SessionExpiredError } = await import('./client.js') + await expect(fetchMe()).rejects.toBeInstanceOf(SessionExpiredError) + }) + + it('throws SessionExpiredError on 401 — unified', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + type: 'basic', + status: 401, + } as unknown as Response) + + const { fetchMe, SessionExpiredError } = await import('./client.js') + await expect(fetchMe()).rejects.toBeInstanceOf(SessionExpiredError) + }) +}) + // ── createEvent tests ───────────────────────────────────────────────────────── describe('createEvent', () => {