[fix] resolves #53 account for all day busy events on caldav availability calculations #57

Merged
eleith merged 1 commits from fix/support-all-day-busy-caldav into main 2026-09-01 00:12:12 +00:00
3 changed files with 78 additions and 1 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
'@when/worker': patch
---
better support for caldav all day busy events when calculating availability
+55
View File
@@ -168,3 +168,58 @@ END:VEVENT
END:VCALENDAR`;
expect(parseBusyEvents(cancelled)).toEqual([]);
});
test('parses all-day event with explicit DTEND', () => {
const alldayWithEnd = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//test//EN
BEGIN:VEVENT
UID:allday-end@test
DTSTAMP:20260101T000000Z
DTSTART;VALUE=DATE:20260415
DTEND;VALUE=DATE:20260416
SUMMARY:All day with DTEND
END:VEVENT
END:VCALENDAR`;
const busy = parseBusyEvents(alldayWithEnd);
expect(busy).toHaveLength(1);
expect(busy[0].uid).toBe('allday-end@test');
expect(busy[0].start.toString()).toBe('2026-04-15T00:00:00Z');
expect(busy[0].end.toString()).toBe('2026-04-16T00:00:00Z');
});
test('parses single-day all-day event without DTEND (RFC 5545 default 1 day duration)', () => {
const alldayNoEnd = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//test//EN
BEGIN:VEVENT
UID:allday-noend@test
DTSTAMP:20260101T000000Z
DTSTART;VALUE=DATE:20260415
SUMMARY:All day single day
END:VEVENT
END:VCALENDAR`;
const busy = parseBusyEvents(alldayNoEnd);
expect(busy).toHaveLength(1);
expect(busy[0].uid).toBe('allday-noend@test');
expect(busy[0].start.toString()).toBe('2026-04-15T00:00:00Z');
expect(busy[0].end.toString()).toBe('2026-04-16T00:00:00Z');
});
test('parses timed event without DTEND or DURATION (0 duration)', () => {
const timedNoEnd = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//test//EN
BEGIN:VEVENT
UID:timed-noend@test
DTSTAMP:20260101T000000Z
DTSTART:20260415T140000Z
SUMMARY:Zero duration
END:VEVENT
END:VCALENDAR`;
const busy = parseBusyEvents(timedNoEnd);
expect(busy).toHaveLength(1);
expect(busy[0].uid).toBe('timed-noend@test');
expect(busy[0].start.toString()).toBe('2026-04-15T14:00:00Z');
expect(busy[0].end.toString()).toBe('2026-04-15T14:00:00Z');
});
+18 -1
View File
@@ -30,7 +30,7 @@ function toBusy(event: IcsEvent): BusyEvent | null {
try {
const startMs = event.start.date.getTime();
const endMs = getEventEnd(event).getTime();
const endMs = eventEnd(event).getTime();
return {
uid: event.uid,
start: Temporal.Instant.fromEpochMilliseconds(startMs),
@@ -48,3 +48,20 @@ function toBusy(event: IcsEvent): BusyEvent | null {
return null;
}
}
function eventEnd(event: IcsEvent): Date {
if (event.end?.date) {
return event.end.date;
}
if (event.duration) {
return getEventEnd(event);
}
if (event.start.type === 'DATE') {
// All-day event without DTEND/DURATION lasts 1 full day per RFC 5545 §3.6.1
const d = new Date(event.start.date.getTime());
d.setUTCDate(d.getUTCDate() + 1);
return d;
}
// DATE-TIME event without DTEND/DURATION has 0 duration per RFC 5545 §3.6.1
return event.start.date;
}