This takes part of the work being done in #96 that was reverted but still useful. Note Tailwind and Fresh weren't upgraded because there's no security vulnerability in either, and I have found the new versions to be worse in performance. Thos will likely stay at those fixed versions going forward.
1916 lines
45 KiB
TypeScript
1916 lines
45 KiB
TypeScript
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);
|
|
});
|