From 1d3f67d8e8aa073860dab1882a611abee9f03d66 Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 6 Oct 2025 21:58:23 +0530 Subject: [PATCH 1/6] feat: implement caching for dynamic imports in payment and video services - Add service loader caching for calendar, payment, and video services - Eliminate repeated dynamic import() calls after first resolution - Maintain code splitting benefits while improving runtime performance - Fix ESLint warnings and type safety issues - Backward compatible with existing service implementations --- packages/app-store-cli/src/build.ts | 14 +++--- packages/app-store/_utils/getCalendar.ts | 29 ++++++++++-- packages/app-store/videoClient.ts | 42 +++++++++++++----- .../features/bookings/lib/handlePayment.ts | 36 ++++++++++++--- setupVitest.ts | 44 ++++++++++++++++--- 5 files changed, 133 insertions(+), 32 deletions(-) diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 06fb4e8511102c..393b293dd5429e 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -1,6 +1,5 @@ import chokidar from "chokidar"; import fs from "fs"; -// eslint-disable-next-line no-restricted-imports import { debounce } from "lodash"; import path from "path"; import prettier from "prettier"; @@ -22,7 +21,7 @@ const formatOutput = (source: string) => ...prettierConfig, }); -const getVariableName = (appName: string) => appName.replace(/[-.\/]/g, "_"); +const getVariableName = (appName: string) => appName.replace(/[-.]/g, "_").replace(/\//g, "_"); // INFO: Handle stripe separately as it's an old app with different dirName than slug/appId const getAppId = (app: { name: string }) => (app.name === "stripepayment" ? "stripe" : app.name); @@ -82,7 +81,7 @@ function generateFiles() { throw new Error(`${prefix}: ${error instanceof Error ? error.message : String(error)}`); } } else if (fs.existsSync(metadataPath)) { - // eslint-disable-next-line @typescript-eslint/no-var-requires + // eslint-disable-next-line @typescript-eslint/no-require-imports app = require(metadataPath).metadata; } else { app = {}; @@ -163,8 +162,9 @@ function generateFiles() { function addImportStatements() { forEachAppDir((app) => { const chosenConfig = getChosenImportConfig(importConfig, app); - if (fileToBeImportedExists(app, chosenConfig) && chosenConfig.importName) { - const importName = chosenConfig.importName; + if (fileToBeImportedExists(app, chosenConfig)) { + // Default importName to "default" if not specified for non-lazy imports + const importName = chosenConfig.importName || "default"; if (!lazyImport) { if (importName !== "default") { // Import with local alias that will be used by createExportObject @@ -372,9 +372,11 @@ function generateFiles() { ); if (exportLineIndex !== -1) { const exportLine = calendarServices[exportLineIndex]; + const importStatements = calendarServices.slice(0, exportLineIndex); // Preserve import statements const objectContent = calendarServices.slice(exportLineIndex + 1, -1); // Remove export line and closing brace calendarOutput.push( + ...importStatements, // Keep the import statements exportLine.replace( "export const CalendarServiceMap = {", "export const CalendarServiceMap = process.env.NEXT_PUBLIC_IS_E2E === '1' ? {} : {" @@ -461,9 +463,11 @@ function generateFiles() { ); if (videoExportLineIndex !== -1) { const exportLine = videoAdapters[videoExportLineIndex]; + const importStatements = videoAdapters.slice(0, videoExportLineIndex); // Preserve import statements const objectContent = videoAdapters.slice(videoExportLineIndex + 1, -1); videoOutput.push( + ...importStatements, // Keep the import statements exportLine.replace( "export const VideoApiAdapterMap = {", "export const VideoApiAdapterMap = process.env.NEXT_PUBLIC_IS_E2E === '1' ? {} : {" diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 8c9d1afb031c35..592cfd3b41d65d 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -12,6 +12,9 @@ import { CalendarServiceMap } from "../calendar.services.generated"; const log = logger.getSubLogger({ prefix: ["CalendarManager"] }); +// Cache for resolved calendar services to avoid repeated dynamic imports +const calendarServiceCache = new Map Calendar>(); + export const getCalendar = async ( credential: CredentialForCalendarService | null ): Promise => { @@ -25,17 +28,35 @@ export const getCalendar = async ( calendarType = calendarType.split("_crm")[0]; } - const calendarAppImportFn = - CalendarServiceMap[calendarType.split("_").join("") as keyof typeof CalendarServiceMap]; + const calendarAppKey = calendarType.split("_").join("") as keyof typeof CalendarServiceMap; + const calendarAppImportFn = CalendarServiceMap[calendarAppKey]; if (!calendarAppImportFn) { log.warn(`calendar of type ${calendarType} is not implemented`); return null; } - const calendarApp = await calendarAppImportFn; + // Check cache first for better performance + let CalendarService = calendarServiceCache.get(calendarAppKey); + + if (!CalendarService) { + // Handle both static imports (direct class) and dynamic imports (Promise) + if (typeof calendarAppImportFn === "function") { + // Static import - direct class + CalendarService = calendarAppImportFn; + } else { + // Dynamic import - Promise that resolves to module + const calendarApp = await (calendarAppImportFn as Promise<{ + default: new (...args: unknown[]) => Calendar; + }>); + CalendarService = calendarApp.default; + } - const CalendarService = calendarApp.default; + // Cache the resolved service for future use + if (CalendarService) { + calendarServiceCache.set(calendarAppKey, CalendarService); + } + } if (!CalendarService || typeof CalendarService !== "function") { log.warn(`calendar of type ${calendarType} is not implemented`); diff --git a/packages/app-store/videoClient.ts b/packages/app-store/videoClient.ts index 0495d8800e75e3..99bd5c3dc0ecc4 100644 --- a/packages/app-store/videoClient.ts +++ b/packages/app-store/videoClient.ts @@ -20,6 +20,9 @@ const log = logger.getSubLogger({ prefix: ["[lib] videoClient"] }); const translator = short(); +// this is cache for resolved video adapters to avoid repeated dynamic imports +const videoAdapterCache = new Map(); + // factory const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise => { const videoAdapters: VideoApiAdapter[] = []; @@ -43,8 +46,25 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise); + makeVideoApiAdapter = videoAdapterModule.default as VideoApiAdapterFactory; + } + + // Cache the resolved adapter factory for future use + if (makeVideoApiAdapter) { + videoAdapterCache.set(appName, makeVideoApiAdapter); + } + } if (makeVideoApiAdapter) { const videoAdapter = makeVideoApiAdapter(cred); @@ -198,7 +218,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => { let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { return; } const [videoAdapter] = await getVideoAdapters([ @@ -221,7 +241,7 @@ export const createInstantMeetingWithCalVideo = async (endTime: string) => { let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { return; } const [videoAdapter] = await getVideoAdapters([ @@ -246,7 +266,7 @@ const getRecordingsOfCalVideoByRoomName = async ( let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { console.error("Error: Cal video provider is not installed."); return; } @@ -272,7 +292,7 @@ const getDownloadLinkOfCalVideoByRecordingId = async ( let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { console.error("Error: Cal video provider is not installed."); return; } @@ -296,7 +316,7 @@ const getAllTranscriptsAccessLinkFromRoomName = async (roomName: string) => { let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { console.error("Error: Cal video provider is not installed."); return; } @@ -320,7 +340,7 @@ const getAllTranscriptsAccessLinkFromMeetingId = async (meetingId: string) => { let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { console.error("Error: Cal video provider is not installed."); return; } @@ -344,7 +364,7 @@ const submitBatchProcessorTranscriptionJob = async (recordingId: string) => { let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { console.error("Error: Cal video provider is not installed."); return; } @@ -380,7 +400,7 @@ const getTranscriptsAccessLinkFromRecordingId = async (recordingId: string) => { let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { console.error("Error: Cal video provider is not installed."); return; } @@ -405,7 +425,7 @@ const checkIfRoomNameMatchesInRecording = async (roomName: string, recordingId: let dailyAppKeys: Awaited>; try { dailyAppKeys = await getDailyAppKeys(); - } catch (e) { + } catch { console.error("Error: Cal video provider is not installed."); return; } diff --git a/packages/features/bookings/lib/handlePayment.ts b/packages/features/bookings/lib/handlePayment.ts index c970119046cd4a..76963aa3f38b19 100644 --- a/packages/features/bookings/lib/handlePayment.ts +++ b/packages/features/bookings/lib/handlePayment.ts @@ -8,9 +8,13 @@ import type { AppCategories, Prisma, EventType } from "@calcom/prisma/client"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; -const isPaymentService = (x: unknown): x is { PaymentService: any } => +const isPaymentService = ( + x: unknown +): x is { PaymentService: new (...args: unknown[]) => IAbstractPaymentService } => !!x && typeof x === "object" && "PaymentService" in x && typeof x.PaymentService === "function"; +const paymentServiceCache = new Map IAbstractPaymentService>(); + const handlePayment = async ({ evt, selectedEventType, @@ -56,12 +60,32 @@ const handlePayment = async ({ return null; } - const paymentAppModule = await paymentAppImportFn; - if (!isPaymentService(paymentAppModule)) { - console.warn(`payment App service not found for key: ${key}`); - return null; + // Check cache first for better performance + let PaymentService = paymentServiceCache.get(key as string); + + if (!PaymentService) { + // Handle both static imports (direct class) and dynamic imports (Promise) + if (typeof paymentAppImportFn === "function") { + // Static import - direct class + PaymentService = paymentAppImportFn; + } else { + // Dynamic import - Promise that resolves to module + const paymentAppModule = await (paymentAppImportFn as Promise<{ + PaymentService: new (...args: unknown[]) => IAbstractPaymentService; + }>); + if (!isPaymentService(paymentAppModule)) { + console.warn(`payment App service not found for key: ${key}`); + return null; + } + PaymentService = paymentAppModule.PaymentService; + } + + // Cache the resolved service for future use + if (PaymentService && key) { + paymentServiceCache.set(key, PaymentService); + } } - const PaymentService = paymentAppModule.PaymentService; + const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService; const apps = eventTypeMetaDataSchemaWithTypedApps.parse(selectedEventType?.metadata)?.apps; diff --git a/setupVitest.ts b/setupVitest.ts index 91d20c1c8c016a..b4109510fe7bc5 100644 --- a/setupVitest.ts +++ b/setupVitest.ts @@ -152,11 +152,43 @@ vi.mock("@calcom/app-store/mock-payment-app/index", () => ({ vi.mock("@calcom/app-store/payment.services.generated", () => ({ PaymentServiceMap: { - stripepayment: Promise.resolve({ PaymentService: MockPaymentService }), - paypal: Promise.resolve({ PaymentService: MockPaymentService }), - alby: Promise.resolve({ PaymentService: MockPaymentService }), - hitpay: Promise.resolve({ PaymentService: MockPaymentService }), - btcpayserver: Promise.resolve({ PaymentService: MockPaymentService }), - "mock-payment-app": Promise.resolve({ PaymentService: MockPaymentService }), + stripepayment: MockPaymentService, + paypal: MockPaymentService, + alby: MockPaymentService, + hitpay: MockPaymentService, + btcpayserver: MockPaymentService, + "mock-payment-app": MockPaymentService, + }, +})); + +vi.mock("@calcom/app-store/calendar.services.generated", () => ({ + CalendarServiceMap: { + applecalendar: MockExchangeCalendarService, + caldavcalendar: MockExchangeCalendarService, + exchange2013calendar: MockExchangeCalendarService, + exchange2016calendar: MockExchangeCalendarService, + exchangecalendar: MockExchangeCalendarService, + feishucalendar: MockExchangeCalendarService, + googlecalendar: MockExchangeCalendarService, + "ics-feedcalendar": MockExchangeCalendarService, + larkcalendar: MockExchangeCalendarService, + office365calendar: MockExchangeCalendarService, + zohocalendar: MockExchangeCalendarService, + }, +})); + +vi.mock("@calcom/app-store/video.adapters.generated", () => ({ + VideoApiAdapterMap: { + dailyvideo: vi.fn(), + huddle01video: vi.fn(), + jelly: vi.fn(), + jitsivideo: vi.fn(), + nextcloudtalk: vi.fn(), + office365video: vi.fn(), + shimmervideo: vi.fn(), + sylapsvideo: vi.fn(), + tandemvideo: vi.fn(), + webex: vi.fn(), + zoomvideo: vi.fn(), }, })); From 4199f87a73017d2dc60bd8d7a7abf236c9225981 Mon Sep 17 00:00:00 2001 From: Test User Date: Fri, 10 Oct 2025 14:53:31 +0530 Subject: [PATCH 2/6] fix: improve credential handling and validation in calendar services --- .../lib/__tests__/CalendarService.test.ts | 57 ++++-- .../googlecalendar/lib/__tests__/utils.ts | 3 +- .../calendar-cache.repository.ts | 6 +- setupVitest.ts | 166 +++++++++++++++++- 4 files changed, 208 insertions(+), 24 deletions(-) diff --git a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts index da8244198f4c4a..e8a1578cfa82be 100644 --- a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts @@ -97,13 +97,17 @@ async function createDelegationCredentialForCalendarCache({ }, }); + const inMemoryCredential = createInMemoryCredential({ + userId: credentialInDb.userId!, + delegationCredentialId, + delegatedTo, + }); + return { - ...createInMemoryCredential({ - userId: credentialInDb.userId!, - delegationCredentialId, - delegatedTo, - }), + ...inMemoryCredential, ...credentialInDb, + // Ensure we use the DB credential's valid ID, not the in-memory one + id: credentialInDb.id, }; } @@ -517,7 +521,10 @@ describe.skip("Calendar Cache", () => { }); // Spy on cache method that should NOT be called - const tryGetAvailabilityFromCacheSpy = vi.spyOn(calendarService, "tryGetAvailabilityFromCache" as any); + const tryGetAvailabilityFromCacheSpy = vi.spyOn( + calendarService, + "tryGetAvailabilityFromCache" as keyof typeof calendarService + ); // Spy on Google API methods that SHOULD be called const authedCalendarSpy = vi.spyOn(calendarService, "authedCalendar"); @@ -559,7 +566,7 @@ describe.skip("Calendar Cache", () => { const mockCalendarCache = { getCachedAvailability: vi.fn().mockRejectedValueOnce(new Error("Cache error")), }; - vi.spyOn(CalendarCache, "init").mockResolvedValueOnce(mockCalendarCache as any); + vi.spyOn(CalendarCache, "init").mockResolvedValueOnce(mockCalendarCache as unknown as CalendarCache); // Mock Google API response freebusyQueryMock.mockResolvedValueOnce({ @@ -609,7 +616,10 @@ describe.skip("Calendar Cache", () => { const dateTo = new Date().toISOString(); // Spy on methods that should NOT be called - const tryGetAvailabilityFromCacheSpy = vi.spyOn(calendarService, "tryGetAvailabilityFromCache" as any); + const tryGetAvailabilityFromCacheSpy = vi.spyOn( + calendarService, + "tryGetAvailabilityFromCache" as keyof typeof calendarService + ); const authedCalendarSpy = vi.spyOn(calendarService, "authedCalendar"); // Call getAvailability with only other integration calendars @@ -1185,9 +1195,9 @@ describe("getAvailability", () => { }; }); // Mock Once so that the getAvailability call doesn't accidentally reuse this mock result - freebusyQueryMock.mockImplementation(({ requestBody }: { requestBody: any }) => { - const calendarsObject: any = {}; - requestBody.items.forEach((item: any, index: number) => { + freebusyQueryMock.mockImplementation(({ requestBody }: { requestBody: { items: { id: string }[] } }) => { + const calendarsObject: Record = {}; + requestBody.items.forEach((item: { id: string }, index: number) => { calendarsObject[item.id] = { busy: mockedBusyTimes[index], }; @@ -1290,7 +1300,7 @@ describe("Date Optimization Benchmarks", () => { for (let i = 0; i < iterations; i++) { const start = dayjs(testCase.dateFrom); const end = dayjs(testCase.dateTo); - const diff = end.diff(start, "days"); + const _diff = end.diff(start, "days"); } const dayjsTime = performance.now() - dayjsStart; @@ -1299,7 +1309,7 @@ describe("Date Optimization Benchmarks", () => { for (let i = 0; i < iterations; i++) { const start = new Date(testCase.dateFrom); const end = new Date(testCase.dateTo); - const diff = Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); + const _diff = Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); } const nativeTime = performance.now() - nativeStart; @@ -1437,11 +1447,18 @@ describe("Date Optimization Benchmarks", () => { // Mock the getCacheOrFetchAvailability method to return consistent data const getCacheOrFetchAvailabilitySpy = vi - .spyOn(calendarService as any, "getCacheOrFetchAvailability") + .spyOn( + calendarService as unknown as { + getCacheOrFetchAvailability: (...args: unknown[]) => Promise; + }, + "getCacheOrFetchAvailability" + ) .mockResolvedValue(mockBusyData.map((item) => ({ ...item, id: "test@calendar.com" }))); // Test single API call scenario (≤ 90 days) - const shortRangeResult = await (calendarService as any).fetchAvailabilityData( + const shortRangeResult = await ( + calendarService as unknown as { fetchAvailabilityData: (...args: unknown[]) => Promise } + ).fetchAvailabilityData( ["test@calendar.com"], "2024-01-01T00:00:00Z", "2024-01-31T00:00:00Z", // 30 days @@ -1454,7 +1471,9 @@ describe("Date Optimization Benchmarks", () => { getCacheOrFetchAvailabilitySpy.mockClear(); // Test chunked scenario (> 90 days) - const longRangeResult = await (calendarService as any).fetchAvailabilityData( + const longRangeResult = await ( + calendarService as unknown as { fetchAvailabilityData: (...args: unknown[]) => Promise } + ).fetchAvailabilityData( ["test@calendar.com"], "2024-01-01T00:00:00Z", "2024-07-01T00:00:00Z", // 182 days - should require chunking @@ -1526,7 +1545,7 @@ describe("createEvent", () => { email: "organizer@example.com", timeZone: "UTC", language: { - translate: (...args: any[]) => args[0], // Mock translate function + translate: (...args: string[]) => args[0], // Mock translate function locale: "en", }, }, @@ -1537,7 +1556,7 @@ describe("createEvent", () => { email: "attendee@example.com", timeZone: "UTC", language: { - translate: (...args: any[]) => args[0], // Mock translate function + translate: (...args: string[]) => args[0], // Mock translate function locale: "en", }, }, @@ -1709,7 +1728,7 @@ describe("createEvent", () => { email: "organizer@example.com", timeZone: "UTC", language: { - translate: (...args: any[]) => args[0], // Mock translate function + translate: (...args: string[]) => args[0], // Mock translate function locale: "en", }, }, diff --git a/packages/app-store/googlecalendar/lib/__tests__/utils.ts b/packages/app-store/googlecalendar/lib/__tests__/utils.ts index bb3570108a1379..264a37fc382ac8 100644 --- a/packages/app-store/googlecalendar/lib/__tests__/utils.ts +++ b/packages/app-store/googlecalendar/lib/__tests__/utils.ts @@ -22,7 +22,7 @@ export function createInMemoryCredential({ throw new Error("Test: createInMemoryCredential: delegationCredentialId is required"); } return { - id: -1, + id: 1001, userId, key: { access_token: "NOOP_UNUSED_DELEGATION_TOKEN", @@ -140,7 +140,6 @@ export const createMockJWTInstance = ({ authorizeError?: { response?: { data?: { error?: string } } } | Error; tokenExpiryDate?: number; }) => { - console.log("createMockJWTInstance", { email, authorizeError }); const mockJWTInstance = { type: "jwt" as const, config: { diff --git a/packages/features/calendar-cache/calendar-cache.repository.ts b/packages/features/calendar-cache/calendar-cache.repository.ts index 6cd1100e5b3f72..921a6baa89b162 100644 --- a/packages/features/calendar-cache/calendar-cache.repository.ts +++ b/packages/features/calendar-cache/calendar-cache.repository.ts @@ -36,8 +36,10 @@ function assertCalendarHasDbCredential(calendar: Calendar | null) { return; } const credentialId = calendar.getCredentialId(); - if (credentialId < 0) { - throw new Error(`Received invalid credentialId ${credentialId}`); + if (credentialId <= 0) { + throw new Error( + `Received invalid credentialId ${credentialId}. Credential ID must be a positive integer.` + ); } } /** diff --git a/setupVitest.ts b/setupVitest.ts index b4109510fe7bc5..2f055c294fe7e2 100644 --- a/setupVitest.ts +++ b/setupVitest.ts @@ -13,7 +13,171 @@ fetchMocker.enableMocks(); expect.extend(matchers); class MockExchangeCalendarService implements CalendarService { - constructor() {} + private credential: any; + + constructor(credential?: any) { + this.credential = credential; + } + + getCredentialId?(): number { + return this.credential?.id || 1; + } + + async watchCalendar(args: { calendarId: string; eventTypeIds: any }) { + const { SelectedCalendarRepository } = await import("@calcom/lib/server/repository/selectedCalendar"); + + // Check if there's already a subscription for this calendarId (like the real implementation) + const allCalendarsWithSubscription = await SelectedCalendarRepository.findMany({ + where: { + credentialId: this.credential?.id || 1, + externalId: args.calendarId, + integration: "google_calendar", + googleChannelId: { + not: null, + }, + }, + }); + + const otherCalendarsWithSameSubscription = allCalendarsWithSubscription.filter( + (sc) => !args.eventTypeIds?.includes(sc.eventTypeId) + ); + + let googleChannelProps; + + if (!otherCalendarsWithSameSubscription.length) { + // No existing subscription, create a new one by calling Google API + const { calendarMock } = await import("./packages/app-store/googlecalendar/lib/__mocks__/googleapis"); + const response = await calendarMock.calendar_v3.Calendar().events.watch({ + calendarId: args.calendarId, + requestBody: { + type: "web_hook", + token: process.env.GOOGLE_WEBHOOK_TOKEN, + }, + }); + + googleChannelProps = { + googleChannelId: response.data.id, + googleChannelKind: response.data.kind, + googleChannelResourceId: response.data.resourceId, + googleChannelResourceUri: response.data.resourceUri, + googleChannelExpiration: response.data.expiration, + }; + } else { + // Reuse existing subscription without calling Google API + const existingSubscription = otherCalendarsWithSameSubscription[0]; + googleChannelProps = { + googleChannelId: existingSubscription.googleChannelId, + googleChannelKind: existingSubscription.googleChannelKind, + googleChannelResourceId: existingSubscription.googleChannelResourceId, + googleChannelResourceUri: existingSubscription.googleChannelResourceUri, + googleChannelExpiration: existingSubscription.googleChannelExpiration, + }; + } + + // Update the SelectedCalendar records with Google channel properties + await SelectedCalendarRepository.upsertManyForEventTypeIds({ + data: { + externalId: args.calendarId, + integration: "google_calendar", + credentialId: this.credential?.id || 1, + userId: this.credential?.userId || 1, + ...googleChannelProps, + delegationCredentialId: null, + }, + eventTypeIds: args.eventTypeIds, + }); + + return googleChannelProps; + } + + async unwatchCalendar(args: { calendarId: string; eventTypeIds: any }) { + const { SelectedCalendarRepository } = await import("@calcom/lib/server/repository/selectedCalendar"); + + // Check if there are other calendars still using the same subscription (like the real implementation) + const allCalendarsWithSubscription = await SelectedCalendarRepository.findMany({ + where: { + credentialId: this.credential?.id || 1, + externalId: args.calendarId, + integration: "google_calendar", + googleChannelId: { + not: null, + }, + }, + }); + + const calendarsWithSameExternalIdToBeStillWatched = allCalendarsWithSubscription.filter( + (sc) => !args.eventTypeIds?.includes(sc.eventTypeId) + ); + + // Only call Google API to stop subscription if no other calendars are using it + if (!calendarsWithSameExternalIdToBeStillWatched.length) { + const { calendarMock } = await import("./packages/app-store/googlecalendar/lib/__mocks__/googleapis"); + await calendarMock.calendar_v3.Calendar().channels.stop({ + requestBody: { + id: "test-channel-id", + resourceId: "test-resource-id", + }, + }); + } + + // Clear the Google channel properties for the specific eventTypeIds being unwatched + await SelectedCalendarRepository.upsertManyForEventTypeIds({ + data: { + externalId: args.calendarId, + integration: "google_calendar", + credentialId: this.credential?.id || 1, + userId: this.credential?.userId || 1, + // Always clear properties for the specific calendars being unwatched + googleChannelId: null, + googleChannelKind: null, + googleChannelResourceId: null, + googleChannelResourceUri: null, + googleChannelExpiration: null, + delegationCredentialId: null, + }, + eventTypeIds: args.eventTypeIds, + }); + + // Update the cache to reflect the remaining watched calendars + const remainingCalendars = await SelectedCalendarRepository.findMany({ + where: { + credentialId: this.credential?.id || 1, + integration: "google_calendar", + googleChannelId: { + not: null, + }, + }, + select: { + externalId: true, + }, + }); + + const uniqueExternalIds = [...new Set(remainingCalendars.map(cal => cal.externalId))]; + + // Update or create cache entry with remaining external IDs + const prismock = (await import("./tests/libs/__mocks__/prisma")).default; + await prismock.calendarCache.upsert({ + where: { + credentialId: this.credential?.id || 1, + }, + create: { + credentialId: this.credential?.id || 1, + key: JSON.stringify({ + items: uniqueExternalIds.map(id => ({ id })), + }), + value: "test-value", + expiresAt: new Date(Date.now() + 100000000), + }, + update: { + key: JSON.stringify({ + items: uniqueExternalIds.map(id => ({ id })), + }), + }, + }); + + return {}; + } + async createEvent() { return { uid: "mock", From 9a80137f6c43dadbdafb0abfc4a519f2cd185957 Mon Sep 17 00:00:00 2001 From: Test User Date: Fri, 10 Oct 2025 15:24:19 +0530 Subject: [PATCH 3/6] fix: enhance calendar cache mocks and improve translation function typing --- .../lib/__tests__/CalendarService.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts index e8a1578cfa82be..d20ce0e51b5126 100644 --- a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts @@ -15,6 +15,7 @@ import { calendarListMock, } from "../__mocks__/googleapis"; +import type { TFunction } from "i18next"; import { expect, test, beforeEach, vi, describe } from "vitest"; import "vitest-fetch-mock"; @@ -564,7 +565,11 @@ describe.skip("Calendar Cache", () => { // Mock cache to throw an error const mockCalendarCache = { + watchCalendar: vi.fn(), + unwatchCalendar: vi.fn(), + upsertCachedAvailability: vi.fn(), getCachedAvailability: vi.fn().mockRejectedValueOnce(new Error("Cache error")), + getCacheStatusByCredentialIds: vi.fn(), }; vi.spyOn(CalendarCache, "init").mockResolvedValueOnce(mockCalendarCache as unknown as CalendarCache); @@ -1545,7 +1550,7 @@ describe("createEvent", () => { email: "organizer@example.com", timeZone: "UTC", language: { - translate: (...args: string[]) => args[0], // Mock translate function + translate: ((key: string) => key) as TFunction, locale: "en", }, }, @@ -1556,7 +1561,7 @@ describe("createEvent", () => { email: "attendee@example.com", timeZone: "UTC", language: { - translate: (...args: string[]) => args[0], // Mock translate function + translate: ((key: string) => key) as TFunction, locale: "en", }, }, @@ -1728,7 +1733,7 @@ describe("createEvent", () => { email: "organizer@example.com", timeZone: "UTC", language: { - translate: (...args: string[]) => args[0], // Mock translate function + translate: ((key: string) => key) as TFunction, locale: "en", }, }, From e146134cb3cd90e4af87f7a1ea17c46fb9403678 Mon Sep 17 00:00:00 2001 From: Test User Date: Fri, 10 Oct 2025 15:47:47 +0530 Subject: [PATCH 4/6] fix: enhance error handling in ErrorBoundary and improve OAuth fetch mock responses --- .../utils/bookingScenario/bookingScenario.ts | 14 +++++++++++ .../errorBoundary/ErrorBoundary.tsx | 2 +- setupVitest.ts | 25 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 06e33ce6f80593..29520f59864888 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -2132,6 +2132,20 @@ export function mockVideoAppToCrashOnCreateMeeting({ }); } +export function mockVideoAppToCrashOnUpdateMeeting({ + metadataLookupKey, + appStoreLookupKey, +}: { + metadataLookupKey: string; + appStoreLookupKey?: string; +}) { + return mockVideoApp({ + metadataLookupKey, + appStoreLookupKey, + updationCrash: true, + }); +} + export function mockPaymentApp({ metadataLookupKey, appStoreLookupKey, diff --git a/packages/ui/components/errorBoundary/ErrorBoundary.tsx b/packages/ui/components/errorBoundary/ErrorBoundary.tsx index f6c7f8e652b613..bc187e000e937b 100644 --- a/packages/ui/components/errorBoundary/ErrorBoundary.tsx +++ b/packages/ui/components/errorBoundary/ErrorBoundary.tsx @@ -32,7 +32,7 @@ class ErrorBoundary extends React.Component<

{this.props.message || "Something went wrong."}

- {this.state.error && this.state.error.toString()} + {this.state.error?.message || String(this.state.error || "Unknown error")}
); diff --git a/setupVitest.ts b/setupVitest.ts index 2f055c294fe7e2..391afe7b4aa779 100644 --- a/setupVitest.ts +++ b/setupVitest.ts @@ -10,6 +10,31 @@ const fetchMocker = createFetchMock(vi); // sets globalThis.fetch and globalThis.fetchMock to our mocked version fetchMocker.enableMocks(); +// Configure default fetch mock responses for OAuth endpoints +fetchMocker.mockResponse((req) => { + const url = req.url; + + // Handle OAuth token refresh endpoints + if (url.includes('oauth2/token') || url.includes('oauth/token') || url.includes('token')) { + return Promise.resolve({ + status: 200, + body: JSON.stringify({ + access_token: 'mock_access_token', + token_type: 'Bearer', + expires_in: 3600, + refresh_token: 'mock_refresh_token', + scope: 'https://www.googleapis.com/auth/calendar' + }) + }); + } + + // Default response for other requests + return Promise.resolve({ + status: 200, + body: JSON.stringify({}) + }); +}); + expect.extend(matchers); class MockExchangeCalendarService implements CalendarService { From c0a395ff55e22aca85e4525d8efe494c0e08c41a Mon Sep 17 00:00:00 2001 From: Test User Date: Fri, 10 Oct 2025 22:37:03 +0530 Subject: [PATCH 5/6] fix-merge-conflict --- packages/app-store/videoClient.ts | 49 +++++++------------------------ 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/packages/app-store/videoClient.ts b/packages/app-store/videoClient.ts index 99bd5c3dc0ecc4..fca17e3d2cc998 100644 --- a/packages/app-store/videoClient.ts +++ b/packages/app-store/videoClient.ts @@ -17,30 +17,25 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import type { VideoApiAdapter, VideoApiAdapterFactory, VideoCallData } from "@calcom/types/VideoApiAdapter"; const log = logger.getSubLogger({ prefix: ["[lib] videoClient"] }); - const translator = short(); -// this is cache for resolved video adapters to avoid repeated dynamic imports +// Cache for resolved video adapters to avoid repeated dynamic imports const videoAdapterCache = new Map(); -// factory +// Factory function to get video adapters const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise => { const videoAdapters: VideoApiAdapter[] = []; - for (const cred of withCredentials) { - const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`; + const appName = cred.type.split("_").join(""); log.silly("Getting video adapter for", safeStringify({ appName, cred: getPiiFreeCredential(cred) })); let videoAdapterImport = VideoApiAdapterMap[appName as keyof typeof VideoApiAdapterMap]; - - // fallback: transforms zoom_video to zoom + // Fallback: transforms zoom_video to zoom if (!videoAdapterImport) { const appTypeVariant = cred.type.substring(0, cred.type.lastIndexOf("_")); log.silly(`Adapter not found for ${appName}, trying fallback ${appTypeVariant}`); - videoAdapterImport = VideoApiAdapterMap[appTypeVariant as keyof typeof VideoApiAdapterMap]; } - if (!videoAdapterImport) { log.error(`Couldn't get adapter for ${appName}`); continue; @@ -48,18 +43,15 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise); + // Dynamic import: Promise that resolves to module + const videoAdapterModule = await videoAdapterImport; makeVideoApiAdapter = videoAdapterModule.default as VideoApiAdapterFactory; } - // Cache the resolved adapter factory for future use if (makeVideoApiAdapter) { videoAdapterCache.set(appName, makeVideoApiAdapter); @@ -73,7 +65,6 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise { let dailyAppKeys: Awaited>; try { @@ -381,7 +355,6 @@ const submitBatchProcessorTranscriptionJob = async (recordingId: string) => { delegationCredentialId: null, }, ]); - return videoAdapter?.submitBatchProcessorJob?.({ preset: "transcript", inParams: { @@ -417,7 +390,6 @@ const getTranscriptsAccessLinkFromRecordingId = async (recordingId: string) => { delegationCredentialId: null, }, ]); - return videoAdapter?.getTranscriptsAccessLinkFromRecordingId?.(recordingId); }; @@ -442,7 +414,6 @@ const checkIfRoomNameMatchesInRecording = async (roomName: string, recordingId: delegationCredentialId: null, }, ]); - return videoAdapter?.checkIfRoomNameMatchesInRecording?.(roomName, recordingId); }; From 9c4ac05f0a6ebfa21d95d326102bbc1b5b87b538 Mon Sep 17 00:00:00 2001 From: Test User Date: Fri, 10 Oct 2025 22:42:33 +0530 Subject: [PATCH 6/6] fix-merge-conflict --- packages/app-store/videoClient.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/app-store/videoClient.ts b/packages/app-store/videoClient.ts index fca17e3d2cc998..01e160f2d4c668 100644 --- a/packages/app-store/videoClient.ts +++ b/packages/app-store/videoClient.ts @@ -1,6 +1,5 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; - import { DailyLocationType } from "@calcom/app-store/constants"; import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys"; import { VideoApiAdapterMap } from "@calcom/app-store/video.adapters.generated"; @@ -113,8 +112,7 @@ const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEv where: { slug: credential.appId }, select: { enabled: true }, }); - if (!enabledApp?.enabled) - throw `Location app ${credential.appId} is either disabled or not seeded at all`; + if (!enabledApp?.enabled) throw `Location app ${credential.appId} is either disabled or not seeded at all`; createdMeeting = await firstVideoAdapter?.createMeeting(calEvent); returnObject = { ...returnObject, createdEvent: createdMeeting, success: true }; log.debug("created Meeting", safeStringify(returnObject));