import { assertEquals, assertMatch } from '@std/assert'; import { Calendar, CalendarEvent } from '/lib/models/calendar.ts'; import { CALENDAR_COLOR_OPTIONS, convertRRuleToWords, generateVCalendar, generateVEvent, getCalendarEventStyle, getColorAsHex, getDateRangeForCalendarView, getDaysForWeek, getIdFromVEvent, getWeeksForMonth, parseIcsDate, parseVCalendar, splitTextIntoVEvents, updateIcs, } from './calendar.ts'; Deno.test('that getColorAsHex works', () => { const tests: { input: string; expected: string | undefined }[] = [ { input: 'bg-red-700', expected: '#B51E1F' }, { input: 'bg-green-700', expected: '#148041' }, { input: 'bg-blue-900', expected: '#1E3A89' }, { input: 'bg-purple-800', expected: '#6923A9' }, { input: 'bg-gray-700', expected: '#384354' }, { input: 'invalid-color', expected: '#384354' }, ]; for (const test of tests) { const output = getColorAsHex(test.input as typeof CALENDAR_COLOR_OPTIONS[number]); assertEquals(output, test.expected); } }); Deno.test('that getIdFromVEvent works', () => { const tests: { input: string; expected?: string; shouldBeUUID?: boolean }[] = [ { input: `BEGIN:VEVENT UID:12345-abcde-67890 SUMMARY:Test Event END:VEVENT`, expected: '12345-abcde-67890', }, { input: `BEGIN:VEVENT SUMMARY:No UID Event END:VEVENT`, shouldBeUUID: true, }, { input: `BEGIN:VEVENT UID: spaced-uid SUMMARY:Spaced UID END:VEVENT`, expected: 'spaced-uid', }, ]; for (const test of tests) { const output = getIdFromVEvent(test.input); if (test.expected) { assertEquals(output, test.expected); } else if (test.shouldBeUUID) { assertMatch(output, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); } } }); Deno.test('that splitTextIntoVEvents works', () => { const tests: { input: string; expected: string[] }[] = [ { input: `BEGIN:VEVENT UID:1 SUMMARY:Event 1 END:VEVENT BEGIN:VEVENT UID:2 SUMMARY:Event 2 END:VEVENT`, expected: [ `BEGIN:VEVENT UID:1 SUMMARY:Event 1 END:VEVENT`, `BEGIN:VEVENT UID:2 SUMMARY:Event 2 END:VEVENT`, ], }, { input: `BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT UID:1 SUMMARY:Event 1 END:VEVENT BEGIN:VEVENT UID:2 SUMMARY:Event 2 END:VEVENT END:VCALENDAR`, expected: [ `BEGIN:VEVENT UID:1 SUMMARY:Event 1 END:VEVENT`, `BEGIN:VEVENT UID:2 SUMMARY:Event 2 END:VEVENT`, ], }, { input: `BEGIN:VEVENT UID:single SUMMARY:Single Event END:VEVENT`, expected: [ `BEGIN:VEVENT UID:single SUMMARY:Single Event END:VEVENT`, ], }, { input: '', expected: [], }, { input: `BEGIN:VEVENT UID:incomplete SUMMARY:Incomplete Event`, expected: [], }, ]; for (const test of tests) { const output = splitTextIntoVEvents(test.input); assertEquals(output, test.expected); } }); Deno.test('that getDateRangeForCalendarView works', () => { const baseDate = '2025-01-15'; const dayRange = getDateRangeForCalendarView(baseDate, 'day'); assertEquals(dayRange.start.getDate(), 14); // Previous day assertEquals(dayRange.end.getDate(), 16); // Next day const weekRange = getDateRangeForCalendarView(baseDate, 'week'); assertEquals(weekRange.start.getDate(), 8); // 7 days before assertEquals(weekRange.end.getDate(), 22); // 7 days after const monthRange = getDateRangeForCalendarView(baseDate, 'month'); assertEquals(monthRange.start.getDate(), 8); // 7 days before assertEquals(monthRange.end.getDate(), 15); // 31 days after (wraps to next month) }); Deno.test('that generateVEvent works', () => { const testEvents: { input: { calendarEvent: CalendarEvent; createdDate: Date; }; expected: string; }[] = [ { input: { calendarEvent: { calendarId: 'test-calendar', isAllDay: false, url: 'test-123.ics', uid: 'test-123', title: 'Test Event', startDate: new Date('2025-01-15T10:00:00Z'), endDate: new Date('2025-01-15T11:00:00Z'), organizerEmail: 'test@example.com', transparency: 'opaque', isRecurring: false, recurringRrule: undefined, sequence: 0, description: 'Test description', location: 'Test location', eventUrl: 'https://example.com', attendees: [ { email: 'attendee@example.com', status: 'accepted', name: 'Test Attendee' }, ], reminders: [ { type: 'display', startDate: '2025-01-15T09:45:00Z', description: 'Test reminder' }, ], }, createdDate: new Date('2025-01-15T10:00:00Z'), }, expected: `BEGIN:VEVENT DTSTAMP:20250115T100000 DTSTART:20250115T100000 DTEND:20250115T110000 ORGANIZER;CN=:mailto:test@example.com SUMMARY:Test Event TRANSP:OPAQUE UID:test-123 SEQUENCE:0 CREATED:20250115T100000 LAST-MODIFIED:20250115T100000 DESCRIPTION:Test description LOCATION:Test location URL:https://example.com ATTENDEE;PARTSTAT=ACCEPTED;CN=Test Attendee:mailto:attendee@example.com BEGIN:VALARM ACTION:DISPLAY DESCRIPTION:Test reminder TRIGGER;VALUE=DATE-TIME:20250115T094500 END:VALARM END:VEVENT`, }, { input: { calendarEvent: { calendarId: 'test-calendar', isAllDay: true, url: 'test-123.ics', uid: 'test-123', title: 'Test Event', startDate: new Date('2025-01-15T10:00:00Z'), endDate: new Date('2025-01-15T11:00:00Z'), organizerEmail: 'test@example.com', transparency: 'opaque', isRecurring: false, recurringRrule: undefined, sequence: 0, description: 'Test description', location: 'Test location', eventUrl: 'https://example.com', attendees: [ { email: 'attendee@example.com', status: 'accepted', name: 'Test Attendee' }, ], reminders: [ { type: 'display', startDate: '2025-01-15T09:45:00Z', description: 'Test reminder' }, ], }, createdDate: new Date('2025-01-15T10:00:00Z'), }, expected: `BEGIN:VEVENT DTSTAMP:20250115T100000 DTSTART;VALUE=DATE:20250115 DTEND;VALUE=DATE:20250115 ORGANIZER;CN=:mailto:test@example.com SUMMARY:Test Event TRANSP:OPAQUE UID:test-123 SEQUENCE:0 CREATED:20250115T100000 LAST-MODIFIED:20250115T100000 DESCRIPTION:Test description LOCATION:Test location URL:https://example.com ATTENDEE;PARTSTAT=ACCEPTED;CN=Test Attendee:mailto:attendee@example.com BEGIN:VALARM ACTION:DISPLAY DESCRIPTION:Test reminder TRIGGER;VALUE=DATE-TIME:20250115T094500 END:VALARM END:VEVENT`, }, { input: { calendarEvent: { calendarId: 'test-calendar', isAllDay: false, url: 'test-123.ics', uid: 'test-123', title: 'Test Event', startDate: new Date('2025-01-15T10:00:00Z'), endDate: new Date('2025-01-15T11:00:00Z'), organizerEmail: 'test@example.com', transparency: 'opaque', isRecurring: true, recurringRrule: 'FREQ=WEEKLY;BYDAY=MO', sequence: 1, }, createdDate: new Date('2025-01-15T10:00:00Z'), }, expected: `BEGIN:VEVENT DTSTAMP:20250115T100000 DTSTART:20250115T100000 DTEND:20250115T110000 ORGANIZER;CN=:mailto:test@example.com SUMMARY:Test Event TRANSP:OPAQUE UID:test-123 RRULE:FREQ=WEEKLY;BYDAY=MO SEQUENCE:1 CREATED:20250115T100000 LAST-MODIFIED:20250115T100000 END:VEVENT`, }, ]; for (const testEvent of testEvents) { const output = generateVEvent(testEvent.input.calendarEvent, testEvent.input.createdDate); assertEquals(output, testEvent.expected); } }); Deno.test('that generateVCalendar works', () => { const testEvents: CalendarEvent[] = [ { calendarId: 'test-calendar', isAllDay: false, url: 'test-123.ics', uid: 'event-1', title: 'Event 1', startDate: new Date('2025-01-15T10:00:00Z'), endDate: new Date('2025-01-15T11:00:00Z'), organizerEmail: 'test@example.com', transparency: 'opaque', isRecurring: false, recurringRrule: undefined, sequence: 0, }, { calendarId: 'test-calendar', isAllDay: true, url: 'test-123.ics', uid: 'event-2', title: 'Event 2', startDate: new Date('2025-01-16T10:00:00Z'), endDate: new Date('2025-01-16T11:00:00Z'), organizerEmail: 'test@example.com', transparency: 'opaque', isRecurring: false, recurringRrule: undefined, sequence: 0, }, ]; const output = generateVCalendar(testEvents, new Date('2025-01-15T10:00:00Z')); const expected = `BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20250115T100000 DTSTART:20250115T100000 DTEND:20250115T110000 ORGANIZER;CN=:mailto:test@example.com SUMMARY:Event 1 TRANSP:OPAQUE UID:event-1 SEQUENCE:0 CREATED:20250115T100000 LAST-MODIFIED:20250115T100000 END:VEVENT BEGIN:VEVENT DTSTAMP:20250115T100000 DTSTART;VALUE=DATE:20250116 DTEND;VALUE=DATE:20250116 ORGANIZER;CN=:mailto:test@example.com SUMMARY:Event 2 TRANSP:OPAQUE UID:event-2 SEQUENCE:0 CREATED:20250115T100000 LAST-MODIFIED:20250115T100000 END:VEVENT END:VCALENDAR`; assertEquals(output, expected); }); Deno.test('that updateIcs works', () => { const testEvents: { input: { originalIcs: string; updates: CalendarEvent; }; expected: string; }[] = [ { input: { originalIcs: `BEGIN:VEVENT UID:test-123 SUMMARY:Original Title DTSTART:20250115T100000Z DTEND:20250115T110000Z END:VEVENT`, updates: { calendarId: 'test-calendar', isAllDay: false, url: 'test-123.ics', uid: 'test-123', title: 'Updated Title', startDate: new Date('2025-01-16T10:00:00Z'), endDate: new Date('2025-01-16T11:00:00Z'), organizerEmail: 'test@example.com', transparency: 'transparent', isRecurring: false, recurringRrule: undefined, sequence: 0, description: 'New description', location: 'New location', eventUrl: 'https://updated.com', }, }, expected: `BEGIN:VEVENT UID:test-123 SUMMARY:Updated Title DTSTART:20250116T100000 DTEND:20250116T110000 DESCRIPTION:New description URL:https://updated.com LOCATION:New location END:VEVENT`, }, { input: { originalIcs: `BEGIN:VEVENT UID:test-123 SUMMARY:Original Title DTSTART:20250115T100000 DTEND:20250115T110000 URL:https://example.com LOCATION:Example location END:VEVENT`, updates: { calendarId: 'test-calendar', isAllDay: true, url: 'test-123.ics', uid: 'test-123', title: 'Updated Title', startDate: new Date('2025-01-16T10:00:00Z'), endDate: new Date('2025-01-16T11:00:00Z'), organizerEmail: 'test@example.com', transparency: 'transparent', isRecurring: false, recurringRrule: undefined, sequence: 0, description: 'New description', eventUrl: 'https://updated.com', }, }, expected: `BEGIN:VEVENT UID:test-123 SUMMARY:Updated Title DTSTART;VALUE=DATE:20250116 DTEND;VALUE=DATE:20250116 URL:https://updated.com LOCATION:Example location DESCRIPTION:New description END:VEVENT`, }, { input: { originalIcs: `BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN BEGIN:VEVENT UID:test-123 SUMMARY:Original Title DTSTART:20250115T100000 DTEND:20250115T110000 URL:https://example.com LOCATION:Example location END:VEVENT END:VCALENDAR`, updates: { calendarId: 'test-calendar', isAllDay: true, url: 'test-123.ics', uid: 'test-123', title: 'Updated Title', startDate: new Date('2025-01-16T10:00:00Z'), endDate: new Date('2025-01-16T11:00:00Z'), organizerEmail: 'test@example.com', transparency: 'transparent', isRecurring: false, recurringRrule: undefined, sequence: 0, description: 'New description', eventUrl: 'https://updated.com', }, }, expected: `BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN BEGIN:VEVENT UID:test-123 SUMMARY:Updated Title DTSTART;VALUE=DATE:20250116 DTEND;VALUE=DATE:20250116 URL:https://updated.com LOCATION:Example location DESCRIPTION:New description END:VEVENT END:VCALENDAR`, }, { input: { originalIcs: `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN BEGIN:VTIMEZONE TZID:Europe/Lisbon BEGIN:STANDARD DTSTART:19111231T232315 RDATE:19111231T232315 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:-003645 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19161101T010000 RDATE:19161101T010000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19171015T000000 RDATE:19171015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19181015T000000 RDATE:19181015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19191015T000000 RDATE:19191015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19201015T000000 RDATE:19201015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19211015T000000 RDATE:19211015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19241005T000000 RDATE:19241005T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19261003T000000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19291006T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19311004T000000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19341007T000000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19391119T000000 RDATE:19391119T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19401008T000000 RDATE:19401008T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19411006T000000 RDATE:19411006T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19421025T000000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19451028T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19461006T000000 RDATE:19461006T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19471005T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19651003T030000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19661002T030000 RDATE:19661002T030000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+010000 END:STANDARD BEGIN:STANDARD DTSTART:19760926T010000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19770925T010000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19781001T020000 RDATE:19781001T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19790930T020000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19800928T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19810927T010000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19850929T010000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19860928T020000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19910929T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19920927T020000 RDATE:19920927T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+010000 END:STANDARD BEGIN:STANDARD DTSTART:19930926T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T030000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:STANDARD BEGIN:STANDARD DTSTART:19961027T020000 RDATE:19961027T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU TZNAME:(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:DAYLIGHT DTSTART:19160617T230000 RDATE:19160617T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19170301T000000 RDATE:19170301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19180301T000000 RDATE:19180301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19190301T000000 RDATE:19190301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19200301T000000 RDATE:19200301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19210301T000000 RDATE:19210301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19240416T230000 RDATE:19240416T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19260417T230000 RDATE:19260417T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19270409T230000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=2SA;UNTIL=19280414T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19290420T230000 RDATE:19290420T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19310418T230000 RDATE:19310418T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19320402T230000 RDATE:19320402T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19340407T230000 RDATE:19340407T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19350330T230000 RDATE:19350330T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19360418T230000 RDATE:19360418T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19370403T230000 RDATE:19370403T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19380326T230000 RDATE:19380326T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19390415T230000 RDATE:19390415T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19400224T230000 RDATE:19400224T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19410405T230000 RDATE:19410405T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19420314T230000 RDATE:19420314T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19420425T230000 RDATE:19420425T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19420816T000000 RDATE:19420816T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19430313T230000 RDATE:19430313T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19430417T230000 RDATE:19430417T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19430829T000000 RDATE:19430829T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19440311T230000 RDATE:19440311T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19440422T230000 RDATE:19440422T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19440827T000000 RDATE:19440827T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450310T230000 RDATE:19450310T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450421T230000 RDATE:19450421T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450826T000000 RDATE:19450826T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19460406T230000 RDATE:19460406T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19470406T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=19660403T020000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19770327T000000 RDATE:19770327T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19780402T010000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=19800406T010000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19810329T000000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19850331T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19860330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19920329T010000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19930328T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19950326T020000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19960331T020000 RDATE:19960331T020000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU TZNAME:(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT X-TZINFO:Europe/Lisbon[2025b] END:VTIMEZONE BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 DTSTART;TZID=Europe/Lisbon:20250720T090000 DTEND;TZID=Europe/Lisbon:20250720T100000 CREATED:20250720T075329Z DTSTAMP:20250720T075414Z RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20250817T080000Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT END:VCALENDAR`, updates: { calendarId: 'test-calendar', isAllDay: false, url: '99e15556-fd88-4cb9-818e-fcbf853bc443.ics', uid: '99e15556-fd88-4cb9-818e-fcbf853bc443', title: 'Updated Title', startDate: new Date('2025-07-20T09:00:00Z'), endDate: new Date('2025-07-20T10:00:00Z'), organizerEmail: 'test@example.com', transparency: 'opaque', isRecurring: true, recurringRrule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20250817T080000Z', sequence: 0, description: 'New description', }, }, expected: `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN BEGIN:VTIMEZONE TZID:Europe/Lisbon BEGIN:STANDARD DTSTART:19111231T232315 RDATE:19111231T232315 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:-003645 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19161101T010000 RDATE:19161101T010000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19171015T000000 RDATE:19171015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19181015T000000 RDATE:19181015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19191015T000000 RDATE:19191015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19201015T000000 RDATE:19201015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19211015T000000 RDATE:19211015T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19241005T000000 RDATE:19241005T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19261003T000000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19291006T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19311004T000000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19341007T000000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19391119T000000 RDATE:19391119T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19401008T000000 RDATE:19401008T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19411006T000000 RDATE:19411006T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19421025T000000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19451028T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19461006T000000 RDATE:19461006T000000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19471005T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19651003T030000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19661002T030000 RDATE:19661002T030000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+010000 END:STANDARD BEGIN:STANDARD DTSTART:19760926T010000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19770925T010000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19781001T020000 RDATE:19781001T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19790930T020000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19800928T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19810927T010000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19850929T010000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19860928T020000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19910929T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19920927T020000 RDATE:19920927T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+010000 END:STANDARD BEGIN:STANDARD DTSTART:19930926T030000 RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T030000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:STANDARD BEGIN:STANDARD DTSTART:19961027T020000 RDATE:19961027T020000 TZNAME:Europe/Lisbon(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:STANDARD DTSTART:19971026T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU TZNAME:(STD) TZOFFSETFROM:+010000 TZOFFSETTO:+000000 END:STANDARD BEGIN:DAYLIGHT DTSTART:19160617T230000 RDATE:19160617T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19170301T000000 RDATE:19170301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19180301T000000 RDATE:19180301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19190301T000000 RDATE:19190301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19200301T000000 RDATE:19200301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19210301T000000 RDATE:19210301T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19240416T230000 RDATE:19240416T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19260417T230000 RDATE:19260417T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19270409T230000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=2SA;UNTIL=19280414T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19290420T230000 RDATE:19290420T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19310418T230000 RDATE:19310418T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19320402T230000 RDATE:19320402T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19340407T230000 RDATE:19340407T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19350330T230000 RDATE:19350330T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19360418T230000 RDATE:19360418T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19370403T230000 RDATE:19370403T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19380326T230000 RDATE:19380326T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19390415T230000 RDATE:19390415T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19400224T230000 RDATE:19400224T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19410405T230000 RDATE:19410405T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19420314T230000 RDATE:19420314T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19420425T230000 RDATE:19420425T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19420816T000000 RDATE:19420816T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19430313T230000 RDATE:19430313T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19430417T230000 RDATE:19430417T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19430829T000000 RDATE:19430829T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19440311T230000 RDATE:19440311T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19440422T230000 RDATE:19440422T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19440827T000000 RDATE:19440827T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450310T230000 RDATE:19450310T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450421T230000 RDATE:19450421T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450826T000000 RDATE:19450826T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+020000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19460406T230000 RDATE:19460406T230000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19470406T020000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=19660403T020000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19770327T000000 RDATE:19770327T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19780402T010000 RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=19800406T010000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19810329T000000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19850331T000000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19860330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19920329T010000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19930328T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19950326T020000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+020000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19960331T020000 RDATE:19960331T020000 TZNAME:Europe/Lisbon(DST) TZOFFSETFROM:+010000 TZOFFSETTO:+010000 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19970330T010000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU TZNAME:(DST) TZOFFSETFROM:+000000 TZOFFSETTO:+010000 END:DAYLIGHT X-TZINFO:Europe/Lisbon[2025b] END:VTIMEZONE BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 DTSTART:20250720T090000 DTEND:20250720T100000 CREATED:20250720T075329Z DTSTAMP:20250720T075414Z RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20250817T080000Z SUMMARY:Updated Title TRANSP:OPAQUE DESCRIPTION:New description END:VEVENT END:VCALENDAR`, }, ]; for (const test of testEvents) { const output = updateIcs(test.input.originalIcs, test.input.updates); assertEquals(output, test.expected); } }); Deno.test('that parseVCalendar works', () => { const testIcs = `BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VEVENT UID:test-123 SUMMARY:Test Event DTSTART:20250115T100000Z DTEND:20250115T110000Z ORGANIZER;CN=:mailto:test@example.com TRANSP:OPAQUE DESCRIPTION:Test description LOCATION:Test location URL:https://example.com ATTENDEE;PARTSTAT=ACCEPTED;CN=Test Attendee:mailto:attendee@example.com BEGIN:VALARM ACTION:DISPLAY DESCRIPTION:Test reminder TRIGGER;VALUE=DATE-TIME:20250115T094500Z END:VALARM END:VEVENT END:VCALENDAR`; const output = parseVCalendar(testIcs); assertEquals(output.length, 1); const event = output[0]; assertEquals(event.uid, 'test-123'); assertEquals(event.title, 'Test Event'); assertEquals(event.description, 'Test description'); assertEquals(event.location, 'Test location'); assertEquals(event.eventUrl, 'https://example.com'); assertEquals(event.transparency, 'opaque'); assertEquals(event.attendees?.length, 1); assertEquals(event.reminders?.length, 1); }); Deno.test('that parseVCalendar handles multiple events', () => { const testIcs = `BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT UID:event-1 SUMMARY:Event 1 DTSTART:20250115T100000Z DTEND:20250115T110000Z END:VEVENT BEGIN:VEVENT UID:event-2 SUMMARY:Event 2 DTSTART:20250116T100000Z DTEND:20250116T110000Z END:VEVENT END:VCALENDAR`; const output = parseVCalendar(testIcs); assertEquals(output.length, 2); assertEquals(output[0].uid, 'event-1'); assertEquals(output[1].uid, 'event-2'); }); Deno.test('that parseVCalendar handles recurring events', () => { const testIcs = `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250721T080000Z DTSTART:20250721T080000Z DTEND:20250721T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250722T080000Z DTSTART:20250722T080000Z DTEND:20250722T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250723T080000Z DTSTART:20250723T080000Z DTEND:20250723T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250724T080000Z DTSTART:20250724T080000Z DTEND:20250724T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250725T080000Z DTSTART:20250725T080000Z DTEND:20250725T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250728T080000Z DTSTART:20250728T080000Z DTEND:20250728T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250729T080000Z DTSTART:20250729T080000Z DTEND:20250729T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250730T080000Z DTSTART:20250730T080000Z DTEND:20250730T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250731T080000Z DTSTART:20250731T080000Z DTEND:20250731T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250801T080000Z DTSTART:20250801T080000Z DTEND:20250801T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT BEGIN:VEVENT UID:99e15556-fd88-4cb9-818e-fcbf853bc443 RECURRENCE-ID:20250804T080000Z DTSTART:20250804T080000Z DTEND:20250804T090000Z CREATED:20250720T075329Z DTSTAMP:20250720T075414Z LAST-MODIFIED:20250720T075414Z SUMMARY:Recurring Standup TRANSP:OPAQUE END:VEVENT END:VCALENDAR`; const output = parseVCalendar(testIcs); assertEquals(output.length, 11); assertEquals(output[0].uid, '99e15556-fd88-4cb9-818e-fcbf853bc443:20250721T080000Z'); assertEquals(output[0].isRecurring, true); assertEquals(output[0].recurrenceMasterUid, '99e15556-fd88-4cb9-818e-fcbf853bc443'); assertEquals(output[0].recurrenceId, '20250721T080000Z'); assertEquals(output[0].title, 'Recurring Standup'); assertEquals(output[1].uid, '99e15556-fd88-4cb9-818e-fcbf853bc443:20250722T080000Z'); assertEquals(output[1].isRecurring, true); assertEquals(output[1].recurrenceMasterUid, '99e15556-fd88-4cb9-818e-fcbf853bc443'); assertEquals(output[1].recurrenceId, '20250722T080000Z'); assertEquals(output[1].title, 'Recurring Standup'); }); Deno.test('that getWeeksForMonth works', () => { const testDate = new Date('2025-01-15'); const weeks = getWeeksForMonth(testDate); // January 2025 starts on Wednesday, so it should have 5 weeks assertEquals(weeks.length, 5); // First week should start with December 30, 2024 (Monday) assertEquals(weeks[0][0].date.getDate(), 30); assertEquals(weeks[0][0].date.getMonth(), 11); // Last week should end with February 2, 2025 (Sunday) assertEquals(weeks[4][6].date.getDate(), 2); assertEquals(weeks[4][6].date.getMonth(), 1); }); Deno.test('that getDaysForWeek works', () => { const testDate = new Date('2025-01-15'); const days = getDaysForWeek(testDate); assertEquals(days.length, 7); // Should start with Monday (January 13, 2025) assertEquals(days[0].date.getDate(), 13); assertEquals(days[0].date.getDay(), 1); // Should end with Sunday (January 19, 2025) assertEquals(days[6].date.getDate(), 19); assertEquals(days[6].date.getDay(), 0); // Each day should have 24 hours assertEquals(days[0].hours.length, 24); }); Deno.test('that getCalendarEventStyle works', () => { const calendars: Calendar[] = [ { url: 'cal-1', isVisible: true, uid: 'cal-1', name: 'Calendar 1', calendarColor: '#B51E1F' }, { url: 'cal-2', isVisible: true, uid: 'cal-2', name: 'Calendar 2', calendarColor: '#1E3A89' }, ]; const opaqueEvent: CalendarEvent = { calendarId: 'cal-1', isAllDay: false, url: 'event-1.ics', uid: 'event-1', title: 'Opaque Event', startDate: new Date(), endDate: new Date(), organizerEmail: 'test@example.com', transparency: 'opaque', isRecurring: false, recurringRrule: undefined, sequence: 0, }; const transparentEvent: CalendarEvent = { calendarId: 'cal-2', isAllDay: false, url: 'event-2.ics', uid: 'event-2', title: 'Transparent Event', startDate: new Date(), endDate: new Date(), organizerEmail: 'test@example.com', transparency: 'transparent', isRecurring: false, recurringRrule: undefined, sequence: 0, }; assertEquals(getCalendarEventStyle(opaqueEvent, calendars), { backgroundColor: '#B51E1F' }); assertEquals(getCalendarEventStyle(transparentEvent, calendars), { border: '1px solid #1E3A89' }); }); Deno.test('that getCalendarEventStyle returns default color for unknown calendar', () => { const calendars: Calendar[] = []; const event: CalendarEvent = { calendarId: 'unknown-cal', isAllDay: false, url: 'event-1.ics', uid: 'event-1', title: 'Unknown Calendar Event', startDate: new Date(), endDate: new Date(), organizerEmail: 'test@example.com', transparency: 'opaque', isRecurring: false, recurringRrule: undefined, sequence: 0, }; assertEquals(getCalendarEventStyle(event, calendars), { backgroundColor: '#384354' }); }); Deno.test('that parseIcsDate works', () => { const tests: { input: string; expected: string }[] = [ { input: '20250101T000000Z', expected: '2025-01-01T00:00:00.000Z' }, { input: '20250201T000300', expected: '2025-02-01T00:03:00.000Z' }, { input: '20250103T050000', expected: '2025-01-03T05:00:00.000Z' }, ]; for (const test of tests) { const output = parseIcsDate(test.input); assertEquals(output.toISOString(), test.expected); } }); Deno.test('that convertRRuleToWords works', () => { const tests: { input: string; expected: string }[] = [ { input: 'RRULE:FREQ=DAILY', expected: 'Every day' }, { input: 'RRULE:FREQ=WEEKLY;BYDAY=MO', expected: 'Every week on Monday' }, { input: 'RRULE:FREQ=MONTHLY;BYMONTHDAY=15', expected: 'Every month on the 15th' }, { input: 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR', expected: 'Every week on Monday, Wednesday, Friday' }, { input: 'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;INTERVAL=2', expected: 'Every 2 months on the 1st' }, { input: 'RRULE:FREQ=DAILY;COUNT=5', expected: 'Every day for 5 times' }, { input: 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20250101T000000Z', expected: 'Every week on Monday, Wednesday, Friday until 2025-01-01', }, { input: 'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;INTERVAL=2;UNTIL=20250101T000000Z', expected: 'Every 2 months on the 1st until 2025-01-01', }, ]; for (const test of tests) { const output = convertRRuleToWords(test.input); assertEquals(output, test.expected); } }); Deno.test('that convertRRuleToWords handles invalid rules', () => { const output = convertRRuleToWords('INVALID:RULE'); assertEquals(output, ''); }); Deno.test('that parseVCalendar handles vCalendar 1.0', () => { const testIcs = `BEGIN:VCALENDAR VERSION:1.0 BEGIN:VEVENT UID:test-123 SUMMARY:Test Event DTSTART:20250115T100000Z DTEND:20250115T110000Z END:VEVENT END:VCALENDAR`; const output = parseVCalendar(testIcs); assertEquals(output.length, 1); assertEquals(output[0].uid, 'test-123'); }); Deno.test('that parseVCalendar handles multiline fields', () => { const testIcs = `BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT UID:test-123 SUMMARY:Test Event with long title DESCRIPTION:Test description with very long text.\\n\\nAnd a new line. DTSTART:20250115T100000Z DTEND:20250115T110000Z END:VEVENT END:VCALENDAR`; const output = parseVCalendar(testIcs); assertEquals(output.length, 1); assertEquals(output[0].title, 'Test Event with long title'); assertEquals(output[0].description, 'Test description with very long text.\n\nAnd a new line.'); }); Deno.test('that parseVCalendar handles empty fields gracefully', () => { const testIcs = `BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT UID:test-123 SUMMARY: DESCRIPTION: LOCATION: END:VEVENT END:VCALENDAR`; const output = parseVCalendar(testIcs); assertEquals(output.length, 1); assertEquals(output[0].uid, 'test-123'); assertEquals(output[0].title, undefined); assertEquals(output[0].location, undefined); assertEquals(output[0].description, undefined); });