Files
bewcloud/lib/utils/calendar_test.ts
Bruno Bernardino 49dbc724c8 Fix creating and deleting calendars
This fixes the creation and deletion of calendars to include a color and the proper chosen name.
2025-09-06 19:17:48 +01:00

1917 lines
45 KiB
TypeScript

import { assertEquals } from 'std/assert/assert_equals.ts';
import { assertMatch } from 'std/assert/assert_match.ts';
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);
});