More tests

payment-booking-requested-webhook
Hariom Balhara 2023-09-06 16:31:57 +05:30
parent 3fe6d8b781
commit 82bea948ea
2 changed files with 484 additions and 197 deletions

View File

@ -19,7 +19,7 @@ declare global {
expect.extend({ expect.extend({
toHaveEmail( toHaveEmail(
testEmails: ReturnType<Fixtures["emails"]["get"]>, emails: Fixtures["emails"],
expectedEmail: { expectedEmail: {
//TODO: Support email HTML parsing to target specific elements //TODO: Support email HTML parsing to target specific elements
htmlToContain?: string; htmlToContain?: string;
@ -27,7 +27,7 @@ expect.extend({
}, },
to: string to: string
) { ) {
const testEmail = testEmails.find((email) => email.to === to); const testEmail = emails.get().find((email) => email.to.includes(to));
if (!testEmail) { if (!testEmail) {
return { return {
pass: false, pass: false,
@ -111,3 +111,124 @@ export function expectBookingToBeInDatabase(booking: Partial<Booking> & Pick<Boo
}); });
expect(actualBooking).toEqual(expect.objectContaining(booking)); expect(actualBooking).toEqual(expect.objectContaining(booking));
} }
export function expectSuccessfulBookingCreationEmails({
emails,
organizer,
booker,
}: {
emails: Fixtures["emails"];
organizer: { email: string; name: string };
booker: { email: string; name: string };
}) {
expect(emails).toHaveEmail(
{
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${organizer.email}`,
},
`${organizer.email}`
);
expect(emails).toHaveEmail(
{
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${booker.name} <${booker.email}>`,
},
`${booker.name} <${booker.email}>`
);
}
export function expectAwaitingPaymentEmails({
emails,
organizer,
booker,
}: {
emails: Fixtures["emails"];
organizer: { email: string; name: string };
booker: { email: string; name: string };
}) {
expect(emails).toHaveEmail(
{
htmlToContain: "<title>awaiting_payment_subject</title>",
to: `${booker.name} <${booker.email}>`,
},
`${booker.email}`
);
}
export function expectBookingRequestedEmails({
emails,
organizer,
booker,
}: {
emails: Fixtures["emails"];
organizer: { email: string; name: string };
booker: { email: string; name: string };
}) {
expect(emails).toHaveEmail(
{
htmlToContain: "<title>event_awaiting_approval_subject</title>",
to: `${organizer.email}`,
},
`${organizer.email}`
);
expect(emails).toHaveEmail(
{
htmlToContain: "<title>booking_submitted_subject</title>",
to: `${booker.email}`,
},
`${booker.email}`
);
}
export function expectBookingRequestedWebhookToHaveBeenFired({
organizer,
booker,
location,
subscriberUrl,
paidEvent,
}: {
organizer: { email: string; name: string };
booker: { email: string; name: string };
subscriberUrl: string;
location: string;
paidEvent?: boolean;
}) {
// There is an inconsistency in the way we send the data to the webhook for paid events and unpaid events. Fix that and then remove this if statement.
if (!paidEvent) {
expectWebhookToHaveBeenCalledWith(subscriberUrl, {
triggerEvent: "BOOKING_REQUESTED",
payload: {
metadata: {
// In a Pending Booking Request, we don't send the video call url
},
responses: {
name: { label: "your_name", value: booker.name },
email: { label: "email_address", value: booker.email },
location: {
label: "location",
value: { optionValue: "", value: location },
},
},
},
});
} else {
expectWebhookToHaveBeenCalledWith(subscriberUrl, {
triggerEvent: "BOOKING_REQUESTED",
payload: {
metadata: {
// In a Pending Booking Request, we don't send the video call url
},
responses: {
name: { label: "name", value: booker.name },
email: { label: "email", value: booker.email },
location: {
label: "location",
value: { optionValue: "", value: location },
},
},
},
});
}
}

View File

@ -31,17 +31,20 @@ import {
import { import {
expectWorkflowToBeTriggered, expectWorkflowToBeTriggered,
expectSuccessfulBookingCreationEmails,
expectBookingToBeInDatabase, expectBookingToBeInDatabase,
expectWebhookToHaveBeenCalledWith, expectWebhookToHaveBeenCalledWith,
expectAwaitingPaymentEmails,
expectBookingRequestedEmails,
expectBookingRequestedWebhookToHaveBeenFired,
} from "@calcom/web/test/utils/bookingScenario/expects"; } from "@calcom/web/test/utils/bookingScenario/expects";
type CustomNextApiRequest = NextApiRequest & Request; type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response; type CustomNextApiResponse = NextApiResponse & Response;
// Local test runs sometime gets too slow // Local test runs sometime gets too slow
const timeout = process.env.CI ? 5000 : 20000; const timeout = process.env.CI ? 5000 : 20000;
describe.sequential("handleNewBooking", () => { describe("handleNewBooking", () => {
beforeEach(() => { beforeEach(() => {
// Required to able to generate token in email in some cases // Required to able to generate token in email in some cases
process.env.CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui"; process.env.CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui";
@ -52,12 +55,12 @@ describe.sequential("handleNewBooking", () => {
fetchMock.resetMocks(); fetchMock.resetMocks();
}); });
describe.sequential("Frontend:", () => { describe("Frontend:", () => {
test( test(
`should create a successful booking with Cal Video(Daily Video) if no explicit location is provided `should create a successful booking with Cal Video(Daily Video) if no explicit location is provided
1. Should create a booking in the database 1. Should create a booking in the database
2. Should send emails to the booker as well as organizer 2. Should send emails to the booker as well as organizer
3. Should trigger BOOKING_CREATED webhook 3. Should trigger BOOKING_CREATED webhook
`, `,
async ({ emails }) => { async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
@ -75,23 +78,7 @@ describe.sequential("handleNewBooking", () => {
selectedCalendars: [TestData.selectedCalendars.google], selectedCalendars: [TestData.selectedCalendars.google],
}); });
const mockBookingData = getMockRequestDataForBooking({ createBookingScenario(getScenarioData({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const scenarioData = getScenarioData({
webhooks: [ webhooks: [
{ {
userId: organizer.id, userId: organizer.id,
@ -116,14 +103,29 @@ describe.sequential("handleNewBooking", () => {
], ],
organizer, organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
}); }));
mockSuccessfulVideoMeetingCreation({ mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo", metadataLookupKey: "dailyvideo",
}); });
mockCalendarToHaveNoBusySlots("googlecalendar"); mockCalendarToHaveNoBusySlots("googlecalendar");
createBookingScenario(scenarioData);
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const createdBooking = await handleNewBooking(req); const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({ expect(createdBooking.responses).toContain({
@ -145,15 +147,8 @@ describe.sequential("handleNewBooking", () => {
expectWorkflowToBeTriggered(); expectWorkflowToBeTriggered();
const testEmails = emails.get(); expectSuccessfulBookingCreationEmails({booker, organizer, emails})
expect(testEmails).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${organizer.email}`,
}, `${organizer.email}`);
expect(testEmails).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${booker.name} <${booker.email}>`,
}, `${booker.name} <${booker.email}>`);
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_CREATED", triggerEvent: "BOOKING_CREATED",
payload: { payload: {
@ -174,14 +169,17 @@ describe.sequential("handleNewBooking", () => {
timeout timeout
); );
describe('Event Type that requires confirmation', () => {
test( test(
`should submit a booking request for event requiring confirmation `should create a booking request for event that requires confirmation
1. Should create a booking in the database with status PENDING 1. Should create a booking in the database with status PENDING
2. Should send emails to the booker as well as organizer for booking request and awaiting approval 2. Should send emails to the booker as well as organizer for booking request and awaiting approval
3. Should trigger BOOKING_REQUESTED webhook 3. Should trigger BOOKING_REQUESTED webhook
`, `,
async ({ emails }) => { async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const subscriberUrl = "http://my-webhook.example.com"
const booker = getBooker({ const booker = getBooker({
email: "booker@example.com", email: "booker@example.com",
name: "Booker", name: "Booker",
@ -196,28 +194,12 @@ describe.sequential("handleNewBooking", () => {
selectedCalendars: [TestData.selectedCalendars.google], selectedCalendars: [TestData.selectedCalendars.google],
}); });
const mockBookingData = getMockRequestDataForBooking({ createBookingScenario(getScenarioData({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const scenarioData = getScenarioData({
webhooks: [ webhooks: [
{ {
userId: organizer.id, userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"], eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com", subscriberUrl ,
active: true, active: true,
eventTypeId: 1, eventTypeId: 1,
appId: null, appId: null,
@ -238,14 +220,29 @@ describe.sequential("handleNewBooking", () => {
], ],
organizer, organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
}); }));
mockSuccessfulVideoMeetingCreation({ mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo", metadataLookupKey: "dailyvideo",
}); });
mockCalendarToHaveNoBusySlots("googlecalendar"); mockCalendarToHaveNoBusySlots("googlecalendar");
createBookingScenario(scenarioData);
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const createdBooking = await handleNewBooking(req); const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({ expect(createdBooking.responses).toContain({
@ -267,22 +264,127 @@ describe.sequential("handleNewBooking", () => {
expectWorkflowToBeTriggered(); expectWorkflowToBeTriggered();
const testEmails = emails.get(); expectBookingRequestedEmails({
expect(testEmails).toHaveEmail({ booker,
htmlToContain: "<title>event_awaiting_approval_subject</title>", organizer,
to: `${organizer.email}`, emails,
}, `${organizer.email}`); })
expect(testEmails).toHaveEmail({ expectBookingRequestedWebhookToHaveBeenFired({
htmlToContain: "<title>booking_submitted_subject</title>", booker,
to: `${booker.email}`, organizer,
}, `${booker.email}`); location: "integrations:daily",
subscriberUrl
})
},
timeout
);
test(
`should create a booking for event that requires confirmation based on a booking notice duration threshold, if threshold is not met
1. Should create a booking in the database with status ACCEPTED
2. Should send emails to the booker as well as organizer
3. Should trigger BOOKING_CREATED webhook
`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
createBookingScenario(getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
requiresConfirmation: true,
metadata: {
requiresConfirmationThreshold: {
time: 30,
unit: "minutes"
}
},
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
}));
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
});
expect(createdBooking).toContain({
location: "integrations:daily",
});
expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: createdBooking.id!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
});
expectWorkflowToBeTriggered();
expectSuccessfulBookingCreationEmails({booker, organizer, emails})
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_REQUESTED", triggerEvent: "BOOKING_CREATED",
payload: { payload: {
metadata: { metadata: {
// In a Pending Booking Request, we don't send the video call url videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
}, },
responses: { responses: {
name: { label: "your_name", value: "Booker" }, name: { label: "your_name", value: "Booker" },
@ -298,6 +400,118 @@ describe.sequential("handleNewBooking", () => {
timeout timeout
); );
test(
`should create a booking for event that requires confirmation based on a booking notice duration threshold, if threshold IS MET
1. Should create a booking in the database with status PENDING
2. Should send emails to the booker as well as organizer for booking request and awaiting approval
3. Should trigger BOOKING_REQUESTED webhook
`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const subscriberUrl = "http://my-webhook.example.com";
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
createBookingScenario(getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl,
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
requiresConfirmation: true,
metadata: {
requiresConfirmationThreshold: {
time: 120,
unit: "hours"
}
},
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
}));
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
});
expect(createdBooking).toContain({
location: "integrations:daily",
});
expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: createdBooking.id!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING,
});
expectWorkflowToBeTriggered();
expectBookingRequestedEmails({booker, organizer, emails})
expectBookingRequestedWebhookToHaveBeenFired({
booker,
organizer,
location: "integrations:daily",
subscriberUrl
})
},
timeout
);
})
test( test(
`if booking with Cal Video(Daily Video) fails, booking creation fails with uncaught error`, `if booking with Cal Video(Daily Video) fails, booking creation fails with uncaught error`,
async ({}) => { async ({}) => {
@ -307,21 +521,8 @@ describe.sequential("handleNewBooking", () => {
name: "Booker", name: "Booker",
}); });
const organizer = TestData.users.example; const organizer = TestData.users.example;
const { req } = createMockNextJsRequest({
method: "POST",
body: getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
}),
});
const scenarioData = { createBookingScenario( {
hosts: [], hosts: [],
eventTypes: [ eventTypes: [
{ {
@ -345,14 +546,27 @@ describe.sequential("handleNewBooking", () => {
}, },
], ],
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
}; });
mockErrorOnVideoMeetingCreation({ mockErrorOnVideoMeetingCreation({
metadataLookupKey: "dailyvideo", metadataLookupKey: "dailyvideo",
}); });
mockCalendarToHaveNoBusySlots("googlecalendar"); mockCalendarToHaveNoBusySlots("googlecalendar");
createBookingScenario(scenarioData); const { req } = createMockNextJsRequest({
method: "POST",
body: getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
}),
});
try { try {
await handleNewBooking(req); await handleNewBooking(req);
@ -381,22 +595,7 @@ describe.sequential("handleNewBooking", () => {
credentials: [getZoomAppCredential()], credentials: [getZoomAppCredential()],
selectedCalendars: [TestData.selectedCalendars.google], selectedCalendars: [TestData.selectedCalendars.google],
}); });
createBookingScenario(getScenarioData({
const { req } = createMockNextJsRequest({
method: "POST",
body: getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:zoom" },
},
},
}),
});
const bookingScenario = getScenarioData({
organizer, organizer,
eventTypes: [ eventTypes: [
{ {
@ -421,24 +620,27 @@ describe.sequential("handleNewBooking", () => {
appId: null, appId: null,
}, },
], ],
}); }));
createBookingScenario(bookingScenario);
mockSuccessfulVideoMeetingCreation({ mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "zoomvideo", metadataLookupKey: "zoomvideo",
}); });
const { req } = createMockNextJsRequest({
method: "POST",
body: getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:zoom" },
},
},
}),
});
await handleNewBooking(req); await handleNewBooking(req);
const testEmails = emails.get(); expectSuccessfulBookingCreationEmails({booker, organizer, emails})
expect(testEmails).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${organizer.email}`,
}, `${organizer.email}`);
expect(testEmails).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${booker.name} <${booker.email}>`,
}, `${booker.name} <${booker.email}>`);
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_CREATED", triggerEvent: "BOOKING_CREATED",
@ -459,6 +661,7 @@ describe.sequential("handleNewBooking", () => {
}, },
timeout timeout
); );
describe("Paid Events", ()=>{ describe("Paid Events", ()=>{
test( test(
`Event Type that doesn't require confirmation `Event Type that doesn't require confirmation
@ -483,23 +686,7 @@ describe.sequential("handleNewBooking", () => {
selectedCalendars: [TestData.selectedCalendars.google], selectedCalendars: [TestData.selectedCalendars.google],
}); });
const mockBookingData = getMockRequestDataForBooking({ createBookingScenario(getScenarioData({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const scenarioData = getScenarioData({
webhooks: [ webhooks: [
{ {
userId: organizer.id, userId: organizer.id,
@ -517,6 +704,7 @@ describe.sequential("handleNewBooking", () => {
requiresConfirmation: false, requiresConfirmation: false,
metadata: { metadata: {
apps: { apps: {
// EventType is connected to stripe.
stripe: { stripe: {
price: 100, price: 100,
enabled: true, enabled: true,
@ -538,31 +726,40 @@ describe.sequential("handleNewBooking", () => {
TestData.apps["daily-video"], TestData.apps["daily-video"],
TestData.apps["stripe-payment"], TestData.apps["stripe-payment"],
], ],
}); }));
mockSuccessfulVideoMeetingCreation({ mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo", metadataLookupKey: "dailyvideo",
}); });
const { paymentUid, externalId } = mockPaymentApp({ const { paymentUid, externalId } = mockPaymentApp({
metadataLookupKey: "stripe", metadataLookupKey: "stripe",
appStoreLookupKey: "stripepayment", appStoreLookupKey: "stripepayment",
}); });
mockCalendarToHaveNoBusySlots("googlecalendar"); mockCalendarToHaveNoBusySlots("googlecalendar");
createBookingScenario(scenarioData); const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const createdBooking = await handleNewBooking(req); const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({ expect(createdBooking.responses).toContain({
email: booker.email, email: booker.email,
name: booker.name, name: booker.name,
}); });
expect(createdBooking).toContain({ expect(createdBooking).toContain({
location: "integrations:daily", location: "integrations:daily",
paymentUid: paymentUid, paymentUid: paymentUid,
}); });
expectBookingToBeInDatabase({ expectBookingToBeInDatabase({
description: "", description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -570,16 +767,8 @@ describe.sequential("handleNewBooking", () => {
eventTypeId: mockBookingData.eventTypeId, eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING, status: BookingStatus.PENDING,
}); });
expectWorkflowToBeTriggered(); expectWorkflowToBeTriggered();
expectAwaitingPaymentEmails({organizer, booker, emails})
const testEmails = emails.get();
expect(testEmails).toHaveEmail({
htmlToContain: "<title>awaiting_payment_subject</title>",
to: `${booker.name} <${booker.email}>`,
}, `${booker.name} <${booker.email}>`);
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_PAYMENT_INITIATED", triggerEvent: "BOOKING_PAYMENT_INITIATED",
payload: { payload: {
@ -596,7 +785,9 @@ describe.sequential("handleNewBooking", () => {
}, },
}, },
}); });
const {webhookResponse} = await mockPaymentSuccessWebhookFromStripe({externalId}); const {webhookResponse} = await mockPaymentSuccessWebhookFromStripe({externalId});
expect(webhookResponse?.statusCode).toBe(200) expect(webhookResponse?.statusCode).toBe(200)
expectBookingToBeInDatabase({ expectBookingToBeInDatabase({
description: "", description: "",
@ -605,7 +796,6 @@ describe.sequential("handleNewBooking", () => {
eventTypeId: mockBookingData.eventTypeId, eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED, status: BookingStatus.ACCEPTED,
}); });
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_CREATED", triggerEvent: "BOOKING_CREATED",
payload: { payload: {
@ -636,6 +826,7 @@ describe.sequential("handleNewBooking", () => {
`, `,
async ({ emails }) => { async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const subscriberUrl = "http://my-webhook.example.com"
const booker = getBooker({ const booker = getBooker({
email: "booker@example.com", email: "booker@example.com",
name: "Booker", name: "Booker",
@ -650,12 +841,12 @@ describe.sequential("handleNewBooking", () => {
selectedCalendars: [TestData.selectedCalendars.google], selectedCalendars: [TestData.selectedCalendars.google],
}); });
const scenarioData = getScenarioData({ createBookingScenario(getScenarioData({
webhooks: [ webhooks: [
{ {
userId: organizer.id, userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"], eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com", subscriberUrl,
active: true, active: true,
eventTypeId: 1, eventTypeId: 1,
appId: null, appId: null,
@ -689,8 +880,16 @@ describe.sequential("handleNewBooking", () => {
TestData.apps["daily-video"], TestData.apps["daily-video"],
TestData.apps["stripe-payment"], TestData.apps["stripe-payment"],
], ],
}));
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
}); });
createBookingScenario(scenarioData); const { paymentUid, externalId } = mockPaymentApp({
metadataLookupKey: "stripe",
appStoreLookupKey: "stripepayment",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
const mockBookingData = getMockRequestDataForBooking({ const mockBookingData = getMockRequestDataForBooking({
data: { data: {
@ -702,34 +901,20 @@ describe.sequential("handleNewBooking", () => {
}, },
}, },
}); });
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
});
const { paymentUid, externalId } = mockPaymentApp({
metadataLookupKey: "stripe",
appStoreLookupKey: "stripepayment",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
const { req } = createMockNextJsRequest({ const { req } = createMockNextJsRequest({
method: "POST", method: "POST",
body: mockBookingData, body: mockBookingData,
}); });
const createdBooking = await handleNewBooking(req); const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({ expect(createdBooking.responses).toContain({
email: booker.email, email: booker.email,
name: booker.name, name: booker.name,
}); });
expect(createdBooking).toContain({ expect(createdBooking).toContain({
location: "integrations:daily", location: "integrations:daily",
paymentUid: paymentUid, paymentUid: paymentUid,
}); });
expectBookingToBeInDatabase({ expectBookingToBeInDatabase({
description: "", description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -737,16 +922,8 @@ describe.sequential("handleNewBooking", () => {
eventTypeId: mockBookingData.eventTypeId, eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING, status: BookingStatus.PENDING,
}); });
expectWorkflowToBeTriggered(); expectWorkflowToBeTriggered();
expectAwaitingPaymentEmails({organizer, booker, emails})
const testEmails = emails.get();
expect(testEmails).toHaveEmail({
htmlToContain: "<title>awaiting_payment_subject</title>",
to: `${booker.name} <${booker.email}>`,
}, `${booker.name} <${booker.email}>`);
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_PAYMENT_INITIATED", triggerEvent: "BOOKING_PAYMENT_INITIATED",
payload: { payload: {
@ -764,10 +941,9 @@ describe.sequential("handleNewBooking", () => {
}, },
}); });
const {webhookResponse} = await mockPaymentSuccessWebhookFromStripe({externalId}); const {webhookResponse} = await mockPaymentSuccessWebhookFromStripe({externalId});
expect(webhookResponse?.statusCode).toBe(200) expect(webhookResponse?.statusCode).toBe(200)
expectBookingToBeInDatabase({ expectBookingToBeInDatabase({
description: "", description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -775,23 +951,13 @@ describe.sequential("handleNewBooking", () => {
eventTypeId: mockBookingData.eventTypeId, eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING, status: BookingStatus.PENDING,
}); });
expectBookingRequestedWebhookToHaveBeenFired({
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { booker,
triggerEvent: "BOOKING_REQUESTED", organizer,
payload: { location: "integrations:daily",
// FIXME: File this bug and link ticket here. This is a bug in the code. metadata must be sent here like other BOOKING_CREATED webhook subscriberUrl,
metadata: null, paidEvent: true
location: "integrations:daily", })
responses: {
name: { label: "name", value: "Booker" },
email: { label: "email", value: "booker@example.com" },
location: {
label: "location",
value: { optionValue: "", value: "integrations:daily" },
},
},
},
});
}, },
timeout timeout
); );