Files
familysync/apps/api/tests/broker/sync.test.ts
T
Lucas Berger 90b99296e8 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)
2026-06-04 10:30:53 -04:00

181 lines
6.4 KiB
TypeScript

/**
* Broker: syncCalendar event upsert + all-day handling
*
* 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, expect, vi, beforeEach } from 'vitest'
import {
SAMPLE_VEVENT_TIMED,
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', () => ({
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 { syncCalendar } = await import('../../src/broker/sync.js')
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,
}
await syncCalendar(mockClient as never, mockDavCal as never, 1)
// 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()
expect(eventValuesArg.dtstartUtc).toBeNull()
})
it('stores timed events with dtstart_utc (TIMESTAMP) and dtstart_date=NULL', async () => {
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)
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 { 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 eventArg = mockValues.mock.calls[1][0]
expect(eventArg.allDay).toBe(true)
})
it('upserts on duplicate UID within the same calendar (onDuplicateKeyUpdate called for events)', async () => {
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)
// 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 { 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 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 { 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)
// 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')
})
})