fix: `videoCallUrl` not updating when rescheduling with a broken Calendar integration (#11923)

This commit is contained in:
Hariom Balhara 2023-10-17 16:46:24 +05:30 committed by GitHub
parent 7a014761dc
commit d12a5c5883
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 277 additions and 101 deletions

View File

@ -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",

View File

@ -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<Omit<Prisma.EventTypeCreateInput, "users" | "schedule">>;
type InputBooking = {
type WhiteListedBookingProps = {
id?: number;
uid?: string;
userId?: number;
@ -118,6 +118,8 @@ type InputBooking = {
})[];
};
type InputBooking = Partial<Omit<Booking, keyof WhiteListedBookingProps>> & 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 });
};

View File

@ -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<R> {
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: "<title>confirmed_event_type_subject</title>",
title: "confirmed_event_type_subject",
to: `${organizer.email}`,
ics: {
filename: "event.ics",
@ -207,7 +257,7 @@ export function expectSuccessfulBookingCreationEmails({
expect(emails).toHaveEmail(
{
htmlToContain: "<title>confirmed_event_type_subject</title>",
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: "<title>confirmed_event_type_subject</title>",
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: "<title>confirmed_event_type_subject</title>",
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: "<title>broken_integration</title>",
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: "<title>confirmed_event_type_subject</title>",
// 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: "<title>broken_integration</title>",
title: "broken_integration",
to: `${organizer.email}`,
ics: {
filename: "event.ics",
@ -306,7 +356,7 @@ export function expectCalendarEventCreationFailureEmails({
expect(emails).toHaveEmail(
{
htmlToContain: "<title>calendar_event_creation_failure_subject</title>",
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: "<title>event_type_has_been_rescheduled_on_time_date</title>",
title: "event_type_has_been_rescheduled_on_time_date",
to: `${organizer.email}`,
ics: {
filename: "event.ics",
iCalUID,
},
appsStatus,
},
`${organizer.email}`
);
expect(emails).toHaveEmail(
{
htmlToContain: "<title>event_type_has_been_rescheduled_on_time_date</title>",
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: "<title>awaiting_payment_subject</title>",
title: "awaiting_payment_subject",
to: `${booker.name} <${booker.email}>`,
noIcs: true,
},
@ -381,7 +434,7 @@ export function expectBookingRequestedEmails({
}) {
expect(emails).toHaveEmail(
{
htmlToContain: "<title>event_awaiting_approval_subject</title>",
title: "event_awaiting_approval_subject",
to: `${organizer.email}`,
noIcs: true,
},
@ -390,7 +443,7 @@ export function expectBookingRequestedEmails({
expect(emails).toHaveEmail(
{
htmlToContain: "<title>booking_submitted_subject</title>",
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<string, unknown>;
}) {
expectWebhookToHaveBeenCalledWith(subscriberUrl, {
triggerEvent: "BOOKING_RESCHEDULED",
payload: {
...payload,
metadata: {
...(videoCallUrl ? { videoCallUrl } : null),
},

View File

@ -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 }) =>
<Info
label={t("apps_status")}
description={
<ul style={{ lineHeight: "24px" }}>
<ul style={{ lineHeight: "24px" }} data-testid="appsStatus">
{props.calEvent.appsStatus.map((status) => (
<li key={status.type} style={{ fontWeight: 400 }}>
{status.appName}{" "}

View File

@ -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<AdditionalInformation & { url?: string | undefined; iCalUID?: string | undefined }>[];
}) {
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,

View File

@ -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,
});
},

View File

@ -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

View File

@ -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

View File

@ -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"