diff --git a/apps/web/test/lib/getSchedule/delegation-credential.test.ts b/apps/web/test/lib/getSchedule/delegation-credential.test.ts index 24b7b60a80..968273f30e 100644 --- a/apps/web/test/lib/getSchedule/delegation-credential.test.ts +++ b/apps/web/test/lib/getSchedule/delegation-credential.test.ts @@ -60,7 +60,7 @@ describe("getSchedule", () => { await createDelegationCredential(org.id); - const googleCalendarMock = mockCalendar("googlecalendar", { + const googleCalendarMock = await mockCalendar("googlecalendar", { create: { uid: "MOCK_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -151,7 +151,7 @@ describe("getSchedule", () => { // Create Delegation credential for the org user isn't part of await createDelegationCredential(org.id); - const googleCalendarMock = mockCalendar("googlecalendar", { + const googleCalendarMock = await mockCalendar("googlecalendar", { create: { uid: "MOCK_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", diff --git a/apps/web/test/lib/getSchedule/selectedSlots.test.ts b/apps/web/test/lib/getSchedule/selectedSlots.test.ts index 0cdaff3543..645e339aa6 100644 --- a/apps/web/test/lib/getSchedule/selectedSlots.test.ts +++ b/apps/web/test/lib/getSchedule/selectedSlots.test.ts @@ -3,6 +3,7 @@ import { createBookingScenario, Timezones, TestData, + mockCalendarToHaveNoBusySlots, type ScenarioData, } from "../../utils/bookingScenario/bookingScenario"; @@ -94,6 +95,8 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); + await mockCalendarToHaveNoBusySlots("googlecalendar"); + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -129,6 +132,8 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); + await mockCalendarToHaveNoBusySlots("googlecalendar"); + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), ctx: { @@ -184,6 +189,8 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); + await mockCalendarToHaveNoBusySlots("googlecalendar"); + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -239,6 +246,8 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); + await mockCalendarToHaveNoBusySlots("googlecalendar"); + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -288,6 +297,8 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); + await mockCalendarToHaveNoBusySlots("googlecalendar"); + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 0869ef6c00..e5a3068670 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -42,6 +42,15 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential"; import { getMockPaymentService } from "./MockPaymentService"; import type { getMockRequestDataForBooking } from "./getMockRequestDataForBooking"; +vi.mock("@calcom/app-store/calendar.services.generated", () => ({ + CalendarServiceMap: { + googlecalendar: Promise.resolve({ default: vi.fn() }), + office365calendar: Promise.resolve({ default: vi.fn() }), + applecalendar: Promise.resolve({ default: vi.fn() }), + caldavcalendar: Promise.resolve({ default: vi.fn() }), + }, +})); + // We don't need to test it. Also, it causes Formbricks error when imported vi.mock("@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic", () => ({ default: {}, @@ -1715,7 +1724,7 @@ export type CalendarServiceMethodMock = { * @param metadataLookupKey * @param calendarData Specify uids and other data to be faked to be returned by createEvent and updateEvent */ -export function mockCalendar( +export async function mockCalendar( metadataLookupKey: keyof typeof appStoreMetadata, calendarData?: { create?: { @@ -1749,7 +1758,7 @@ export function mockCalendar( updationCrash?: boolean; getAvailabilityCrash?: boolean; } -): CalendarServiceMethodMock { +): Promise { const appStoreLookupKey = metadataLookupKey; const normalizedCalendarData = calendarData || { create: { @@ -1767,138 +1776,173 @@ export function mockCalendar( const getAvailabilityCalls: GetAvailabilityMethodMockCall[] = []; const app = appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata]; - const appMock = appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default]; + const { CalendarServiceMap } = await import("@calcom/app-store/calendar.services.generated"); + const calendarServiceKey = appStoreLookupKey as keyof typeof CalendarServiceMap; - appMock && - `mockResolvedValue` in appMock && - appMock.mockResolvedValue({ - lib: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - CalendarService: function MockCalendarService(credential: CredentialForCalendarService) { - return { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createEvent: async function (...rest: any[]): Promise { - if (calendarData?.creationCrash) { - throw new Error("MockCalendarService.createEvent fake error"); - } - const [calEvent, credentialId, externalCalendarId] = rest; - log.debug( - "mockCalendar.createEvent", - JSON.stringify({ calEvent, credentialId, externalCalendarId }) - ); - createEventCalls.push({ - args: { - calEvent, - credentialId, - externalCalendarId, - }, - calendarServiceConstructorArgs: { - credential, - }, - }); - const isGoogleMeetLocation = calEvent?.location === BookingLocations.GoogleMeet; - return Promise.resolve({ - type: app.type, - additionalInfo: {}, - uid: "PROBABLY_UNUSED_UID", + const calendarServicePromise = CalendarServiceMap[calendarServiceKey]; + if (calendarServicePromise) { + const resolvedService = await calendarServicePromise; + vi.mocked(resolvedService.default as any).mockImplementation(function MockCalendarService( + credential: any + ) { + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createEvent: async function (...rest: any[]): Promise { + if (calendarData?.creationCrash) { + throw new Error("MockCalendarService.createEvent fake error"); + } + const [calEvent, credentialId, externalCalendarId] = rest; + log.debug( + "mockCalendar.createEvent", + JSON.stringify({ calEvent, credentialId, externalCalendarId }) + ); + createEventCalls.push({ + args: { + calEvent, + credentialId, + externalCalendarId, + }, + calendarServiceConstructorArgs: { + credential, + }, + }); + const isGoogleMeetLocation = calEvent?.location === BookingLocations.GoogleMeet; + if (app.type === "google_calendar") { + return Promise.resolve({ + type: app.type, + additionalInfo: { hangoutLink: - (isGoogleMeetLocation - ? normalizedCalendarData.create?.appSpecificData?.googleCalendar?.hangoutLink - : null) || "https://UNUSED_URL", - // A Calendar is always expected to return an id. - id: normalizedCalendarData.create?.id || "FALLBACK_MOCK_CALENDAR_EVENT_ID", - iCalUID: normalizedCalendarData.create?.iCalUID, - // Password and URL seems useless for CalendarService, plan to remove them if that's the case - password: "MOCK_PASSWORD", - url: "https://UNUSED_URL", - }); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - updateEvent: async function (...rest: any[]): Promise { - if (calendarData?.updationCrash) { - throw new Error("MockCalendarService.updateEvent fake error"); - } - const [uid, event, externalCalendarId] = rest; - log.silly("mockCalendar.updateEvent", JSON.stringify({ uid, event, externalCalendarId })); - // eslint-disable-next-line prefer-rest-params - updateEventCalls.push({ - args: { - uid, - event, - externalCalendarId, - }, - calendarServiceConstructorArgs: { - credential, - }, - }); - const isGoogleMeetLocation = event.location === BookingLocations.GoogleMeet; - return Promise.resolve({ - type: app.type, - additionalInfo: {}, - uid: "PROBABLY_UNUSED_UID", - iCalUID: normalizedCalendarData.update?.iCalUID, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - id: normalizedCalendarData.update?.uid || "FALLBACK_MOCK_ID", - // Password and URL seems useless for CalendarService, plan to remove them if that's the case - password: "MOCK_PASSWORD", - url: "https://UNUSED_URL", - location: isGoogleMeetLocation ? "https://UNUSED_URL" : undefined, - hangoutLink: - (isGoogleMeetLocation - ? normalizedCalendarData.update?.appSpecificData?.googleCalendar?.hangoutLink - : null) || "https://UNUSED_URL", - conferenceData: isGoogleMeetLocation ? event.conferenceData : undefined, - }); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - deleteEvent: async (...rest: any[]) => { - log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest })); - // eslint-disable-next-line prefer-rest-params - deleteEventCalls.push({ - args: { - uid: rest[0], - event: rest[1], - externalCalendarId: rest[2], - }, - calendarServiceConstructorArgs: { - credential, - }, - }); - }, - getAvailability: async (...rest: any[]): Promise => { - if (calendarData?.getAvailabilityCrash) { - throw new Error("MockCalendarService.getAvailability fake error"); - } - const [dateFrom, dateTo, selectedCalendars, shouldServeCache] = rest; - getAvailabilityCalls.push({ - args: { - dateFrom, - dateTo, - selectedCalendars, - shouldServeCache, - }, - calendarServiceConstructorArgs: { - credential, - }, - }); - return new Promise((resolve) => { - resolve(calendarData?.busySlots || []); - }); - }, - }; + normalizedCalendarData.create?.appSpecificData?.googleCalendar?.hangoutLink || + "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + }, + hangoutLink: + normalizedCalendarData.create?.appSpecificData?.googleCalendar?.hangoutLink || + "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + uid: normalizedCalendarData.create?.uid || "GOOGLE_CALENDAR_EVENT_ID", + id: normalizedCalendarData.create?.id || "GOOGLE_CALENDAR_EVENT_ID", + iCalUID: + normalizedCalendarData.create?.iCalUID || calEvent.iCalUID || "GOOGLE_CALENDAR_EVENT_ID", + password: "MOCK_PASSWORD", + url: + normalizedCalendarData.create?.appSpecificData?.googleCalendar?.hangoutLink || + "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + }); + } else if (app.type === "office365_calendar") { + return Promise.resolve({ + type: app.type, + additionalInfo: {}, + uid: normalizedCalendarData.create?.uid || "OFFICE_365_CALENDAR_EVENT_ID", + id: normalizedCalendarData.create?.id || "OFFICE_365_CALENDAR_EVENT_ID", + iCalUID: + normalizedCalendarData.create?.iCalUID || calEvent.iCalUID || "OFFICE_365_CALENDAR_EVENT_ID", + password: "MOCK_PASSWORD", + url: + normalizedCalendarData.create?.appSpecificData?.office365Calendar?.url || + "https://UNUSED_URL", + }); + } else { + return Promise.resolve({ + type: app.type, + additionalInfo: {}, + uid: "PROBABLY_UNUSED_UID", + hangoutLink: + (isGoogleMeetLocation + ? normalizedCalendarData.create?.appSpecificData?.googleCalendar?.hangoutLink + : null) || "https://UNUSED_URL", + // A Calendar is always expected to return an id. + id: normalizedCalendarData.create?.id || "FALLBACK_MOCK_CALENDAR_EVENT_ID", + iCalUID: normalizedCalendarData.create?.iCalUID, + // Password and URL seems useless for CalendarService, plan to remove them if that's the case + password: "MOCK_PASSWORD", + url: "https://UNUSED_URL", + }); + } }, - }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateEvent: async function (...rest: any[]): Promise { + if (calendarData?.updationCrash) { + throw new Error("MockCalendarService.updateEvent fake error"); + } + const [uid, event, externalCalendarId] = rest; + log.silly("mockCalendar.updateEvent", JSON.stringify({ uid, event, externalCalendarId })); + // eslint-disable-next-line prefer-rest-params + updateEventCalls.push({ + args: { + uid, + event, + externalCalendarId, + }, + calendarServiceConstructorArgs: { + credential, + }, + }); + const isGoogleMeetLocation = event.location === BookingLocations.GoogleMeet; + return Promise.resolve({ + type: app.type, + additionalInfo: {}, + uid: "PROBABLY_UNUSED_UID", + iCalUID: normalizedCalendarData.update?.iCalUID, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: normalizedCalendarData.update?.uid || "FALLBACK_MOCK_ID", + // Password and URL seems useless for CalendarService, plan to remove them if that's the case + password: "MOCK_PASSWORD", + url: "https://UNUSED_URL", + location: isGoogleMeetLocation ? "https://UNUSED_URL" : undefined, + hangoutLink: + (isGoogleMeetLocation + ? normalizedCalendarData.update?.appSpecificData?.googleCalendar?.hangoutLink + : null) || "https://UNUSED_URL", + conferenceData: isGoogleMeetLocation ? event.conferenceData : undefined, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deleteEvent: async (...rest: any[]) => { + log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest })); + // eslint-disable-next-line prefer-rest-params + deleteEventCalls.push({ + args: { + uid: rest[0], + event: rest[1], + externalCalendarId: rest[2], + }, + calendarServiceConstructorArgs: { + credential, + }, + }); + }, + getAvailability: async (...rest: any[]): Promise => { + if (calendarData?.getAvailabilityCrash) { + throw new Error("MockCalendarService.getAvailability fake error"); + } + const [dateFrom, dateTo, selectedCalendars, shouldServeCache] = rest; + getAvailabilityCalls.push({ + args: { + dateFrom, + dateTo, + selectedCalendars, + shouldServeCache, + }, + calendarServiceConstructorArgs: { + credential, + }, + }); + return new Promise((resolve) => { + resolve(calendarData?.busySlots || []); + }); + }, + }; }); + } + return { createEventCalls, - deleteEventCalls, updateEventCalls, + deleteEventCalls, getAvailabilityCalls, }; } -export function mockCalendarToHaveNoBusySlots( +export async function mockCalendarToHaveNoBusySlots( metadataLookupKey: keyof typeof appStoreMetadata, calendarData?: Parameters[1] ) { @@ -1910,15 +1954,15 @@ export function mockCalendarToHaveNoBusySlots( uid: "UPDATED_MOCK_ID", }, }; - return mockCalendar(metadataLookupKey, { ...calendarData, busySlots: [] }); + return await mockCalendar(metadataLookupKey, { ...calendarData, busySlots: [] }); } -export function mockCalendarToCrashOnCreateEvent(metadataLookupKey: keyof typeof appStoreMetadata) { - return mockCalendar(metadataLookupKey, { creationCrash: true }); +export async function mockCalendarToCrashOnCreateEvent(metadataLookupKey: keyof typeof appStoreMetadata) { + return await mockCalendar(metadataLookupKey, { creationCrash: true }); } -export function mockCalendarToCrashOnUpdateEvent(metadataLookupKey: keyof typeof appStoreMetadata) { - return mockCalendar(metadataLookupKey, { updationCrash: true }); +export async function mockCalendarToCrashOnUpdateEvent(metadataLookupKey: keyof typeof appStoreMetadata) { + return await mockCalendar(metadataLookupKey, { updationCrash: true }); } export function mockVideoApp({ diff --git a/package.json b/package.json index f98b25c37f..221477d6c4 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "start": "turbo run start --filter=\"@calcom/web\"", "tdd": "vitest watch", "e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web", - "e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store", + "e2e:app-store": "NEXT_PUBLIC_IS_E2E=1 QUICK=true yarn playwright test --project=@calcom/app-store", "e2e:embed": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/embed-core", "e2e:embed-react": "QUICK=true yarn playwright test --project=@calcom/embed-react", "test-e2e": "yarn db-seed && yarn e2e", diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 1d9c5d208c..c64cc8008a 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -21,7 +21,7 @@ const formatOutput = (source: string) => ...prettierConfig, }); -const getVariableName = (appName: string) => appName.replace(/[-.]/g, "_"); +const getVariableName = (appName: string) => appName.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); @@ -345,6 +345,39 @@ function generateFiles() { ) ); + const calendarOutput = []; + const calendarServices = getExportedObject( + "CalendarServiceMap", + { + importConfig: { + fileToBeImported: "lib/CalendarService.ts", + importName: "default", + }, + lazyImport: true, + }, + isCalendarApp + ); + + // Find the export line and wrap it with E2E conditional + const exportLineIndex = calendarServices.findIndex((line) => + line.startsWith("export const CalendarServiceMap") + ); + if (exportLineIndex !== -1) { + const exportLine = calendarServices[exportLineIndex]; + const objectContent = calendarServices.slice(exportLineIndex + 1, -1); // Remove export line and closing brace + + calendarOutput.push( + exportLine.replace( + "export const CalendarServiceMap = {", + "export const CalendarServiceMap = process.env.NEXT_PUBLIC_IS_E2E ? {} : {" + ), + ...objectContent, + "};" + ); + } else { + calendarOutput.push(...calendarServices); + } + const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. Don't modify this file manually. @@ -358,6 +391,7 @@ function generateFiles() { ["apps.keys-schemas.generated.ts", appKeysSchemasOutput], ["bookerApps.metadata.generated.ts", bookerMetadataOutput], ["crm.apps.generated.ts", crmOutput], + ["calendar.services.generated.ts", calendarOutput], ]; filesToGenerate.forEach(([fileName, output]) => { fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); @@ -405,3 +439,7 @@ function isBookerApp(app: App) { function isCrmApp(app: App) { return !!app.categories?.includes("crm"); } + +function isCalendarApp(app: App) { + return !!app.categories?.includes("calendar"); +} diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 9fb865c203..1c3d3d3e6a 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -2,7 +2,7 @@ import logger from "@calcom/lib/logger"; import type { Calendar, CalendarClass } from "@calcom/types/Calendar"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; -import appStore from ".."; +import { CalendarServiceMap } from "../calendar.services.generated"; interface CalendarApp { lib: { @@ -36,19 +36,22 @@ export const getCalendar = async ( calendarType = calendarType.split("_crm")[0]; } - const calendarAppImportFn = appStore[calendarType.split("_").join("") as keyof typeof appStore]; + const calendarAppImportFn = + CalendarServiceMap[calendarType.split("_").join("") as keyof typeof CalendarServiceMap]; if (!calendarAppImportFn) { log.warn(`calendar of type ${calendarType} is not implemented`); return null; } - const calendarApp = await calendarAppImportFn(); + const calendarApp = await calendarAppImportFn; - if (!isCalendarService(calendarApp)) { + const CalendarService = calendarApp.default; + + if (!CalendarService || typeof CalendarService !== "function") { log.warn(`calendar of type ${calendarType} is not implemented`); return null; } - const CalendarService = calendarApp.lib.CalendarService; - return new CalendarService(credential); + + return new CalendarService(credential as any); }; diff --git a/packages/app-store/applecalendar/lib/CalendarService.ts b/packages/app-store/applecalendar/lib/CalendarService.ts index 4565ba2d7d..ad8c25d7f7 100644 --- a/packages/app-store/applecalendar/lib/CalendarService.ts +++ b/packages/app-store/applecalendar/lib/CalendarService.ts @@ -1,7 +1,7 @@ -import CalendarService from "@calcom/lib/CalendarService"; +import BaseCalendarService from "@calcom/lib/CalendarService"; import type { CredentialPayload } from "@calcom/types/Credential"; -export default class AppleCalendarService extends CalendarService { +export default class AppleCalendarService extends BaseCalendarService { constructor(credential: CredentialPayload) { super(credential, "apple_calendar", "https://caldav.icloud.com"); } diff --git a/packages/app-store/caldavcalendar/lib/CalendarService.ts b/packages/app-store/caldavcalendar/lib/CalendarService.ts index 7c6b2db659..bd49314d38 100644 --- a/packages/app-store/caldavcalendar/lib/CalendarService.ts +++ b/packages/app-store/caldavcalendar/lib/CalendarService.ts @@ -1,7 +1,7 @@ -import CalendarService from "@calcom/lib/CalendarService"; +import BaseCalendarService from "@calcom/lib/CalendarService"; import type { CredentialPayload } from "@calcom/types/Credential"; -export default class CalDavCalendarService extends CalendarService { +export default class CalDavCalendarService extends BaseCalendarService { constructor(credential: CredentialPayload) { super(credential, "caldav_calendar"); } diff --git a/packages/app-store/calendar.services.generated.ts b/packages/app-store/calendar.services.generated.ts new file mode 100644 index 0000000000..c6f07b8910 --- /dev/null +++ b/packages/app-store/calendar.services.generated.ts @@ -0,0 +1,19 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const CalendarServiceMap = process.env.NEXT_PUBLIC_IS_E2E + ? {} + : { + applecalendar: import("./applecalendar/lib/CalendarService"), + caldavcalendar: import("./caldavcalendar/lib/CalendarService"), + exchange2013calendar: import("./exchange2013calendar/lib/CalendarService"), + exchange2016calendar: import("./exchange2016calendar/lib/CalendarService"), + exchangecalendar: import("./exchangecalendar/lib/CalendarService"), + feishucalendar: import("./feishucalendar/lib/CalendarService"), + googlecalendar: import("./googlecalendar/lib/CalendarService"), + "ics-feedcalendar": import("./ics-feedcalendar/lib/CalendarService"), + larkcalendar: import("./larkcalendar/lib/CalendarService"), + office365calendar: import("./office365calendar/lib/CalendarService"), + zohocalendar: import("./zohocalendar/lib/CalendarService"), + }; diff --git a/packages/app-store/constants.ts b/packages/app-store/constants.ts new file mode 100644 index 0000000000..ab3cd2d5fb --- /dev/null +++ b/packages/app-store/constants.ts @@ -0,0 +1,3 @@ +export const MeetLocationType = "integrations:google:meet"; + +export const MSTeamsLocationType = "integrations:office365_video"; diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 28331cccef..ef3e9c57e2 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -4,7 +4,7 @@ import type { GaxiosResponse } from "googleapis-common"; import { RRule } from "rrule"; import { v4 as uuid } from "uuid"; -import { MeetLocationType } from "@calcom/app-store/locations"; +import { MeetLocationType } from "@calcom/app-store/constants"; import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache"; import type { FreeBusyArgs } from "@calcom/features/calendar-cache/calendar-cache.repository.interface"; import { getTimeMax, getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache"; diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 8ddea439c5..f481f7c79a 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -47,12 +47,29 @@ const appStore = { function createCachedImport(importFunc: () => Promise): () => Promise { let cachedModule: T | undefined; + let isLoading = false; + let loadingPromise: Promise | undefined; return async () => { - if (!cachedModule) { - cachedModule = await importFunc(); - } - return cachedModule; + if (cachedModule) return cachedModule; + if (isLoading && loadingPromise) return loadingPromise; + + isLoading = true; + loadingPromise = importFunc() + .then((module) => { + cachedModule = module; + isLoading = false; + return module; + }) + .catch((err) => { + throw err; + }) + .finally(() => { + isLoading = false; + loadingPromise = undefined; + }); + + return loadingPromise; }; } diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index d47194ab85..7e646b7d7c 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -10,6 +10,13 @@ import { BookingStatus } from "@calcom/prisma/enums"; import type { Ensure, Optional } from "@calcom/types/utils"; import type { EventLocationTypeFromAppMeta } from "../types/App"; +import { + MeetLocationType as importedMeetLocationType, + MSTeamsLocationType as importedMSTeamsLocationType, +} from "./constants"; + +export const MeetLocationType = importedMeetLocationType; +export const MSTeamsLocationType = importedMSTeamsLocationType; export type DefaultEventLocationType = { default: true; @@ -64,9 +71,6 @@ export type EventLocationType = DefaultEventLocationType | EventLocationTypeFrom export const DailyLocationType = "integrations:daily"; export const CalVideoLocationType = DailyLocationType; -export const MeetLocationType = "integrations:google:meet"; - -export const MSTeamsLocationType = "integrations:office365_video"; /** * This isn't an actual location app type. It is a special value that informs to use the Organizer's default conferencing app during booking diff --git a/packages/app-store/office365calendar/lib/CalendarService.ts b/packages/app-store/office365calendar/lib/CalendarService.ts index cb9b6c18a4..84c35b3773 100644 --- a/packages/app-store/office365calendar/lib/CalendarService.ts +++ b/packages/app-store/office365calendar/lib/CalendarService.ts @@ -1,7 +1,7 @@ import type { Calendar as OfficeCalendar, User, Event } from "@microsoft/microsoft-graph-types-beta"; import type { DefaultBodyType } from "msw"; -import { MSTeamsLocationType } from "@calcom/app-store/locations"; +import { MSTeamsLocationType } from "@calcom/app-store/constants"; import dayjs from "@calcom/dayjs"; import { getLocation, getRichDescriptionHTML } from "@calcom/lib/CalEventParser"; import { diff --git a/packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts b/packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts index 6e89b6d986..fd019dd793 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts @@ -135,7 +135,7 @@ describe("handleNewBooking", () => { }); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "GOOGLE_CALENDAR_EVENT_ID", uid: "MOCK_ID", @@ -191,7 +191,6 @@ describe("handleNewBooking", () => { uid: "GOOGLE_CALENDAR_EVENT_ID", meetingId: "GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], iCalUID: createdBooking.iCalUID, diff --git a/packages/features/bookings/lib/handleNewBooking/test/date-overrides.test.ts b/packages/features/bookings/lib/handleNewBooking/test/date-overrides.test.ts index 19f4292d4b..70d7565602 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/date-overrides.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/date-overrides.test.ts @@ -120,7 +120,7 @@ describe("handleNewBooking", () => { }); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "GOOGLE_CALENDAR_EVENT_ID", uid: "MOCK_ID", @@ -177,7 +177,6 @@ describe("handleNewBooking", () => { uid: "GOOGLE_CALENDAR_EVENT_ID", meetingId: "GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], iCalUID: createdBooking.iCalUID, @@ -298,7 +297,7 @@ describe("handleNewBooking", () => { }); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "GOOGLE_CALENDAR_EVENT_ID", uid: "MOCK_ID", @@ -355,7 +354,6 @@ describe("handleNewBooking", () => { uid: "GOOGLE_CALENDAR_EVENT_ID", meetingId: "GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], iCalUID: createdBooking.iCalUID, diff --git a/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts b/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts index 46edb0d2fa..63ce5025b6 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts @@ -37,6 +37,7 @@ import { } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; +import { vi } from "vitest"; import { describe, expect } from "vitest"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; @@ -44,6 +45,108 @@ import { BookingStatus } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; +vi.mock("@calcom/app-store/calendar.services.generated", () => { + class MockGoogleCalendarService { + credential: any; + + constructor(credential: any) { + this.credential = credential; + } + + getCredentialId() { + return this.credential.id; + } + + async createEvent(calEvent: any, credentialId: any, externalCalendarId?: string) { + return { + type: "google_calendar", + additionalInfo: { + hangoutLink: "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + }, + uid: "GOOGLE_CALENDAR_EVENT_ID", + id: "GOOGLE_CALENDAR_EVENT_ID", + iCalUID: calEvent.iCalUID || "GOOGLE_CALENDAR_EVENT_ID", + password: "MOCK_PASSWORD", + url: "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + createdEvent: { + hangoutLink: "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + }, + }; + } + + async updateEvent() { + return {}; + } + + async deleteEvent() { + return {}; + } + + async getAvailability() { + return []; + } + + async getAvailabilityWithTimeZones() { + return []; + } + + async listCalendars() { + return []; + } + } + + class MockOffice365CalendarService { + credential: any; + + constructor(credential: any) { + this.credential = credential; + } + + getCredentialId() { + return this.credential.id; + } + + async createEvent(calEvent: any, credentialId: any, externalCalendarId?: string) { + return { + type: "office365_calendar", + additionalInfo: {}, + uid: "OFFICE_365_CALENDAR_EVENT_ID", + id: "OFFICE_365_CALENDAR_EVENT_ID", + iCalUID: calEvent.iCalUID || "OFFICE_365_CALENDAR_EVENT_ID", + password: "MOCK_PASSWORD", + url: "https://UNUSED_URL", + }; + } + + async updateEvent() { + return {}; + } + + async deleteEvent() { + return {}; + } + + async getAvailability() { + return []; + } + + async getAvailabilityWithTimeZones() { + return []; + } + + async listCalendars() { + return []; + } + } + + return { + CalendarServiceMap: { + googlecalendar: Promise.resolve({ default: MockGoogleCalendarService }), + office365calendar: Promise.resolve({ default: MockOffice365CalendarService }), + }, + }; +}); + // Local test runs sometime gets too slow const timeout = process.env.CI ? 5000 : 20000; @@ -136,7 +239,7 @@ describe("handleNewBooking", () => { ); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "GOOGLE_CALENDAR_EVENT_ID", uid: "MOCK_ID", @@ -314,7 +417,7 @@ describe("handleNewBooking", () => { ); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "GOOGLE_CALENDAR_EVENT_ID", uid: "MOCK_ID", @@ -501,7 +604,7 @@ describe("handleNewBooking", () => { }); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("office365calendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("office365calendar", { create: { id: "OFFICE_365_CALENDAR_EVENT_ID", uid: "MOCK_ID", @@ -562,7 +665,6 @@ describe("handleNewBooking", () => { uid: "OFFICE_365_CALENDAR_EVENT_ID", meetingId: "OFFICE_365_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", // Verify Delegation credential was used delegationCredentialId: delegationCredential.id, }, @@ -700,7 +802,7 @@ describe("handleNewBooking", () => { }); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "GOOGLE_CALENDAR_EVENT_ID", uid: "MOCK_ID", @@ -856,7 +958,7 @@ describe("handleNewBooking", () => { ); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "GOOGLE_CALENDAR_EVENT_ID", uid: "MOCK_ID", diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index 1d75498d55..10c0cd461e 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -160,7 +160,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", }, @@ -214,7 +214,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], iCalUID: createdBooking.iCalUID, @@ -326,7 +325,7 @@ describe("handleNewBooking", () => { }); // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "GOOGLE_CALENDAR_EVENT_ID", uid: "MOCK_ID", @@ -379,7 +378,6 @@ describe("handleNewBooking", () => { uid: "GOOGLE_CALENDAR_EVENT_ID", meetingId: "GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], iCalUID: createdBooking.iCalUID, @@ -489,7 +487,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", id: "GOOGLE_CALENDAR_EVENT_ID", @@ -542,7 +540,6 @@ describe("handleNewBooking", () => { uid: "GOOGLE_CALENDAR_EVENT_ID", meetingId: "GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], iCalUID: createdBooking.iCalUID, @@ -775,7 +772,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", id: "GOOGLE_CALENDAR_EVENT_ID", @@ -829,7 +826,6 @@ describe("handleNewBooking", () => { uid: "GOOGLE_CALENDAR_EVENT_ID", meetingId: "GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], iCalUID: createdBooking.iCalUID, @@ -934,7 +930,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("applecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("applecalendar", { create: { uid: "MOCK_ID", id: "MOCKED_APPLE_CALENDAR_EVENT_ID", @@ -989,7 +985,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_APPLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_APPLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -3191,7 +3186,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", }, @@ -3245,7 +3240,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], iCalUID: createdBooking.iCalUID, diff --git a/packages/features/bookings/lib/handleNewBooking/test/handleNewRecurringBooking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/handleNewRecurringBooking.test.ts index 193fd5176b..0b1b2b6f06 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/handleNewRecurringBooking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/handleNewRecurringBooking.test.ts @@ -117,7 +117,7 @@ describe("handleNewRecurringBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -195,7 +195,6 @@ describe("handleNewRecurringBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); diff --git a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts index 704bccb02f..4ead5d4050 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts @@ -119,7 +119,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -193,7 +193,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -338,7 +337,7 @@ describe("handleNewBooking", () => { }, }); - mockCalendarToHaveNoBusySlots("googlecalendar", { + await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -472,7 +471,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -545,7 +544,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -691,7 +689,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -764,7 +762,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -949,7 +946,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -1104,7 +1101,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -1325,7 +1322,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index a5ed4e9134..6103d8b892 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -157,7 +157,7 @@ describe("handleNewBooking", () => { metadataLookupKey: "dailyvideo", }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", }, @@ -396,7 +396,7 @@ describe("handleNewBooking", () => { metadataLookupKey: "dailyvideo", }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", }, @@ -785,7 +785,7 @@ describe("handleNewBooking", () => { metadataLookupKey: "dailyvideo", }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", }, @@ -1013,7 +1013,7 @@ describe("handleNewBooking", () => { metadataLookupKey: "dailyvideo", }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", }, @@ -1261,7 +1261,7 @@ describe("handleNewBooking", () => { }) ); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", }, @@ -1478,7 +1478,7 @@ describe("handleNewBooking", () => { metadataLookupKey: "dailyvideo", }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", }, @@ -1720,7 +1720,7 @@ describe("handleNewBooking", () => { metadataLookupKey: "dailyvideo", }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "MOCK_ID", }, @@ -2908,7 +2908,7 @@ describe("handleNewBooking", () => { metadataLookupKey: "dailyvideo", }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "NEW_EVENT_ID" }, update: { uid: "UPDATED_EVENT_ID" }, }); diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-reschedule-destination-calendar.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-reschedule-destination-calendar.test.ts index d57f848b50..d8728ea4aa 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-reschedule-destination-calendar.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-reschedule-destination-calendar.test.ts @@ -176,7 +176,7 @@ describe("handleNewBooking", () => { }, }); - const organizerCalendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const organizerCalendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { update: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_2", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID_2", diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts index 90a70966b0..193b6dfa1e 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts @@ -152,7 +152,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -201,7 +201,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -335,7 +334,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -457,7 +456,7 @@ describe("handleNewBooking", () => { url: `http://mock-dailyvideo.example.com/meeting-1`, }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -503,7 +502,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -682,7 +680,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -732,7 +730,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -917,7 +914,7 @@ describe("handleNewBooking", () => { }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -967,7 +964,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -1412,7 +1408,7 @@ describe("handleNewBooking", () => { url: `http://mock-dailyvideo.example.com/meeting-1`, }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -1457,7 +1453,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -1613,7 +1608,7 @@ describe("handleNewBooking", () => { url: `http://mock-zoomvideo.example.com/meeting-1`, }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -1657,7 +1652,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -1819,7 +1813,7 @@ describe("handleNewBooking", () => { url: `http://mock-dailyvideo.example.com/meeting-1`, }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -1864,7 +1858,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); @@ -2013,7 +2006,7 @@ describe("handleNewBooking", () => { url: `http://mock-dailyvideo.example.com/meeting-1`, }, }); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -2059,7 +2052,6 @@ describe("handleNewBooking", () => { uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", meetingPassword: "MOCK_PASSWORD", - meetingUrl: "https://UNUSED_URL", }, ], }); diff --git a/packages/features/ee/round-robin/roundRobinDeleteEvents.test.ts b/packages/features/ee/round-robin/roundRobinDeleteEvents.test.ts index 807ffbfa81..24011a8f23 100644 --- a/packages/features/ee/round-robin/roundRobinDeleteEvents.test.ts +++ b/packages/features/ee/round-robin/roundRobinDeleteEvents.test.ts @@ -108,7 +108,7 @@ describe("roundRobinReassignment test", () => { }) ); - const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + const calendarMock = await mockCalendarToHaveNoBusySlots("googlecalendar", { create: { uid: "NEW_EVENT_ID" }, update: { uid: "UPDATED_EVENT_ID" }, }); diff --git a/packages/lib/CalendarManager.ts b/packages/lib/CalendarManager.ts index 76868fba4a..a6c2a84189 100644 --- a/packages/lib/CalendarManager.ts +++ b/packages/lib/CalendarManager.ts @@ -35,7 +35,7 @@ export const getCalendarCredentials = (credentials: Array app.type.endsWith("_calendar")) .flatMap((app) => { const credentials = app.credentials.flatMap((credential) => { - const calendar = getCalendar(credential); + const calendar = () => getCalendar(credential); return app.variant === "calendar" ? [{ integration: app, credential, calendar }] : []; }); @@ -71,7 +71,18 @@ export const getConnectedCalendars = async ( delegationCredentialId, }; } - const cals = await calendar.listCalendars(); + const calendarInstance = await calendar(); + if (!calendarInstance) { + return { + integration: safeToSendIntegration, + credentialId, + delegationCredentialId, + error: { + message: "Could not get calendar instance", + }, + }; + } + const cals = await calendarInstance.listCalendars(); const calendars = sortBy( cals.map((cal: IntegrationCalendar) => { if (cal.externalId === destinationCalendarExternalId) destinationCalendar = cal; diff --git a/packages/lib/getCalendarsEvents.test.ts b/packages/lib/getCalendarsEvents.test.ts index bf6a1d7937..6252e11999 100644 --- a/packages/lib/getCalendarsEvents.test.ts +++ b/packages/lib/getCalendarsEvents.test.ts @@ -19,6 +19,83 @@ vi.mock("./crypto", () => ({ 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, @@ -87,10 +164,12 @@ describe("getCalendarsEvents", () => { }, userId: 808, teamId: null, + user: { + email: "test@example.com", + }, appId: "exampleApp", invalid: false, delegationCredentialId: null, - user: null, }; }); @@ -339,7 +418,9 @@ describe("getCalendarsEventsWithTimezones", () => { }, userId: 808, teamId: null, - user: null, + user: { + email: "test@example.com", + }, appId: "exampleApp", invalid: false, delegationCredentialId: null, diff --git a/packages/lib/payment/handlePayment.ts b/packages/lib/payment/handlePayment.ts index 536ecf73cd..74e72dc5c3 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -1,6 +1,5 @@ import type { AppCategories, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; import type { CompleteEventType } from "@calcom/prisma/zod"; import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils"; @@ -52,6 +51,8 @@ const handlePayment = async ({ }) => { if (isDryRun) return null; const key = paymentAppCredentials?.app?.dirName; + + const appStore = await import("@calcom/app-store").then((m) => m.default); if (!isKeyOf(appStore, key)) { console.warn(`key: ${key} is not a valid key in appStore`); return null; diff --git a/packages/lib/videoClient.ts b/packages/lib/videoClient.ts index 1b4c51b505..3c474e4311 100644 --- a/packages/lib/videoClient.ts +++ b/packages/lib/videoClient.ts @@ -1,7 +1,6 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; -import appStore from "@calcom/app-store"; import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys"; import { DailyLocationType } from "@calcom/app-store/locations"; import { sendBrokenIntegrationEmail } from "@calcom/emails"; @@ -27,6 +26,8 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise m.default); const appImportFn = appStore[appName as keyof typeof appStore]; // Static Link Video Apps don't exist in packages/app-store/index.ts(it's manually maintained at the moment) and they aren't needed there anyway. diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index e705a54530..60a986574e 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -1041,7 +1041,6 @@ export class AvailableSlotsService { queuedFormResponseId, }); } - const { qualifiedRRHosts, allFallbackRRHosts, fixedHosts } = await this.dependencies.qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({ eventType, diff --git a/setupVitest.ts b/setupVitest.ts index 5fdcc0be08..96f7aefab3 100644 --- a/setupVitest.ts +++ b/setupVitest.ts @@ -10,3 +10,46 @@ const fetchMocker = createFetchMock(vi); fetchMocker.enableMocks(); expect.extend(matchers); + +class MockExchangeCalendarService implements CalendarService { + constructor() {} + async createEvent() { + return { + uid: "mock", + id: "mock", + password: "", + type: "", + url: "", + additionalInfo: {}, + }; + } + async updateEvent() { + return { + uid: "mock", + id: "mock", + password: "", + type: "", + url: "", + additionalInfo: {}, + }; + } + async deleteEvent() {} + async getAvailability() { + return []; + } + async listCalendars() { + return []; + } +} + +vi.mock("@calcom/exchangecalendar/lib/CalendarService", () => ({ + default: MockExchangeCalendarService, +})); + +vi.mock("@calcom/exchange2013calendar/lib/CalendarService", () => ({ + default: MockExchangeCalendarService, +})); + +vi.mock("@calcom/exchange2016calendar/lib/CalendarService", () => ({ + default: MockExchangeCalendarService, +})); diff --git a/yarn.lock b/yarn.lock index ef36aed2ac..fdbc4dfd91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3066,32 +3066,6 @@ __metadata: languageName: unknown linkType: soft -"@calcom/exchange2013calendar@workspace:packages/app-store/exchange2013calendar": - version: 0.0.0-use.local - resolution: "@calcom/exchange2013calendar@workspace:packages/app-store/exchange2013calendar" - dependencies: - "@calcom/lib": "*" - "@calcom/prisma": "*" - "@calcom/types": "*" - "@calcom/ui": "*" - ews-javascript-api: ^0.11.0 - react-hook-form: ^7.43.3 - languageName: unknown - linkType: soft - -"@calcom/exchange2016calendar@workspace:packages/app-store/exchange2016calendar": - version: 0.0.0-use.local - resolution: "@calcom/exchange2016calendar@workspace:packages/app-store/exchange2016calendar" - dependencies: - "@calcom/lib": "*" - "@calcom/prisma": "*" - "@calcom/types": "*" - "@calcom/ui": "*" - ews-javascript-api: ^0.11.0 - react-hook-form: ^7.43.3 - languageName: unknown - linkType: soft - "@calcom/exchangecalendar@workspace:packages/app-store/exchangecalendar": version: 0.0.0-use.local resolution: "@calcom/exchangecalendar@workspace:packages/app-store/exchangecalendar"