test(03-01): add Wave 0 RED test scaffold for all Phase 3 behaviors
- 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
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* RED test scaffold: broker/vevent.ts — VEVENT builder (CAL-04, CAL-07)
|
||||
*
|
||||
* Behaviors under test:
|
||||
* 1. buildVeventString produces a VCALENDAR with a VEVENT for a timed event
|
||||
* with DTSTART using UTC 'Z' suffix (D-13 contract — no TZID)
|
||||
* 2. buildVeventString produces a VCALENDAR with DTSTART as a DATE value
|
||||
* (no time component, no TZID) for all-day events (D-13, Pitfall 3)
|
||||
* 3. buildVeventString with rruleString produces a VCALENDAR with an RRULE property (CAL-07)
|
||||
*
|
||||
* These tests FAIL (RED) because broker/vevent.ts does not exist yet.
|
||||
* They will turn GREEN in Plan 03-02 when the implementation is added.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
// This import fails (RED) — broker/vevent.ts does not exist yet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore intentional RED import
|
||||
import { buildVeventString } from '../../src/broker/vevent.js'
|
||||
|
||||
describe('buildVeventString', () => {
|
||||
it('produces a VCALENDAR string containing a VEVENT for a timed event', () => {
|
||||
const result = buildVeventString({
|
||||
summary: 'Team standup',
|
||||
allDay: false,
|
||||
dtstart: new Date('2026-06-10T09:00:00Z'),
|
||||
dtend: new Date('2026-06-10T09:30:00Z'),
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('uid')
|
||||
expect(result).toHaveProperty('icsString')
|
||||
expect(result.icsString).toContain('BEGIN:VCALENDAR')
|
||||
expect(result.icsString).toContain('BEGIN:VEVENT')
|
||||
expect(result.icsString).toContain('SUMMARY:Team standup')
|
||||
expect(result.icsString).toContain('END:VEVENT')
|
||||
expect(result.icsString).toContain('END:VCALENDAR')
|
||||
})
|
||||
|
||||
it('produces DTSTART with Z suffix (UTC) for a timed event — not TZID', () => {
|
||||
const result = buildVeventString({
|
||||
summary: 'Morning meeting',
|
||||
allDay: false,
|
||||
dtstart: new Date('2026-06-10T14:00:00Z'),
|
||||
dtend: new Date('2026-06-10T15:00:00Z'),
|
||||
})
|
||||
|
||||
// D-13 timed contract: DATETIME in UTC → 'Z' suffix, no TZID
|
||||
expect(result.icsString).toMatch(/DTSTART:\d{8}T\d{6}Z/)
|
||||
expect(result.icsString).not.toMatch(/DTSTART;TZID=/)
|
||||
})
|
||||
|
||||
it('produces DTSTART as DATE (no time, no TZID) for an all-day event', () => {
|
||||
const result = buildVeventString({
|
||||
summary: 'Birthday',
|
||||
allDay: true,
|
||||
dtstart: '2026-06-15',
|
||||
dtend: '2026-06-16',
|
||||
})
|
||||
|
||||
// D-13 all-day contract: VALUE=DATE, no time component, no TZID
|
||||
// ical.js represents DATE as DTSTART;VALUE=DATE:YYYYMMDD
|
||||
expect(result.icsString).toMatch(/DTSTART[^:]*:20260615/)
|
||||
// Must NOT contain a time component (no 'T' after the date)
|
||||
expect(result.icsString).not.toMatch(/DTSTART[^:]*:20260615T/)
|
||||
// Must NOT contain TZID on the DTSTART property
|
||||
expect(result.icsString).not.toMatch(/DTSTART;TZID=/)
|
||||
})
|
||||
|
||||
it('includes an RRULE property when rruleString is provided (CAL-07)', () => {
|
||||
const result = buildVeventString({
|
||||
summary: 'Weekly sync',
|
||||
allDay: false,
|
||||
dtstart: new Date('2026-06-09T10:00:00Z'),
|
||||
dtend: new Date('2026-06-09T11:00:00Z'),
|
||||
rruleString: 'FREQ=WEEKLY;BYDAY=MO',
|
||||
})
|
||||
|
||||
expect(result.icsString).toContain('RRULE:FREQ=WEEKLY;BYDAY=MO')
|
||||
})
|
||||
|
||||
it('does NOT include RRULE when rruleString is omitted', () => {
|
||||
const result = buildVeventString({
|
||||
summary: 'One-off lunch',
|
||||
allDay: false,
|
||||
dtstart: new Date('2026-06-10T12:00:00Z'),
|
||||
dtend: new Date('2026-06-10T13:00:00Z'),
|
||||
})
|
||||
|
||||
expect(result.icsString).not.toContain('RRULE:')
|
||||
})
|
||||
|
||||
it('uses the provided uid when given', () => {
|
||||
const uid = 'custom-uid-001@familysync'
|
||||
const result = buildVeventString({
|
||||
uid,
|
||||
summary: 'Test event',
|
||||
allDay: false,
|
||||
dtstart: new Date('2026-06-10T09:00:00Z'),
|
||||
dtend: new Date('2026-06-10T10:00:00Z'),
|
||||
})
|
||||
|
||||
expect(result.uid).toBe(uid)
|
||||
expect(result.icsString).toContain(`UID:${uid}`)
|
||||
})
|
||||
|
||||
it('generates a uid when none is provided', () => {
|
||||
const result = buildVeventString({
|
||||
summary: 'Auto uid event',
|
||||
allDay: false,
|
||||
dtstart: new Date('2026-06-10T09:00:00Z'),
|
||||
dtend: new Date('2026-06-10T10:00:00Z'),
|
||||
})
|
||||
|
||||
expect(result.uid).toBeTruthy()
|
||||
expect(result.uid.length).toBeGreaterThan(10)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user