- vevent.test.ts: DTSTART UTC 'Z' for timed, DATE for all-day (D-13), RRULE (CAL-04/07) - write.test.ts: createCalendarEvent uid.ics filename, updateCalendarEvent/deleteCalendarEvent etag/If-Match shapes (CAL-04/05/06, D-08) - outboxWorker.test.ts: pending→done on 204, pending→failed on 412 (no retry), pending→backoff on 500, pending→dead at MAX_ATTEMPTS, edit-as-move create-before-delete ordering (D-04/D-07/D-08) - events.test.ts (extended): POST /create 202+outbox row, PATCH /edit 202+etag, DELETE /:uid 202, GET /sync-status, GET /writable-calendars D-03 access control, 403 unauthorized calendar (V4) - InstallPrompt.test.tsx: isIOSSafariNonStandalone UA detection, useAndroidInstallPrompt canInstall lifecycle (PWA-01/PWA-02) All tests fail RED — implementation modules do not exist yet
120 lines
4.4 KiB
TypeScript
120 lines
4.4 KiB
TypeScript
/**
|
|
* RED test scaffold: broker/write.ts — tsdav PUT/DELETE wrappers (CAL-04, CAL-05, CAL-06)
|
|
*
|
|
* Behaviors under test:
|
|
* 1. createCalendarEvent calls client.createCalendarObject with `${uid}.ics` filename
|
|
* 2. updateCalendarEvent passes etag into the calendarObject (If-Match for D-08)
|
|
* 3. deleteCalendarEvent passes etag into the calendarObject (If-Match for D-08)
|
|
* 4. Each returns the raw Response from the tsdav client mock
|
|
*
|
|
* These tests FAIL (RED) because broker/write.ts does not exist yet.
|
|
* They will turn GREEN in Plan 03-02 when the implementation is added.
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
|
|
// This import fails (RED) — broker/write.ts does not exist yet.
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore intentional RED import
|
|
import {
|
|
createCalendarEvent,
|
|
updateCalendarEvent,
|
|
deleteCalendarEvent,
|
|
} from '../../src/broker/write.js'
|
|
|
|
const makeMockResponse = (status: number): Response =>
|
|
({ status, ok: status >= 200 && status < 300, headers: new Headers() }) as unknown as Response
|
|
|
|
describe('createCalendarEvent', () => {
|
|
it('calls client.createCalendarObject with uid.ics filename', async () => {
|
|
const mockClient = {
|
|
createCalendarObject: vi.fn().mockResolvedValue(makeMockResponse(201)),
|
|
}
|
|
const mockCalendar = { url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/' }
|
|
const uid = 'abc-123@familysync'
|
|
const icsString = 'BEGIN:VCALENDAR\r\nEND:VCALENDAR'
|
|
|
|
const result = await createCalendarEvent(mockClient as never, mockCalendar as never, uid, icsString)
|
|
|
|
expect(mockClient.createCalendarObject).toHaveBeenCalledWith({
|
|
calendar: mockCalendar,
|
|
filename: `${uid}.ics`,
|
|
iCalString: icsString,
|
|
})
|
|
expect(result.status).toBe(201)
|
|
})
|
|
|
|
it('returns the raw Response from client', async () => {
|
|
const mockClient = {
|
|
createCalendarObject: vi.fn().mockResolvedValue(makeMockResponse(204)),
|
|
}
|
|
const mockCalendar = { url: 'https://caldav.fastmail.com/' }
|
|
const result = await createCalendarEvent(mockClient as never, mockCalendar as never, 'uid@fs', 'ICS')
|
|
expect(result).toBeDefined()
|
|
expect(result.status).toBe(204)
|
|
})
|
|
})
|
|
|
|
describe('updateCalendarEvent', () => {
|
|
it('calls client.updateCalendarObject with etag in calendarObject (If-Match)', async () => {
|
|
const mockClient = {
|
|
updateCalendarObject: vi.fn().mockResolvedValue(makeMockResponse(204)),
|
|
}
|
|
const calendarObjectUrl = 'https://caldav.fastmail.com/dav/calendars/user/test/Default/uid.ics'
|
|
const icsString = 'BEGIN:VCALENDAR\r\nEND:VCALENDAR'
|
|
const etag = '"etag-123"'
|
|
|
|
const result = await updateCalendarEvent(mockClient as never, calendarObjectUrl, icsString, etag)
|
|
|
|
expect(mockClient.updateCalendarObject).toHaveBeenCalledWith({
|
|
calendarObject: {
|
|
url: calendarObjectUrl,
|
|
data: icsString,
|
|
etag,
|
|
},
|
|
})
|
|
expect(result.status).toBe(204)
|
|
})
|
|
|
|
it('passes empty string as etag when etag is null (safe for If-Match)', async () => {
|
|
const mockClient = {
|
|
updateCalendarObject: vi.fn().mockResolvedValue(makeMockResponse(204)),
|
|
}
|
|
await updateCalendarEvent(mockClient as never, 'https://caldav.fastmail.com/uid.ics', 'ICS', null)
|
|
|
|
const callArg = mockClient.updateCalendarObject.mock.calls[0][0]
|
|
expect(callArg.calendarObject.etag).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('deleteCalendarEvent', () => {
|
|
it('calls client.deleteCalendarObject with etag in calendarObject (If-Match)', async () => {
|
|
const mockClient = {
|
|
deleteCalendarObject: vi.fn().mockResolvedValue(makeMockResponse(204)),
|
|
}
|
|
const calendarObjectUrl = 'https://caldav.fastmail.com/dav/calendars/user/test/Default/uid.ics'
|
|
const etag = '"etag-456"'
|
|
|
|
const result = await deleteCalendarEvent(mockClient as never, calendarObjectUrl, etag)
|
|
|
|
expect(mockClient.deleteCalendarObject).toHaveBeenCalledWith({
|
|
calendarObject: {
|
|
url: calendarObjectUrl,
|
|
data: '',
|
|
etag,
|
|
},
|
|
})
|
|
expect(result.status).toBe(204)
|
|
})
|
|
|
|
it('passes empty string as etag when etag is null', async () => {
|
|
const mockClient = {
|
|
deleteCalendarObject: vi.fn().mockResolvedValue(makeMockResponse(204)),
|
|
}
|
|
await deleteCalendarEvent(mockClient as never, 'https://caldav.fastmail.com/uid.ics', null)
|
|
|
|
const callArg = mockClient.deleteCalendarObject.mock.calls[0][0]
|
|
expect(callArg.calendarObject.etag).toBe('')
|
|
})
|
|
})
|