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)
This commit is contained in:
@@ -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:
|
* Tests syncCalendar in src/broker/sync.ts.
|
||||||
* apps/api/src/broker/sync.ts (Plan 03)
|
* Key behaviors verified (D-13, T-03-05):
|
||||||
*
|
|
||||||
* Tests will be filled GREEN in Plan 03 when syncCalendar is implemented.
|
|
||||||
*
|
|
||||||
* Key behaviors to verify (D-13):
|
|
||||||
* - All-day events: dtstart_date (DATE) set, dtstart_utc NULL, allDay=true
|
* - All-day events: dtstart_date (DATE) set, dtstart_utc NULL, allDay=true
|
||||||
* - Timed events: dtstart_utc (TIMESTAMP UTC) set, dtstart_date NULL, allDay=false
|
* - 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
|
* - 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', () => {
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user