Files
calendar/packages/lib/getCalendarsEvents.test.ts
T
c28eb90928 chore: Upgrade prisma to 6.7.0 (#21264)
* chore: Upgrade prisma to 6.7.0

* Build fixes

* type fixes

Signed-off-by: Omar López <zomars@me.com>

* Update schema.prisma

* Patching

* Revert "Update schema.prisma"

This reverts commit 47d8618bf89ef4d007b30084df766f17281e21a1.

* Revert "Patching"

This reverts commit a1d2e3040e71690a44d4324db95d73b4d68c6adb.

* Revert schema changes

Signed-off-by: Omar López <zomars@me.com>

* WIP

Signed-off-by: Omar López <zomars@me.com>

* Update getPublicEvent.ts

* Update imports

Signed-off-by: Omar López <zomars@me.com>

* Update gitignore

Signed-off-by: Omar López <zomars@me.com>

* update remaining imports

Signed-off-by: Omar López <zomars@me.com>

* Delete .cursor/config.json

* Discard changes to packages/features/eventtypes/lib/getPublicEvent.ts

* Update _get.ts

* Update user.ts

* Update .gitignore

* update

* Update WorkflowStepContainer.tsx

* Update next-auth-custom-adapter.ts

* Update getPublicEvent.ts

* Update workflow.ts

* Update next-auth-custom-adapter.ts

* Update next-auth-options.ts

* Update bookingScenario.ts

* fix missing imports

* upgrades prismock

Signed-off-by: Omar López <zomars@me.com>

* patches prismock

Signed-off-by: Omar López <zomars@me.com>

* Update reschedule.test.ts

* Update prisma.ts

* patch prismock

Signed-off-by: Omar López <zomars@me.com>

* fix enums imports

Signed-off-by: Omar López <zomars@me.com>

* Revert "Update prisma.ts"

This reverts commit 64edcf8db54171ff4456c209d563b5d431d99619.

* Revert "patch prismock"

This reverts commit e95819113dc9d88e7130947aa120cd42710977c8.

* fix patch

* Fix test that overrun the boundary, it shouldn't test too much

* Move prisma import to changeSMSLockState

* Bring back broken test without illegal imports

* Merge with main and fix filter hosts by same round robin host

* Fixed buildDryRunBooking fn tests

* Fix and move ooo create or update handler test

* Fix packages/features/eventtypes/lib/isCurrentlyAvailable.test.ts

* Fix packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts

* Mock @calcom/prisma

* Fix: verify-email.test.ts

* fix: Moved WebhookService test and fixed default import mock

* Fix: Added missing prisma mock, handleNewBooking uses that of course

* We're not testing createContext here

* fix: Prisma mock fix for listMembers.test.ts

* More fixes to broken testcases

* Forgot to remove borked test

* Prevent the need to mock a lot of dependencies by moving out buildBaseWhereCondition to its own file

* Temporarily skip getCalendarEvents, needs a rewrite

* Fix: turns out you can access protected in testcases

* fix further mocks

* Added packages/features/insights/server/buildBaseWhereCondition.ts, types

* Always great to have a mock and then not use it

* And one less again.

* fix: confirm.handler.test, didn't mock prisma

* fix: Address minor nit by @eunjae & fix ImpersonationProvider test

* Updated isPrismaAvailableCheck that doesn't crash on import

* fix: Get Prisma directly from the client, it usually involves the Validator and does not need 'local' inclusion

* Add zod-prisma-types without the generator enabled (commented out)

* Uncomment and see what happens

* Change method of import as imports did not work in Input Schemas

* Remove custom 'zod' booking model, it does not belong with Prisma

* Fix all other global Model imports

* Rewrite most schema includes AND remove barrel file

* Add bookingCreateBodySchema to features/bookings

* Flurry of type fixes for compatibility with new zod gen

* Refactor out the custom prisma type createEventTypeInput

* Work around nullable eventTypeLocations

* HandlePayment type fix

* More fixes, final fix remaining is CompleteEventType

* Should fix a bunch more booking related type errors

* Missed one

* Some props missing from BookingCreateBodySchema

* Fix location type in handleChildrenEventTypes

* Little bit hacky imo but it works

* Final type error \o/

* Forgot to include Prisma

* Do not include zod-utils in booker/types

* Oops, was already including Booker/types

* Fix membership type, also disallow updating createdAt/updatedAt, make part of patch/post

* Fix api v1 type errors

* Fix EventTypeDescription typings

* Remove getParserWithGeneric, use userBodySchema with UserSchema

* use centralized timeZoneSchema

* Implement feedback by @zomars

* Couple of WIP pushes

* Fix tests

* Type fixes in `handleChildrenEventTypes` test

* Try and parse metadata before use

* Change zod-prisma-types configuration for optimal performance

* Fix prisma validator error in `prisma/selects/credential`

* Disable seperate relations model, hits a bug

* Import absolute - this makes rollup work in @platform/libraries

* Attempt at removing resolutions override

* Refactor using `Prisma.validator` to `satisfies`

* Build atoms using @calcom/prisma/client

* Build atoms using @calcom/prisma/client

* fixes

* Update eventTypeSelect.ts

* Adjust `eventTypeMetaDataSchemaWithUntypedApps` from `unknown` to `record(any)`

* `EventTypeDescription` rely on `descriptionAsSafeHTML` instead of `description`

* Add `seatsPerTimeSlot` to event type public select

* Fix typing in `users-public-view` getServerSide props

* Add missing `schedulingType` to prop

* chore: bump platform libraries

* Function return type is illegal, not sure how this passed eslint (#21567)

* Merged with main

* Update updateTokenObject.ts

* Update handleResponse.ts

* Update index.ts

* Update handleChildrenEventTypes.ts

* Update booking-idempotency-key.ts

* Update WebhookService.test.ts

* Update events.test.ts

* Update queued-response.test.ts

* Update events.test.ts

* Update getRoutedUrl.test.ts

* fix: type checks

Signed-off-by: Omar López <zomars@me.com>

* fixes

Signed-off-by: Omar López <zomars@me.com>

* chore: bump platform libraries

* Update yarn.lock

* more fixes

Signed-off-by: Omar López <zomars@me.com>

* fixes

Signed-off-by: Omar López <zomars@me.com>

* biuld fixes

* chore: bump platform libraries

* Update conferencing.repository.ts

* Update conferencing.repository.ts

* Update getCalendarsEvents.test.ts

* Update vite.config.js

* chore: bump platform libraries

* Update users.ts

* Discard changes to docs/api-reference/v2/openapi.json

* Update vite.config.ts

* updated platform libraries

* Update get.handler.test.ts

* Update get.handler.test.ts

* Update schema.prisma

* Discard changes to docs/api-reference/v2/openapi.json

* Update next-auth-custom-adapter.ts

* Update team.ts

* Flurry of type fixes

* Fix majority of insight related type errors

* Type fixes for unlink of account

* Make user nullable again

* Fixed a bunch of unit tests and one type error

* Attempted mock fix

* Attempted fix for Attribute type

* Ensure default import becomes prisma, but not direct usage

* Import default as prisma in prisma.module

* Add attributeOption to attribute type

* Fix calcom/prisma mock

* Refactor Prisma client imports to @calcom/prisma/client

Updated all imports from '@prisma/client' to '@calcom/prisma/client' across tests and repository files for consistency and to use the correct Prisma client package. This change improves maintainability and ensures the correct client is referenced throughout the codebase.

* Undo removal of max-warnings=0 to get main to merge

* Remove unit tests for e2e fixtures, provide new prisma mock

* Mock @calcom/prisma in event manager

* Mock @calcom/prisma in event manager

* Add correct format even with --no-verify

* Mock prisma in CalendarManager

* Add mock for permission-check.service

* Better injection in PrismaApiKeyRepository imports

* More mock fixes :)

* Fix listMembers.handler.test

* Fix User import

* Appropriately adjust all types to be imported as types, there were a lot of types imported as normal deps

* Why was this a thing?

* Strictly speaking; Not using prismock anymore

* Ditched patch file for prismock

* Fix output.service.ts platform type imports, need concrete for plainToClass

* Better typing and tests for unlinkConnectedAccount.handler

* Small type fix

* Disable calendar cache tests as they are dependent on prismock

* chore: bump platform lib

* getRoutedUrl test remove of unused import

* Extract select to external const on getEventTypesFromDB

* Direct select of userSelect from selects/user

* fix type error from merging 23653

* Fixed integration tests by removing hardcoded values that were possible due to mocking, but as its now directly hitting the db no longer

* fix: vite config atoms prisma client type location

* revert: example app prisma client

* revert: example app prisma client

* bump platform libs

* fix: use class instead of type for DI of PlatformBookingsService

* update platform libs

* remove unused variable

* chore: generate prisma client for api v2

* fix: api v2 e2e

* fix: atoms e2e

* fix: atoms e2e

* fix: atoms e2e

* fix: api v2 e2e

* fix: tsconfig apiv2 enums

* publish libraries

* Simplify check for existence teamId

---------

Signed-off-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-09-11 15:27:50 +01:00

787 lines
22 KiB
TypeScript

import "../../tests/libs/__mocks__/prisma";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
import OfficeCalendarService from "@calcom/app-store/office365calendar/lib/CalendarService";
import logger from "@calcom/lib/logger";
import type { SelectedCalendar } from "@calcom/prisma/client";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
import { symmetricDecrypt } from "./crypto";
import getCalendarsEvents, {
getCalendarsEventsWithTimezones,
filterSelectedCalendarsForCredential,
} from "./getCalendarsEvents";
vi.mock("./crypto", () => ({
symmetricDecrypt: vi.fn(),
}));
const mockedSymmetricDecrypt = vi.mocked(symmetricDecrypt);
vi.mock("@calcom/app-store/calendar.services.generated", () => {
class MockGoogleCalendarService {
constructor(credential: any) {
this.credential = credential;
}
getCredentialId() {
return this.credential.id;
}
async createEvent() {
return {};
}
async updateEvent() {
return {};
}
async deleteEvent() {
return {};
}
async getAvailability() {
return [];
}
async getAvailabilityWithTimeZones() {
return [];
}
async listCalendars() {
return [];
}
}
class MockOfficeCalendarService {
constructor(credential: any) {
this.credential = credential;
}
getCredentialId() {
return this.credential.id;
}
async createEvent() {
return {};
}
async updateEvent() {
return {};
}
async deleteEvent() {
return {};
}
async getAvailability() {
return [];
}
async getAvailabilityWithTimeZones() {
return [];
}
async listCalendars() {
return [];
}
}
return {
CalendarServiceMap: {
googlecalendar: vi.importActual("@calcom/app-store/googlecalendar/lib/CalendarService"),
office365calendar: vi.importActual("@calcom/app-store/office365calendar/lib/CalendarService"),
},
};
});
function buildDelegationCredential(credential: CredentialPayload): CredentialForCalendarService {
return {
...credential,
id: -1,
delegatedTo: {
serviceAccountKey: {
client_email: "client_email",
tenant_id: "tenant_id",
client_id: "client_id",
private_key: "private_key",
},
},
};
}
function buildRegularCredential(credential: CredentialPayload): CredentialForCalendarService {
return {
...credential,
delegatedTo: null,
delegatedToId: null,
};
}
function buildSelectedCalendar(credential: {
credentialId: number;
externalId: string;
integration: string;
userId: number;
id: string;
}): SelectedCalendar {
return {
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
eventTypeId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
error: null,
createdAt: new Date(),
updatedAt: new Date(),
lastErrorAt: null,
watchAttempts: 0,
unwatchAttempts: 0,
maxAttempts: 3,
...credential,
};
}
describe("getCalendarsEvents", () => {
let credential: CredentialPayload;
beforeEach(() => {
vi.spyOn(logger.constructor.prototype, "debug");
credential = {
id: 303,
type: "google_calendar",
key: {
scope: "example scope",
token_type: "Bearer",
expiry_date: Date.now() + 84000,
access_token: "access token",
refresh_token: "refresh token",
},
userId: 808,
teamId: null,
user: {
email: "test@example.com",
},
appId: "exampleApp",
invalid: false,
delegationCredentialId: null,
};
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Regular Credentials", () => {
it("should return empty array if no calendar credentials", async () => {
const result = await getCalendarsEvents(
[
buildRegularCredential({
...credential,
type: "totally_unrelated",
}),
],
"2010-12-01",
"2010-12-02",
[]
);
expect(result).toEqual([]);
});
it("should return unknown calendars as empty", async () => {
const result = await getCalendarsEvents(
[
buildRegularCredential({
...credential,
type: "unknown_calendar",
}),
],
"2010-12-01",
"2010-12-02",
[]
);
expect(result).toEqual([[]]);
});
it("should return unmatched calendars as empty", async () => {
const selectedCalendar: SelectedCalendar = buildSelectedCalendar({
credentialId: 100,
externalId: "externalId",
integration: "office365_calendar",
userId: 200,
id: "id",
});
const result = await getCalendarsEvents(
[
buildRegularCredential({
...credential,
type: "google_calendar",
}),
],
"2010-12-01",
"2010-12-02",
[selectedCalendar]
);
expect(result).toEqual([[]]);
});
it("should return availability from selected calendar", async () => {
const availability: EventBusyDate[] = [
{
start: new Date(2010, 11, 2),
end: new Date(2010, 11, 3),
},
{
start: new Date(2010, 11, 2, 4),
end: new Date(2010, 11, 2, 16),
},
];
const getAvailabilitySpy = vi
.spyOn(GoogleCalendarService.prototype, "getAvailability")
.mockReturnValue(Promise.resolve(availability));
const selectedCalendar: SelectedCalendar = buildSelectedCalendar({
credentialId: 100,
externalId: "externalId",
integration: "google_calendar",
userId: 200,
id: "id",
});
const result = await getCalendarsEvents(
[
buildRegularCredential({
...credential,
type: "google_calendar",
}),
],
"2010-12-01",
"2010-12-04",
[selectedCalendar]
);
expect(getAvailabilitySpy).toHaveBeenCalledWith(
"2010-12-01",
"2010-12-04",
[selectedCalendar],
undefined,
false
);
expect(result).toEqual([
availability.map((av) => ({
...av,
source: "exampleApp",
})),
]);
});
it("should return availability from multiple calendars", async () => {
const googleAvailability: EventBusyDate[] = [
{
start: new Date(2010, 11, 2),
end: new Date(2010, 11, 3),
},
];
const officeAvailability: EventBusyDate[] = [
{
start: new Date(2010, 11, 2, 4),
end: new Date(2010, 11, 2, 16),
},
];
const getGoogleAvailabilitySpy = vi
.spyOn(GoogleCalendarService.prototype, "getAvailability")
.mockReturnValue(Promise.resolve(googleAvailability));
const getOfficeAvailabilitySpy = vi
.spyOn(OfficeCalendarService.prototype, "getAvailability")
.mockReturnValue(Promise.resolve(officeAvailability));
const selectedGoogleCalendar: SelectedCalendar = buildSelectedCalendar({
credentialId: 100,
externalId: "externalId",
integration: "google_calendar",
userId: 200,
id: "id",
});
const selectedOfficeCalendar: SelectedCalendar = buildSelectedCalendar({
credentialId: 100,
externalId: "externalId",
integration: "office365_calendar",
userId: 200,
id: "id",
});
const result = await getCalendarsEvents(
[
buildRegularCredential({
...credential,
type: "google_calendar",
}),
buildRegularCredential({
...credential,
type: "office365_calendar",
key: {
access_token: "access",
refresh_token: "refresh",
expires_in: Date.now() + 86400,
},
}),
],
"2010-12-01",
"2010-12-04",
[selectedGoogleCalendar, selectedOfficeCalendar]
);
expect(getGoogleAvailabilitySpy).toHaveBeenCalledWith(
"2010-12-01",
"2010-12-04",
[selectedGoogleCalendar],
undefined,
false
);
expect(getOfficeAvailabilitySpy).toHaveBeenCalledWith(
"2010-12-01",
"2010-12-04",
[selectedOfficeCalendar],
undefined,
false
);
expect(result).toEqual([
googleAvailability.map((av) => ({
...av,
source: "exampleApp",
})),
officeAvailability.map((av) => ({
...av,
source: "exampleApp",
})),
]);
});
it("should not call getAvailability if selectedCalendars is empty", async () => {
const getAvailabilitySpy = vi
.spyOn(GoogleCalendarService.prototype, "getAvailability")
.mockReturnValue(Promise.resolve([]));
const result = await getCalendarsEvents(
[buildRegularCredential(credential)],
"2010-12-01",
"2010-12-02",
[]
);
expect(getAvailabilitySpy).not.toHaveBeenCalled();
expect(result).toEqual([[]]);
});
});
describe("Delegation Credentials", () => {
it("should allow getAvailability call even without any selected calendars with allowFallbackToPrimary=true", async () => {
const startDate = "2010-12-01";
const endDate = "2010-12-02";
const delegationCredential: CredentialForCalendarService = buildDelegationCredential(credential);
const credentials = [delegationCredential];
const getAvailabilitySpy = vi
.spyOn(GoogleCalendarService.prototype, "getAvailability")
.mockReturnValue(Promise.resolve([]));
const result = await getCalendarsEvents(credentials, startDate, endDate, []);
expect(getAvailabilitySpy).toHaveBeenCalledWith(startDate, endDate, [], undefined, true);
expect(result).toEqual([[]]);
});
});
});
describe("getCalendarsEventsWithTimezones", () => {
let credential: CredentialPayload;
beforeEach(() => {
vi.spyOn(logger.constructor.prototype, "debug");
credential = {
id: 303,
type: "google_calendar",
key: {
scope: "example scope",
token_type: "Bearer",
expiry_date: Date.now() + 84000,
access_token: "access token",
refresh_token: "refresh token",
},
userId: 808,
teamId: null,
user: {
email: "test@example.com",
},
appId: "exampleApp",
invalid: false,
delegationCredentialId: null,
};
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Regular Credentials", () => {
it("should return empty array if no calendar credentials", async () => {
const result = await getCalendarsEventsWithTimezones(
[
buildRegularCredential({
...credential,
type: "totally_unrelated",
}),
],
"2010-12-01",
"2010-12-02",
[]
);
expect(result).toEqual([]);
});
it("should return unknown calendars as empty", async () => {
const result = await getCalendarsEventsWithTimezones(
[
buildRegularCredential({
...credential,
type: "unknown_calendar",
}),
],
"2010-12-01",
"2010-12-02",
[]
);
expect(result).toEqual([]);
});
it("should return unmatched calendars as empty", async () => {
const selectedCalendar: SelectedCalendar = buildSelectedCalendar({
credentialId: 100,
externalId: "externalId",
integration: "office365_calendar",
userId: 200,
id: "id",
});
const result = await getCalendarsEventsWithTimezones(
[
buildRegularCredential({
...credential,
type: "google_calendar",
}),
],
"2010-12-01",
"2010-12-02",
[selectedCalendar]
);
expect(result).toEqual([[]]);
});
it("should return availability from selected calendar", async () => {
const availability = [
{
start: new Date(2010, 11, 2),
end: new Date(2010, 11, 3),
timeZone: "America/New_York",
},
{
start: new Date(2010, 11, 2, 4),
end: new Date(2010, 11, 2, 16),
timeZone: "America/New_York",
},
];
const getAvailabilityWithTimezonesSpy = vi
.spyOn(GoogleCalendarService.prototype, "getAvailabilityWithTimeZones")
.mockReturnValue(Promise.resolve(availability));
const selectedCalendar: SelectedCalendar = buildSelectedCalendar({
credentialId: 100,
externalId: "externalId",
integration: "google_calendar",
userId: 200,
id: "id",
});
const result = await getCalendarsEventsWithTimezones(
[
buildRegularCredential({
...credential,
type: "google_calendar",
}),
],
"2010-12-01",
"2010-12-04",
[selectedCalendar]
);
expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith(
"2010-12-01",
"2010-12-04",
[selectedCalendar],
false
);
expect(result).toEqual([
availability.map((av) => ({
...av,
})),
]);
});
it("should not call getAvailabilityWithTimezones if selectedCalendars is empty", async () => {
const getAvailabilityWithTimezonesSpy = vi
.spyOn(GoogleCalendarService.prototype, "getAvailabilityWithTimeZones")
.mockReturnValue(Promise.resolve([]));
const result = await getCalendarsEventsWithTimezones(
[buildRegularCredential(credential)],
"2010-12-01",
"2010-12-02",
[]
);
expect(getAvailabilityWithTimezonesSpy).not.toHaveBeenCalled();
expect(result).toEqual([[]]);
});
});
describe("Delegation Credentials", () => {
it("should allow getAvailabilityWithTimezones call even without any selected calendars with allowFallbackToPrimary=true", async () => {
const startDate = "2010-12-01";
const endDate = "2010-12-02";
const delegationCredential: CredentialForCalendarService = buildDelegationCredential(credential);
const credentials = [delegationCredential];
const getAvailabilityWithTimezonesSpy = vi
.spyOn(GoogleCalendarService.prototype, "getAvailabilityWithTimeZones")
.mockReturnValue(Promise.resolve([]));
const result = await getCalendarsEventsWithTimezones(credentials, startDate, endDate, []);
expect(getAvailabilityWithTimezonesSpy).toHaveBeenCalledWith(startDate, endDate, [], true);
expect(result).toEqual([[]]);
});
});
});
// CalDAV Credential Leak Prevention Tests
describe("CalDAV credential leak prevention", () => {
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 buildCalDAVSelectedCalendar(data: {
id: string;
externalId: string;
credentialId?: number;
}): SelectedCalendar {
return {
id: data.id,
userId: 1,
integration: "caldav_calendar",
externalId: data.externalId,
credentialId: data.credentialId || null,
createdAt: new Date(),
updatedAt: new Date(),
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
error: null,
lastErrorAt: null,
watchAttempts: 0,
unwatchAttempts: 0,
maxAttempts: 3,
eventTypeId: null,
};
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("filterSelectedCalendarsForCredential", () => {
it("prevents CalDAV credential leak by matching server URLs", () => {
// Setup: Two CalDAV servers with different URLs
const serverACredential = buildCalDAVCredential({
id: 1,
key: "encrypted_server_a_key",
});
const serverBCredential = buildCalDAVCredential({
id: 2,
key: "encrypted_server_b_key",
});
// Mock encrypted credential data for different servers
mockedSymmetricDecrypt
.mockReturnValueOnce(
JSON.stringify({
username: "user_a",
password: "pass_a",
url: "https://server-a.example.com/dav/calendars/user/",
})
)
.mockReturnValueOnce(
JSON.stringify({
username: "user_b",
password: "pass_b",
url: "https://server-b.example.com/dav/calendars/user/",
})
);
// Selected calendars from both servers
const selectedCalendars = [
buildCalDAVSelectedCalendar({
id: "cal_1",
externalId: "https://server-a.example.com/dav/calendars/user/calendar1/",
credentialId: 1,
}),
buildCalDAVSelectedCalendar({
id: "cal_2",
externalId: "https://server-b.example.com/dav/calendars/user/calendar2/",
credentialId: 2,
}),
];
// Test Server A credential - should only return Server A calendars
const serverACalendars = filterSelectedCalendarsForCredential(selectedCalendars, serverACredential);
expect(serverACalendars).toHaveLength(1);
expect(serverACalendars[0].externalId).toBe(
"https://server-a.example.com/dav/calendars/user/calendar1/"
);
// Test Server B credential - should only return Server B calendars
const serverBCalendars = filterSelectedCalendarsForCredential(selectedCalendars, serverBCredential);
expect(serverBCalendars).toHaveLength(1);
expect(serverBCalendars[0].externalId).toBe(
"https://server-b.example.com/dav/calendars/user/calendar2/"
);
});
it("demonstrates the credential leak that existed before the fix", () => {
// This test shows what WOULD happen with naive filtering (integration type only)
const serverACredential = buildCalDAVCredential({
id: 1,
key: "encrypted_server_a_key",
});
const selectedCalendars = [
buildCalDAVSelectedCalendar({
id: "cal_1",
externalId: "https://server-a.example.com/dav/calendars/user/calendar1/",
credentialId: 1,
}),
buildCalDAVSelectedCalendar({
id: "cal_2",
externalId: "https://server-b.example.com/dav/calendars/user/calendar2/",
credentialId: 2,
}),
];
// Legacy filtering (type-only) would return ALL CalDAV calendars
const legacyFiltering = selectedCalendars.filter((sc) => sc.integration === "caldav_calendar");
expect(legacyFiltering).toHaveLength(2); // This demonstrates the leak - both calendars returned
// Our new filtering prevents this
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user_a",
password: "pass_a",
url: "https://server-a.example.com/dav/calendars/user/",
})
);
const secureFiltering = filterSelectedCalendarsForCredential(selectedCalendars, serverACredential);
expect(secureFiltering).toHaveLength(1); // Only calendars from matching server
expect(secureFiltering[0].externalId).toContain("server-a.example.com");
});
it("handles non-CalDAV calendars normally", () => {
const googleCredential: CredentialForCalendarService = {
id: 1,
type: "google_calendar",
key: "google_key",
userId: 1,
user: { email: "test@example.com" },
teamId: null,
appId: "google-calendar",
invalid: false,
delegatedTo: null,
delegationCredentialId: null,
};
const selectedCalendars = [
buildCalDAVSelectedCalendar({
id: "cal_1",
externalId: "https://server-a.example.com/dav/calendars/user/calendar1/",
}),
{
...buildCalDAVSelectedCalendar({
id: "cal_2",
externalId: "primary",
}),
integration: "google_calendar",
},
];
const googleCalendars = filterSelectedCalendarsForCredential(selectedCalendars, googleCredential);
expect(googleCalendars).toHaveLength(1);
expect(googleCalendars[0].integration).toBe("google_calendar");
});
it("handles invalid CalDAV credential URLs gracefully", () => {
const invalidCredential = buildCalDAVCredential({
id: 1,
key: "encrypted_invalid_key",
});
mockedSymmetricDecrypt.mockReturnValue(
JSON.stringify({
username: "user",
password: "pass",
url: "invalid-url-format",
})
);
const selectedCalendars = [
buildCalDAVSelectedCalendar({
id: "cal_1",
externalId: "https://server-a.example.com/dav/calendars/user/calendar1/",
}),
];
const result = filterSelectedCalendarsForCredential(selectedCalendars, invalidCredential);
expect(result).toHaveLength(0); // Should return empty array for safety
});
});
});