From 90b99296e853482dcd95cda50c0e877d257ba87c Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 4 Jun 2026 10:30:53 -0400 Subject: [PATCH] test(01-03): refine sync RED tests with richer db mock (select + insert) - Use separate mock functions for insert/select chain to enable per-test assertions - Reset mock implementations in beforeEach after vi.clearAllMocks - Mock returns {id:42} from select (calendar ID for event upsert) --- apps/api/tests/broker/sync.test.ts | 75 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/apps/api/tests/broker/sync.test.ts b/apps/api/tests/broker/sync.test.ts index 80a19c6..5e406c2 100644 --- a/apps/api/tests/broker/sync.test.ts +++ b/apps/api/tests/broker/sync.test.ts @@ -16,23 +16,37 @@ import { SAMPLE_VEVENT_ALLDAY, } from '../helpers/db.js' +// Track calls for assertions +const mockOnDuplicateKeyUpdate = vi.fn().mockResolvedValue([{ insertId: 1 }]) +const mockValues = vi.fn().mockReturnValue({ onDuplicateKeyUpdate: mockOnDuplicateKeyUpdate }) +const mockInsert = vi.fn().mockReturnValue({ values: mockValues }) +const mockLimit = vi.fn().mockResolvedValue([{ id: 42 }]) +const mockWhere = vi.fn().mockReturnValue({ limit: mockLimit }) +const mockFrom = vi.fn().mockReturnValue({ where: mockWhere }) +const mockSelect = vi.fn().mockReturnValue({ from: mockFrom }) + // 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 }, - } -}) +vi.mock('../../src/db/client.js', () => ({ + db: { + insert: mockInsert, + select: mockSelect, + }, +})) describe('syncCalendar', () => { beforeEach(() => { vi.clearAllMocks() + // Reset mock implementations after clearAllMocks + mockOnDuplicateKeyUpdate.mockResolvedValue([{ insertId: 1 }]) + mockValues.mockReturnValue({ onDuplicateKeyUpdate: mockOnDuplicateKeyUpdate }) + mockInsert.mockReturnValue({ values: mockValues }) + mockLimit.mockResolvedValue([{ id: 42 }]) + mockWhere.mockReturnValue({ limit: mockLimit }) + mockFrom.mockReturnValue({ where: mockWhere }) + mockSelect.mockReturnValue({ from: mockFrom }) }) 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') const mockClient = { @@ -49,19 +63,16 @@ describe('syncCalendar', () => { await syncCalendar(mockClient as never, mockDavCal as never, 1) - const insertMock = vi.mocked(db.insert) - // Should have been called at least twice: once for calendars, once for calendarEvents - expect(insertMock).toHaveBeenCalledTimes(2) - - // The second insert call is for calendarEvents - const eventValuesArg = vi.mocked(insertMock).mock.results[1].value.values.mock.calls[0][0] + // insert called twice: calendars + calendarEvents + expect(mockInsert).toHaveBeenCalledTimes(2) + // Event insert: second call's values arg + const eventValuesArg = mockValues.mock.calls[1][0] expect(eventValuesArg.allDay).toBe(true) - expect(eventValuesArg.dtstartDate).toBeTruthy() // YYYY-MM-DD string + expect(eventValuesArg.dtstartDate).toBeTruthy() 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 = { @@ -78,17 +89,14 @@ describe('syncCalendar', () => { 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(mockInsert).toHaveBeenCalledTimes(2) + const eventValuesArg = mockValues.mock.calls[1][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 = { @@ -104,13 +112,11 @@ describe('syncCalendar', () => { } 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] + const eventArg = mockValues.mock.calls[1][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') + it('upserts on duplicate UID within the same calendar (onDuplicateKeyUpdate called for events)', async () => { const { syncCalendar } = await import('../../src/broker/sync.js') const mockClient = { @@ -127,16 +133,11 @@ describe('syncCalendar', () => { 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) + // onDuplicateKeyUpdate must be called for both the calendar upsert and the event upsert + expect(mockOnDuplicateKeyUpdate).toHaveBeenCalledTimes(2) }) 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 = { @@ -152,13 +153,11 @@ describe('syncCalendar', () => { } 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] + const eventArg = mockValues.mock.calls[1][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 = { @@ -173,8 +172,8 @@ describe('syncCalendar', () => { 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] + // Calendar insert values must include the new ctag and syncToken + const calValuesArg = mockValues.mock.calls[0][0] expect(calValuesArg.ctag).toBe('new-ctag-123') expect(calValuesArg.syncToken).toBe('sync-token-abc') })