-
Notifications
You must be signed in to change notification settings - Fork 13.2k
fix(caldav): default calendar not written on booking #28943
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,18 +17,22 @@ import type { | |
| TeamMember, | ||
| } from "@calcom/types/Calendar"; | ||
| import type { CredentialPayload } from "@calcom/types/Credential"; | ||
| import { ErrorCode } from "@calcom/lib/errorCodes"; | ||
| import { ErrorWithCode } from "@calcom/lib/errors"; | ||
| import ICAL from "ical.js"; | ||
| import type { Attendee, DateArray, DurationObject } from "ics"; | ||
| import { createEvent } from "ics"; | ||
| import type { DAVAccount, DAVCalendar, DAVObject } from "tsdav"; | ||
| import type { DAVAccount, DAVCalendar, DAVObject, DAVClient } from "tsdav"; | ||
| import { | ||
| createAccount, | ||
| createCalendarObject, | ||
| createDAVClient, | ||
| deleteCalendarObject, | ||
| fetchCalendarObjects, | ||
| fetchCalendars, | ||
| getBasicAuthHeaders, | ||
| updateCalendarObject, | ||
| DAVNamespace, | ||
| } from "tsdav"; | ||
| import { v4 as uuidv4 } from "uuid"; | ||
| import { getLocation, getRichDescription } from "./CalEventParser"; | ||
|
|
@@ -392,6 +396,7 @@ export default abstract class BaseCalendarService implements Calendar { | |
| protected integrationName = ""; | ||
| private log: typeof logger; | ||
| private credential: CredentialPayload; | ||
| private client?: DAVClient; | ||
|
|
||
| constructor(credential: CredentialPayload, integrationName: string, url?: string) { | ||
| this.integrationName = integrationName; | ||
|
|
@@ -463,23 +468,47 @@ export default abstract class BaseCalendarService implements Calendar { | |
| : undefined; | ||
|
|
||
| // We create the event directly on iCal | ||
| // 1. If a specific destination calendar is requested, use it. | ||
| // 2. Else prefer the calendar identified as CALDAV:schedule-default-calendar-URL (RFC 6638 Section 9.2). | ||
| // 3. Else fall back to the first available calendar and log for debuggability. | ||
| let targetCalendars: IntegrationCalendar[]; | ||
|
|
||
| if (mainHostDestinationCalendar?.externalId) { | ||
| targetCalendars = calendars.filter((c) => c.externalId === mainHostDestinationCalendar.externalId); | ||
| } else { | ||
| const defaultCalendar = calendars.find((c) => c.isDefault); | ||
| if (defaultCalendar) { | ||
| targetCalendars = [defaultCalendar]; | ||
| this.log.debug(`CalDAV: Using schedule-default-calendar-URL calendar: ${defaultCalendar.externalId}`); | ||
| } else { | ||
| // RFC does not guarantee PROPFIND response order — log the fallback choice | ||
| targetCalendars = calendars.slice(0, 1); | ||
| this.log.warn( | ||
| `CalDAV: No default calendar found via schedule-default-calendar-URL. ` + | ||
| `Falling back to first available calendar: ${targetCalendars[0]?.externalId ?? "none"}` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| if (targetCalendars.length === 0) { | ||
| // TODO: consider adding ErrorCode.NoTargetCalendarsFound for a more specific error | ||
| throw new ErrorWithCode( | ||
| ErrorCode.InternalServerError, | ||
| "No target calendars found to create CalDAV calendar entry" | ||
| ); | ||
| } | ||
|
|
||
| const responses = await Promise.all( | ||
| calendars | ||
| .filter((c) => | ||
| mainHostDestinationCalendar?.externalId | ||
| ? c.externalId === mainHostDestinationCalendar.externalId | ||
| : true | ||
| ) | ||
| .map((calendar) => | ||
| createCalendarObject({ | ||
| calendar: { | ||
| url: calendar.externalId, | ||
| }, | ||
| filename: `${uid}.ics`, | ||
| iCalString: injectScheduleAgent(iCalStringWithTimezone), | ||
| headers: this.headers, | ||
| }) | ||
| ) | ||
| targetCalendars.map((calendar) => | ||
| createCalendarObject({ | ||
| calendar: { | ||
| url: calendar.externalId, | ||
| }, | ||
| filename: `${uid}.ics`, | ||
| iCalString: injectScheduleAgent(iCalStringWithTimezone), | ||
| headers: this.headers, | ||
| }) | ||
| ) | ||
| ); | ||
|
|
||
| if (responses.some((r) => !r.ok)) { | ||
|
|
@@ -560,7 +589,7 @@ export default abstract class BaseCalendarService implements Calendar { | |
| if (response.status >= 200 && response.status < 300) { | ||
| return { | ||
| uid, | ||
| type: this.credentials.type, | ||
| type: this.credential.type, | ||
| id: typeof calendarEvent.uid === "string" ? calendarEvent.uid : "-1", | ||
| password: "", | ||
| url: calendarEvent.url, | ||
|
|
@@ -665,7 +694,7 @@ export default abstract class BaseCalendarService implements Calendar { | |
|
|
||
| const userId = this.getUserId(selectedCalendars); | ||
| // we use the userId from selectedCalendars to fetch the user's timeZone from the database primarily for all-day events without any timezone information | ||
| const userTimeZone = userId ? await this.getUserTimezoneFromDB(userId) : "Europe/London"; | ||
| const userTimeZone = userId ? (await this.getUserTimezoneFromDB(userId)) ?? "Europe/London" : "Europe/London"; | ||
| const events: { start: string; end: string }[] = []; | ||
| objects.forEach((object) => { | ||
| if (!object || object.data == null || JSON.stringify(object.data) == "{}") return; | ||
|
|
@@ -817,27 +846,38 @@ export default abstract class BaseCalendarService implements Calendar { | |
|
|
||
| async listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]> { | ||
| try { | ||
| const account = await this.getAccount(); | ||
| const client = await this.getClient(); | ||
|
|
||
| const calendars = (await fetchCalendars({ | ||
| account, | ||
| headers: this.headers, | ||
| })) /** @url https://github.com/natelindev/tsdav/pull/139 */ as (Omit<DAVCalendar, "displayName"> & { | ||
| const calendars = (await client.fetchCalendars()) /** @url https://github.com/natelindev/tsdav/pull/139 */ as (Omit< | ||
| DAVCalendar, | ||
| "displayName" | ||
| > & { | ||
| displayName?: string | Record<string, unknown>; | ||
| })[]; | ||
|
|
||
| // Attempt to resolve the scheduling default calendar URL from the principal. | ||
| // This implements RFC 6638 Section 9.2: default calendar should be identified | ||
| // via the CALDAV:schedule-default-calendar-URL property. | ||
| const defaultCalendarUrl = await this.resolveDefaultCalendarUrl(client); | ||
|
|
||
| return calendars.reduce<IntegrationCalendar[]>((newCalendars, calendar) => { | ||
| if (!calendar.components?.includes("VEVENT")) return newCalendars; | ||
| const [mainHostDestinationCalendar] = event?.destinationCalendar ?? []; | ||
|
|
||
| const isDefault = defaultCalendarUrl | ||
| ? calendar.url === defaultCalendarUrl | ||
| : !!(calendar as any).props?.["schedule-default-calendar-URL"]; // fallback: check on the calendar object itself | ||
|
|
||
| newCalendars.push({ | ||
| externalId: calendar.url, | ||
| /** @url https://github.com/calcom/cal.diy/issues/7186 */ | ||
| name: typeof calendar.displayName === "string" ? calendar.displayName : "", | ||
| isDefault, | ||
| primary: mainHostDestinationCalendar?.externalId | ||
| ? mainHostDestinationCalendar.externalId === calendar.url | ||
| : false, | ||
| integration: this.integrationName, | ||
| email: this.credentials.username ?? "", | ||
| email: this.credentials["username"] ?? "", | ||
| }); | ||
| return newCalendars; | ||
| }, []); | ||
|
|
@@ -1015,9 +1055,73 @@ export default abstract class BaseCalendarService implements Calendar { | |
| account: { | ||
| serverUrl: this.url, | ||
| accountType: DEFAULT_CALENDAR_TYPE, | ||
| credentials: this.credentials, | ||
| credentials: { | ||
| username: this.credentials["username"], | ||
| password: this.credentials["password"], | ||
| }, | ||
| }, | ||
| headers: this.headers, | ||
| }); | ||
| } | ||
|
|
||
| private async getClient(): Promise<DAVClient> { | ||
| if (this.client) return this.client; | ||
| this.client = await createDAVClient({ | ||
| serverUrl: this.url, | ||
| credentials: { | ||
| username: this.credentials["username"], | ||
| password: this.credentials["password"], | ||
| }, | ||
| authMethod: "Basic", | ||
| defaultAccountType: "caldav", | ||
| }); | ||
| return this.client; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the scheduling default calendar URL from the principal properties. | ||
| * RFC 6638 Section 9.2: Checking schedule-default-calendar-URL property on the Inbox. | ||
| */ | ||
| private async resolveDefaultCalendarUrl(client: DAVClient): Promise<string | undefined> { | ||
| try { | ||
| const principalUrl = await client.fetchPrincipalUrl(); | ||
|
|
||
| // Priority 1: CALDAV:schedule-default-calendar-URL (RFC 6638 Section 9.2) | ||
| const principalProps = await client.propfind({ | ||
| url: principalUrl, | ||
| props: [{ | ||
| name: "schedule-default-calendar-URL", | ||
| namespace: DAVNamespace.CALDAV, | ||
| }], | ||
| depth: "0", | ||
| }); | ||
|
|
||
| const defaultUrl = (principalProps?.[0] as any)?.props?.["schedule-default-calendar-URL"]?.href; | ||
| if (defaultUrl) { | ||
| this.log.debug(`CalDAV: Found default calendar via schedule-default-calendar-URL: ${defaultUrl}`); | ||
| return defaultUrl; | ||
| } | ||
|
|
||
| // Priority 2: CALDAV:calendar-user-address-set (Fallback) | ||
| const addressSetProps = await client.propfind({ | ||
| url: principalUrl, | ||
| props: [{ | ||
| name: "calendar-user-address-set", | ||
| namespace: DAVNamespace.CALDAV, | ||
| }], | ||
| depth: "0", | ||
| }); | ||
|
|
||
| const fallbackUrl = (addressSetProps?.[0] as any)?.props?.["calendar-user-address-set"]?.href; | ||
| if (fallbackUrl) { | ||
| this.log.debug(`CalDAV: Found default calendar via calendar-user-address-set: ${fallbackUrl}`); | ||
| return fallbackUrl; | ||
| } | ||
|
|
||
| return undefined; | ||
| } catch (e) { | ||
| this.log.warn("CalDAV: Could not resolve default calendar URL from inbox/principal", e); | ||
| return undefined; | ||
| } | ||
|
Comment on lines
+1105
to
+1125
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: According to RFC 6638, Section 2.4.1, the CALDAV:calendar-user-address-set property identifies the calendar addresses of the associated principal resource. It is a multivalued property defined as , containing one or more DAV:href elements. These href values are URIs that represent the calendar addresses, such as mailto: URIs (e.g., mailto:[email protected]). The RFC example uses mailto: URIs exclusively. While DAV:href allows any URI (including http/https URLs for calendar collections or principal URIs), the semantic and normative example specify mailto: URIs as the primary type for calendar user addresses, consistent with iCalendar CAL-ADDRESS (RFC 5545), which requires mailto: for email transports. The description notes that if no well-defined identifier exists, the principal resource URI can be used as a fallback, but mailto: URIs are the standard value type shown. Citations:
🏁 Script executed: # First, locate and examine the CalendarService.ts file
fd -t f -e ts CalendarService
# Get the file size to determine best approach
wc -l packages/lib/CalendarService.ts 2>/dev/null || trueRepository: calcom/cal.diy Length of output: 1180 🏁 Script executed: # Read the specific lines mentioned in the review
sed -n '1105,1125p' packages/lib/CalendarService.tsRepository: calcom/cal.diy Length of output: 785 🏁 Script executed: # Check for `as any` casts in the file
rg 'as any' packages/lib/CalendarService.ts -n
# Also check the broader context of resolveDefaultCalendarUrl to understand impact
rg -A 5 -B 5 'resolveDefaultCalendarUrl' packages/lib/CalendarService.ts | head -100Repository: calcom/cal.diy Length of output: 1553 🏁 Script executed: # Check the context around line 869 to see how defaultCalendarUrl is used
sed -n '860,875p' packages/lib/CalendarService.tsRepository: calcom/cal.diy Length of output: 896 Remove the Per RFC 6638 §2.4.1, Additionally: The Suggested fix: Remove the entire Priority 2 fallback block (lines 1105–1119) and let line 869's secondary fallback handle the case when no schedule-default-calendar-URL is found. 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,119 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import BaseCalendarService from "../CalendarService"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ErrorCode } from "@calcom/lib/errorCodes"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ErrorWithCode } from "@calcom/lib/errors"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mock the dependencies | ||||||||||||||||||||||||||||||||||||||||||||||||||
| vi.mock("../crypto", () => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| symmetricDecrypt: vi.fn().mockReturnValue(JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username: "user", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| password: "pass", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: "https://caldav.example.com" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| vi.mock("./logger", () => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| default: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| getSubLogger: vi.fn().mockReturnThis(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| debug: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| warn: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| error: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In Vitest, a relative path passed to vi.mock is resolved relative to the test file (the file containing the vi.mock call), not relative to the file under test. Citations:
🏁 Script executed: head -120 packages/lib/__tests__/CalendarService.caldav-defaults.test.ts | tail -25Repository: calcom/cal.diy Length of output: 988 🏁 Script executed: # Find and examine the CalendarService.ts file to see how it handles the event object
find . -name "CalendarService.ts" -type f | head -5Repository: calcom/cal.diy Length of output: 358 🏁 Script executed: # Check the test file to see which CalendarService it imports
grep -n "import.*CalendarService\|from.*CalendarService" packages/lib/__tests__/CalendarService.caldav-defaults.test.tsRepository: calcom/cal.diy Length of output: 113 🏁 Script executed: # Examine the CalendarService.ts file in packages/lib/ to see how createEvent handles the event object
cat -n packages/lib/CalendarService.ts | head -500Repository: calcom/cal.diy Length of output: 21542 🏁 Script executed: # Check other tests or usages of ics.createEvent to see if title is required
rg "createEvent" packages/lib/__tests__/ -A 5 -B 2Repository: calcom/cal.diy Length of output: 704 🏁 Script executed: # Search for other usages of ics.createEvent to see the typical structure
rg "createEvent\(" packages/lib/CalendarService.ts -A 15 | head -40Repository: calcom/cal.diy Length of output: 1780
🛠 Fix-vi.mock("./logger", () => ({
+vi.mock("../logger", () => ({
default: {
getSubLogger: vi.fn().mockReturnThis(),
debug: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));Event object missing The event object (lines 108-117) lacks a Add const event: any = {
startTime: "2024-01-01T10:00:00Z",
endTime: "2024-01-01T11:00:00Z",
organizer: { timeZone: "UTC", email: "[email protected]" },
attendees: [],
+ title: "Test Event",
};🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| vi.mock("tsdav", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const actual = await vi.importActual("tsdav"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ...actual, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| createDAVClient: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchCalendarObjects: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| createCalendarObject: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Concrete class for testing the abstract BaseCalendarService | ||||||||||||||||||||||||||||||||||||||||||||||||||
| class TestCalendarService extends BaseCalendarService { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(credential: any) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| super(credential, "test-caldav"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| getAccountEmail() { return "[email protected]"; } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("BaseCalendarService - CalDAV Default Calendar Selection", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockCredential = { key: "mock-key", user: { email: "[email protected]" } }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let service: TestCalendarService; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| vi.clearAllMocks(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| service = new TestCalendarService(mockCredential); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it("should resolve default calendar using CALDAV:schedule-default-calendar-URL", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { createDAVClient } = await import("tsdav"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockClient = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchCalendars: vi.fn().mockResolvedValue([ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { url: "https://caldav.example.com/cal1/", components: ["VEVENT"], displayName: "Calendar 1" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { url: "https://caldav.example.com/default/", components: ["VEVENT"], displayName: "Default Calendar" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchPrincipalUrl: vi.fn().mockResolvedValue("https://caldav.example.com/principal/"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| propfind: vi.fn().mockResolvedValue([ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| props: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "schedule-default-calendar-URL": { href: "https://caldav.example.com/default/" } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (createDAVClient as any).mockResolvedValue(mockClient); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const calendars = await service.listCalendars(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(calendars).toHaveLength(2); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(calendars.find(c => c.externalId.includes("default"))?.isDefault).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(calendars.find(c => c.externalId.includes("cal1"))?.isDefault).toBe(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it("should fall back to calendar-user-address-set if schedule-default-calendar-URL is missing", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { createDAVClient } = await import("tsdav"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockClient = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchCalendars: vi.fn().mockResolvedValue([ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { url: "https://caldav.example.com/fallback/", components: ["VEVENT"] }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchPrincipalUrl: vi.fn().mockResolvedValue("https://caldav.example.com/principal/"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| propfind: vi.fn() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .mockResolvedValueOnce([]) // First call for schedule-default-calendar-URL returns nothing | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .mockResolvedValueOnce([ // Second call for fallback | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| props: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "calendar-user-address-set": { href: "https://caldav.example.com/fallback/" } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (createDAVClient as any).mockResolvedValue(mockClient); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const calendars = await service.listCalendars(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(calendars[0].isDefault).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it("should throw ErrorWithCode.InternalServerError when no target calendars are found", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { createDAVClient } = await import("tsdav"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockClient = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchCalendars: vi.fn().mockResolvedValue([]), // No calendars returned | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchPrincipalUrl: vi.fn().mockResolvedValue("url"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| propfind: vi.fn().mockResolvedValue([]), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (createDAVClient as any).mockResolvedValue(mockClient); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const event: any = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| startTime: "2024-01-01T10:00:00Z", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| endTime: "2024-01-01T11:00:00Z", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| organizer: { timeZone: "UTC", email: "[email protected]" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attendees: [], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| await expect(service.createEvent(event, 1)).rejects.toThrow( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| new ErrorWithCode(ErrorCode.InternalServerError, "No target calendars found to create CalDAV calendar entry") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+108
to
+118
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test will not exercise the "no target calendars" path — it will throw earlier. The 🛠 Suggested fix: give the event the minimum fields required to pass ics validation, so we actually reach the guard const event: any = {
+ type: "test-caldav",
+ title: "Test Event",
startTime: "2024-01-01T10:00:00Z",
endTime: "2024-01-01T11:00:00Z",
organizer: { timeZone: "UTC", email: "[email protected]" },
attendees: [],
};Also consider asserting on the error instance directly to avoid accidental message-substring matches: await expect(service.createEvent(event, 1)).rejects.toBeInstanceOf(ErrorWithCode);
await expect(service.createEvent(event, 1)).rejects.toMatchObject({
code: ErrorCode.InternalServerError,
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: calcom/cal.diy
Length of output: 40
🏁 Script executed:
Repository: calcom/cal.diy
Length of output: 2569
🏁 Script executed:
Repository: calcom/cal.diy
Length of output: 1538
🏁 Script executed:
Repository: calcom/cal.diy
Length of output: 40
🏁 Script executed:
Repository: calcom/cal.diy
Length of output: 160
🏁 Script executed:
Repository: calcom/cal.diy
Length of output: 40
🏁 Script executed:
# Check imports and type definitions at top of CalendarService.ts head -100 packages/lib/CalendarService.tsRepository: calcom/cal.diy
Length of output: 3442
🏁 Script executed:
Repository: calcom/cal.diy
Length of output: 414
🏁 Script executed:
Repository: calcom/cal.diy
Length of output: 40
Remove
as anycasts — define a type-safe shape for propfind responses.The code uses
as anyat lines 869, 1099, and 1115 to accesspropson tsdav responses, directly violating the guideline "Never useas any- use proper type-safe solutions instead."Define a local type to capture the expected shape of the propfind response instead:
🤖 Prompt for AI Agents