From 0e9b419a072fa1d3216937113bdd4859845b8642 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 22 May 2025 17:51:25 +0530 Subject: [PATCH] fix: Delegation Credentials - Use Cal Video if set as the default conferencing app (#21431) * fix google-meet being used instead of Default Cal Video preference if set * Review addressed --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- .../getLocationValuesForDb.ts | 12 +- .../test/delegation-credential.test.ts | 135 ++++++++++++++ .../test/getLocationValueForDb.test.ts | 173 ++++++++++++++++++ 3 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 packages/features/bookings/lib/handleNewBooking/test/getLocationValueForDb.test.ts diff --git a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts index 966b48d1e4..acbd66133c 100644 --- a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts +++ b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts @@ -15,7 +15,7 @@ const sortUsersByDynamicList = ( }); }; -const _getLocationValuesForDb = < +export const _getLocationValuesForDb = < TUser extends { username: string | null; metadata: Prisma.JsonValue; @@ -42,9 +42,15 @@ const _getLocationValuesForDb = < credentials: firstDynamicGroupMember.credentials, }); + const defaultConferencingApp = firstDynamicGroupMemberMetadata?.defaultConferencingApp; + + const hasMemberSetConferencingPreference = + !!defaultConferencingApp?.appSlug || !!defaultConferencingApp?.appLink; + firstDynamicGroupMemberDefaultLocationUrl = - firstDynamicGroupMemberMetadata?.defaultConferencingApp?.appLink || - firstDynamicGroupMemberDelegationCredentialConferencingAppLocation; + (hasMemberSetConferencingPreference + ? defaultConferencingApp?.appLink + : firstDynamicGroupMemberDelegationCredentialConferencingAppLocation) ?? null; locationBodyString = firstDynamicGroupMemberDefaultLocationUrl || locationBodyString; } 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 2adf102f79..46edb0d2fa 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts @@ -925,5 +925,140 @@ describe("handleNewBooking", () => { }, timeout ); + + test( + `should use Cal Video as the location if that is the default conferencing app set by the user. It must not use Google Meet coming from Delegation credential.`, + async () => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + + const org = await createOrganization({ + name: "Test Org", + slug: "testorg", + }); + + const payloadToMakePartOfOrganization = [ + { + membership: { + accepted: true, + role: MembershipRole.ADMIN, + }, + team: { + id: org.id, + name: "Test Org", + slug: "testorg", + }, + }, + ]; + + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const groupUser1 = getOrganizer({ + name: "group-user-1", + username: "group-user-1", + email: "group-user-1@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + selectedCalendars: [TestData.selectedCalendars.google], + teams: payloadToMakePartOfOrganization, + credentials: [], + destinationCalendar: TestData.selectedCalendars.google, + metadata: { + defaultConferencingApp: { + appSlug: "daily-video", + }, + }, + }); + + const groupUser2 = getOrganizer({ + name: "group-user-2", + username: "group-user-2", + email: "group-user-2@example.com", + id: 102, + schedules: [TestData.schedules.IstWorkHours], + selectedCalendars: [TestData.selectedCalendars.google], + teams: payloadToMakePartOfOrganization, + credentials: [], + destinationCalendar: TestData.selectedCalendars.google, + metadata: { + defaultConferencingApp: { + appSlug: "daily-video", + }, + }, + }); + + await createDelegationCredential(org.id); + + await createBookingScenario( + getScenarioData({ + eventTypes: [], + users: [groupUser1, groupUser2], + apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "GOOGLE_CALENDAR_EVENT_ID", + uid: "MOCK_ID", + appSpecificData: { + googleCalendar: { + hangoutLink: "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + }, + }, + }, + }); + + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + + const mockBookingData = getMockRequestDataForDynamicGroupBooking({ + data: { + start: `${plus1DateString}T05:00:00.000Z`, + end: `${plus1DateString}T05:30:00.000Z`, + eventTypeId: 0, + eventTypeSlug: "group-user-1+group-user-2", + user: "group-user-1+group-user-2", + responses: { + email: booker.email, + name: booker.name, + // location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); + + await expectBookingToBeInDatabase({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: null, + status: BookingStatus.ACCEPTED, + location: BookingLocations.CalVideo, + references: [ + { + type: appStoreMetadata.dailyvideo.type, + // Verify Delegation credential was not used + delegationCredentialId: null, + }, + ], + iCalUID: createdBooking.iCalUID, + }); + }, + timeout + ); }); }); diff --git a/packages/features/bookings/lib/handleNewBooking/test/getLocationValueForDb.test.ts b/packages/features/bookings/lib/handleNewBooking/test/getLocationValueForDb.test.ts new file mode 100644 index 0000000000..acef560d54 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/test/getLocationValueForDb.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect, vi } from "vitest"; + +import type { Prisma } from "@calcom/prisma/client"; +import type { CredentialForCalendarService } from "@calcom/types/Credential"; + +import { _getLocationValuesForDb } from "../getLocationValuesForDb"; + +vi.mock("@calcom/prisma/zod-utils", () => ({ + userMetadata: { parse: (metadata: any) => metadata }, +})); + +vi.mock("@calcom/lib/delegationCredential/server", () => ({ + getFirstDelegationConferencingCredentialAppLocation: ({ + credentials, + }: { + credentials: CredentialForCalendarService[]; + }) => (credentials[0] && (credentials[0] as any).__test__appLink) || null, +})); + +type TestUser = { + username: string | null; + metadata: Prisma.JsonValue; + credentials: CredentialForCalendarService[]; +}; + +describe("_getLocationValuesForDb", () => { + it("returns the provided location for single user bookings", () => { + const users: TestUser[] = [ + { + username: "alice", + metadata: {}, + credentials: [], + }, + ]; + const result = _getLocationValuesForDb({ + dynamicUserList: ["alice"], + users, + location: "https://meet.example.com/alice", + }); + expect(result.locationBodyString).toBe("https://meet.example.com/alice"); + expect(result.organizerOrFirstDynamicGroupMemberDefaultLocationUrl).toBeUndefined(); + }); + + it("uses the first group member's default conferencing appLink if set", () => { + const users: TestUser[] = [ + { + username: "bob", + metadata: { + defaultConferencingApp: { + appSlug: "zoom", + appLink: "https://zoom.us/bob", + }, + }, + credentials: [], + }, + { + username: "carol", + metadata: {}, + credentials: [], + }, + ]; + const result = _getLocationValuesForDb({ + dynamicUserList: ["bob", "carol"], + users, + location: "https://meet.example.com/group", + }); + expect(result.locationBodyString).toBe("https://zoom.us/bob"); + expect(result.organizerOrFirstDynamicGroupMemberDefaultLocationUrl).toBe("https://zoom.us/bob"); + }); + + it("uses the first group member's first delegation credential's conferencing app if no preference is set", () => { + const users: TestUser[] = [ + { + username: "dave", + metadata: {}, + credentials: [ + { + __test__appLink: "https://meet.google.com/dave", + } as unknown as CredentialForCalendarService, + ], + }, + { + username: "eve", + metadata: {}, + credentials: [], + }, + ]; + const result = _getLocationValuesForDb({ + dynamicUserList: ["dave", "eve"], + users, + location: "https://meet.example.com/group", + }); + expect(result.locationBodyString).toBe("https://meet.google.com/dave"); + expect(result.organizerOrFirstDynamicGroupMemberDefaultLocationUrl).toBe("https://meet.google.com/dave"); + }); + + it("falls back to provided location if no preferences or credentials are set", () => { + const users: TestUser[] = [ + { + username: "frank", + metadata: {}, + credentials: [], + }, + { + username: "grace", + metadata: {}, + credentials: [], + }, + ]; + const result = _getLocationValuesForDb({ + dynamicUserList: ["frank", "grace"], + users, + location: "https://meet.example.com/group", + }); + expect(result.locationBodyString).toBe("https://meet.example.com/group"); + expect(result.organizerOrFirstDynamicGroupMemberDefaultLocationUrl).toBeNull(); + }); + + it("falls back to provided location if preference is set but has no appLink", () => { + const users: TestUser[] = [ + { + username: "frank", + metadata: { + defaultConferencingApp: { + appSlug: "daily-video", + }, + }, + credentials: [], + }, + { + username: "grace", + metadata: {}, + credentials: [], + }, + ]; + const result = _getLocationValuesForDb({ + dynamicUserList: ["frank", "grace"], + users, + location: "https://meet.example.com/group", + }); + expect(result.locationBodyString).toBe("https://meet.example.com/group"); + expect(result.organizerOrFirstDynamicGroupMemberDefaultLocationUrl).toBeNull(); + }); + + it("sorts users according to dynamicUserList before picking the first member", () => { + const users: TestUser[] = [ + { + username: "zara", + metadata: { + defaultConferencingApp: { + appSlug: "teams", + appLink: "https://teams.microsoft.com/zara", + }, + }, + credentials: [], + }, + { + username: "yanni", + metadata: {}, + credentials: [], + }, + ]; + // yanni is first in dynamicUserList, but zara is first in users array + const result = _getLocationValuesForDb({ + dynamicUserList: ["yanni", "zara"], + users, + location: "https://meet.example.com/group", + }); + // After sorting, yanni is first, has no preference, so fallback to location + expect(result.locationBodyString).toBe("https://meet.example.com/group"); + expect(result.organizerOrFirstDynamicGroupMemberDefaultLocationUrl).toBeNull(); + }); +});