import { describe, it, expect, vi, beforeEach } from "vitest"; import { createEvent as createIcsEvent } from "ics"; import { createCalendarObject, updateCalendarObject } from "tsdav"; vi.mock("ics", () => ({ createEvent: vi.fn(), })); vi.mock("tsdav", () => ({ createAccount: vi.fn(), fetchCalendars: vi.fn(), fetchCalendarObjects: vi.fn(), createCalendarObject: vi.fn().mockResolvedValue({ ok: true }), updateCalendarObject: vi.fn().mockResolvedValue({ status: 200 }), deleteCalendarObject: vi.fn(), getBasicAuthHeaders: vi.fn().mockReturnValue({}), })); vi.mock("@calcom/lib/logger", () => ({ default: { getSubLogger: () => ({ debug: vi.fn(), error: vi.fn(), warn: vi.fn(), }), }, })); vi.mock("@calcom/lib/crypto", () => ({ symmetricDecrypt: vi.fn().mockImplementation((text) => { if (typeof text === "object") { return JSON.stringify(text); } return text; }), })); vi.mock("./CalEventParser", () => ({ getLocation: vi.fn().mockReturnValue("Test Location"), getRichDescription: vi.fn().mockReturnValue("Test Description"), })); import type { CalendarServiceEvent } from "@calcom/types/Calendar"; import BaseCalendarService from "./CalendarService"; const createMockEvent = (overrides: Partial = {}): CalendarServiceEvent => ({ type: "caldav", title: "Test Event", startTime: "2023-01-01T10:00:00Z", endTime: "2023-01-01T11:00:00Z", organizer: { name: "Test", email: "test@example.com", timeZone: "UTC", language: { translate: ((key: string) => key) as never, locale: "en" }, }, attendees: [], calendarDescription: "Test Description", ...overrides, }); class TestCalendarService extends BaseCalendarService { constructor() { super( { id: 1, type: "caldav_calendar", delegationCredentialId: null, user: { email: "test@example.com" }, userId: 1, teamId: null, appId: "caldav", invalid: false, key: { username: "test", password: "test", url: "https://caldav.example.com", }, }, "caldav", "https://caldav.example.com" ); } async listCalendars() { return [ { externalId: "https://caldav.example.com/calendar/", name: "Test Calendar", primary: true, readOnly: false, email: "test@example.com", integrationName: "caldav", credentialId: 1, }, ]; } async testUpdateEvent(uid: string, event: CalendarServiceEvent, externalCalendarId: string) { return this.updateEvent(uid, event); } } describe("CalendarService - SCHEDULE-AGENT injection", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("injectScheduleAgent", () => { it("should inject SCHEDULE-AGENT=CLIENT into ATTENDEE lines", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nMETHOD:PUBLISH\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const unfolded = iCalString.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain("ATTENDEE;CN=Guest;SCHEDULE-AGENT=CLIENT:mailto:guest@example.com"); expect(unfolded).not.toContain("METHOD:PUBLISH"); }); it("should inject SCHEDULE-AGENT=CLIENT into ORGANIZER lines", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nORGANIZER;CN=Test:mailto:test@example.com\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const unfolded = iCalString.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain("ORGANIZER;CN=Test;SCHEDULE-AGENT=CLIENT:mailto:test@example.com"); expect(unfolded).toContain("ATTENDEE;CN=Guest;SCHEDULE-AGENT=CLIENT:mailto:guest@example.com"); }); it("should not duplicate SCHEDULE-AGENT if already present", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;SCHEDULE-AGENT=SERVER;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const scheduleAgentCount = (iCalString.match(/SCHEDULE-AGENT/g) || []).length; expect(scheduleAgentCount).toBe(1); }); it("should handle quoted colons in CN parameter", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN="John: CEO":mailto:john@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const unfolded = iCalString.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain('ATTENDEE;CN="John: CEO";SCHEDULE-AGENT=CLIENT:mailto:john@example.com'); }); it("should remove METHOD:PUBLISH per RFC 4791", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nMETHOD:PUBLISH\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; expect(iCalString).not.toContain("METHOD:PUBLISH"); }); it("should handle case-insensitive ATTENDEE matching", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nattendee;cn=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const unfolded = iCalString.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT"); }); }); describe("RFC 5545 Line Folding", () => { it("should fold long lines at 75 octets", async () => { const service = new TestCalendarService(); const longName = "A".repeat(60); const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN=${longName}:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const lines = iCalString.split("\r\n"); lines.forEach((line) => { const byteLength = new TextEncoder().encode(line).length; expect(byteLength).toBeLessThanOrEqual(75); }); }); it("should handle UTF-8 multi-byte characters correctly", async () => { const service = new TestCalendarService(); const unicodeName = "\u4E2D".repeat(30); const mockIcsOutput = `BEGIN:VCALENDAR\r\nORGANIZER;CN=${unicodeName}:mailto:test@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const lines = iCalString.split("\r\n"); lines.forEach((line) => { const byteLength = new TextEncoder().encode(line).length; expect(byteLength).toBeLessThanOrEqual(75); }); const unfolded = iCalString.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT"); }); it("should handle folded input lines correctly", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN=Very Long Name That Will Be\r\n Folded:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const unfolded = iCalString.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT"); }); }); describe("updateEvent", () => { it("should inject SCHEDULE-AGENT=CLIENT on update", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); (service as unknown as Record).getEventsByUID = vi.fn().mockResolvedValue([ { uid: "test-uid", url: "https://caldav.example.com/calendar/test.ics", etag: '"etag123"', }, ]); const event = createMockEvent({ uid: "test-uid" }); await service.testUpdateEvent("test-uid", event, "https://caldav.example.com/calendar/"); const calledArg = vi.mocked(updateCalendarObject).mock.calls[0][0]; const data = calledArg.calendarObject.data; const unfolded = data.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT"); }); }); describe("Edge cases", () => { it("should handle ATTENDEE with multiple parameters", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Guest:mailto:guest@example.com\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const unfolded = iCalString.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain("PARTSTAT=NEEDS-ACTION"); expect(unfolded).toContain("RSVP=TRUE"); expect(unfolded).toContain("SCHEDULE-AGENT=CLIENT"); }); it("should handle ATTENDEE with http URI", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nATTENDEE;CN=Guest:http://example.com/user\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; const unfolded = iCalString.replace(/\r?\n[ \t]/g, ""); expect(unfolded).toContain("ATTENDEE;CN=Guest;SCHEDULE-AGENT=CLIENT:http://example.com/user"); }); it("should preserve other iCal properties unchanged", async () => { const service = new TestCalendarService(); const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Cal.com//NONSGML//EN\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nDTSTART:20230101T100000Z\r\nDTEND:20230101T110000Z\r\nEND:VCALENDAR`; vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: mockIcsOutput, }); const event = createMockEvent(); await service.createEvent(event, 1); const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0]; const iCalString = calledArg.iCalString; expect(iCalString).toContain("VERSION:2.0"); expect(iCalString).toContain("PRODID:-//Cal.com//NONSGML//EN"); expect(iCalString).toContain("DTSTART:20230101T100000Z"); expect(iCalString).toContain("DTEND:20230101T110000Z"); }); it("should handle empty iCalString gracefully", async () => { const service = new TestCalendarService(); vi.mocked(createIcsEvent).mockReturnValue({ error: null as unknown as Error, value: "", }); const event = createMockEvent(); await expect(service.createEvent(event, 1)).rejects.toThrow(); }); }); });