From ae21541e971aea66b1a2e03e8530cafad5f362d4 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 4 Jun 2026 10:29:29 -0400 Subject: [PATCH] test(01-03): add failing tests for syncCalendar (D-13 all-day DATE handling) - 6 tests: all-day DATE vs timed TIMESTAMP, allDay flag, upsert on dup UID, rawVevent blob, ctag update - Mocks db singleton at module level with vi.mock hoisting pattern - Uses SAMPLE_VEVENT_TIMED/ALLDAY fixtures from tests/helpers/db.ts - RED gate: all fail (src/broker/sync.ts does not exist yet) --- apps/api/tests/broker/sync.test.ts | 180 ++++++++++++++++++++++++++--- 1 file changed, 166 insertions(+), 14 deletions(-) diff --git a/apps/api/tests/broker/sync.test.ts b/apps/api/tests/broker/sync.test.ts index 4c4880a..80a19c6 100644 --- a/apps/api/tests/broker/sync.test.ts +++ b/apps/api/tests/broker/sync.test.ts @@ -1,29 +1,181 @@ /** - * Wave 0 stub — Broker: syncCalendar event upsert + all-day handling + * Broker: syncCalendar event upsert + all-day handling * - * These tests are RED stubs. Implementation lives in: - * apps/api/src/broker/sync.ts (Plan 03) - * - * Tests will be filled GREEN in Plan 03 when syncCalendar is implemented. - * - * Key behaviors to verify (D-13): + * Tests syncCalendar in src/broker/sync.ts. + * Key behaviors verified (D-13, T-03-05): * - All-day events: dtstart_date (DATE) set, dtstart_utc NULL, allDay=true * - Timed events: dtstart_utc (TIMESTAMP UTC) set, dtstart_date NULL, allDay=false * - UID used as idempotency key: second sync of same UID is an upsert, not duplicate + * - Raw VEVENT blob stored verbatim + * - Calendar ctag/syncToken updated after sync */ -import { describe, it } from 'vitest' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { + SAMPLE_VEVENT_TIMED, + SAMPLE_VEVENT_ALLDAY, +} from '../helpers/db.js' + +// Mock the db singleton at module level (Vitest hoisting) +vi.mock('../../src/db/client.js', () => { + const onDuplicateKeyUpdate = vi.fn().mockResolvedValue([{ id: 1 }]) + const values = vi.fn().mockReturnValue({ onDuplicateKeyUpdate }) + const insert = vi.fn().mockReturnValue({ values }) + return { + db: { insert }, + } +}) describe('syncCalendar', () => { - it.todo('stores all-day events with dtstart_date (DATE) and dtstart_utc=NULL (Plan 03)') + beforeEach(() => { + vi.clearAllMocks() + }) - it.todo('stores timed events with dtstart_utc (TIMESTAMP) and dtstart_date=NULL (Plan 03)') + it('stores all-day events with dtstart_date (DATE) and dtstart_utc=NULL', async () => { + const { db } = await import('../../src/db/client.js') + const { syncCalendar } = await import('../../src/broker/sync.js') - it.todo('sets allDay=true for all-day events, allDay=false for timed (Plan 03)') + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { data: SAMPLE_VEVENT_ALLDAY, etag: '"etag-allday"', url: '/cal/allday.ics' }, + ]), + } + const mockDavCal = { + url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/', + displayName: 'Test Calendar', + ctag: 'ctag-v1', + syncToken: null, + } - it.todo('upserts on duplicate UID within the same calendar (Plan 03)') + await syncCalendar(mockClient as never, mockDavCal as never, 1) - it.todo('stores the raw VEVENT blob in rawVevent column (Plan 03)') + const insertMock = vi.mocked(db.insert) + // Should have been called at least twice: once for calendars, once for calendarEvents + expect(insertMock).toHaveBeenCalledTimes(2) - it.todo('updates the calendar ctag/syncToken after a successful sync (Plan 03)') + // The second insert call is for calendarEvents + const eventValuesArg = vi.mocked(insertMock).mock.results[1].value.values.mock.calls[0][0] + expect(eventValuesArg.allDay).toBe(true) + expect(eventValuesArg.dtstartDate).toBeTruthy() // YYYY-MM-DD string + expect(eventValuesArg.dtstartUtc).toBeNull() + }) + + it('stores timed events with dtstart_utc (TIMESTAMP) and dtstart_date=NULL', async () => { + const { db } = await import('../../src/db/client.js') + const { syncCalendar } = await import('../../src/broker/sync.js') + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { data: SAMPLE_VEVENT_TIMED, etag: '"etag-timed"', url: '/cal/timed.ics' }, + ]), + } + const mockDavCal = { + url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/', + displayName: 'Test Calendar', + ctag: 'ctag-v1', + syncToken: null, + } + + await syncCalendar(mockClient as never, mockDavCal as never, 1) + + const insertMock = vi.mocked(db.insert) + expect(insertMock).toHaveBeenCalledTimes(2) + + const eventValuesArg = vi.mocked(insertMock).mock.results[1].value.values.mock.calls[0][0] + expect(eventValuesArg.allDay).toBe(false) + expect(eventValuesArg.dtstartUtc).toBeInstanceOf(Date) + expect(eventValuesArg.dtstartDate).toBeNull() + }) + + it('sets allDay=true for all-day events, allDay=false for timed', async () => { + const { db } = await import('../../src/db/client.js') + const { syncCalendar } = await import('../../src/broker/sync.js') + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { data: SAMPLE_VEVENT_ALLDAY, etag: '"allday"', url: '/allday.ics' }, + ]), + } + const mockDavCal = { + url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Test/', + displayName: 'Test', + ctag: null, + syncToken: null, + } + + await syncCalendar(mockClient as never, mockDavCal as never, 1) + const insertMock = vi.mocked(db.insert) + const eventArg = vi.mocked(insertMock).mock.results[1].value.values.mock.calls[0][0] + expect(eventArg.allDay).toBe(true) + }) + + it('upserts on duplicate UID within the same calendar (onDuplicateKeyUpdate called)', async () => { + const { db } = await import('../../src/db/client.js') + const { syncCalendar } = await import('../../src/broker/sync.js') + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { data: SAMPLE_VEVENT_TIMED, etag: '"etag1"', url: '/timed.ics' }, + ]), + } + const mockDavCal = { + url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/', + displayName: 'Test', + ctag: 'v2', + syncToken: null, + } + + await syncCalendar(mockClient as never, mockDavCal as never, 1) + + // Both calendar insert and event insert must use onDuplicateKeyUpdate + const insertMock = vi.mocked(db.insert) + const calOnDup = vi.mocked(insertMock).mock.results[0].value.values.mock.results[0].value.onDuplicateKeyUpdate + const evtOnDup = vi.mocked(insertMock).mock.results[1].value.values.mock.results[0].value.onDuplicateKeyUpdate + expect(calOnDup).toHaveBeenCalledTimes(1) + expect(evtOnDup).toHaveBeenCalledTimes(1) + }) + + it('stores the raw VEVENT blob in rawVevent column', async () => { + const { db } = await import('../../src/db/client.js') + const { syncCalendar } = await import('../../src/broker/sync.js') + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { data: SAMPLE_VEVENT_TIMED, etag: '"etag"', url: '/timed.ics' }, + ]), + } + const mockDavCal = { + url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/', + displayName: 'Test', + ctag: 'v1', + syncToken: null, + } + + await syncCalendar(mockClient as never, mockDavCal as never, 1) + const insertMock = vi.mocked(db.insert) + const eventArg = vi.mocked(insertMock).mock.results[1].value.values.mock.calls[0][0] + expect(eventArg.rawVevent).toBe(SAMPLE_VEVENT_TIMED) + }) + + it('updates the calendar ctag/syncToken after a successful sync', async () => { + const { db } = await import('../../src/db/client.js') + const { syncCalendar } = await import('../../src/broker/sync.js') + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([]), + } + const mockDavCal = { + url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/', + displayName: 'Test Calendar', + ctag: 'new-ctag-123', + syncToken: 'sync-token-abc', + } + + await syncCalendar(mockClient as never, mockDavCal as never, 1) + + const insertMock = vi.mocked(db.insert) + const calValuesArg = vi.mocked(insertMock).mock.results[0].value.values.mock.calls[0][0] + expect(calValuesArg.ctag).toBe('new-ctag-123') + expect(calValuesArg.syncToken).toBe('sync-token-abc') + }) })