2023-09-06 19:23:53 +00:00
/ * *
* How to ensure that unmocked prisma queries aren ' t called ?
* /
2023-09-30 04:52:32 +00:00
import prismaMock from "../../../../tests/libs/__mocks__/prisma" ;
2023-09-06 19:23:53 +00:00
import type { Request , Response } from "express" ;
import type { NextApiRequest , NextApiResponse } from "next" ;
import { createMocks } from "node-mocks-http" ;
import { describe , expect , beforeEach } from "vitest" ;
import { WEBAPP_URL } from "@calcom/lib/constants" ;
2023-09-30 04:52:32 +00:00
import logger from "@calcom/lib/logger" ;
2023-09-06 19:23:53 +00:00
import { BookingStatus } from "@calcom/prisma/enums" ;
import { test } from "@calcom/web/test/fixtures/fixtures" ;
import {
createBookingScenario ,
getDate ,
getGoogleCalendarCredential ,
TestData ,
getOrganizer ,
getBooker ,
getScenarioData ,
getZoomAppCredential ,
2023-09-30 04:52:32 +00:00
enableEmailFeature ,
2023-09-06 19:23:53 +00:00
mockNoTranslations ,
mockErrorOnVideoMeetingCreation ,
mockSuccessfulVideoMeetingCreation ,
mockCalendarToHaveNoBusySlots ,
2023-09-30 04:52:32 +00:00
getStripeAppCredential ,
2023-09-06 19:23:53 +00:00
MockError ,
2023-09-30 04:52:32 +00:00
mockPaymentApp ,
mockPaymentSuccessWebhookFromStripe ,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario" ;
import {
expectWorkflowToBeTriggered ,
expectSuccessfulBookingCreationEmails ,
expectBookingToBeInDatabase ,
expectAwaitingPaymentEmails ,
expectBookingRequestedEmails ,
expectBookingRequestedWebhookToHaveBeenFired ,
expectBookingCreatedWebhookToHaveBeenFired ,
expectBookingPaymentIntiatedWebhookToHaveBeenFired ,
expectBookingRescheduledWebhookToHaveBeenFired ,
expectSuccessfulBookingRescheduledEmails ,
expectSuccessfulCalendarEventUpdationInCalendar ,
expectSuccessfulVideoMeetingUpdationInCalendar ,
} from "@calcom/web/test/utils/bookingScenario/expects" ;
2023-09-06 19:23:53 +00:00
type CustomNextApiRequest = NextApiRequest & Request ;
2023-09-30 04:52:32 +00:00
2023-09-06 19:23:53 +00:00
type CustomNextApiResponse = NextApiResponse & Response ;
// Local test runs sometime gets too slow
const timeout = process . env . CI ? 5000 : 20000 ;
2023-09-30 04:52:32 +00:00
describe ( "handleNewBooking" , ( ) = > {
2023-09-06 19:23:53 +00:00
beforeEach ( ( ) = > {
// Required to able to generate token in email in some cases
2023-09-30 04:52:32 +00:00
process . env . CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui" ;
process . env . STRIPE_WEBHOOK_SECRET = "MOCK_STRIPE_WEBHOOK_SECRET" ;
2023-09-06 19:23:53 +00:00
mockNoTranslations ( ) ;
2023-09-30 04:52:32 +00:00
// mockEnableEmailFeature();
enableEmailFeature ( ) ;
2023-09-06 19:23:53 +00:00
globalThis . testEmails = [ ] ;
fetchMock . resetMocks ( ) ;
} ) ;
2023-09-30 04:52:32 +00:00
describe ( "Fresh Booking:" , ( ) = > {
2023-09-06 19:23:53 +00:00
test (
` should create a successful booking with Cal Video(Daily Video) if no explicit location is provided
2023-09-30 04:52:32 +00:00
1 . Should create a booking in the database
2 . Should send emails to the booker as well as organizer
3 . Should trigger BOOKING_CREATED webhook
2023-09-06 19:23:53 +00:00
` ,
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 ] ,
} ) ;
2023-09-30 04:52:32 +00:00
await 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 ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
organizer ,
apps : [ TestData . apps [ "google-calendar" ] , TestData . apps [ "daily-video" ] ] ,
} )
) ;
mockSuccessfulVideoMeetingCreation ( {
metadataLookupKey : "dailyvideo" ,
videoMeetingData : {
id : "MOCK_ID" ,
password : "MOCK_PASS" ,
url : ` http://mock-dailyvideo.example.com ` ,
} ,
} ) ;
mockCalendarToHaveNoBusySlots ( "googlecalendar" , {
create : {
uid : "MOCK_ID" ,
} ,
} ) ;
2023-09-06 19:23:53 +00:00
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" ,
} ) ;
2023-09-30 04:52:32 +00:00
await expectBookingToBeInDatabase ( {
2023-09-06 19:23:53 +00:00
description : "" ,
2023-09-30 04:52:32 +00:00
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
2023-09-06 19:23:53 +00:00
status : BookingStatus.ACCEPTED ,
2023-09-30 04:52:32 +00:00
references : [
{
type : "daily_video" ,
uid : "MOCK_ID" ,
meetingId : "MOCK_ID" ,
meetingPassword : "MOCK_PASS" ,
meetingUrl : "http://mock-dailyvideo.example.com" ,
} ,
{
type : "google_calendar" ,
uid : "MOCK_ID" ,
meetingId : "MOCK_ID" ,
meetingPassword : "MOCK_PASSWORD" ,
meetingUrl : "https://UNUSED_URL" ,
} ,
] ,
2023-09-06 19:23:53 +00:00
} ) ;
expectWorkflowToBeTriggered ( ) ;
2023-09-30 04:52:32 +00:00
expectSuccessfulBookingCreationEmails ( { booker , organizer , emails } ) ;
expectBookingCreatedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl : "http://my-webhook.example.com" ,
videoCallUrl : ` ${ WEBAPP_URL } /video/DYNAMIC_UID ` ,
2023-09-06 19:23:53 +00:00
} ) ;
} ,
timeout
) ;
2023-09-30 04:52:32 +00:00
describe ( "Event Type that requires confirmation" , ( ) = > {
test (
` should create a booking request for event that requires confirmation
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
2023-09-06 19:23:53 +00:00
` ,
2023-09-30 04:52:32 +00:00
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" ,
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
const organizer = getOrganizer ( {
name : "Organizer" ,
email : "organizer@example.com" ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
credentials : [ getGoogleCalendarCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ) ;
const scenarioData = getScenarioData ( {
webhooks : [
{
userId : organizer.id ,
eventTriggers : [ "BOOKING_CREATED" ] ,
subscriberUrl ,
active : true ,
eventTypeId : 1 ,
appId : null ,
} ,
] ,
eventTypes : [
{
id : 1 ,
slotInterval : 45 ,
requiresConfirmation : true ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
organizer ,
apps : [ TestData . apps [ "google-calendar" ] , TestData . apps [ "daily-video" ] ] ,
} ) ;
await createBookingScenario ( scenarioData ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
mockSuccessfulVideoMeetingCreation ( {
metadataLookupKey : "dailyvideo" ,
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
mockCalendarToHaveNoBusySlots ( "googlecalendar" ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
const mockBookingData = getMockRequestDataForBooking ( {
data : {
2023-09-06 19:23:53 +00:00
eventTypeId : 1 ,
2023-09-30 04:52:32 +00:00
responses : {
email : booker.email ,
name : booker.name ,
location : { optionValue : "" , value : "integrations:daily" } ,
} ,
2023-09-06 19:23:53 +00:00
} ,
2023-09-30 04:52:32 +00:00
} ) ;
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" ,
} ) ;
await expectBookingToBeInDatabase ( {
description : "" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
status : BookingStatus.PENDING ,
} ) ;
expectWorkflowToBeTriggered ( ) ;
expectBookingRequestedEmails ( {
booker ,
organizer ,
emails ,
} ) ;
expectBookingRequestedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl ,
eventType : scenarioData.eventTypes [ 0 ] ,
} ) ;
} ,
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 subscriberUrl = "http://my-webhook.example.com" ;
const organizer = getOrganizer ( {
name : "Organizer" ,
email : "organizer@example.com" ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
credentials : [ getGoogleCalendarCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ) ;
await createBookingScenario (
getScenarioData ( {
webhooks : [
2023-09-06 19:23:53 +00:00
{
2023-09-30 04:52:32 +00:00
userId : organizer.id ,
eventTriggers : [ "BOOKING_CREATED" ] ,
subscriberUrl ,
active : true ,
eventTypeId : 1 ,
appId : null ,
} ,
] ,
eventTypes : [
{
id : 1 ,
slotInterval : 45 ,
requiresConfirmation : true ,
metadata : {
requiresConfirmationThreshold : {
time : 30 ,
unit : "minutes" ,
} ,
} ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
2023-09-06 19:23:53 +00:00
} ,
] ,
2023-09-30 04:52:32 +00:00
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" } ,
} ,
2023-09-06 19:23:53 +00:00
} ,
2023-09-30 04:52:32 +00:00
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
const { req } = createMockNextJsRequest ( {
method : "POST" ,
body : mockBookingData ,
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
const createdBooking = await handleNewBooking ( req ) ;
expect ( createdBooking . responses ) . toContain ( {
email : booker.email ,
name : booker.name ,
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
expect ( createdBooking ) . toContain ( {
location : "integrations:daily" ,
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
await expectBookingToBeInDatabase ( {
description : "" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
status : BookingStatus.ACCEPTED ,
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
expectWorkflowToBeTriggered ( ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
expectSuccessfulBookingCreationEmails ( { booker , organizer , emails } ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
expectBookingCreatedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl ,
videoCallUrl : ` ${ WEBAPP_URL } /video/DYNAMIC_UID ` ,
} ) ;
} ,
timeout
) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
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" ,
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
const organizer = getOrganizer ( {
name : "Organizer" ,
email : "organizer@example.com" ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
credentials : [ getGoogleCalendarCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ) ;
const scenarioData = 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" ] ] ,
} ) ;
await createBookingScenario ( scenarioData ) ;
mockSuccessfulVideoMeetingCreation ( {
metadataLookupKey : "dailyvideo" ,
} ) ;
mockCalendarToHaveNoBusySlots ( "googlecalendar" ) ;
const mockBookingData = getMockRequestDataForBooking ( {
data : {
eventTypeId : 1 ,
responses : {
email : booker.email ,
name : booker.name ,
location : { optionValue : "" , value : "integrations:daily" } ,
2023-09-06 19:23:53 +00:00
} ,
} ,
2023-09-30 04:52:32 +00:00
} ) ;
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" ,
} ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
await expectBookingToBeInDatabase ( {
description : "" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
status : BookingStatus.PENDING ,
} ) ;
expectWorkflowToBeTriggered ( ) ;
expectBookingRequestedEmails ( { booker , organizer , emails } ) ;
expectBookingRequestedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl ,
eventType : scenarioData.eventTypes [ 0 ] ,
} ) ;
} ,
timeout
) ;
} ) ;
// FIXME: We shouldn't throw error here, the behaviour should be fixed.
2023-09-06 19:23:53 +00:00
test (
` if booking with Cal Video(Daily Video) fails, booking creation fails with uncaught error ` ,
async ( { } ) = > {
const handleNewBooking = ( await import ( "@calcom/features/bookings/lib/handleNewBooking" ) ) . default ;
const booker = getBooker ( {
email : "booker@example.org" ,
name : "Booker" ,
} ) ;
const organizer = TestData . users . example ;
2023-09-30 04:52:32 +00:00
await createBookingScenario ( {
2023-09-06 19:23:53 +00:00
eventTypes : [
{
id : 1 ,
slotInterval : 45 ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
users : [
{
. . . organizer ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
credentials : [ getGoogleCalendarCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ,
] ,
apps : [ TestData . apps [ "google-calendar" ] , TestData . apps [ "daily-video" ] ] ,
2023-09-30 04:52:32 +00:00
} ) ;
2023-09-06 19:23:53 +00:00
mockErrorOnVideoMeetingCreation ( {
metadataLookupKey : "dailyvideo" ,
} ) ;
2023-09-30 04:52:32 +00:00
2023-09-06 19:23:53 +00:00
mockCalendarToHaveNoBusySlots ( "googlecalendar" ) ;
2023-09-30 04:52:32 +00:00
const { req } = createMockNextJsRequest ( {
method : "POST" ,
body : getMockRequestDataForBooking ( {
data : {
eventTypeId : 1 ,
responses : {
email : booker.email ,
name : booker.name ,
location : { optionValue : "" , value : "integrations:daily" } ,
} ,
} ,
} ) ,
} ) ;
2023-09-06 19:23:53 +00:00
try {
await handleNewBooking ( req ) ;
} catch ( e ) {
expect ( e ) . toBeInstanceOf ( MockError ) ;
expect ( ( e as { message : string } ) . message ) . toBe ( "Error creating Video meeting" ) ;
}
} ,
timeout
) ;
test (
` should create a successful booking with Zoom if used ` ,
async ( { emails } ) = > {
const handleNewBooking = ( await import ( "@calcom/features/bookings/lib/handleNewBooking" ) ) . default ;
2023-09-30 04:52:32 +00:00
const subscriberUrl = "http://my-webhook.example.com" ;
2023-09-06 19:23:53 +00:00
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 : [ getZoomAppCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ) ;
2023-09-30 04:52:32 +00:00
await createBookingScenario (
getScenarioData ( {
organizer ,
eventTypes : [
{
id : 1 ,
slotInterval : 45 ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
apps : [ TestData . apps [ "zoomvideo" ] ] ,
webhooks : [
{
userId : organizer.id ,
eventTriggers : [ "BOOKING_CREATED" ] ,
subscriberUrl ,
active : true ,
eventTypeId : 1 ,
appId : null ,
} ,
] ,
} )
) ;
mockSuccessfulVideoMeetingCreation ( {
metadataLookupKey : "zoomvideo" ,
} ) ;
2023-09-06 19:23:53 +00:00
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 ) ;
2023-09-30 04:52:32 +00:00
expectSuccessfulBookingCreationEmails ( { booker , organizer , emails } ) ;
2023-09-06 19:23:53 +00:00
2023-09-30 04:52:32 +00:00
expectBookingCreatedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:zoom" ,
subscriberUrl ,
videoCallUrl : "http://mock-zoomvideo.example.com" ,
2023-09-13 04:19:01 +00:00
} ) ;
} ,
timeout
) ;
test (
` should create a successful booking when location is provided as label of an option(Done for Organizer Address)
1 . Should create a booking in the database
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 ] ,
} ) ;
const mockBookingData = getMockRequestDataForBooking ( {
data : {
eventTypeId : 1 ,
responses : {
email : booker.email ,
name : booker.name ,
location : { optionValue : "" , value : "New York" } ,
} ,
} ,
} ) ;
const { req } = createMockNextJsRequest ( {
method : "POST" ,
body : mockBookingData ,
} ) ;
const scenarioData = 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 ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
organizer ,
apps : [ TestData . apps [ "google-calendar" ] , TestData . apps [ "daily-video" ] ] ,
} ) ;
mockCalendarToHaveNoBusySlots ( "googlecalendar" ) ;
2023-09-30 04:52:32 +00:00
await createBookingScenario ( scenarioData ) ;
2023-09-13 04:19:01 +00:00
const createdBooking = await handleNewBooking ( req ) ;
expect ( createdBooking . responses ) . toContain ( {
email : booker.email ,
name : booker.name ,
} ) ;
expect ( createdBooking ) . toContain ( {
location : "New York" ,
} ) ;
2023-09-30 04:52:32 +00:00
await expectBookingToBeInDatabase ( {
2023-09-13 04:19:01 +00:00
description : "" ,
2023-09-30 04:52:32 +00:00
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
2023-09-13 04:19:01 +00:00
status : BookingStatus.ACCEPTED ,
} ) ;
expectWorkflowToBeTriggered ( ) ;
2023-09-30 04:52:32 +00:00
expectSuccessfulBookingCreationEmails ( { booker , organizer , emails } ) ;
expectBookingCreatedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "New York" ,
subscriberUrl : "http://my-webhook.example.com" ,
2023-09-13 04:19:01 +00:00
} ) ;
2023-09-30 04:52:32 +00:00
} ,
timeout
) ;
describe ( "Paid Events" , ( ) = > {
test (
` Event Type that doesn't require confirmation
1 . Should create a booking in the database with status PENDING
2 . Should send email to the booker for Payment request
3 . Should trigger BOOKING_PAYMENT_INITIATED webhook
4 . Once payment is successful , 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 ( ) , getStripeAppCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ) ;
const scenarioData = getScenarioData ( {
webhooks : [
{
userId : organizer.id ,
eventTriggers : [ "BOOKING_CREATED" ] ,
subscriberUrl : "http://my-webhook.example.com" ,
active : true ,
eventTypeId : 1 ,
appId : null ,
} ,
] ,
eventTypes : [
{
id : 1 ,
title : "Paid Event" ,
description : "It's a test Paid Event" ,
slotInterval : 45 ,
requiresConfirmation : false ,
metadata : {
apps : {
// EventType is connected to stripe.
stripe : {
price : 100 ,
enabled : true ,
currency : "inr" /*, credentialId: 57*/ ,
} ,
} ,
} ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
organizer ,
apps : [
TestData . apps [ "google-calendar" ] ,
TestData . apps [ "daily-video" ] ,
TestData . apps [ "stripe-payment" ] ,
] ,
} ) ;
await createBookingScenario ( scenarioData ) ;
mockSuccessfulVideoMeetingCreation ( {
metadataLookupKey : "dailyvideo" ,
} ) ;
const { paymentUid , externalId } = mockPaymentApp ( {
metadataLookupKey : "stripe" ,
appStoreLookupKey : "stripepayment" ,
} ) ;
mockCalendarToHaveNoBusySlots ( "googlecalendar" ) ;
const mockBookingData = getMockRequestDataForBooking ( {
data : {
eventTypeId : 1 ,
responses : {
email : booker.email ,
name : booker.name ,
location : { optionValue : "" , value : "integrations:daily" } ,
} ,
2023-09-13 04:19:01 +00:00
} ,
2023-09-30 04:52:32 +00:00
} ) ;
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" ,
paymentUid : paymentUid ,
} ) ;
await expectBookingToBeInDatabase ( {
description : "" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
status : BookingStatus.PENDING ,
} ) ;
expectWorkflowToBeTriggered ( ) ;
expectAwaitingPaymentEmails ( { organizer , booker , emails } ) ;
expectBookingPaymentIntiatedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl : "http://my-webhook.example.com" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
paymentId : createdBooking.paymentId ! ,
} ) ;
const { webhookResponse } = await mockPaymentSuccessWebhookFromStripe ( { externalId } ) ;
logger . info ( "webhookResponse" , webhookResponse ) ;
expect ( webhookResponse ? . statusCode ) . toBe ( 200 ) ;
await expectBookingToBeInDatabase ( {
description : "" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
status : BookingStatus.ACCEPTED ,
} ) ;
expectBookingCreatedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl : "http://my-webhook.example.com" ,
videoCallUrl : ` ${ WEBAPP_URL } /video/DYNAMIC_UID ` ,
paidEvent : true ,
} ) ;
} ,
timeout
) ;
// TODO: We should introduce a new state BOOKING.PAYMENT_PENDING that can clearly differentiate b/w pending confirmation(stuck on Organizer) and pending payment(stuck on booker)
test (
` Event Type that requires confirmation
1 . Should create a booking in the database with status PENDING
2 . Should send email to the booker for Payment request
3 . Should trigger BOOKING_PAYMENT_INITIATED webhook
4 . Once payment is successful , should trigger BOOKING_REQUESTED webhook
5 . Booking should still stay in pending state
` ,
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 ( ) , getStripeAppCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ) ;
const scenarioData = getScenarioData ( {
webhooks : [
{
userId : organizer.id ,
eventTriggers : [ "BOOKING_CREATED" ] ,
subscriberUrl ,
active : true ,
eventTypeId : 1 ,
appId : null ,
2023-09-13 04:19:01 +00:00
} ,
2023-09-30 04:52:32 +00:00
] ,
eventTypes : [
{
id : 1 ,
slotInterval : 45 ,
requiresConfirmation : true ,
metadata : {
apps : {
stripe : {
price : 100 ,
enabled : true ,
currency : "inr" /*, credentialId: 57*/ ,
} ,
} ,
} ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
organizer ,
apps : [
TestData . apps [ "google-calendar" ] ,
TestData . apps [ "daily-video" ] ,
TestData . apps [ "stripe-payment" ] ,
] ,
} ) ;
await createBookingScenario ( scenarioData ) ;
mockSuccessfulVideoMeetingCreation ( {
metadataLookupKey : "dailyvideo" ,
} ) ;
const { paymentUid , externalId } = mockPaymentApp ( {
metadataLookupKey : "stripe" ,
appStoreLookupKey : "stripepayment" ,
} ) ;
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" ,
paymentUid : paymentUid ,
} ) ;
await expectBookingToBeInDatabase ( {
description : "" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
status : BookingStatus.PENDING ,
} ) ;
expectWorkflowToBeTriggered ( ) ;
expectAwaitingPaymentEmails ( { organizer , booker , emails } ) ;
expectBookingPaymentIntiatedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl : "http://my-webhook.example.com" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
paymentId : createdBooking.paymentId ! ,
} ) ;
const { webhookResponse } = await mockPaymentSuccessWebhookFromStripe ( { externalId } ) ;
expect ( webhookResponse ? . statusCode ) . toBe ( 200 ) ;
await expectBookingToBeInDatabase ( {
description : "" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
status : BookingStatus.PENDING ,
} ) ;
expectBookingRequestedWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl ,
paidEvent : true ,
eventType : scenarioData.eventTypes [ 0 ] ,
} ) ;
} ,
timeout
) ;
} ) ;
} ) ;
describe ( "Reschedule" , ( ) = > {
test (
` should rechedule a booking successfully with Cal Video(Daily Video) if no explicit location is provided
1 . Should cancel the booking
2 . Should create a new booking in the database
3 . Should send emails to the booker as well as organizer
4 . Should trigger BOOKING_RESCHEDULED 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 ] ,
} ) ;
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP" ;
await 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 ,
length : 45 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
bookings : [
{
uid : uidOfBookingToBeRescheduled ,
eventTypeId : 1 ,
status : BookingStatus.ACCEPTED ,
startTime : ` ${ plus1DateString } T05:00:00.000Z ` ,
endTime : ` ${ plus1DateString } T05:15:00.000Z ` ,
references : [
{
type : "daily_video" ,
uid : "MOCK_ID" ,
meetingId : "MOCK_ID" ,
meetingPassword : "MOCK_PASS" ,
meetingUrl : "http://mock-dailyvideo.example.com" ,
} ,
{
type : "google_calendar" ,
uid : "MOCK_ID" ,
meetingId : "MOCK_ID" ,
meetingPassword : "MOCK_PASSWORD" ,
meetingUrl : "https://UNUSED_URL" ,
externalCalendarId : "MOCK_EXTERNAL_CALENDAR_ID" ,
credentialId : undefined ,
} ,
] ,
} ,
] ,
organizer ,
apps : [ TestData . apps [ "google-calendar" ] , TestData . apps [ "daily-video" ] ] ,
} )
) ;
const videoMock = mockSuccessfulVideoMeetingCreation ( {
metadataLookupKey : "dailyvideo" ,
} ) ;
const calendarMock = mockCalendarToHaveNoBusySlots ( "googlecalendar" , {
create : {
uid : "MOCK_ID" ,
} ,
update : {
uid : "UPDATED_MOCK_ID" ,
} ,
} ) ;
const mockBookingData = getMockRequestDataForBooking ( {
data : {
eventTypeId : 1 ,
rescheduleUid : uidOfBookingToBeRescheduled ,
start : ` ${ plus1DateString } T04:00:00.000Z ` ,
end : ` ${ plus1DateString } T04:15:00.000Z ` ,
responses : {
email : booker.email ,
name : booker.name ,
location : { optionValue : "" , value : "integrations:daily" } ,
2023-09-06 19:23:53 +00:00
} ,
} ,
} ) ;
2023-09-30 04:52:32 +00:00
const { req } = createMockNextJsRequest ( {
method : "POST" ,
body : mockBookingData ,
} ) ;
const createdBooking = await handleNewBooking ( req ) ;
const previousBooking = await prismaMock . booking . findUnique ( {
where : {
uid : uidOfBookingToBeRescheduled ,
} ,
} ) ;
logger . silly ( {
previousBooking ,
allBookings : await prismaMock . booking . findMany ( ) ,
} ) ;
// Expect previous booking to be cancelled
await expectBookingToBeInDatabase ( {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : uidOfBookingToBeRescheduled ,
status : BookingStatus.CANCELLED ,
} ) ;
expect ( previousBooking ? . status ) . toBe ( BookingStatus . CANCELLED ) ;
/ * *
* Booking Time should be new time
* /
expect ( createdBooking . startTime ? . toISOString ( ) ) . toBe ( ` ${ plus1DateString } T04:00:00.000Z ` ) ;
expect ( createdBooking . endTime ? . toISOString ( ) ) . toBe ( ` ${ plus1DateString } T04:15:00.000Z ` ) ;
expect ( createdBooking . responses ) . toContain ( {
email : booker.email ,
name : booker.name ,
} ) ;
expect ( createdBooking ) . toContain ( {
location : "integrations:daily" ,
} ) ;
// Expect new booking to be there.
await expectBookingToBeInDatabase ( {
description : "" ,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid : createdBooking.uid ! ,
eventTypeId : mockBookingData.eventTypeId ,
status : BookingStatus.ACCEPTED ,
references : [
{
type : "daily_video" ,
uid : "MOCK_ID" ,
meetingId : "MOCK_ID" ,
meetingPassword : "MOCK_PASS" ,
meetingUrl : "http://mock-dailyvideo.example.com" ,
} ,
{
type : "google_calendar" ,
// IssueToBeFiled-Hariom: It isn' UPDATED_MOCK_ID as references are due to some reason intentionally kept the same after reschedule. See https://github.com/calcom/cal.com/blob/57b48b0a90e13b9eefc1a93abc0044633561b515/packages/core/EventManager.ts#L317
uid : "MOCK_ID" ,
meetingId : "MOCK_ID" ,
meetingPassword : "MOCK_PASSWORD" ,
meetingUrl : "https://UNUSED_URL" ,
externalCalendarId : "MOCK_EXTERNAL_CALENDAR_ID" ,
} ,
] ,
} ) ;
expectWorkflowToBeTriggered ( ) ;
expectSuccessfulVideoMeetingUpdationInCalendar ( videoMock , {
calEvent : {
location : "http://mock-dailyvideo.example.com" ,
} ,
bookingRef : {
type : "daily_video" ,
uid : "MOCK_ID" ,
meetingId : "MOCK_ID" ,
meetingPassword : "MOCK_PASS" ,
meetingUrl : "http://mock-dailyvideo.example.com" ,
} ,
} ) ;
expectSuccessfulCalendarEventUpdationInCalendar ( calendarMock , {
externalCalendarId : "MOCK_EXTERNAL_CALENDAR_ID" ,
calEvent : {
location : "http://mock-dailyvideo.example.com" ,
} ,
uid : "MOCK_ID" ,
} ) ;
expectSuccessfulBookingRescheduledEmails ( { booker , organizer , emails } ) ;
expectBookingRescheduledWebhookToHaveBeenFired ( {
booker ,
organizer ,
location : "integrations:daily" ,
subscriberUrl : "http://my-webhook.example.com" ,
videoCallUrl : ` ${ WEBAPP_URL } /video/DYNAMIC_UID ` ,
} ) ;
2023-09-06 19:23:53 +00:00
} ,
timeout
) ;
} ) ;
} ) ;
function createMockNextJsRequest ( . . . args : Parameters < typeof createMocks > ) {
return createMocks < CustomNextApiRequest , CustomNextApiResponse > ( . . . args ) ;
}
function getBasicMockRequestDataForBooking() {
return {
start : ` ${ getDate ( { dateIncrement : 1 } ).dateString}T04:00:00.000Z ` ,
end : ` ${ getDate ( { dateIncrement : 1 } ).dateString}T04:30:00.000Z ` ,
eventTypeSlug : "no-confirmation" ,
timeZone : "Asia/Calcutta" ,
language : "en" ,
user : "teampro" ,
metadata : { } ,
hasHashedBookingLink : false ,
hashedLink : null ,
} ;
}
function getMockRequestDataForBooking ( {
data ,
} : {
data : Partial < ReturnType < typeof getBasicMockRequestDataForBooking > > & {
eventTypeId : number ;
2023-09-30 04:52:32 +00:00
rescheduleUid? : string ;
bookingUid? : string ;
2023-09-06 19:23:53 +00:00
responses : {
email : string ;
name : string ;
location : { optionValue : "" ; value : string } ;
} ;
} ;
} ) {
return {
. . . getBasicMockRequestDataForBooking ( ) ,
. . . data ,
} ;
}