fix: ics compat with older ical specced calendars (#19582)

* Migrate DURATION to DTEND + mark as busy

* Adjusted tests for DTEND support
This commit is contained in:
Alex van Andel authored and GitHub committed 2025-02-26 12:29:07 -07:00
1 parent af524d9e93
commit 48b8afd8af
3 files changed
+23 -49

No files matched your search

@@ -1,12 +1,11 @@
import { describe, expect } from "vitest";
import dayjs from "@calcom/dayjs";
import { buildCalendarEvent, buildPerson } from "@calcom/lib/test/builder";
import { buildVideoCallData } from "@calcom/lib/test/builder";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { test } from "@calcom/web/test/fixtures/fixtures";
import { buildCalendarEvent, buildPerson } from "../../../lib/test/builder";
import { buildVideoCallData } from "../../../lib/test/builder";
import generateIcsString from "../generateIcsString";
import generateIcsString from "./generateIcsString";
const assertHasIcsString = (icsString: string | undefined) => {
if (!icsString) throw new Error("icsString is undefined");
@@ -25,10 +24,8 @@ const testIcsStringContains = ({
event: CalendarEvent;
status: string;
}) => {
const DTSTART = event.startTime.split(".")[0].replace(/[-:]/g, "");
const startTime = dayjs(event.startTime);
const endTime = dayjs(event.endTime);
const duration = endTime.diff(startTime, "minute");
const DTSTART = `${event.startTime.split(".")[0].split(":").slice(0, 2).join(":").replace(/[-:]/g, "")}00Z`;
const DTEND = `${event.endTime.split(".")[0].split(":").slice(0, 2).join(":").replace(/[-:]/g, "")}00Z`;
expect(icsString).toEqual(expect.stringContaining(`UID:${event.iCalUID}`));
// Sometimes the deeply equal stringMatching error appears. Don't want to add flakey tests
@@ -37,7 +34,7 @@ const testIcsStringContains = ({
expect(icsString).toEqual(
expect.stringContaining(`ORGANIZER;CN=${event.organizer.name}:mailto:${event.organizer.email}`)
);
expect(icsString).toEqual(expect.stringContaining(`DURATION:PT${duration}M`));
expect(icsString).toEqual(expect.stringContaining(`DTEND:${DTEND}`));
expect(icsString).toEqual(expect.stringContaining(`STATUS:${status}`));
// Getting an error expected icsString to deeply equal stringMatching
// for (const attendee of event.attendees) {
@@ -57,15 +54,10 @@ describe("generateIcsString", () => {
attendees: [buildPerson()],
});
const title = "new_event_scheduled_recurring";
const subtitle = "emailed_you_and_any_other_attendees";
const status = "CONFIRMED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
@@ -78,15 +70,10 @@ describe("generateIcsString", () => {
iCalSequence: 0,
attendees: [buildPerson()],
});
const title = "event_request_cancelled";
const subtitle = "emailed_you_and_any_other_attendees";
const status = "CANCELLED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
@@ -99,15 +86,10 @@ describe("generateIcsString", () => {
iCalSequence: 0,
attendees: [buildPerson()],
});
const title = "event_type_has_been_rescheduled";
const subtitle = "emailed_you_and_any_other_attendees";
const status = "CONFIRMED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
@@ -120,15 +102,10 @@ describe("generateIcsString", () => {
iCalSequence: 0,
attendees: [buildPerson()],
});
const title = "request_reschedule_title_organizer";
const subtitle = "request_reschedule_subtitle_organizer";
const status = "CANCELLED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
@@ -148,19 +125,14 @@ describe("generateIcsString", () => {
},
true
);
const title = "request_reschedule_title_organizer";
const subtitle = "request_reschedule_subtitle_organizer";
const status = "CANCELLED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
const assertedIcsString = assertHasIcsString(icsString);
assertHasIcsString(icsString);
expect(icsString).toEqual(expect.stringContaining(`LOCATION:${videoCallData.url}`));
});
@@ -174,19 +146,14 @@ describe("generateIcsString", () => {
},
true
);
const title = "request_reschedule_title_organizer";
const subtitle = "request_reschedule_subtitle_organizer";
const status = "CANCELLED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
const assertedIcsString = assertHasIcsString(icsString);
assertHasIcsString(icsString);
expect(icsString).toEqual(expect.stringContaining(`LOCATION:${event.location}`));
});
+14 -7
View File
@@ -3,7 +3,6 @@ import { createEvent } from "ics";
import type { TFunction } from "next-i18next";
import { RRule } from "rrule";
import dayjs from "@calcom/dayjs";
import { getRichDescription } from "@calcom/lib/CalEventParser";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
@@ -33,6 +32,17 @@ export type ICSCalendarEvent = Pick<
| "hideCalendarEventDetails"
>;
const toICalDateArray = (date: string): DateArray => {
const d = new Date(date);
return [
d.getUTCFullYear(),
d.getUTCMonth() + 1, // Convert 0-based month to 1-based
d.getUTCDate(),
d.getUTCHours(),
d.getUTCMinutes(),
] satisfies DateArray;
};
const generateIcsString = ({
event,
status,
@@ -57,16 +67,12 @@ const generateIcsString = ({
const icsEvent = createEvent({
uid: event.iCalUID || event.uid!,
sequence: event.iCalSequence || 0,
start: dayjs(event.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
start: toICalDateArray(event.startTime),
end: toICalDateArray(event.endTime),
startInputType: "utc",
productId: "calcom/ics",
title: event.title,
description: getRichDescription(event, t),
duration: { minutes: dayjs(event.endTime).diff(dayjs(event.startTime), "minute") },
organizer: { name: event.organizer.name, email: event.organizer.email },
...{ recurrenceRule },
attendees: [
@@ -91,6 +97,7 @@ const generateIcsString = ({
method: "REQUEST",
status,
...(event.hideCalendarEventDetails ? { classification: "PRIVATE" } : {}),
busyStatus: "BUSY",
});
if (icsEvent.error) {
throw icsEvent.error;
@@ -4,7 +4,7 @@ import { APP_NAME } from "@calcom/lib/constants";
import { buildCalendarEvent } from "@calcom/lib/test/builder";
import { test } from "@calcom/web/test/fixtures/fixtures";
import getICalUID from "../getICalUID";
import getICalUID from "./getICalUID";
describe("getICalUid", () => {
test("returns iCalUID when passing a uid", () => {