From d12a5c58836165064db5f08c619e18cd1189e556 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 17 Oct 2023 16:46:24 +0530 Subject: [PATCH] fix: `videoCallUrl` not updating when rescheduling with a broken Calendar integration (#11923) --- apps/web/package.json | 1 + .../utils/bookingScenario/bookingScenario.ts | 38 ++++- .../web/test/utils/bookingScenario/expects.ts | 150 ++++++++++++------ packages/emails/src/components/AppsStatus.tsx | 4 +- .../features/bookings/lib/handleNewBooking.ts | 90 +++++++---- .../test/fresh-booking.test.ts | 12 +- .../handleNewBooking/test/reschedule.test.ts | 51 +++++- .../collective-scheduling.test.ts | 6 +- yarn.lock | 26 ++- 9 files changed, 277 insertions(+), 101 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 3a5951e886..ab5d5396e1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -166,6 +166,7 @@ "env-cmd": "^10.1.0", "module-alias": "^2.2.2", "msw": "^0.42.3", + "node-html-parser": "^6.1.10", "postcss": "^8.4.18", "tailwindcss": "^3.3.1", "tailwindcss-animate": "^1.0.6", diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 51d286df99..4b023574ad 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -2,7 +2,7 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store"; import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n"; import prismock from "../../../../../tests/libs/__mocks__/prisma"; -import type { BookingReference, Attendee } from "@prisma/client"; +import type { BookingReference, Attendee, Booking } from "@prisma/client"; import type { Prisma } from "@prisma/client"; import type { WebhookTriggerEvents } from "@prisma/client"; import type Stripe from "stripe"; @@ -102,7 +102,7 @@ export type InputEventType = { schedule?: InputUser["schedules"][number]; } & Partial>; -type InputBooking = { +type WhiteListedBookingProps = { id?: number; uid?: string; userId?: number; @@ -118,6 +118,8 @@ type InputBooking = { })[]; }; +type InputBooking = Partial> & WhiteListedBookingProps; + export const Timezones = { "+5:30": "Asia/Kolkata", "+6:00": "Asia/Dhaka", @@ -1203,3 +1205,35 @@ export const enum BookingLocations { CalVideo = "integrations:daily", ZoomVideo = "integrations:zoom", } + +const getMockAppStatus = ({ + slug, + failures, + success, +}: { + slug: string; + failures: number; + success: number; +}) => { + const foundEntry = Object.entries(appStoreMetadata).find(([, app]) => { + return app.slug === slug; + }); + if (!foundEntry) { + throw new Error("App not found for the slug"); + } + const foundApp = foundEntry[1]; + return { + appName: foundApp.slug, + type: foundApp.type, + failures, + success, + errors: [], + }; +}; +export const getMockFailingAppStatus = ({ slug }: { slug: string }) => { + return getMockAppStatus({ slug, failures: 1, success: 0 }); +}; + +export const getMockPassingAppStatus = ({ slug }: { slug: string }) => { + return getMockAppStatus({ slug, failures: 0, success: 1 }); +}; diff --git a/apps/web/test/utils/bookingScenario/expects.ts b/apps/web/test/utils/bookingScenario/expects.ts index 18e2e92fc4..48a0165417 100644 --- a/apps/web/test/utils/bookingScenario/expects.ts +++ b/apps/web/test/utils/bookingScenario/expects.ts @@ -1,6 +1,7 @@ import prismaMock from "../../../../../tests/libs/__mocks__/prisma"; import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalendar } from "@prisma/client"; +import { parse } from "node-html-parser"; import ical from "node-ical"; import { expect } from "vitest"; import "vitest-fetch-mock"; @@ -8,6 +9,7 @@ import "vitest-fetch-mock"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { BookingStatus } from "@calcom/prisma/enums"; +import type { AppsStatus } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { Fixtures } from "@calcom/web/test/fixtures/fixtures"; @@ -19,14 +21,14 @@ declare global { interface Matchers { toHaveEmail( expectedEmail: { - //TODO: Support email HTML parsing to target specific elements - htmlToContain?: string; + title?: string; to: string; noIcs?: true; ics?: { filename: string; iCalUID: string; }; + appsStatus?: AppsStatus[]; }, to: string ): R; @@ -38,21 +40,23 @@ expect.extend({ toHaveEmail( emails: Fixtures["emails"], expectedEmail: { - //TODO: Support email HTML parsing to target specific elements - htmlToContain?: string; + title?: string; to: string; ics: { filename: string; iCalUID: string; }; noIcs: true; + appsStatus: AppsStatus[]; }, to: string ) { + const { isNot } = this; const testEmail = emails.get().find((email) => email.to.includes(to)); const emailsToLog = emails .get() .map((email) => ({ to: email.to, html: email.html, ics: email.icalEvent })); + if (!testEmail) { logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog })); return { @@ -63,48 +67,93 @@ expect.extend({ const ics = testEmail.icalEvent; const icsObject = ics?.content ? ical.sync.parseICS(ics?.content) : null; - let isHtmlContained = true; let isToAddressExpected = true; const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true; const isIcsUIDExpected = expectedEmail.ics ? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null) : true; + const emailDom = parse(testEmail.html); - if (expectedEmail.htmlToContain) { - isHtmlContained = testEmail.html.includes(expectedEmail.htmlToContain); + const actualEmailContent = { + title: emailDom.querySelector("title")?.innerText, + subject: emailDom.querySelector("subject")?.innerText, + }; + + const expectedEmailContent = { + title: expectedEmail.title, + }; + + const isEmailContentMatched = this.equals( + actualEmailContent, + expect.objectContaining(expectedEmailContent) + ); + + if (!isEmailContentMatched) { + logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog })); + + return { + pass: false, + message: () => `Email content ${isNot ? "is" : "is not"} matching`, + actual: actualEmailContent, + expected: expectedEmailContent, + }; } isToAddressExpected = expectedEmail.to === testEmail.to; - - if (!isHtmlContained || !isToAddressExpected) { + if (!isToAddressExpected) { logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog })); + return { + pass: false, + message: () => `To address ${isNot ? "is" : "is not"} matching`, + actual: testEmail.to, + expected: expectedEmail.to, + }; + } + + if (!expectedEmail.noIcs && !isIcsFilenameExpected) { + return { + pass: false, + actual: ics?.filename, + expected: expectedEmail.ics.filename, + message: () => `ICS Filename ${isNot ? "is" : "is not"} matching`, + }; + } + + if (!expectedEmail.noIcs && !isIcsUIDExpected) { + return { + pass: false, + actual: JSON.stringify(icsObject), + expected: expectedEmail.ics.iCalUID, + message: () => `Expected ICS UID ${isNot ? "is" : "isn't"} present in actual`, + }; + } + + if (expectedEmail.appsStatus) { + const actualAppsStatus = emailDom.querySelectorAll('[data-testid="appsStatus"] li').map((li) => { + return li.innerText.trim(); + }); + const expectedAppStatus = expectedEmail.appsStatus.map((appStatus) => { + if (appStatus.success && !appStatus.failures) { + return `${appStatus.appName} ✅`; + } + return `${appStatus.appName} ❌`; + }); + + const isAppsStatusCorrect = this.equals(actualAppsStatus, expectedAppStatus); + + if (!isAppsStatusCorrect) { + return { + pass: false, + actual: actualAppsStatus, + expected: expectedAppStatus, + message: () => `AppsStatus ${isNot ? "is" : "isn't"} matching`, + }; + } } return { - pass: - isHtmlContained && - isToAddressExpected && - (expectedEmail.noIcs ? true : isIcsFilenameExpected && isIcsUIDExpected), - message: () => { - if (!isHtmlContained) { - return `Email HTML is not as expected. Expected:"${expectedEmail.htmlToContain}" isn't contained in "${testEmail.html}"`; - } - - if (!isToAddressExpected) { - return `Email To address is not as expected. Expected:${expectedEmail.to} isn't equal to ${testEmail.to}`; - } - - if (!isIcsFilenameExpected) { - return `ICS Filename is not as expected. Expected:${expectedEmail.ics.filename} isn't equal to ${ics?.filename}`; - } - - if (!isIcsUIDExpected) { - return `ICS UID is not as expected. Expected:${ - expectedEmail.ics.iCalUID - } isn't present in ${JSON.stringify(icsObject)}`; - } - throw new Error("Unknown error"); - }, + pass: true, + message: () => `Email ${isNot ? "is" : "isn't"} correct`, }; }, }); @@ -139,9 +188,10 @@ export function expectWebhookToHaveBeenCalledWith( const parsedBody = JSON.parse((body as string) || "{}"); expect(parsedBody.triggerEvent).toBe(data.triggerEvent); + if (parsedBody.payload.metadata?.videoCallUrl) { parsedBody.payload.metadata.videoCallUrl = parsedBody.payload.metadata.videoCallUrl - ? parsedBody.payload.metadata.videoCallUrl.replace(/\/video\/[a-zA-Z0-9]{22}/, "/video/DYNAMIC_UID") + ? parsedBody.payload.metadata.videoCallUrl : parsedBody.payload.metadata.videoCallUrl; } if (data.payload) { @@ -195,7 +245,7 @@ export function expectSuccessfulBookingCreationEmails({ }) { expect(emails).toHaveEmail( { - htmlToContain: "confirmed_event_type_subject", + title: "confirmed_event_type_subject", to: `${organizer.email}`, ics: { filename: "event.ics", @@ -207,7 +257,7 @@ export function expectSuccessfulBookingCreationEmails({ expect(emails).toHaveEmail( { - htmlToContain: "confirmed_event_type_subject", + title: "confirmed_event_type_subject", to: `${booker.name} <${booker.email}>`, ics: { filename: "event.ics", @@ -221,7 +271,7 @@ export function expectSuccessfulBookingCreationEmails({ otherTeamMembers.forEach((otherTeamMember) => { expect(emails).toHaveEmail( { - htmlToContain: "confirmed_event_type_subject", + title: "confirmed_event_type_subject", // Don't know why but organizer and team members of the eventType don'thave their name here like Booker to: `${otherTeamMember.email}`, ics: { @@ -238,7 +288,7 @@ export function expectSuccessfulBookingCreationEmails({ guests.forEach((guest) => { expect(emails).toHaveEmail( { - htmlToContain: "confirmed_event_type_subject", + title: "confirmed_event_type_subject", to: `${guest.email}`, ics: { filename: "event.ics", @@ -261,7 +311,7 @@ export function expectBrokenIntegrationEmails({ // Broken Integration email is only sent to the Organizer expect(emails).toHaveEmail( { - htmlToContain: "broken_integration", + title: "broken_integration", to: `${organizer.email}`, // No ics goes in case of broken integration email it seems // ics: { @@ -274,7 +324,7 @@ export function expectBrokenIntegrationEmails({ // expect(emails).toHaveEmail( // { - // htmlToContain: "confirmed_event_type_subject", + // title: "confirmed_event_type_subject", // to: `${booker.name} <${booker.email}>`, // }, // `${booker.name} <${booker.email}>` @@ -294,7 +344,7 @@ export function expectCalendarEventCreationFailureEmails({ }) { expect(emails).toHaveEmail( { - htmlToContain: "broken_integration", + title: "broken_integration", to: `${organizer.email}`, ics: { filename: "event.ics", @@ -306,7 +356,7 @@ export function expectCalendarEventCreationFailureEmails({ expect(emails).toHaveEmail( { - htmlToContain: "calendar_event_creation_failure_subject", + title: "calendar_event_creation_failure_subject", to: `${booker.name} <${booker.email}>`, ics: { filename: "event.ics", @@ -322,27 +372,30 @@ export function expectSuccessfulBookingRescheduledEmails({ organizer, booker, iCalUID, + appsStatus, }: { emails: Fixtures["emails"]; organizer: { email: string; name: string }; booker: { email: string; name: string }; iCalUID: string; + appsStatus: AppsStatus[]; }) { expect(emails).toHaveEmail( { - htmlToContain: "event_type_has_been_rescheduled_on_time_date", + title: "event_type_has_been_rescheduled_on_time_date", to: `${organizer.email}`, ics: { filename: "event.ics", iCalUID, }, + appsStatus, }, `${organizer.email}` ); expect(emails).toHaveEmail( { - htmlToContain: "event_type_has_been_rescheduled_on_time_date", + title: "event_type_has_been_rescheduled_on_time_date", to: `${booker.name} <${booker.email}>`, ics: { filename: "event.ics", @@ -362,7 +415,7 @@ export function expectAwaitingPaymentEmails({ }) { expect(emails).toHaveEmail( { - htmlToContain: "awaiting_payment_subject", + title: "awaiting_payment_subject", to: `${booker.name} <${booker.email}>`, noIcs: true, }, @@ -381,7 +434,7 @@ export function expectBookingRequestedEmails({ }) { expect(emails).toHaveEmail( { - htmlToContain: "event_awaiting_approval_subject", + title: "event_awaiting_approval_subject", to: `${organizer.email}`, noIcs: true, }, @@ -390,7 +443,7 @@ export function expectBookingRequestedEmails({ expect(emails).toHaveEmail( { - htmlToContain: "booking_submitted_subject", + title: "booking_submitted_subject", to: `${booker.email}`, noIcs: true, }, @@ -509,6 +562,7 @@ export function expectBookingRescheduledWebhookToHaveBeenFired({ location, subscriberUrl, videoCallUrl, + payload, }: { organizer: { email: string; name: string }; booker: { email: string; name: string }; @@ -516,10 +570,12 @@ export function expectBookingRescheduledWebhookToHaveBeenFired({ location: string; paidEvent?: boolean; videoCallUrl?: string; + payload?: Record; }) { expectWebhookToHaveBeenCalledWith(subscriberUrl, { triggerEvent: "BOOKING_RESCHEDULED", payload: { + ...payload, metadata: { ...(videoCallUrl ? { videoCallUrl } : null), }, diff --git a/packages/emails/src/components/AppsStatus.tsx b/packages/emails/src/components/AppsStatus.tsx index d7b35960e6..a42d8aefa9 100644 --- a/packages/emails/src/components/AppsStatus.tsx +++ b/packages/emails/src/components/AppsStatus.tsx @@ -1,4 +1,4 @@ -import { TFunction } from "next-i18next"; +import type { TFunction } from "next-i18next"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -11,7 +11,7 @@ export const AppsStatus = (props: { calEvent: CalendarEvent; t: TFunction }) => +
    {props.calEvent.appsStatus.map((status) => (
  • {status.appName}{" "} diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 5b7df766ba..fc5f0b367f 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1431,7 +1431,7 @@ async function handler( metadata.hangoutLink = updatedEvent.hangoutLink; metadata.conferenceData = updatedEvent.conferenceData; metadata.entryPoints = updatedEvent.entryPoints; - handleAppsStatus(results, newBooking); + evt.appsStatus = handleAppsStatus(results, newBooking); } } } @@ -2105,7 +2105,7 @@ async function handler( booking: (Booking & { appsStatus?: AppsStatus[] }) | null ) { // Taking care of apps status - const resultStatus: AppsStatus[] = results.map((app) => ({ + let resultStatus: AppsStatus[] = results.map((app) => ({ appName: app.appName, type: app.type, success: app.success ? 1 : 0, @@ -2118,8 +2118,7 @@ async function handler( if (booking !== null) { booking.appsStatus = resultStatus; } - evt.appsStatus = resultStatus; - return; + return resultStatus; } // From down here we can assume reqAppsStatus is not undefined anymore // Other status exist, so this is the last booking of a series, @@ -2134,7 +2133,8 @@ async function handler( } return prev; }, {} as { [key: string]: AppsStatus }); - evt.appsStatus = Object.values(calcAppsStatus); + resultStatus = Object.values(calcAppsStatus); + return resultStatus; } let videoCallUrl; @@ -2174,44 +2174,42 @@ async function handler( results = updateManager.results; referencesToCreate = updateManager.referencesToCreate; - if (results.length > 0 && results.some((res) => !res.success)) { + const isThereAnIntegrationError = results && results.some((res) => !res.success); + if (isThereAnIntegrationError) { const error = { errorCode: "BookingReschedulingMeetingFailed", message: "Booking Rescheduling failed", }; - loggerWithEventDetails.error(`Booking ${organizerUser.name} failed`, safeStringify({ error, results })); + loggerWithEventDetails.error( + `EventManager.create failure in some of the integrations ${organizerUser.username}`, + safeStringify({ error, results }) + ); } else { - const metadata: AdditionalInformation = {}; const calendarResult = results.find((result) => result.type.includes("_calendar")); evt.iCalUID = Array.isArray(calendarResult?.updatedEvent) ? calendarResult?.updatedEvent[0]?.iCalUID : calendarResult?.updatedEvent?.iCalUID || undefined; + } - if (results.length) { - // TODO: Handle created event metadata more elegantly - const [updatedEvent] = Array.isArray(results[0].updatedEvent) - ? results[0].updatedEvent - : [results[0].updatedEvent]; - if (updatedEvent) { - metadata.hangoutLink = updatedEvent.hangoutLink; - metadata.conferenceData = updatedEvent.conferenceData; - metadata.entryPoints = updatedEvent.entryPoints; - handleAppsStatus(results, booking); - videoCallUrl = metadata.hangoutLink || videoCallUrl || updatedEvent?.url; - } - } - if (noEmail !== true && isConfirmedByDefault) { - const copyEvent = cloneDeep(evt); - loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation"); - await sendRescheduledEmails({ - ...copyEvent, - additionalInformation: metadata, - additionalNotes, // Resets back to the additionalNote input and not the override value - cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email - }); - } + const { metadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({ + results, + }); + + videoCallUrl = _videoCallUrl; + evt.appsStatus = handleAppsStatus(results, booking); + + // If there is an integration error, we don't send successful rescheduling email, instead broken integration email should be sent that are handled by either CalendarManager or videoClient + if (noEmail !== true && isConfirmedByDefault && !isThereAnIntegrationError) { + const copyEvent = cloneDeep(evt); + loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation"); + await sendRescheduledEmails({ + ...copyEvent, + additionalInformation: metadata, + additionalNotes, // Resets back to the additionalNote input and not the override value + cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email + }); } // If it's not a reschedule, doesn't require confirmation and there's no price, // Create a booking @@ -2234,7 +2232,7 @@ async function handler( }; loggerWithEventDetails.error( - `Failure in creating events in some of the integrations ${organizerUser.username} failed`, + `EventManager.create failure in some of the integrations ${organizerUser.username}`, safeStringify({ error, results }) ); } else { @@ -2296,7 +2294,7 @@ async function handler( metadata.hangoutLink = results[0].createdEvent?.hangoutLink; metadata.conferenceData = results[0].createdEvent?.conferenceData; metadata.entryPoints = results[0].createdEvent?.entryPoints; - handleAppsStatus(results, booking); + evt.appsStatus = handleAppsStatus(results, booking); videoCallUrl = metadata.hangoutLink || organizerOrFirstDynamicGroupMemberDefaultLocationUrl || videoCallUrl; } @@ -2374,6 +2372,7 @@ async function handler( videoCallUrl: getVideoCallUrlFromCalEvent(evt), } : undefined; + const webhookData = { ...evt, ...eventTypeInfo, @@ -2558,6 +2557,31 @@ async function handler( export default handler; +function getVideoCallDetails({ + results, +}: { + results: EventResult[]; +}) { + const firstVideoResult = results.find((result) => result.type.includes("_video")); + const metadata: AdditionalInformation = {}; + let updatedVideoEvent = null; + + if (firstVideoResult && firstVideoResult.success) { + updatedVideoEvent = Array.isArray(firstVideoResult.updatedEvent) + ? firstVideoResult.updatedEvent[0] + : firstVideoResult.updatedEvent; + + if (updatedVideoEvent) { + metadata.hangoutLink = updatedVideoEvent.hangoutLink; + metadata.conferenceData = updatedVideoEvent.conferenceData; + metadata.entryPoints = updatedVideoEvent.entryPoints; + } + } + const videoCallUrl = metadata.hangoutLink || updatedVideoEvent?.url; + + return { videoCallUrl, metadata, updatedVideoEvent }; +} + function getRequiresConfirmationFlags({ eventType, bookingStartTime, 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 59ae2809e4..0d75bc4bfd 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -206,7 +206,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -353,7 +353,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -499,7 +499,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -760,7 +760,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -1447,7 +1447,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl, - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -1883,7 +1883,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, paidEvent: true, }); }, diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index f82f804008..fc023bc88e 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -21,6 +21,8 @@ import { BookingLocations, getMockBookingReference, getMockBookingAttendee, + getMockFailingAppStatus, + getMockPassingAppStatus, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; import { expectWorkflowToBeTriggered, @@ -103,6 +105,9 @@ describe("handleNewBooking", () => { status: BookingStatus.ACCEPTED, startTime: `${plus1DateString}T05:00:00.000Z`, endTime: `${plus1DateString}T05:15:00.000Z`, + metadata: { + videoCallUrl: "https://existing-daily-video-call-url.example.com", + }, references: [ { type: appStoreMetadata.dailyvideo.type, @@ -254,6 +259,10 @@ describe("handleNewBooking", () => { organizer, emails, iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + appsStatus: [ + getMockPassingAppStatus({ slug: appStoreMetadata.dailyvideo.slug }), + getMockPassingAppStatus({ slug: appStoreMetadata.googlecalendar.slug }), + ], }); expectBookingRescheduledWebhookToHaveBeenFired({ @@ -261,7 +270,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -464,7 +473,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -525,6 +534,9 @@ describe("handleNewBooking", () => { status: BookingStatus.ACCEPTED, startTime: `${plus1DateString}T05:00:00.000Z`, endTime: `${plus1DateString}T05:15:00.000Z`, + metadata: { + videoCallUrl: "https://existing-daily-video-call-url.example.com", + }, references: [ { type: appStoreMetadata.dailyvideo.type, @@ -551,6 +563,9 @@ describe("handleNewBooking", () => { ); const _calendarMock = mockCalendarToCrashOnUpdateEvent("googlecalendar"); + const _videoMock = mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); const mockBookingData = getMockRequestDataForBooking({ data: { @@ -559,7 +574,7 @@ describe("handleNewBooking", () => { responses: { email: booker.email, name: booker.name, - location: { optionValue: "", value: "New York" }, + location: { optionValue: "", value: BookingLocations.CalVideo }, }, }, }); @@ -577,16 +592,27 @@ describe("handleNewBooking", () => { }, to: { description: "", - location: "New York", + location: "integrations:daily", // eslint-disable-next-line @typescript-eslint/no-non-null-assertion uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, + metadata: { + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking?.uid}`, + }, responses: expect.objectContaining({ email: booker.email, name: booker.name, }), + // Booking References still use the original booking's references - Not sure how intentional it is. references: [ + { + type: appStoreMetadata.dailyvideo.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + }, { type: appStoreMetadata.googlecalendar.type, // A reference is still created in case of event creation failure, with nullish values. Not sure what's the purpose for this. @@ -607,8 +633,18 @@ describe("handleNewBooking", () => { expectBookingRescheduledWebhookToHaveBeenFired({ booker, organizer, - location: "New York", + location: "integrations:daily", subscriberUrl: "http://my-webhook.example.com", + payload: { + uid: createdBooking.uid, + appsStatus: [ + expect.objectContaining(getMockPassingAppStatus({ slug: appStoreMetadata.dailyvideo.slug })), + expect.objectContaining( + getMockFailingAppStatus({ slug: appStoreMetadata.googlecalendar.slug }) + ), + ], + }, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking?.uid}`, }); }, timeout @@ -1048,7 +1084,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -1497,12 +1533,13 @@ describe("handleNewBooking", () => { emails, iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", }); + expectBookingRescheduledWebhookToHaveBeenFired({ booker, organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout 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 09e98d14dd..4eedf072bd 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 @@ -225,7 +225,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -537,7 +537,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout @@ -854,7 +854,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, }); }, timeout diff --git a/yarn.lock b/yarn.lock index 09fcaa1514..acad17e7a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4576,6 +4576,7 @@ __metadata: next-i18next: ^13.2.2 next-seo: ^6.0.0 next-themes: ^0.2.0 + node-html-parser: ^6.1.10 nodemailer: ^6.7.8 otplib: ^12.0.1 postcss: ^8.4.18 @@ -18514,6 +18515,19 @@ __metadata: languageName: node linkType: hard +"css-select@npm:^5.1.0": + version: 5.1.0 + resolution: "css-select@npm:5.1.0" + dependencies: + boolbase: ^1.0.0 + css-what: ^6.1.0 + domhandler: ^5.0.2 + domutils: ^3.0.1 + nth-check: ^2.0.1 + checksum: 2772c049b188d3b8a8159907192e926e11824aea525b8282981f72ba3f349cf9ecd523fdf7734875ee2cb772246c22117fc062da105b6d59afe8dcd5c99c9bda + languageName: node + linkType: hard + "css-to-react-native@npm:^3.0.0": version: 3.0.0 resolution: "css-to-react-native@npm:3.0.0" @@ -18532,7 +18546,7 @@ __metadata: languageName: node linkType: hard -"css-what@npm:^6.0.1": +"css-what@npm:^6.0.1, css-what@npm:^6.1.0": version: 6.1.0 resolution: "css-what@npm:6.1.0" checksum: b975e547e1e90b79625918f84e67db5d33d896e6de846c9b584094e529f0c63e2ab85ee33b9daffd05bff3a146a1916bec664e18bb76dd5f66cbff9fc13b2bbe @@ -29541,6 +29555,16 @@ __metadata: languageName: node linkType: hard +"node-html-parser@npm:^6.1.10": + version: 6.1.10 + resolution: "node-html-parser@npm:6.1.10" + dependencies: + css-select: ^5.1.0 + he: 1.2.0 + checksum: 927f6a38b3b1cbc042bce609e24fb594d3b1e0f1067ffb416a925fa5a699e907be31980f349e094d55bab706dc16a71958b08f8dcdab62faf7b12013f29442bc + languageName: node + linkType: hard + "node-ical@npm:^0.16.1": version: 0.16.1 resolution: "node-ical@npm:0.16.1"