* 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>
633 lines
19 KiB
TypeScript
633 lines
19 KiB
TypeScript
import { prisma } from "@calcom/prisma/__mocks__/prisma";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@calcom/features/watchlist/lib/utils/normalization", () => ({
|
|
normalizeEmail: vi.fn(),
|
|
extractDomainFromEmail: vi.fn(),
|
|
normalizeDomain: vi.fn(),
|
|
}));
|
|
vi.mock("@calcom/features/watchlist/lib/service/GlobalBlockingService", () => ({
|
|
GlobalBlockingService: vi.fn(),
|
|
}));
|
|
vi.mock("@calcom/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain", () => ({
|
|
checkIfFreeEmailDomain: vi.fn(),
|
|
}));
|
|
vi.mock("@calcom/features/watchlist/operations/check-if-users-are-blocked.controller", () => ({
|
|
checkIfUsersAreBlocked: vi.fn(),
|
|
}));
|
|
vi.mock("@calcom/features/watchlist/lib/telemetry", () => ({
|
|
sentrySpan: vi.fn(),
|
|
}));
|
|
vi.mock("@calcom/lib/i18n", () => ({
|
|
locales: ["en"],
|
|
localeOptions: [{ value: "en", label: "English" }],
|
|
defaultLocaleOption: { value: "en", label: "English" },
|
|
}));
|
|
|
|
import process from "node:process";
|
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
|
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
|
import EventManager from "./EventManager";
|
|
|
|
vi.mock("@calcom/prisma", () => ({
|
|
prisma,
|
|
}));
|
|
|
|
vi.mock("@calcom/lib/crypto", () => ({
|
|
symmetricDecrypt: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@calcom/features/credentials/repositories/CredentialRepository", () => ({
|
|
CredentialRepository: {
|
|
findCredentialForCalendarServiceById: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("@calcom/app-store/delegationCredential", () => ({
|
|
enrichHostsWithDelegationCredentials: vi.fn(),
|
|
getUsersCredentialsIncludeServiceAccountKey: vi.fn(),
|
|
getCredentialForSelectedCalendar: vi.fn(),
|
|
}));
|
|
|
|
const mockedSymmetricDecrypt = vi.mocked(symmetricDecrypt);
|
|
const mockedCredentialRepository = vi.mocked(CredentialRepository);
|
|
|
|
function buildCalDAVCredential(data: {
|
|
id: number;
|
|
key: string;
|
|
userId?: number;
|
|
}): CredentialForCalendarService {
|
|
return {
|
|
id: data.id,
|
|
type: "caldav_calendar",
|
|
key: data.key,
|
|
userId: data.userId || 1,
|
|
user: { email: "test@example.com" },
|
|
teamId: null,
|
|
appId: "caldav-calendar",
|
|
invalid: false,
|
|
delegatedTo: null,
|
|
delegationCredentialId: null,
|
|
};
|
|
}
|
|
|
|
function buildDestinationCalendar(data: {
|
|
id: number;
|
|
integration: string;
|
|
externalId: string | null;
|
|
}): DestinationCalendar {
|
|
return {
|
|
id: data.id,
|
|
integration: data.integration,
|
|
externalId: data.externalId || "",
|
|
userId: 1,
|
|
eventTypeId: null,
|
|
credentialId: null,
|
|
primaryEmail: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
delegationCredentialId: null,
|
|
};
|
|
}
|
|
|
|
function buildCalendarCredential(data: {
|
|
id: number;
|
|
type?: string;
|
|
userId?: number;
|
|
delegatedToId?: string | null;
|
|
}): CredentialForCalendarService {
|
|
return {
|
|
id: data.id,
|
|
type: data.type || "google_calendar",
|
|
key: {},
|
|
userId: data.userId || 1,
|
|
user: { email: "test@example.com" },
|
|
teamId: null,
|
|
appId: "google-calendar",
|
|
invalid: false,
|
|
delegatedTo: null,
|
|
delegationCredentialId: null,
|
|
delegatedToId: data.delegatedToId || null,
|
|
};
|
|
}
|
|
|
|
function buildVideoCredential(data: {
|
|
id: number;
|
|
type?: string;
|
|
userId?: number;
|
|
}): CredentialForCalendarService {
|
|
return {
|
|
id: data.id,
|
|
type: data.type || "zoom_video",
|
|
key: {},
|
|
userId: data.userId || 1,
|
|
user: { email: "test@example.com" },
|
|
teamId: null,
|
|
appId: "zoom",
|
|
invalid: false,
|
|
delegatedTo: null,
|
|
delegationCredentialId: null,
|
|
};
|
|
}
|
|
|
|
describe("EventManager CalDAV credential validation", () => {
|
|
let eventManager: EventManager;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
eventManager = new EventManager({
|
|
credentials: [],
|
|
destinationCalendar: null,
|
|
});
|
|
});
|
|
|
|
describe("extractServerUrlFromCredential", () => {
|
|
it("should extract server URL from valid CalDAV credential", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "https://caldav.example.com/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).extractServerUrlFromCredential(credential);
|
|
expect(result).toBe("https://caldav.example.com");
|
|
expect(mockedSymmetricDecrypt).toHaveBeenCalledWith(
|
|
"encrypted_key",
|
|
process.env.CALENDSO_ENCRYPTION_KEY || ""
|
|
);
|
|
});
|
|
|
|
it("should return null for non-CalDAV credential", () => {
|
|
const credential = {
|
|
...buildCalDAVCredential({ id: 1, key: "encrypted_key" }),
|
|
type: "google_calendar",
|
|
} as CredentialForCalendarService;
|
|
|
|
const result = (eventManager as any).extractServerUrlFromCredential(credential);
|
|
expect(result).toBeNull();
|
|
expect(mockedSymmetricDecrypt).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return null when decryption fails", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "invalid_key",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockImplementation(() => {
|
|
throw new Error("Decryption failed");
|
|
});
|
|
|
|
const result = (eventManager as any).extractServerUrlFromCredential(credential);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should return null when decrypted data has no URL", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).extractServerUrlFromCredential(credential);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should return null when URL is invalid", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "invalid-url",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).extractServerUrlFromCredential(credential);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should handle URLs with different ports", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "https://caldav.example.com:8443/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).extractServerUrlFromCredential(credential);
|
|
expect(result).toBe("https://caldav.example.com:8443");
|
|
});
|
|
|
|
it("should handle HTTP URLs", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "http://caldav.internal.com/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).extractServerUrlFromCredential(credential);
|
|
expect(result).toBe("http://caldav.internal.com");
|
|
});
|
|
});
|
|
|
|
describe("extractServerUrlFromDestination", () => {
|
|
it("should extract server URL from CalDAV destination calendar", () => {
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "https://caldav.example.com/dav/calendars/user/calendar1/",
|
|
});
|
|
|
|
const result = (eventManager as any).extractServerUrlFromDestination(destination);
|
|
expect(result).toBe("https://caldav.example.com");
|
|
});
|
|
|
|
it("should return null for non-CalDAV destination", () => {
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "google_calendar",
|
|
externalId: "calendar@gmail.com",
|
|
});
|
|
|
|
const result = (eventManager as any).extractServerUrlFromDestination(destination);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should return null when externalId is null", () => {
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: null,
|
|
});
|
|
|
|
const result = (eventManager as any).extractServerUrlFromDestination(destination);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should return null when externalId is invalid URL", () => {
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "invalid-url",
|
|
});
|
|
|
|
const result = (eventManager as any).extractServerUrlFromDestination(destination);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should handle URLs with different ports", () => {
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "https://caldav.example.com:8443/dav/calendars/user/calendar1/",
|
|
});
|
|
|
|
const result = (eventManager as any).extractServerUrlFromDestination(destination);
|
|
expect(result).toBe("https://caldav.example.com:8443");
|
|
});
|
|
|
|
it("should handle HTTP URLs", () => {
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "http://caldav.internal.com/dav/calendars/user/calendar1/",
|
|
});
|
|
|
|
const result = (eventManager as any).extractServerUrlFromDestination(destination);
|
|
expect(result).toBe("http://caldav.internal.com");
|
|
});
|
|
});
|
|
|
|
describe("credentialMatchesDestination", () => {
|
|
it("should return true for matching CalDAV server URLs", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "https://caldav.example.com/dav/calendars/user/calendar1/",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "https://caldav.example.com/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("should return false for non-matching CalDAV server URLs", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "https://different.example.com/dav/calendars/user/calendar1/",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "https://caldav.example.com/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should return true for non-CalDAV credentials", () => {
|
|
const credential = {
|
|
...buildCalDAVCredential({ id: 1, key: "encrypted_key" }),
|
|
type: "google_calendar",
|
|
} as CredentialForCalendarService;
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "google_calendar",
|
|
externalId: "calendar@gmail.com",
|
|
});
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(true);
|
|
expect(symmetricDecrypt).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return true when credential is CalDAV but destination is not", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "google_calendar",
|
|
externalId: "calendar@gmail.com",
|
|
});
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(true);
|
|
expect(symmetricDecrypt).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return false when credential URL extraction fails", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "invalid_key",
|
|
});
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "https://caldav.example.com/dav/calendars/user/calendar1/",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockImplementation(() => {
|
|
throw new Error("Decryption failed");
|
|
});
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should return false when destination URL extraction fails", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "invalid-url",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "https://caldav.example.com/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should handle URLs with different paths but same server", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "https://caldav.example.com/different/path/calendar1/",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "https://caldav.example.com/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("should handle URLs with different ports as non-matching", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "https://caldav.example.com:8443/dav/calendars/user/calendar1/",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "https://caldav.example.com:9443/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should handle HTTP vs HTTPS as non-matching", () => {
|
|
const credential = buildCalDAVCredential({
|
|
id: 1,
|
|
key: "encrypted_key",
|
|
});
|
|
|
|
const destination = buildDestinationCalendar({
|
|
id: 1,
|
|
integration: "caldav_calendar",
|
|
externalId: "https://caldav.example.com/dav/calendars/user/calendar1/",
|
|
});
|
|
|
|
mockedSymmetricDecrypt.mockReturnValue(
|
|
JSON.stringify({
|
|
username: "user",
|
|
password: "pass",
|
|
url: "http://caldav.example.com/dav/calendars/user/",
|
|
})
|
|
);
|
|
|
|
const result = (eventManager as any).credentialMatchesDestination(credential, destination);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("EventManager credential lookup methods", () => {
|
|
let eventManager: EventManager;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("getVideoCredential", () => {
|
|
it("returns a cached credential when credentialId matches", async () => {
|
|
const videoCredential = buildVideoCredential({ id: 42, type: "zoom_video" });
|
|
eventManager = new EventManager({
|
|
credentials: [videoCredential],
|
|
destinationCalendar: null,
|
|
});
|
|
|
|
const result = await (eventManager as any).getVideoCredential(42, "zoom_video");
|
|
|
|
expect(result).toMatchObject({ id: 42, type: "zoom_video" });
|
|
expect(mockedCredentialRepository.findCredentialForCalendarServiceById).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fetches credential from repository when not cached locally", async () => {
|
|
const dbCredential = buildVideoCredential({ id: 7, type: "zoom_video" });
|
|
eventManager = new EventManager({
|
|
credentials: [],
|
|
destinationCalendar: null,
|
|
});
|
|
mockedCredentialRepository.findCredentialForCalendarServiceById.mockResolvedValue(dbCredential as any);
|
|
|
|
const result = await (eventManager as any).getVideoCredential(7, "zoom_video");
|
|
|
|
expect(result).toEqual(dbCredential);
|
|
expect(mockedCredentialRepository.findCredentialForCalendarServiceById).toHaveBeenCalledWith({ id: 7 });
|
|
});
|
|
|
|
it("falls back to credential type when credentialId is missing", async () => {
|
|
const zoomCredential = buildVideoCredential({ id: 1, type: "zoom_video" });
|
|
eventManager = new EventManager({
|
|
credentials: [zoomCredential],
|
|
destinationCalendar: null,
|
|
});
|
|
|
|
const result = await (eventManager as any).getVideoCredential(null, "zoom_video");
|
|
|
|
expect(result).toMatchObject({ type: "zoom_video" });
|
|
expect(mockedCredentialRepository.findCredentialForCalendarServiceById).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("getCalendarCredential", () => {
|
|
it("prefers delegation credentials when delegationCredentialId is provided", async () => {
|
|
const delegatedCredential = buildCalendarCredential({
|
|
id: 10,
|
|
delegatedToId: "delegation-123",
|
|
});
|
|
eventManager = new EventManager({
|
|
credentials: [delegatedCredential],
|
|
destinationCalendar: null,
|
|
});
|
|
|
|
const result = await (eventManager as any).getCalendarCredential(
|
|
99,
|
|
"google_calendar",
|
|
"delegation-123"
|
|
);
|
|
|
|
expect(result).toMatchObject({ id: 10, delegatedToId: "delegation-123" });
|
|
expect(mockedCredentialRepository.findCredentialForCalendarServiceById).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fetches credential from repository when local cache misses", async () => {
|
|
const dbCredential = buildCalendarCredential({ id: 5, type: "google_calendar" });
|
|
eventManager = new EventManager({
|
|
credentials: [],
|
|
destinationCalendar: null,
|
|
});
|
|
mockedCredentialRepository.findCredentialForCalendarServiceById.mockResolvedValue(dbCredential as any);
|
|
|
|
const result = await (eventManager as any).getCalendarCredential(5, "google_calendar");
|
|
|
|
expect(result).toEqual(dbCredential);
|
|
expect(mockedCredentialRepository.findCredentialForCalendarServiceById).toHaveBeenCalledWith({ id: 5 });
|
|
});
|
|
|
|
it("falls back to matching credential type when credentialId is absent", async () => {
|
|
const calendarCredential = buildCalendarCredential({ id: 22, type: "google_calendar" });
|
|
eventManager = new EventManager({
|
|
credentials: [calendarCredential],
|
|
destinationCalendar: null,
|
|
});
|
|
|
|
const result = await (eventManager as any).getCalendarCredential(null, "google_calendar");
|
|
|
|
expect(result).toMatchObject({ id: 22, type: "google_calendar" });
|
|
expect(mockedCredentialRepository.findCredentialForCalendarServiceById).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|