From 6400ce693c4b32fa2f7f1bbd768d5fe953e9eecb Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 5 Jun 2026 18:25:00 -0400 Subject: [PATCH] test(03-05): add failing tests for write client calls and eventForm store keys - createEvent: POST /api/events/create with credentials:include, returns uid - updateEvent: PATCH /api/events/:uid/edit - fetchWritableCalendars: GET /api/events/writable-calendars, returns calendars array - calendarStore: eventFormOpen, eventFormMode, eventFormUid defaults and setEventForm setter --- apps/pwa/src/api/client.test.ts | 251 ++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 apps/pwa/src/api/client.test.ts diff --git a/apps/pwa/src/api/client.test.ts b/apps/pwa/src/api/client.test.ts new file mode 100644 index 0000000..7f6a2f5 --- /dev/null +++ b/apps/pwa/src/api/client.test.ts @@ -0,0 +1,251 @@ +/** + * client.ts unit tests — Task 1 (TDD RED → GREEN) + * + * Tests for write client calls added in Plan 03-05: + * - createEvent: POSTs to /api/events/create with credentials:'include' + * - updateEvent: PATCHes /api/events/:uid/edit + * - fetchWritableCalendars: GETs /api/events/writable-calendars, returns WritableCalendar[] + * Tests for Zustand store new keys: + * - eventFormOpen: boolean, defaults false + * - eventFormMode: 'create'|'edit', defaults 'create' + * - eventFormUid: string|null, defaults null + * - setEventForm: setter for all three + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// ── createEvent tests ───────────────────────────────────────────────────────── + +describe('createEvent', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('POSTs to /api/events/create with credentials:include', async () => { + const mockFetch = vi.mocked(fetch) + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ uid: 'test-uid-123' }), + } as Response) + + const { createEvent } = await import('./client.js') + const payload = { + title: 'Team Meeting', + allDay: false, + start: '2026-06-15T10:00:00', + end: '2026-06-15T11:00:00', + recurrence: 'none' as const, + } + await createEvent(payload) + + expect(mockFetch).toHaveBeenCalledWith( + '/api/events/create', + expect.objectContaining({ + method: 'POST', + credentials: 'include', + headers: expect.objectContaining({ 'Content-Type': 'application/json' }), + }), + ) + }) + + it('returns { uid } from 202 response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + json: async () => ({ uid: 'returned-uid-456' }), + } as Response) + + const { createEvent } = await import('./client.js') + const result = await createEvent({ + title: 'Test', + allDay: true, + start: '2026-06-20', + end: '2026-06-20', + recurrence: 'none' as const, + }) + + expect(result).toEqual({ uid: 'returned-uid-456' }) + }) + + it('throws on non-ok response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 400, + json: async () => ({ error: 'Bad Request' }), + } as Response) + + const { createEvent } = await import('./client.js') + await expect( + createEvent({ + title: '', + allDay: false, + start: '2026-06-15T10:00:00', + end: '2026-06-15T11:00:00', + recurrence: 'none' as const, + }), + ).rejects.toThrow() + }) +}) + +// ── updateEvent tests ───────────────────────────────────────────────────────── + +describe('updateEvent', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('PATCHes /api/events/:uid/edit', async () => { + const mockFetch = vi.mocked(fetch) + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ uid: 'edit-uid-789' }), + } as Response) + + const { updateEvent } = await import('./client.js') + await updateEvent('edit-uid-789', { + title: 'Updated Meeting', + allDay: false, + start: '2026-06-15T10:00:00', + end: '2026-06-15T11:00:00', + recurrence: 'weekly' as const, + }) + + expect(mockFetch).toHaveBeenCalledWith( + '/api/events/edit-uid-789/edit', + expect.objectContaining({ + method: 'PATCH', + credentials: 'include', + }), + ) + }) + + it('returns { uid } on success', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + json: async () => ({ uid: 'patched-uid' }), + } as Response) + + const { updateEvent } = await import('./client.js') + const result = await updateEvent('some-uid', { + title: 'Patched', + allDay: false, + start: '2026-06-15T10:00:00', + end: '2026-06-15T11:00:00', + recurrence: 'none' as const, + }) + + expect(result).toEqual({ uid: 'patched-uid' }) + }) +}) + +// ── fetchWritableCalendars tests ────────────────────────────────────────────── + +describe('fetchWritableCalendars', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) + }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('GETs /api/events/writable-calendars with credentials:include', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + json: async () => ({ + calendars: [ + { url: 'https://caldav.fastmail.com/cal1', displayName: 'My Calendar', color: '#4A90D9', isShared: false }, + ], + }), + } as Response) + + const { fetchWritableCalendars } = await import('./client.js') + await fetchWritableCalendars() + + expect(vi.mocked(fetch)).toHaveBeenCalledWith( + '/api/events/writable-calendars', + expect.objectContaining({ credentials: 'include' }), + ) + }) + + it('returns WritableCalendar[] from the calendars envelope', async () => { + const mockCalendars = [ + { url: 'https://caldav.fastmail.com/cal1', displayName: 'My Calendar', color: '#4A90D9', isShared: false }, + { url: 'https://caldav.fastmail.com/cal2', displayName: 'Family', color: '#F25C7A', isShared: true }, + ] + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + json: async () => ({ calendars: mockCalendars }), + } as Response) + + const { fetchWritableCalendars } = await import('./client.js') + const result = await fetchWritableCalendars() + + expect(result).toEqual(mockCalendars) + expect(result).toHaveLength(2) + }) + + it('throws on non-ok response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 401, + } as Response) + + const { fetchWritableCalendars } = await import('./client.js') + await expect(fetchWritableCalendars()).rejects.toThrow() + }) +}) + +// ── Zustand store new keys tests ────────────────────────────────────────────── + +describe('calendarStore — eventForm keys', () => { + it('eventFormOpen defaults to false', async () => { + const { useCalendarStore } = await import('../store/calendarStore.js') + const state = useCalendarStore.getState() + expect(state.eventFormOpen).toBe(false) + }) + + it('eventFormMode defaults to create', async () => { + const { useCalendarStore } = await import('../store/calendarStore.js') + const state = useCalendarStore.getState() + expect(state.eventFormMode).toBe('create') + }) + + it('eventFormUid defaults to null', async () => { + const { useCalendarStore } = await import('../store/calendarStore.js') + const state = useCalendarStore.getState() + expect(state.eventFormUid).toBeNull() + }) + + it('setEventForm(true, edit, some-uid) updates all three keys', async () => { + const { useCalendarStore } = await import('../store/calendarStore.js') + useCalendarStore.getState().setEventForm(true, 'edit', 'some-uid') + const state = useCalendarStore.getState() + expect(state.eventFormOpen).toBe(true) + expect(state.eventFormMode).toBe('edit') + expect(state.eventFormUid).toBe('some-uid') + }) + + it('setEventForm(false) closes the form and preserves other defaults', async () => { + const { useCalendarStore } = await import('../store/calendarStore.js') + // Open first + useCalendarStore.getState().setEventForm(true, 'edit', 'uid-abc') + // Close + useCalendarStore.getState().setEventForm(false) + const state = useCalendarStore.getState() + expect(state.eventFormOpen).toBe(false) + }) + + it('setEventForm(true) with no mode/uid defaults to create/null', async () => { + const { useCalendarStore } = await import('../store/calendarStore.js') + useCalendarStore.getState().setEventForm(true) + const state = useCalendarStore.getState() + expect(state.eventFormOpen).toBe(true) + expect(state.eventFormMode).toBe('create') + expect(state.eventFormUid).toBeNull() + }) +})