test: Add collective scheduling tests (#11670)
parent
1456e2d4d5
commit
2faf24fb98
|
@ -9,12 +9,14 @@ import { v4 as uuidv4 } from "uuid";
|
|||
import "vitest-fetch-mock";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import type { getMockRequestDataForBooking } from "@calcom/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking";
|
||||
import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook";
|
||||
import type { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
import type { NewCalendarEventType } from "@calcom/types/Calendar";
|
||||
import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
|
||||
|
@ -22,10 +24,6 @@ import { getMockPaymentService } from "./MockPaymentService";
|
|||
|
||||
logger.setSettings({ minLevel: "silly" });
|
||||
const log = logger.getChildLogger({ prefix: ["[bookingScenario]"] });
|
||||
type App = {
|
||||
slug: string;
|
||||
dirName: string;
|
||||
};
|
||||
|
||||
type InputWebhook = {
|
||||
appId: string | null;
|
||||
|
@ -52,24 +50,27 @@ type ScenarioData = {
|
|||
/**
|
||||
* Prisma would return these apps
|
||||
*/
|
||||
apps?: App[];
|
||||
apps?: Partial<AppMeta>[];
|
||||
bookings?: InputBooking[];
|
||||
webhooks?: InputWebhook[];
|
||||
};
|
||||
|
||||
type InputCredential = typeof TestData.credentials.google;
|
||||
type InputCredential = typeof TestData.credentials.google & {
|
||||
id?: number;
|
||||
};
|
||||
|
||||
type InputSelectedCalendar = typeof TestData.selectedCalendars.google;
|
||||
|
||||
type InputUser = typeof TestData.users.example & { id: number } & {
|
||||
type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
|
||||
id: number;
|
||||
defaultScheduleId?: number | null;
|
||||
credentials?: InputCredential[];
|
||||
selectedCalendars?: InputSelectedCalendar[];
|
||||
schedules: {
|
||||
id: number;
|
||||
// Allows giving id in the input directly so that it can be referenced somewhere else as well
|
||||
id?: number;
|
||||
name: string;
|
||||
availability: {
|
||||
userId: number | null;
|
||||
eventTypeId: number | null;
|
||||
days: number[];
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
|
@ -97,7 +98,8 @@ export type InputEventType = {
|
|||
afterEventBuffer?: number;
|
||||
requiresConfirmation?: boolean;
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
} & Partial<Omit<Prisma.EventTypeCreateInput, "users">>;
|
||||
schedule?: InputUser["schedules"][number];
|
||||
} & Partial<Omit<Prisma.EventTypeCreateInput, "users" | "schedule">>;
|
||||
|
||||
type InputBooking = {
|
||||
id?: number;
|
||||
|
@ -122,37 +124,75 @@ type InputBooking = {
|
|||
}[];
|
||||
};
|
||||
|
||||
const Timezones = {
|
||||
export const Timezones = {
|
||||
"+5:30": "Asia/Kolkata",
|
||||
"+6:00": "Asia/Dhaka",
|
||||
};
|
||||
|
||||
async function addEventTypesToDb(
|
||||
eventTypes: (Omit<Prisma.EventTypeCreateInput, "users" | "worflows" | "destinationCalendar"> & {
|
||||
eventTypes: (Omit<
|
||||
Prisma.EventTypeCreateInput,
|
||||
"users" | "worflows" | "destinationCalendar" | "schedule"
|
||||
> & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
users?: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
workflows?: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
destinationCalendar?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
schedule?: any;
|
||||
})[]
|
||||
) {
|
||||
log.silly("TestData: Add EventTypes to DB", JSON.stringify(eventTypes));
|
||||
await prismock.eventType.createMany({
|
||||
data: eventTypes,
|
||||
});
|
||||
const allEventTypes = await prismock.eventType.findMany({
|
||||
include: {
|
||||
users: true,
|
||||
workflows: true,
|
||||
destinationCalendar: true,
|
||||
schedule: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a hack to get the relationship of schedule to be established with eventType. Looks like a prismock bug that creating eventType along with schedule.create doesn't establish the relationship.
|
||||
* HACK STARTS
|
||||
*/
|
||||
log.silly("Fixed possible prismock bug by creating schedule separately");
|
||||
for (let i = 0; i < eventTypes.length; i++) {
|
||||
const eventType = eventTypes[i];
|
||||
const createdEventType = allEventTypes[i];
|
||||
|
||||
if (eventType.schedule) {
|
||||
log.silly("TestData: Creating Schedule for EventType", JSON.stringify(eventType));
|
||||
await prismock.schedule.create({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
data: {
|
||||
...eventType.schedule.create,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: createdEventType.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/***
|
||||
* HACK ENDS
|
||||
*/
|
||||
|
||||
log.silly(
|
||||
"TestData: All EventTypes in DB are",
|
||||
JSON.stringify({
|
||||
eventTypes: await prismock.eventType.findMany({
|
||||
include: {
|
||||
users: true,
|
||||
workflows: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
}),
|
||||
eventTypes: allEventTypes,
|
||||
})
|
||||
);
|
||||
return allEventTypes;
|
||||
}
|
||||
|
||||
async function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
|
||||
|
@ -197,10 +237,22 @@ async function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser
|
|||
create: eventType.destinationCalendar,
|
||||
}
|
||||
: eventType.destinationCalendar,
|
||||
schedule: eventType.schedule
|
||||
? {
|
||||
create: {
|
||||
...eventType.schedule,
|
||||
availability: {
|
||||
createMany: {
|
||||
data: eventType.schedule.availability,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: eventType.schedule,
|
||||
};
|
||||
});
|
||||
log.silly("TestData: Creating EventType", JSON.stringify(eventTypesWithUsers));
|
||||
await addEventTypesToDb(eventTypesWithUsers);
|
||||
return await addEventTypesToDb(eventTypesWithUsers);
|
||||
}
|
||||
|
||||
function addBookingReferencesToDB(bookingReferences: Prisma.BookingReferenceCreateManyInput[]) {
|
||||
|
@ -289,10 +341,21 @@ async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma
|
|||
await prismock.user.createMany({
|
||||
data: users,
|
||||
});
|
||||
|
||||
log.silly(
|
||||
"Added users to Db",
|
||||
safeStringify({
|
||||
allUsers: await prismock.user.findMany(),
|
||||
allUsers: await prismock.user.findMany({
|
||||
include: {
|
||||
credentials: true,
|
||||
schedules: {
|
||||
include: {
|
||||
availability: true,
|
||||
},
|
||||
},
|
||||
destinationCalendar: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -343,16 +406,28 @@ async function addUsers(users: InputUser[]) {
|
|||
await addUsersToDb(prismaUsersCreate);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function addAppsToDb(apps: any[]) {
|
||||
log.silly("TestData: Creating Apps", JSON.stringify({ apps }));
|
||||
await prismock.app.createMany({
|
||||
data: apps,
|
||||
});
|
||||
const allApps = await prismock.app.findMany();
|
||||
log.silly("TestData: Apps as in DB", JSON.stringify({ apps: allApps }));
|
||||
}
|
||||
export async function createBookingScenario(data: ScenarioData) {
|
||||
log.silly("TestData: Creating Scenario", JSON.stringify({ data }));
|
||||
await addUsers(data.users);
|
||||
|
||||
const eventType = await addEventTypes(data.eventTypes, data.users);
|
||||
if (data.apps) {
|
||||
prismock.app.createMany({
|
||||
data: data.apps,
|
||||
});
|
||||
await addAppsToDb(
|
||||
data.apps.map((app) => {
|
||||
// Enable the app by default
|
||||
return { enabled: true, ...app };
|
||||
})
|
||||
);
|
||||
}
|
||||
const eventTypes = await addEventTypes(data.eventTypes, data.users);
|
||||
|
||||
data.bookings = data.bookings || [];
|
||||
// allowSuccessfulBookingCreation();
|
||||
await addBookings(data.bookings);
|
||||
|
@ -360,7 +435,7 @@ export async function createBookingScenario(data: ScenarioData) {
|
|||
await addWebhooks(data.webhooks || []);
|
||||
// addPaymentMock();
|
||||
return {
|
||||
eventType,
|
||||
eventTypes,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -483,12 +558,11 @@ export const TestData = {
|
|||
},
|
||||
schedules: {
|
||||
IstWorkHours: {
|
||||
id: 1,
|
||||
name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT",
|
||||
availability: [
|
||||
{
|
||||
userId: null,
|
||||
eventTypeId: null,
|
||||
// userId: null,
|
||||
// eventTypeId: null,
|
||||
days: [0, 1, 2, 3, 4, 5, 6],
|
||||
startTime: new Date("1970-01-01T09:30:00.000Z"),
|
||||
endTime: new Date("1970-01-01T18:00:00.000Z"),
|
||||
|
@ -497,21 +571,50 @@ export const TestData = {
|
|||
],
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
/**
|
||||
* Has an overlap with IstEveningShift from 5PM to 6PM IST(11:30AM to 12:30PM GMT)
|
||||
*/
|
||||
IstMorningShift: {
|
||||
name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT",
|
||||
availability: [
|
||||
{
|
||||
// userId: null,
|
||||
// eventTypeId: null,
|
||||
days: [0, 1, 2, 3, 4, 5, 6],
|
||||
startTime: new Date("1970-01-01T09:30:00.000Z"),
|
||||
endTime: new Date("1970-01-01T18:00:00.000Z"),
|
||||
date: null,
|
||||
},
|
||||
],
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
/**
|
||||
* Has an overlap with IstMorningShift from 5PM to 6PM IST(11:30AM to 12:30PM GMT)
|
||||
*/
|
||||
IstEveningShift: {
|
||||
name: "5:00PM to 10PM in India - 11:30AM to 16:30PM in GMT",
|
||||
availability: [
|
||||
{
|
||||
// userId: null,
|
||||
// eventTypeId: null,
|
||||
days: [0, 1, 2, 3, 4, 5, 6],
|
||||
startTime: new Date("1970-01-01T17:00:00.000Z"),
|
||||
endTime: new Date("1970-01-01T22:00:00.000Z"),
|
||||
date: null,
|
||||
},
|
||||
],
|
||||
timeZone: Timezones["+5:30"],
|
||||
},
|
||||
IstWorkHoursWithDateOverride: (dateString: string) => ({
|
||||
id: 1,
|
||||
name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT but with a Date Override for 2PM to 6PM IST(in GST time it is 8:30AM to 12:30PM)",
|
||||
availability: [
|
||||
{
|
||||
userId: null,
|
||||
eventTypeId: null,
|
||||
days: [0, 1, 2, 3, 4, 5, 6],
|
||||
startTime: new Date("1970-01-01T09:30:00.000Z"),
|
||||
endTime: new Date("1970-01-01T18:00:00.000Z"),
|
||||
date: null,
|
||||
},
|
||||
{
|
||||
userId: null,
|
||||
eventTypeId: null,
|
||||
days: [0, 1, 2, 3, 4, 5, 6],
|
||||
startTime: new Date(`1970-01-01T14:00:00.000Z`),
|
||||
endTime: new Date(`1970-01-01T18:00:00.000Z`),
|
||||
|
@ -532,9 +635,7 @@ export const TestData = {
|
|||
},
|
||||
apps: {
|
||||
"google-calendar": {
|
||||
slug: "google-calendar",
|
||||
enabled: true,
|
||||
dirName: "whatever",
|
||||
...appStoreMetadata.googlecalendar,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
keys: {
|
||||
|
@ -545,9 +646,7 @@ export const TestData = {
|
|||
},
|
||||
},
|
||||
"daily-video": {
|
||||
slug: "daily-video",
|
||||
dirName: "whatever",
|
||||
enabled: true,
|
||||
...appStoreMetadata.dailyvideo,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
keys: {
|
||||
|
@ -560,9 +659,7 @@ export const TestData = {
|
|||
},
|
||||
},
|
||||
zoomvideo: {
|
||||
slug: "zoom",
|
||||
enabled: true,
|
||||
dirName: "whatever",
|
||||
...appStoreMetadata.zoomvideo,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
keys: {
|
||||
|
@ -575,10 +672,7 @@ export const TestData = {
|
|||
},
|
||||
},
|
||||
"stripe-payment": {
|
||||
//TODO: Read from appStoreMeta
|
||||
slug: "stripe",
|
||||
enabled: true,
|
||||
dirName: "stripepayment",
|
||||
...appStoreMetadata.stripepayment,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
keys: {
|
||||
|
@ -608,6 +702,7 @@ export function getOrganizer({
|
|||
credentials,
|
||||
selectedCalendars,
|
||||
destinationCalendar,
|
||||
defaultScheduleId,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
|
@ -615,6 +710,7 @@ export function getOrganizer({
|
|||
schedules: InputUser["schedules"];
|
||||
credentials?: InputCredential[];
|
||||
selectedCalendars?: InputSelectedCalendar[];
|
||||
defaultScheduleId?: number | null;
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
}) {
|
||||
return {
|
||||
|
@ -626,6 +722,7 @@ export function getOrganizer({
|
|||
credentials,
|
||||
selectedCalendars,
|
||||
destinationCalendar,
|
||||
defaultScheduleId,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -856,7 +953,9 @@ export function mockVideoApp({
|
|||
url: `http://mock-${metadataLookupKey}.example.com`,
|
||||
};
|
||||
log.silly("mockSuccessfulVideoMeetingCreation", JSON.stringify({ metadataLookupKey, appStoreLookupKey }));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const createMeetingCalls: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const updateMeetingCalls: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
|
@ -866,42 +965,50 @@ export function mockVideoApp({
|
|||
lib: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
VideoApiAdapter: () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createMeeting: (...rest: any[]) => {
|
||||
if (creationCrash) {
|
||||
throw new Error("MockVideoApiAdapter.createMeeting fake error");
|
||||
}
|
||||
createMeetingCalls.push(rest);
|
||||
VideoApiAdapter: (credential) => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createMeeting: (...rest: any[]) => {
|
||||
if (creationCrash) {
|
||||
throw new Error("MockVideoApiAdapter.createMeeting fake error");
|
||||
}
|
||||
createMeetingCalls.push({
|
||||
credential,
|
||||
args: rest,
|
||||
});
|
||||
|
||||
return Promise.resolve({
|
||||
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
|
||||
...videoMeetingData,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateMeeting: async (...rest: any[]) => {
|
||||
if (updationCrash) {
|
||||
throw new Error("MockVideoApiAdapter.updateMeeting fake error");
|
||||
}
|
||||
const [bookingRef, calEvent] = rest;
|
||||
updateMeetingCalls.push(rest);
|
||||
if (!bookingRef.type) {
|
||||
throw new Error("bookingRef.type is not defined");
|
||||
}
|
||||
if (!calEvent.organizer) {
|
||||
throw new Error("calEvent.organizer is not defined");
|
||||
}
|
||||
log.silly(
|
||||
"mockSuccessfulVideoMeetingCreation.updateMeeting",
|
||||
JSON.stringify({ bookingRef, calEvent })
|
||||
);
|
||||
return Promise.resolve({
|
||||
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
|
||||
...videoMeetingData,
|
||||
});
|
||||
},
|
||||
}),
|
||||
return Promise.resolve({
|
||||
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
|
||||
...videoMeetingData,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateMeeting: async (...rest: any[]) => {
|
||||
if (updationCrash) {
|
||||
throw new Error("MockVideoApiAdapter.updateMeeting fake error");
|
||||
}
|
||||
const [bookingRef, calEvent] = rest;
|
||||
updateMeetingCalls.push({
|
||||
credential,
|
||||
args: rest,
|
||||
});
|
||||
if (!bookingRef.type) {
|
||||
throw new Error("bookingRef.type is not defined");
|
||||
}
|
||||
if (!calEvent.organizer) {
|
||||
throw new Error("calEvent.organizer is not defined");
|
||||
}
|
||||
log.silly(
|
||||
"mockSuccessfulVideoMeetingCreation.updateMeeting",
|
||||
JSON.stringify({ bookingRef, calEvent })
|
||||
);
|
||||
return Promise.resolve({
|
||||
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
|
||||
...videoMeetingData,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -1029,3 +1136,25 @@ export async function mockPaymentSuccessWebhookFromStripe({ externalId }: { exte
|
|||
}
|
||||
return { webhookResponse };
|
||||
}
|
||||
|
||||
export function getExpectedCalEventForBookingRequest({
|
||||
bookingRequest,
|
||||
eventType,
|
||||
}: {
|
||||
bookingRequest: ReturnType<typeof getMockRequestDataForBooking>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
eventType: any;
|
||||
}) {
|
||||
return {
|
||||
// keep adding more fields as needed, so that they can be verified in all scenarios
|
||||
type: eventType.title,
|
||||
// Not sure why, but milliseconds are missing in cal Event.
|
||||
startTime: bookingRequest.start.replace(".000Z", "Z"),
|
||||
endTime: bookingRequest.end.replace(".000Z", "Z"),
|
||||
};
|
||||
}
|
||||
|
||||
export const enum BookingLocations {
|
||||
CalVideo = "integrations:daily",
|
||||
ZoomVideo = "integrations:zoom",
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { WebhookTriggerEvents, Booking, BookingReference } from "@prisma/client";
|
||||
import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalendar } from "@prisma/client";
|
||||
import ical from "node-ical";
|
||||
import { expect } from "vitest";
|
||||
import "vitest-fetch-mock";
|
||||
|
@ -182,11 +182,15 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
emails,
|
||||
organizer,
|
||||
booker,
|
||||
guests,
|
||||
otherTeamMembers,
|
||||
iCalUID,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string };
|
||||
booker: { email: string; name: string };
|
||||
guests?: { email: string; name: string }[];
|
||||
otherTeamMembers?: { email: string; name: string }[];
|
||||
iCalUID: string;
|
||||
}) {
|
||||
expect(emails).toHaveEmail(
|
||||
|
@ -212,6 +216,39 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
},
|
||||
`${booker.name} <${booker.email}>`
|
||||
);
|
||||
|
||||
if (otherTeamMembers) {
|
||||
otherTeamMembers.forEach((otherTeamMember) => {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
// Don't know why but organizer and team members of the eventType don'thave their name here like Booker
|
||||
to: `${otherTeamMember.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: iCalUID,
|
||||
},
|
||||
},
|
||||
`${otherTeamMember.email}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (guests) {
|
||||
guests.forEach((guest) => {
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
to: `${guest.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: iCalUID,
|
||||
},
|
||||
},
|
||||
`${guest.name} <${guest.email}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function expectBrokenIntegrationEmails({
|
||||
|
@ -537,8 +574,9 @@ export function expectSuccessfulCalendarEventCreationInCalendar(
|
|||
updateEventCalls: any[];
|
||||
},
|
||||
expected: {
|
||||
calendarId: string | null;
|
||||
calendarId?: string | null;
|
||||
videoCallUrl: string;
|
||||
destinationCalendars: Partial<DestinationCalendar>[];
|
||||
}
|
||||
) {
|
||||
expect(calendarMock.createEventCalls.length).toBe(1);
|
||||
|
@ -553,6 +591,8 @@ export function expectSuccessfulCalendarEventCreationInCalendar(
|
|||
externalId: expected.calendarId,
|
||||
}),
|
||||
]
|
||||
: expected.destinationCalendars
|
||||
? expect.arrayContaining(expected.destinationCalendars.map((cal) => expect.objectContaining(cal)))
|
||||
: null,
|
||||
videoCallData: expect.objectContaining({
|
||||
url: expected.videoCallUrl,
|
||||
|
@ -584,7 +624,7 @@ export function expectSuccessfulCalendarEventUpdationInCalendar(
|
|||
expect(externalId).toBe(expected.externalCalendarId);
|
||||
}
|
||||
|
||||
export function expectSuccessfulVideoMeetingCreationInCalendar(
|
||||
export function expectSuccessfulVideoMeetingCreation(
|
||||
videoMock: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createMeetingCalls: any[];
|
||||
|
@ -592,19 +632,20 @@ export function expectSuccessfulVideoMeetingCreationInCalendar(
|
|||
updateMeetingCalls: any[];
|
||||
},
|
||||
expected: {
|
||||
externalCalendarId: string;
|
||||
calEvent: Partial<CalendarEvent>;
|
||||
uid: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
credential: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
calEvent: any;
|
||||
}
|
||||
) {
|
||||
expect(videoMock.createMeetingCalls.length).toBe(1);
|
||||
const call = videoMock.createMeetingCalls[0];
|
||||
const uid = call[0];
|
||||
const calendarEvent = call[1];
|
||||
const externalId = call[2];
|
||||
expect(uid).toBe(expected.uid);
|
||||
expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent));
|
||||
expect(externalId).toBe(expected.externalCalendarId);
|
||||
const callArgs = call.args;
|
||||
const calEvent = callArgs[0];
|
||||
const credential = call.credential;
|
||||
|
||||
expect(credential).toEqual(expected.credential);
|
||||
expect(calEvent).toEqual(expected.calEvent);
|
||||
}
|
||||
|
||||
export function expectSuccessfulVideoMeetingUpdationInCalendar(
|
||||
|
@ -622,8 +663,8 @@ export function expectSuccessfulVideoMeetingUpdationInCalendar(
|
|||
) {
|
||||
expect(videoMock.updateMeetingCalls.length).toBe(1);
|
||||
const call = videoMock.updateMeetingCalls[0];
|
||||
const bookingRef = call[0];
|
||||
const calendarEvent = call[1];
|
||||
const bookingRef = call.args[0];
|
||||
const calendarEvent = call.args[1];
|
||||
expect(bookingRef).toEqual(expect.objectContaining(expected.bookingRef));
|
||||
expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent));
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { getNormalizedAppMetadata } from "./getNormalizedAppMetadata";
|
|||
|
||||
type RawAppStoreMetaData = typeof rawAppStoreMetadata;
|
||||
type AppStoreMetaData = {
|
||||
[key in keyof RawAppStoreMetaData]: AppMeta;
|
||||
[key in keyof RawAppStoreMetaData]: Omit<AppMeta, "dirName"> & { dirName: string };
|
||||
};
|
||||
|
||||
export const appStoreMetadata = {} as AppStoreMetaData;
|
||||
|
|
|
@ -19,7 +19,7 @@ export const getNormalizedAppMetadata = (appMeta: RawAppStoreMetaData[keyof RawA
|
|||
dirName,
|
||||
__template: "",
|
||||
...appMeta,
|
||||
} as AppStoreMetaData[keyof AppStoreMetaData];
|
||||
} as Omit<AppStoreMetaData[keyof AppStoreMetaData], "dirName"> & { dirName: string };
|
||||
metadata.logo = getAppAssetFullPath(metadata.logo, {
|
||||
dirName,
|
||||
isTemplate: metadata.isTemplate,
|
||||
|
|
|
@ -4,6 +4,9 @@ import type { AppCategories } from "@prisma/client";
|
|||
// import appStore from "./index";
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getPiiFreeCredential } from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type { App, AppMeta } from "@calcom/types/App";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
|
@ -52,7 +55,7 @@ function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?
|
|||
|
||||
/** If the app is a globally installed one, let's inject it's key */
|
||||
if (appMeta.isGlobal) {
|
||||
appCredentials.push({
|
||||
const credential = {
|
||||
id: 0,
|
||||
type: appMeta.type,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
@ -65,7 +68,12 @@ function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?
|
|||
team: {
|
||||
name: "Global",
|
||||
},
|
||||
});
|
||||
};
|
||||
logger.debug(
|
||||
`${appMeta.type} is a global app, injecting credential`,
|
||||
safeStringify(getPiiFreeCredential(credential))
|
||||
);
|
||||
appCredentials.push(credential);
|
||||
}
|
||||
|
||||
/** Check if app has location option AND add it if user has credentials for it */
|
||||
|
|
|
@ -460,16 +460,23 @@ export default class EventManager {
|
|||
|
||||
/** @fixme potential bug since Google Meet are saved as `integrations:google:meet` and there are no `google:meet` type in our DB */
|
||||
const integrationName = event.location.replace("integrations:", "");
|
||||
|
||||
let videoCredential = event.conferenceCredentialId
|
||||
? this.videoCredentials.find((credential) => credential.id === event.conferenceCredentialId)
|
||||
: this.videoCredentials
|
||||
// Whenever a new video connection is added, latest credentials are added with the highest ID.
|
||||
// Because you can't rely on having them in the highest first order here, ensure this by sorting in DESC order
|
||||
.sort((a, b) => {
|
||||
return b.id - a.id;
|
||||
})
|
||||
.find((credential: CredentialPayload) => credential.type.includes(integrationName));
|
||||
let videoCredential;
|
||||
if (event.conferenceCredentialId) {
|
||||
videoCredential = this.videoCredentials.find(
|
||||
(credential) => credential.id === event.conferenceCredentialId
|
||||
);
|
||||
} else {
|
||||
videoCredential = this.videoCredentials
|
||||
// Whenever a new video connection is added, latest credentials are added with the highest ID.
|
||||
// Because you can't rely on having them in the highest first order here, ensure this by sorting in DESC order
|
||||
.sort((a, b) => {
|
||||
return b.id - a.id;
|
||||
})
|
||||
.find((credential: CredentialPayload) => credential.type.includes(integrationName));
|
||||
log.warn(
|
||||
`Could not find conferenceCredentialId for event with location: ${event.location}, trying to use last added video credential`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This might happen if someone tries to use a location with a missing credential, so we fallback to Cal Video.
|
||||
|
|
|
@ -9,6 +9,7 @@ import { buildDateRanges, subtract } from "@calcom/lib/date-ranges";
|
|||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimit";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { checkBookingLimit } from "@calcom/lib/server";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import { getTotalBookingDuration } from "@calcom/lib/server/queries";
|
||||
|
@ -25,6 +26,7 @@ import type {
|
|||
|
||||
import { getBusyTimes, getBusyTimesForLimitChecks } from "./getBusyTimes";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["getUserAvailability"] });
|
||||
const availabilitySchema = z
|
||||
.object({
|
||||
dateFrom: stringToDayjs,
|
||||
|
@ -161,7 +163,12 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
|||
if (userId) where.id = userId;
|
||||
|
||||
const user = initialData?.user || (await getUser(where));
|
||||
|
||||
if (!user) throw new HttpError({ statusCode: 404, message: "No user found" });
|
||||
log.debug(
|
||||
"getUserAvailability for user",
|
||||
safeStringify({ user: { id: user.id }, slot: { dateFrom, dateTo } })
|
||||
);
|
||||
|
||||
let eventType: EventType | null = initialData?.eventType || null;
|
||||
if (!eventType && eventTypeId) eventType = await getEventType(eventTypeId);
|
||||
|
@ -225,10 +232,17 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
|||
(schedule) => !user?.defaultScheduleId || schedule.id === user?.defaultScheduleId
|
||||
)[0];
|
||||
|
||||
const schedule =
|
||||
!eventType?.metadata?.config?.useHostSchedulesForTeamEvent && eventType?.schedule
|
||||
? eventType.schedule
|
||||
: userSchedule;
|
||||
const useHostSchedulesForTeamEvent = eventType?.metadata?.config?.useHostSchedulesForTeamEvent;
|
||||
const schedule = !useHostSchedulesForTeamEvent && eventType?.schedule ? eventType.schedule : userSchedule;
|
||||
log.debug(
|
||||
"Using schedule:",
|
||||
safeStringify({
|
||||
chosenSchedule: schedule,
|
||||
eventTypeSchedule: eventType?.schedule,
|
||||
userSchedule: userSchedule,
|
||||
useHostSchedulesForTeamEvent: eventType?.metadata?.config?.useHostSchedulesForTeamEvent,
|
||||
})
|
||||
);
|
||||
|
||||
const startGetWorkingHours = performance.now();
|
||||
|
||||
|
@ -270,7 +284,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
|||
|
||||
const dateRangesInWhichUserIsAvailable = subtract(dateRanges, formattedBusyTimes);
|
||||
|
||||
logger.debug(
|
||||
log.debug(
|
||||
`getWorkingHours took ${endGetWorkingHours - startGetWorkingHours}ms for userId ${userId}`,
|
||||
JSON.stringify({
|
||||
workingHoursInUtc: workingHours,
|
||||
|
|
|
@ -55,7 +55,7 @@ const getBusyVideoTimes = async (withCredentials: CredentialPayload[]) =>
|
|||
|
||||
const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEvent) => {
|
||||
const uid: string = getUid(calEvent);
|
||||
log.silly(
|
||||
log.debug(
|
||||
"createMeeting",
|
||||
safeStringify({
|
||||
credential: getPiiFreeCredential(credential),
|
||||
|
@ -100,11 +100,13 @@ const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEv
|
|||
},
|
||||
});
|
||||
|
||||
if (!enabledApp?.enabled) throw "Current location app is not enabled";
|
||||
if (!enabledApp?.enabled)
|
||||
throw `Location app ${credential.appId} is either disabled or not seeded at all`;
|
||||
|
||||
createdMeeting = await firstVideoAdapter?.createMeeting(calEvent);
|
||||
|
||||
returnObject = { ...returnObject, createdEvent: createdMeeting, success: true };
|
||||
log.debug("created Meeting", safeStringify(returnObject));
|
||||
} catch (err) {
|
||||
await sendBrokenIntegrationEmail(calEvent, "video");
|
||||
log.error("createMeeting failed", safeStringify({ err, calEvent: getPiiFreeCalendarEvent(calEvent) }));
|
||||
|
|
|
@ -379,7 +379,6 @@ async function ensureAvailableUsers(
|
|||
)
|
||||
: undefined;
|
||||
|
||||
log.debug("getUserAvailability for users", JSON.stringify({ users: eventType.users.map((u) => u.id) }));
|
||||
/** Let's start checking for availability */
|
||||
for (const user of eventType.users) {
|
||||
const { dateRanges, busy: bufferedBusyTimes } = await getUserAvailability(
|
||||
|
@ -968,7 +967,7 @@ async function handler(
|
|||
if (
|
||||
availableUsers.filter((user) => user.isFixed).length !== users.filter((user) => user.isFixed).length
|
||||
) {
|
||||
throw new Error("Some users are unavailable for booking.");
|
||||
throw new Error("Some of the hosts are unavailable for booking.");
|
||||
}
|
||||
// Pushing fixed user before the luckyUser guarantees the (first) fixed user as the organizer.
|
||||
users = [...availableUsers.filter((user) => user.isFixed), ...luckyUsers];
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { describe } from "vitest";
|
||||
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
describe("Booking Limits", () => {
|
||||
test.todo("Test these cases that were failing earlier https://github.com/calcom/cal.com/pull/10480");
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { describe } from "vitest";
|
||||
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
test.todo("Dynamic Group Booking");
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,7 @@
|
|||
import { createMocks } from "node-mocks-http";
|
||||
|
||||
import type { CustomNextApiRequest, CustomNextApiResponse } from "../fresh-booking.test";
|
||||
|
||||
export function createMockNextJsRequest(...args: Parameters<typeof createMocks>) {
|
||||
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { getDate } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
|
||||
export 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,
|
||||
};
|
||||
}
|
||||
export function getMockRequestDataForBooking({
|
||||
data,
|
||||
}: {
|
||||
data: Partial<ReturnType<typeof getBasicMockRequestDataForBooking>> & {
|
||||
eventTypeId: number;
|
||||
rescheduleUid?: string;
|
||||
bookingUid?: string;
|
||||
responses: {
|
||||
email: string;
|
||||
name: string;
|
||||
location: { optionValue: ""; value: string };
|
||||
};
|
||||
};
|
||||
}) {
|
||||
return {
|
||||
...getBasicMockRequestDataForBooking(),
|
||||
...data,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { beforeEach, afterEach } from "vitest";
|
||||
|
||||
import {
|
||||
enableEmailFeature,
|
||||
mockNoTranslations,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
|
||||
export function setupAndTeardown() {
|
||||
beforeEach(() => {
|
||||
// Required to able to generate token in email in some cases
|
||||
process.env.CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui";
|
||||
process.env.STRIPE_WEBHOOK_SECRET = "MOCK_STRIPE_WEBHOOK_SECRET";
|
||||
// We are setting it in vitest.config.ts because otherwise it's too late to set it.
|
||||
// process.env.DAILY_API_KEY = "MOCK_DAILY_API_KEY";
|
||||
mockNoTranslations();
|
||||
// mockEnableEmailFeature();
|
||||
enableEmailFeature();
|
||||
globalThis.testEmails = [];
|
||||
fetchMock.resetMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env.CALENDSO_ENCRYPTION_KEY;
|
||||
delete process.env.STRIPE_WEBHOOK_SECRET;
|
||||
delete process.env.DAILY_API_KEY;
|
||||
globalThis.testEmails = [];
|
||||
fetchMock.resetMocks();
|
||||
// process.env.DAILY_API_KEY = "MOCK_DAILY_API_KEY";
|
||||
});
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { describe } from "vitest";
|
||||
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
test.todo("Managed Event Type booking");
|
||||
});
|
|
@ -0,0 +1,608 @@
|
|||
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/apps.metadata.generated";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
import {
|
||||
createBookingScenario,
|
||||
getDate,
|
||||
getGoogleCalendarCredential,
|
||||
TestData,
|
||||
getOrganizer,
|
||||
getBooker,
|
||||
getScenarioData,
|
||||
mockSuccessfulVideoMeetingCreation,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
mockCalendarToCrashOnUpdateEvent,
|
||||
BookingLocations,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
expectBookingToBeInDatabase,
|
||||
expectBookingRescheduledWebhookToHaveBeenFired,
|
||||
expectSuccessfulBookingRescheduledEmails,
|
||||
expectSuccessfulCalendarEventUpdationInCalendar,
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar,
|
||||
expectBookingInDBToBeRescheduledFromTo,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
||||
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
|
||||
// Local test runs sometime gets too slow
|
||||
const timeout = process.env.CI ? 5000 : 20000;
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
describe("Reschedule", () => {
|
||||
test(
|
||||
`should rechedule an existing booking successfully with Cal Video(Daily Video)
|
||||
1. Should cancel the existing 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: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
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",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_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: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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`);
|
||||
|
||||
await expectBookingInDBToBeRescheduledFromTo({
|
||||
from: {
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
to: {
|
||||
description: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
location: BookingLocations.CalVideo,
|
||||
responses: expect.objectContaining({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
}),
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
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: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
expectSuccessfulCalendarEventUpdationInCalendar(calendarMock, {
|
||||
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||
calEvent: {
|
||||
videoCallData: expect.objectContaining({
|
||||
url: "http://mock-dailyvideo.example.com",
|
||||
}),
|
||||
},
|
||||
uid: "MOCK_ID",
|
||||
});
|
||||
|
||||
expectSuccessfulBookingRescheduledEmails({
|
||||
booker,
|
||||
organizer,
|
||||
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`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
test(
|
||||
`should rechedule a booking successfully and update the event in the same externalCalendarId as was used in the booking earlier.
|
||||
1. Should cancel the existing 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,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@example.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
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: {
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
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: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
|
||||
/**
|
||||
* 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`);
|
||||
|
||||
await expectBookingInDBToBeRescheduledFromTo({
|
||||
from: {
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
to: {
|
||||
description: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
location: BookingLocations.CalVideo,
|
||||
responses: expect.objectContaining({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
}),
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, {
|
||||
calEvent: {
|
||||
location: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
bookingRef: {
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
// updateEvent uses existing booking's externalCalendarId to update the event in calendar.
|
||||
// and not the event-type's organizer's which is event-type-1@example.com
|
||||
expectSuccessfulCalendarEventUpdationInCalendar(calendarMock, {
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
calEvent: {
|
||||
location: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
uid: "MOCK_ID",
|
||||
});
|
||||
|
||||
expectSuccessfulBookingRescheduledEmails({
|
||||
booker,
|
||||
organizer,
|
||||
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`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
`an error in updating a calendar event should not stop the rescheduling - Current behaviour is wrong as the booking is resheduled but no-one is notified of it`,
|
||||
async ({}) => {
|
||||
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],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
},
|
||||
});
|
||||
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
|
||||
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: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "ORIGINAL_BOOKING_UID",
|
||||
meetingId: "ORIGINAL_MEETING_ID",
|
||||
meetingPassword: "ORIGINAL_MEETING_PASSWORD",
|
||||
meetingUrl: "https://ORIGINAL_MEETING_URL",
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
credentialId: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
const _calendarMock = mockCalendarToCrashOnUpdateEvent("googlecalendar");
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
rescheduleUid: uidOfBookingToBeRescheduled,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "New York" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
|
||||
await expectBookingInDBToBeRescheduledFromTo({
|
||||
from: {
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
to: {
|
||||
description: "",
|
||||
location: "New York",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
responses: expect.objectContaining({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
}),
|
||||
references: [
|
||||
{
|
||||
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.
|
||||
uid: "ORIGINAL_BOOKING_UID",
|
||||
meetingId: "ORIGINAL_MEETING_ID",
|
||||
meetingPassword: "ORIGINAL_MEETING_PASSWORD",
|
||||
meetingUrl: "https://ORIGINAL_MEETING_URL",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
// FIXME: We should send Broken Integration emails on calendar event updation failure
|
||||
// expectBrokenIntegrationEmails({ booker, organizer, emails });
|
||||
|
||||
expectBookingRescheduledWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: "New York",
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,14 @@ import type { Credential, SelectedCalendar, DestinationCalendar } from "@prisma/
|
|||
import type { EventType } from "@calcom/prisma/client";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
function getBooleanStatus(val: unknown) {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return `PiiFree:${!!val}`;
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPiiFreeCalendarEvent(calEvent: CalendarEvent) {
|
||||
return {
|
||||
eventTypeId: calEvent.eventTypeId,
|
||||
|
@ -16,12 +24,13 @@ export function getPiiFreeCalendarEvent(calEvent: CalendarEvent) {
|
|||
recurrence: calEvent.recurrence,
|
||||
requiresConfirmation: calEvent.requiresConfirmation,
|
||||
uid: calEvent.uid,
|
||||
conferenceCredentialId: calEvent.conferenceCredentialId,
|
||||
iCalUID: calEvent.iCalUID,
|
||||
/**
|
||||
* Let's just get a boolean value for PII sensitive fields so that we atleast know if it's present or not
|
||||
*/
|
||||
// Not okay to have title which can have Booker and Organizer names
|
||||
title: !!calEvent.title,
|
||||
title: getBooleanStatus(calEvent.title),
|
||||
// .... Add all other props here that we don't want to be logged. It prevents those properties from being logged accidentally
|
||||
};
|
||||
}
|
||||
|
@ -44,7 +53,7 @@ export function getPiiFreeBooking(booking: {
|
|||
* Let's just get a boolean value for PII sensitive fields so that we atleast know if it's present or not
|
||||
*/
|
||||
// Not okay to have title which can have Booker and Organizer names
|
||||
title: !!booking.title,
|
||||
title: getBooleanStatus(booking.title),
|
||||
// .... Add all other props here that we don't want to be logged. It prevents those properties from being logged accidentally
|
||||
};
|
||||
}
|
||||
|
@ -60,7 +69,7 @@ export function getPiiFreeCredential(credential: Partial<Credential>) {
|
|||
/**
|
||||
* Let's just get a boolean value for PII sensitive fields so that we atleast know if it's present or not
|
||||
*/
|
||||
key: !!credential.key,
|
||||
key: getBooleanStatus(credential.key),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -82,7 +91,7 @@ export function getPiiFreeDestinationCalendar(destinationCalendar: Partial<Desti
|
|||
/**
|
||||
* Let's just get a boolean value for PII sensitive fields so that we atleast know if it's present or not
|
||||
*/
|
||||
externalId: !!destinationCalendar.externalId,
|
||||
externalId: getBooleanStatus(destinationCalendar.externalId),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@ import { defineConfig } from "vitest/config";
|
|||
|
||||
process.env.INTEGRATION_TEST_MODE = "true";
|
||||
|
||||
// We can't set it during tests because it is used as soon as _metadata.ts is imported which happens before tests start running
|
||||
process.env.DAILY_API_KEY = "MOCK_DAILY_API_KEY";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
|
|
Loading…
Reference in New Issue