import { describe, expect, it } from "vitest"; import { parseICalendarBytes } from "@/worker/caldav/ical/parse"; import { expandCalendarData } from "@/worker/caldav/ical/render"; import { calendarObjectMatchesFilters } from "@/worker/caldav/ical/search"; import { utf8ArrayBuffer } from "@tests/worker/helpers/bytes"; const bounds = { maxYears: 5, maxInstances: 10_000 }; function ms(value: string): number { const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(value); if (!match) throw new Error(`Invalid test timestamp ${value}`); return Date.UTC( Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6]), ); } function ical(lines: string[]): string { return [...lines, ""].join("\r\n"); } describe("CalDAV iCalendar helpers", () => { it("indexes recurrence sets with RDATE and EXDATE", () => { const body = ical([ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//tests//EN", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:recurrence-set@example.test", "SUMMARY:Recurrence Set", "DTSTART:20260521T120000Z", "DTEND:20260521T130000Z", "RRULE:FREQ=DAILY;COUNT=2", "RDATE:20260524T120000Z", "EXDATE:20260522T120000Z", "END:VEVENT", "END:VCALENDAR", ]); const parsed = parseICalendarBytes(utf8ArrayBuffer(body), 1_048_576, bounds); expect(parsed.hasRecurrence).toBe(true); expect(parsed.recurrenceMinMs).toBe(ms("20260521T120000Z")); expect(parsed.recurrenceMaxMs).toBe(ms("20260524T130000Z")); expect( calendarObjectMatchesFilters( body, [{ kind: "time-range", range: { startMs: ms("20260522T000000Z"), endMs: ms("20260523T000000Z") } }], bounds, ), ).toBe(false); expect( calendarObjectMatchesFilters( body, [{ kind: "time-range", range: { startMs: ms("20260524T000000Z"), endMs: ms("20260525T000000Z") } }], bounds, ), ).toBe(true); }); it("matches generic property filters and VTODO time ranges", () => { const body = ical([ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//tests//EN", "BEGIN:VTODO", "DTSTAMP:20260101T000000Z", "UID:todo@example.test", "SUMMARY:Todo", "DTSTART:20260521T120000Z", "DUE:20260523T120000Z", "CATEGORIES:Focus,Team", "END:VTODO", "END:VCALENDAR", ]); expect( calendarObjectMatchesFilters( body, [ { kind: "component", component: "VTODO" }, { kind: "property-text", name: "CATEGORIES", text: "focus" }, { kind: "property-defined", name: "LOCATION", defined: false }, { kind: "time-range", range: { startMs: ms("20260522T000000Z"), endMs: ms("20260522T010000Z") } }, ], bounds, ), ).toBe(true); expect( calendarObjectMatchesFilters( body, [{ kind: "time-range", range: { startMs: ms("20260524T000000Z"), endMs: ms("20260525T000000Z") } }], bounds, ), ).toBe(false); }); it("indexes IANA TZID date-times for time-range matching", () => { const body = ical([ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//tests//EN", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:tzid@example.test", "SUMMARY:TZID", "DTSTART;TZID=America/Los_Angeles:20260521T090000", "DTEND;TZID=America/Los_Angeles:20260521T100000", "END:VEVENT", "END:VCALENDAR", ]); const parsed = parseICalendarBytes(utf8ArrayBuffer(body), 1_048_576, bounds); expect(parsed.dtstartMs).toBe(ms("20260521T160000Z")); expect( calendarObjectMatchesFilters( body, [{ kind: "time-range", range: { startMs: ms("20260521T153000Z"), endMs: ms("20260521T163000Z") } }], bounds, ), ).toBe(true); }); it("treats floating date-times as UTC for indexed time-range matching", () => { const body = ical([ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//tests//EN", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:floating@example.test", "SUMMARY:Floating", "DTSTART:20260521T090000", "DTEND:20260521T100000", "END:VEVENT", "END:VCALENDAR", ]); const parsed = parseICalendarBytes(utf8ArrayBuffer(body), 1_048_576, bounds); expect(parsed.dtstartMs).toBe(ms("20260521T090000Z")); expect( calendarObjectMatchesFilters( body, [{ kind: "time-range", range: { startMs: ms("20260521T083000Z"), endMs: ms("20260521T093000Z") } }], bounds, ), ).toBe(true); }); it("rejects unsupported RRULE parts instead of silently broadening results", () => { const body = ical([ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//tests//EN", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:unsupported-rrule@example.test", "SUMMARY:Unsupported", "DTSTART:20260521T120000Z", "DTEND:20260521T130000Z", "RRULE:FREQ=MONTHLY;BYSETPOS=1;BYDAY=MO", "END:VEVENT", "END:VCALENDAR", ]); expect(() => parseICalendarBytes(utf8ArrayBuffer(body), 1_048_576, bounds)).toThrow( "Unsupported RRULE part BYSETPOS", ); }); it("expands recurrence overrides and omits cancelled instances", () => { const body = ical([ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//tests//EN", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:expanded@example.test", "SUMMARY:Daily", "DTSTART:20260521T120000Z", "DTEND:20260521T130000Z", "RRULE:FREQ=DAILY;COUNT=3", "END:VEVENT", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:expanded@example.test", "RECURRENCE-ID:20260522T120000Z", "SUMMARY:Moved Daily", "DTSTART:20260522T150000Z", "DTEND:20260522T160000Z", "END:VEVENT", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:expanded@example.test", "RECURRENCE-ID:20260523T120000Z", "STATUS:CANCELLED", "SUMMARY:Cancelled Daily", "DTSTART:20260523T120000Z", "DTEND:20260523T130000Z", "END:VEVENT", "END:VCALENDAR", ]); const expanded = expandCalendarData( body, { startMs: ms("20260521T000000Z"), endMs: ms("20260524T000000Z") }, bounds, ); expect(expanded).toContain("RECURRENCE-ID:20260522T120000Z"); expect(expanded).toContain("SUMMARY:Moved Daily"); expect(expanded).toContain("DTSTART:20260522T150000Z"); expect(expanded).not.toContain("RRULE:FREQ=DAILY"); expect(expanded).not.toContain("SUMMARY:Cancelled Daily"); }); it("keeps THISANDFUTURE range overrides in expanded output", () => { const body = ical([ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//dab//tests//EN", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:this-and-future@example.test", "SUMMARY:Daily", "DTSTART:20260521T120000Z", "DTEND:20260521T130000Z", "RRULE:FREQ=DAILY;COUNT=4", "END:VEVENT", "BEGIN:VEVENT", "DTSTAMP:20260101T000000Z", "UID:this-and-future@example.test", "RECURRENCE-ID;RANGE=THISANDFUTURE:20260522T120000Z", "SUMMARY:Shift Future", "DTSTART:20260522T150000Z", "DTEND:20260522T160000Z", "END:VEVENT", "END:VCALENDAR", ]); const expanded = expandCalendarData( body, { startMs: ms("20260522T000000Z"), endMs: ms("20260524T000000Z") }, bounds, ); expect(expanded).toContain("RECURRENCE-ID:20260522T120000Z"); expect(expanded).toContain("RECURRENCE-ID:20260523T120000Z"); expect(expanded.match(/SUMMARY:Shift Future/g)?.length).toBeGreaterThanOrEqual(2); }); });