From 48b8afd8af7b526c9b2c29eb44a0785b0eed2605 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Wed, 26 Feb 2025 19:29:07 +0000 Subject: [PATCH] fix: ics compat with older ical specced calendars (#19582) * Migrate DURATION to DTEND + mark as busy * Adjusted tests for DTEND support --- .../lib/{test => }/generateIcsString.test.ts | 49 +++---------------- packages/emails/lib/generateIcsString.ts | 21 +++++--- .../emails/lib/{test => }/getICalUID.test.ts | 2 +- 3 files changed, 23 insertions(+), 49 deletions(-) rename packages/emails/lib/{test => }/generateIcsString.test.ts (71%) rename packages/emails/lib/{test => }/getICalUID.test.ts (96%) diff --git a/packages/emails/lib/test/generateIcsString.test.ts b/packages/emails/lib/generateIcsString.test.ts similarity index 71% rename from packages/emails/lib/test/generateIcsString.test.ts rename to packages/emails/lib/generateIcsString.test.ts index 3cfeedbffe..d271de9e5c 100644 --- a/packages/emails/lib/test/generateIcsString.test.ts +++ b/packages/emails/lib/generateIcsString.test.ts @@ -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}`)); }); diff --git a/packages/emails/lib/generateIcsString.ts b/packages/emails/lib/generateIcsString.ts index 323d537652..d3e327f544 100644 --- a/packages/emails/lib/generateIcsString.ts +++ b/packages/emails/lib/generateIcsString.ts @@ -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; diff --git a/packages/emails/lib/test/getICalUID.test.ts b/packages/emails/lib/getICalUID.test.ts similarity index 96% rename from packages/emails/lib/test/getICalUID.test.ts rename to packages/emails/lib/getICalUID.test.ts index fe66988728..b8c518b9f3 100644 --- a/packages/emails/lib/test/getICalUID.test.ts +++ b/packages/emails/lib/getICalUID.test.ts @@ -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", () => {