Files
calendar/packages/lib/CalendarService.test.ts
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

751 lines
26 KiB
TypeScript

import { createEvent as createIcsEvent } from "ics";
import { createCalendarObject, updateCalendarObject } from "tsdav";
import { beforeEach, describe, expect, it, vi } from "vitest";
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> = {}): 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 - UID Consistency", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should use event.uid when provided, not generate a new UUID", async () => {
const service = new TestCalendarService();
const bookingUid = "booking-uid-from-database-abc123";
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:${bookingUid}\r\nDTSTART:20230615T150000Z\r\nDTEND:20230615T160000Z\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({ uid: bookingUid });
const result = await service.createEvent(event, 1);
expect(result.uid).toBe(bookingUid);
expect(result.id).toBe(bookingUid);
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
expect(calledArg.filename).toBe(`${bookingUid}.ics`);
const icsCallArg = vi.mocked(createIcsEvent).mock.calls[0][0];
expect(icsCallArg.uid).toBe(bookingUid);
});
it("should generate a new UUID when event.uid is not provided", async () => {
const service = new TestCalendarService();
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:generated-uuid\r\nDTSTART:20230615T150000Z\r\nDTEND:20230615T160000Z\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({ uid: undefined });
const result = await service.createEvent(event, 1);
expect(result.uid).toBeTruthy();
expect(typeof result.uid).toBe("string");
expect(result.uid.length).toBeGreaterThan(0);
});
it("should use the same uid for CalDAV filename and ics UID property", async () => {
const service = new TestCalendarService();
const bookingUid = "consistent-uid-xyz789";
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:${bookingUid}\r\nDTSTART:20230615T150000Z\r\nDTEND:20230615T160000Z\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({ uid: bookingUid });
await service.createEvent(event, 1);
const icsCallArg = vi.mocked(createIcsEvent).mock.calls[0][0];
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
expect(icsCallArg.uid).toBe(bookingUid);
expect(calledArg.filename).toBe(`${bookingUid}.ics`);
expect(icsCallArg.uid).toBe(calledArg.filename?.replace(".ics", ""));
});
});
describe("CalendarService - VTIMEZONE Generation", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should include VTIMEZONE block in created CalDAV event", async () => {
const service = new TestCalendarService();
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-uid\r\nDTSTART:20230615T150000Z\r\nDURATION:PT1H\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({
startTime: "2023-06-15T15:00:00Z",
endTime: "2023-06-15T16:00:00Z",
organizer: {
name: "Test",
email: "test@example.com",
timeZone: "America/Chicago",
language: { translate: ((key: string) => key) as never, locale: "en" },
},
});
await service.createEvent(event, 1);
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
const iCalString = calledArg.iCalString;
expect(iCalString).toContain("BEGIN:VTIMEZONE");
expect(iCalString).toContain("END:VTIMEZONE");
expect(iCalString).toContain("TZID:America/Chicago");
});
it("should use TZID in DTSTART instead of UTC Z suffix", async () => {
const service = new TestCalendarService();
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-uid\r\nDTSTART:20230615T150000Z\r\nDURATION:PT1H\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({
startTime: "2023-06-15T15:00:00Z",
endTime: "2023-06-15T16:00:00Z",
organizer: {
name: "Test",
email: "test@example.com",
timeZone: "America/Chicago",
language: { translate: ((key: string) => key) as never, locale: "en" },
},
});
await service.createEvent(event, 1);
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
const iCalString = calledArg.iCalString;
expect(iCalString).toContain("DTSTART;TZID=America/Chicago:");
const unfolded = iCalString.replace(/\r?\n[ \t]/g, "");
expect(unfolded).not.toMatch(/^DTSTART:[0-9]{8}T[0-9]{6}Z/m);
});
it("should convert UTC time to local time correctly for America/Chicago", async () => {
const service = new TestCalendarService();
// 2023-01-15T15:00:00Z = 2023-01-15T09:00:00 in America/Chicago (UTC-6 in January)
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-uid\r\nDTSTART:20230115T150000Z\r\nDURATION:PT1H\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({
startTime: "2023-01-15T15:00:00Z",
endTime: "2023-01-15T16:00:00Z",
organizer: {
name: "Test",
email: "test@example.com",
timeZone: "America/Chicago",
language: { translate: ((key: string) => key) as never, locale: "en" },
},
});
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("DTSTART;TZID=America/Chicago:20230115T090000");
});
it("should place VTIMEZONE block before BEGIN:VEVENT", async () => {
const service = new TestCalendarService();
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-uid\r\nDTSTART:20230615T150000Z\r\nDURATION:PT1H\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({
startTime: "2023-06-15T15:00:00Z",
endTime: "2023-06-15T16:00:00Z",
organizer: {
name: "Test",
email: "test@example.com",
timeZone: "America/Chicago",
language: { translate: ((key: string) => key) as never, locale: "en" },
},
});
await service.createEvent(event, 1);
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
const iCalString = calledArg.iCalString;
const vtimezoneIdx = iCalString.indexOf("BEGIN:VTIMEZONE");
const veventIdx = iCalString.indexOf("BEGIN:VEVENT");
expect(vtimezoneIdx).toBeGreaterThan(-1);
expect(veventIdx).toBeGreaterThan(-1);
expect(vtimezoneIdx).toBeLessThan(veventIdx);
});
it("should produce valid 8-digit DTSTART dates in VTIMEZONE blocks", async () => {
const service = new TestCalendarService();
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-uid\r\nDTSTART:20230615T120000Z\r\nDURATION:PT1H\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({
startTime: "2023-06-15T12:00:00Z",
endTime: "2023-06-15T13:00:00Z",
organizer: {
name: "Test",
email: "test@example.com",
timeZone: "America/New_York",
language: { translate: ((key: string) => key) as never, locale: "en" },
},
});
await service.createEvent(event, 1);
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
const iCalString = calledArg.iCalString;
const vtimezoneBlock = iCalString.slice(
iCalString.indexOf("BEGIN:VTIMEZONE"),
iCalString.indexOf("END:VTIMEZONE") + 13
);
const dtStartMatches = vtimezoneBlock.match(/DTSTART:(\d+)T/g);
expect(dtStartMatches).not.toBeNull();
for (const match of dtStartMatches ?? []) {
const dateStr = match.replace("DTSTART:", "").replace("T", "");
expect(dateStr).toHaveLength(8); // YYYYMMDD = 8 chars
}
});
it("should use pre-transition local time for VTIMEZONE DTSTART (RFC 5545)", async () => {
const service = new TestCalendarService();
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-uid\r\nDTSTART:20230615T120000Z\r\nDURATION:PT1H\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({
startTime: "2023-06-15T12:00:00Z",
endTime: "2023-06-15T13:00:00Z",
organizer: {
name: "Test",
email: "test@example.com",
timeZone: "America/New_York",
language: { translate: ((key: string) => key) as never, locale: "en" },
},
});
await service.createEvent(event, 1);
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
const iCalString = calledArg.iCalString;
const vtimezoneBlock = iCalString.slice(
iCalString.indexOf("BEGIN:VTIMEZONE"),
iCalString.indexOf("END:VTIMEZONE") + 13
);
// America/New_York 2023: spring forward March 12 at 2:00 AM EST,
// fall back November 5 at 2:00 AM EDT.
// RFC 5545 requires DTSTART to use pre-transition local time (02:00),
// not the post-transition time (03:00 for spring forward, 01:00 for fall back).
const daylightBlock = vtimezoneBlock.slice(
vtimezoneBlock.indexOf("BEGIN:DAYLIGHT"),
vtimezoneBlock.indexOf("END:DAYLIGHT")
);
expect(daylightBlock).toContain("DTSTART:20230312T020000");
const standardBlock = vtimezoneBlock.slice(
vtimezoneBlock.indexOf("BEGIN:STANDARD"),
vtimezoneBlock.indexOf("END:STANDARD")
);
expect(standardBlock).toContain("DTSTART:20231105T020000");
});
it("should use correct DST rules for Southern Hemisphere (Australia/Sydney)", async () => {
const service = new TestCalendarService();
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-uid\r\nDTSTART:20230115T020000Z\r\nDURATION:PT1H\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
const event = createMockEvent({
startTime: "2023-01-15T02:00:00Z",
endTime: "2023-01-15T03:00:00Z",
organizer: {
name: "Test",
email: "test@example.com",
timeZone: "Australia/Sydney",
language: { translate: ((key: string) => key) as never, locale: "en" },
},
});
await service.createEvent(event, 1);
const calledArg = vi.mocked(createCalendarObject).mock.calls[0][0];
const iCalString = calledArg.iCalString;
expect(iCalString).toContain("BEGIN:VTIMEZONE");
expect(iCalString).toContain("TZID:Australia/Sydney");
const vtimezoneBlock = iCalString.slice(
iCalString.indexOf("BEGIN:VTIMEZONE"),
iCalString.indexOf("END:VTIMEZONE") + 13
);
expect(vtimezoneBlock).toContain("BEGIN:DAYLIGHT");
expect(vtimezoneBlock).toContain("BEGIN:STANDARD");
});
it("should apply timezone fix to updateEvent as well", async () => {
const service = new TestCalendarService();
const mockIcsOutput = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:test-uid\r\nDTSTART:20230615T150000Z\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nDURATION:PT1H\r\nEND:VEVENT\r\nEND:VCALENDAR`;
vi.mocked(createIcsEvent).mockReturnValue({
error: null as unknown as Error,
value: mockIcsOutput,
});
(service as unknown as Record<string, unknown>).getEventsByUID = vi.fn().mockResolvedValue([
{
uid: "test-uid",
url: "https://caldav.example.com/calendar/test.ics",
etag: '"etag123"',
},
]);
const event = createMockEvent({
uid: "test-uid",
startTime: "2023-06-15T15:00:00Z",
endTime: "2023-06-15T16:00:00Z",
organizer: {
name: "Test",
email: "test@example.com",
timeZone: "America/Chicago",
language: { translate: ((key: string) => key) as never, locale: "en" },
},
});
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;
expect(data).toContain("BEGIN:VTIMEZONE");
expect(data).toContain("TZID:America/Chicago");
expect(data).toContain("DTSTART;TZID=America/Chicago:");
});
});
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<string, unknown>).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.diy//NONSGML//EN\r\nBEGIN:VEVENT\r\nATTENDEE;CN=Guest:mailto:guest@example.com\r\nDTSTART:20230101T100000Z\r\nDTEND:20230101T110000Z\r\nEND:VEVENT\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.diy//NONSGML//EN");
});
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();
});
});
});