Compare commits
19 Commits
main
...
fix/less-r
Author | SHA1 | Date | |
---|---|---|---|
a3632df93f | |||
ef01310a54 | |||
da50df8a50 | |||
d950a9cbc4 | |||
da5f8a96b8 | |||
71d5d096f0 | |||
d3114c2d50 | |||
865bb1284c | |||
d682564fa0 | |||
522a8f7af6 | |||
d60d132d9e | |||
79c9b48719 | |||
1a0367ba63 | |||
b44ac00aa5 | |||
ca73565954 | |||
821531233d | |||
78330181f8 | |||
3669e04975 | |||
1a50841ee8 |
|
@ -0,0 +1,4 @@
|
|||
# Checkly Tests
|
||||
|
||||
Run as `yarn checkly test`
|
||||
Deploy the tests as `yarn checkly deploy`
|
|
@ -0,0 +1,53 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Org", () => {
|
||||
// Because these pages involve next.config.js rewrites, it's better to test them on production
|
||||
test.describe("Embeds - i.cal.com", () => {
|
||||
test("Org Profile Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await page.screenshot({ path: "screenshot.jpg" });
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/meet/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/sales/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
|
||||
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/sales/hipaa/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||
async function expectPageToBeServerSideRendered(page: Page) {
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return window.__NEXT_DATA__.props.pageProps.isEmbed;
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
|
@ -102,6 +102,16 @@ const matcherConfigRootPath = {
|
|||
source: "/",
|
||||
};
|
||||
|
||||
const matcherConfigRootPathEmbed = {
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: orgHostPath,
|
||||
},
|
||||
],
|
||||
source: "/embed",
|
||||
};
|
||||
|
||||
const matcherConfigUserRoute = {
|
||||
has: [
|
||||
{
|
||||
|
@ -245,6 +255,10 @@ const nextConfig = {
|
|||
...matcherConfigRootPath,
|
||||
destination: "/team/:orgSlug?isOrgProfile=1",
|
||||
},
|
||||
{
|
||||
...matcherConfigRootPathEmbed,
|
||||
destination: "/team/:orgSlug/embed?isOrgProfile=1",
|
||||
},
|
||||
{
|
||||
...matcherConfigUserRoute,
|
||||
destination: "/org/:orgSlug/:user",
|
||||
|
|
|
@ -13,29 +13,31 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
|
|||
const session = await getServerSession({ req, res });
|
||||
const createdBookings: BookingResponse[] = [];
|
||||
const allRecurringDates: string[] = data.map((booking) => booking.start);
|
||||
let appsStatus: AppsStatus[] | undefined = undefined;
|
||||
const appsStatus: AppsStatus[] | undefined = undefined;
|
||||
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
const numSlotsToCheckForAvailability = 2;
|
||||
|
||||
// Reversing to accumulate results for noEmail instances first, to then lastly, create the
|
||||
// emailed booking taking into account accumulated results to send app status accurately
|
||||
for (let key = data.length - 1; key >= 0; key--) {
|
||||
for (let key = 0; key < data.length; key++) {
|
||||
const booking = data[key];
|
||||
if (key === 0) {
|
||||
const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
|
||||
.flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
|
||||
.reduce((prev, curr) => {
|
||||
if (prev[curr.type]) {
|
||||
prev[curr.type].failures += curr.failures;
|
||||
prev[curr.type].success += curr.success;
|
||||
} else {
|
||||
prev[curr.type] = curr;
|
||||
}
|
||||
return prev;
|
||||
}, {} as { [key: string]: AppsStatus });
|
||||
appsStatus = Object.values(calcAppsStatus);
|
||||
}
|
||||
// Disable AppStatus in Recurring Booking Email as it requires us to iterate backwards to be able to compute the AppsStatus for all the bookings except the very first slot and then send that slot's email with statuses
|
||||
// It is also doubtful that how useful is to have the AppsStatus of all the bookings in the email.
|
||||
// It is more important to iterate forward and check for conflicts for only first few bookings defined by 'numSlotsToCheckForAvailability'
|
||||
// if (key === 0) {
|
||||
// const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
|
||||
// .flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
|
||||
// .reduce((prev, curr) => {
|
||||
// if (prev[curr.type]) {
|
||||
// prev[curr.type].failures += curr.failures;
|
||||
// prev[curr.type].success += curr.success;
|
||||
// } else {
|
||||
// prev[curr.type] = curr;
|
||||
// }
|
||||
// return prev;
|
||||
// }, {} as { [key: string]: AppsStatus });
|
||||
// appsStatus = Object.values(calcAppsStatus);
|
||||
// }
|
||||
|
||||
const recurringEventReq: NextApiRequest & { userId?: number } = req;
|
||||
|
||||
|
@ -49,6 +51,7 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
|
|||
|
||||
const eachRecurringBooking = await handleNewBooking(recurringEventReq, {
|
||||
isNotAnApiCall: true,
|
||||
skipAvailabilityCheck: key >= numSlotsToCheckForAvailability,
|
||||
});
|
||||
|
||||
createdBookings.push(eachRecurringBooking);
|
||||
|
@ -56,4 +59,6 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
|
|||
return createdBookings;
|
||||
}
|
||||
|
||||
export const handleRecurringEventBooking = handler;
|
||||
|
||||
export default defaultResponder(handler);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "./index";
|
||||
|
||||
export { default } from "./index";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
|
@ -0,0 +1,44 @@
|
|||
import { defineConfig } from "checkly";
|
||||
|
||||
/**
|
||||
* See https://www.checklyhq.com/docs/cli/project-structure/
|
||||
*/
|
||||
const config = defineConfig({
|
||||
/* A human friendly name for your project */
|
||||
projectName: "calcom-monorepo",
|
||||
/** A logical ID that needs to be unique across your Checkly account,
|
||||
* See https://www.checklyhq.com/docs/cli/constructs/ to learn more about logical IDs.
|
||||
*/
|
||||
logicalId: "calcom-monorepo",
|
||||
/* An optional URL to your Git repo */
|
||||
repoUrl: "https://github.com/checkly/checkly-cli",
|
||||
/* Sets default values for Checks */
|
||||
checks: {
|
||||
/* A default for how often your Check should run in minutes */
|
||||
frequency: 10,
|
||||
/* Checkly data centers to run your Checks as monitors */
|
||||
locations: ["us-east-1", "eu-west-1"],
|
||||
/* An optional array of tags to organize your Checks */
|
||||
tags: ["Web"],
|
||||
/** The Checkly Runtime identifier, determining npm packages and the Node.js version available at runtime.
|
||||
* See https://www.checklyhq.com/docs/cli/npm-packages/
|
||||
*/
|
||||
runtimeId: "2023.02",
|
||||
/* A glob pattern that matches the Checks inside your repo, see https://www.checklyhq.com/docs/cli/using-check-test-match/ */
|
||||
checkMatch: "**/__checks__/**/*.check.ts",
|
||||
browserChecks: {
|
||||
/* A glob pattern matches any Playwright .spec.ts files and automagically creates a Browser Check. This way, you
|
||||
* can just write native Playwright code. See https://www.checklyhq.com/docs/cli/using-check-test-match/
|
||||
* */
|
||||
testMatch: "**/__checks__/**/*.spec.ts",
|
||||
},
|
||||
},
|
||||
cli: {
|
||||
/* The default datacenter location to use when running npx checkly test */
|
||||
runLocation: "eu-west-1",
|
||||
/* An array of default reporters to use when a reporter is not specified with the "--reporter" flag */
|
||||
reporters: ["list"],
|
||||
},
|
||||
});
|
||||
|
||||
export default config;
|
|
@ -81,6 +81,7 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@types/jsonwebtoken": "^9.0.3",
|
||||
"c8": "^7.13.0",
|
||||
"checkly": "latest",
|
||||
"dotenv-checker": "^1.1.5",
|
||||
"husky": "^8.0.0",
|
||||
"i18n-unused": "^0.13.0",
|
||||
|
|
|
@ -364,11 +364,7 @@ async function ensureAvailableUsers(
|
|||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>> & {
|
||||
users: IsFixedAwareUser[];
|
||||
},
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
||||
recurringDatesInfo?: {
|
||||
allRecurringDates: string[] | undefined;
|
||||
currentRecurringIndex: number | undefined;
|
||||
}
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType }
|
||||
) {
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
|
||||
|
@ -408,22 +404,7 @@ async function ensureAvailableUsers(
|
|||
|
||||
let foundConflict = false;
|
||||
try {
|
||||
if (
|
||||
eventType.recurringEvent &&
|
||||
recurringDatesInfo?.currentRecurringIndex === 0 &&
|
||||
recurringDatesInfo.allRecurringDates
|
||||
) {
|
||||
const allBookingDates = recurringDatesInfo.allRecurringDates.map((strDate) => new Date(strDate));
|
||||
// Go through each date for the recurring event and check if each one's availability
|
||||
// DONE: Decreased computational complexity from O(2^n) to O(n) by refactoring this loop to stop
|
||||
// running at the first unavailable time.
|
||||
let i = 0;
|
||||
while (!foundConflict && i < allBookingDates.length) {
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, allBookingDates[i++], duration);
|
||||
}
|
||||
} else {
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, duration);
|
||||
}
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, duration);
|
||||
} catch {
|
||||
log.debug({
|
||||
message: "Unable set isAvailableToBeBooked. Using true. ",
|
||||
|
@ -638,10 +619,13 @@ async function handler(
|
|||
req: NextApiRequest & { userId?: number | undefined },
|
||||
{
|
||||
isNotAnApiCall = false,
|
||||
skipAvailabilityCheck = false,
|
||||
}: {
|
||||
isNotAnApiCall?: boolean;
|
||||
skipAvailabilityCheck?: boolean;
|
||||
} = {
|
||||
isNotAnApiCall: false,
|
||||
skipAvailabilityCheck: false,
|
||||
}
|
||||
) {
|
||||
const { userId } = req;
|
||||
|
@ -720,6 +704,7 @@ async function handler(
|
|||
isTeamEventType,
|
||||
eventType: getPiiFreeEventType(eventType),
|
||||
dynamicUserList,
|
||||
skipAvailabilityCheck,
|
||||
paymentAppData: {
|
||||
enabled: paymentAppData.enabled,
|
||||
price: paymentAppData.price,
|
||||
|
@ -917,7 +902,7 @@ async function handler(
|
|||
}
|
||||
}
|
||||
|
||||
if (!eventType.seatsPerTimeSlot) {
|
||||
if (!eventType.seatsPerTimeSlot && !skipAvailabilityCheck) {
|
||||
const availableUsers = await ensureAvailableUsers(
|
||||
{
|
||||
...eventType,
|
||||
|
@ -934,10 +919,6 @@ async function handler(
|
|||
dateTo: reqBody.end,
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
{
|
||||
allRecurringDates,
|
||||
currentRecurringIndex,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,804 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
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,
|
||||
getGoogleCalendarCredential,
|
||||
TestData,
|
||||
getOrganizer,
|
||||
getBooker,
|
||||
getScenarioData,
|
||||
mockSuccessfulVideoMeetingCreation,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
getDate,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
expectSuccessfulBookingCreationEmails,
|
||||
expectBookingToBeInDatabase,
|
||||
expectBookingCreatedWebhookToHaveBeenFired,
|
||||
expectSuccessfulCalendarEventCreationInCalendar,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
||||
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "./lib/setupAndTeardown";
|
||||
|
||||
const DAY_IN_MS = 1000 * 60 * 60 * 24;
|
||||
|
||||
function getPlusDayDate(date: string, days: number) {
|
||||
return new Date(new Date(date).getTime() + days * DAY_IN_MS);
|
||||
}
|
||||
|
||||
// Local test runs sometime gets too slow
|
||||
const timeout = process.env.CI ? 5000 : 20000;
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
describe("Recurring EventType:", () => {
|
||||
test(
|
||||
`should create successful bookings for the number of slots requested
|
||||
1. Should create the same number of bookings as requested slots in the database
|
||||
2. Should send emails for the first booking only to the booker as well as organizer
|
||||
3. Should create a calendar event for every booking in the destination calendar
|
||||
3. Should trigger BOOKING_CREATED webhook for every booking
|
||||
`,
|
||||
async ({ emails }) => {
|
||||
const handleRecurringEventBooking = (await import("@calcom/web/pages/api/book/recurring-event"))
|
||||
.handleRecurringEventBooking;
|
||||
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 recurrence = getRecurrence({
|
||||
type: "weekly",
|
||||
numberOfOccurrences: 3,
|
||||
});
|
||||
const plus1DateString = getDate({ dateIncrement: 1 }).dateString;
|
||||
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,
|
||||
recurringEvent: recurrence,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
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/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const recurringCountInRequest = 2;
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:30:00.000Z`,
|
||||
recurringEventId: uuidv4(),
|
||||
recurringCount: recurringCountInRequest,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const numOfSlotsToBeBooked = 4;
|
||||
const { req, res } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: Array(numOfSlotsToBeBooked)
|
||||
.fill(mockBookingData)
|
||||
.map((mockBookingData, index) => {
|
||||
return {
|
||||
...mockBookingData,
|
||||
start: getPlusDayDate(mockBookingData.start, index).toISOString(),
|
||||
end: getPlusDayDate(mockBookingData.end, index).toISOString(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const createdBookings = await handleRecurringEventBooking(req, res);
|
||||
expect(createdBookings.length).toBe(numOfSlotsToBeBooked);
|
||||
for (const [index, createdBooking] of Object.entries(createdBookings)) {
|
||||
logger.debug("Assertion for Booking with index:", index, { createdBooking });
|
||||
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.ACCEPTED,
|
||||
recurringEventId: mockBookingData.recurringEventId,
|
||||
references: [
|
||||
{
|
||||
type: "daily_video",
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
type: "google_calendar",
|
||||
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectBookingCreatedWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: "integrations:daily",
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
//FIXME: All recurring bookings seem to have the same URL. https://github.com/calcom/cal.com/issues/11955
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`,
|
||||
});
|
||||
}
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
bookingTimeRange: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
start: createdBookings[0].startTime!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
end: createdBookings[0].endTime!,
|
||||
},
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
recurrence: {
|
||||
...recurrence,
|
||||
count: recurringCountInRequest,
|
||||
},
|
||||
});
|
||||
|
||||
expect(emails.get().length).toBe(2);
|
||||
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, [
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
]);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
`should fail recurring booking if second slot is already booked`,
|
||||
async ({}) => {
|
||||
const handleRecurringEventBooking = (await import("@calcom/web/pages/api/book/recurring-event"))
|
||||
.handleRecurringEventBooking;
|
||||
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 recurrence = getRecurrence({
|
||||
type: "weekly",
|
||||
numberOfOccurrences: 3,
|
||||
});
|
||||
const plus1DateString = getDate({ dateIncrement: 1 }).dateString;
|
||||
const plus2DateString = getDate({ dateIncrement: 2 }).dateString;
|
||||
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,
|
||||
recurringEvent: recurrence,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: "booking-1-uid",
|
||||
eventTypeId: 1,
|
||||
userId: organizer.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus2DateString}T04:00:00.000Z`,
|
||||
endTime: `${plus2DateString}T04:30:00.000Z`,
|
||||
},
|
||||
],
|
||||
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/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const recurringCountInRequest = 2;
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:30:00.000Z`,
|
||||
recurringEventId: uuidv4(),
|
||||
recurringCount: recurringCountInRequest,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const numOfSlotsToBeBooked = 4;
|
||||
const { req, res } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: Array(numOfSlotsToBeBooked)
|
||||
.fill(mockBookingData)
|
||||
.map((mockBookingData, index) => {
|
||||
return {
|
||||
...mockBookingData,
|
||||
start: getPlusDayDate(mockBookingData.start, index).toISOString(),
|
||||
end: getPlusDayDate(mockBookingData.end, index).toISOString(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
expect(() => handleRecurringEventBooking(req, res)).rejects.toThrow("No available users found");
|
||||
// Actually the first booking goes through in this case but the status is still a failure. We should do a dry run to check if booking is possible for the 2 slots and if yes, then only go for the actual booking otherwise fail the recurring bookign
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
`should create successful bookings for the number of slots requested even if the third slot is already booked as long as first two slots are free
|
||||
1. Should create the same number of bookings as requested slots in the database
|
||||
2. Should send emails for the first booking only to the booker as well as organizer
|
||||
3. Should create a calendar event for every booking in the destination calendar
|
||||
3. Should trigger BOOKING_CREATED webhook for every booking
|
||||
`,
|
||||
async ({ emails }) => {
|
||||
const recurringCountInRequest = 4;
|
||||
|
||||
const handleRecurringEventBooking = (await import("@calcom/web/pages/api/book/recurring-event"))
|
||||
.handleRecurringEventBooking;
|
||||
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 recurrence = getRecurrence({
|
||||
type: "weekly",
|
||||
numberOfOccurrences: 3,
|
||||
});
|
||||
const plus1DateString = getDate({ dateIncrement: 1 }).dateString;
|
||||
const plus3DateString = getDate({ dateIncrement: 3 }).dateString;
|
||||
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,
|
||||
recurringEvent: recurrence,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: "booking-1-uid",
|
||||
eventTypeId: 1,
|
||||
userId: organizer.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus3DateString}T04:00:00.000Z`,
|
||||
endTime: `${plus3DateString}T04:30:00.000Z`,
|
||||
},
|
||||
],
|
||||
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/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:30:00.000Z`,
|
||||
recurringEventId: uuidv4(),
|
||||
recurringCount: recurringCountInRequest,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const numOfSlotsToBeBooked = 4;
|
||||
const { req, res } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: Array(numOfSlotsToBeBooked)
|
||||
.fill(mockBookingData)
|
||||
.map((mockBookingData, index) => {
|
||||
return {
|
||||
...mockBookingData,
|
||||
start: getPlusDayDate(mockBookingData.start, index).toISOString(),
|
||||
end: getPlusDayDate(mockBookingData.end, index).toISOString(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const createdBookings = await handleRecurringEventBooking(req, res);
|
||||
expect(createdBookings.length).toBe(numOfSlotsToBeBooked);
|
||||
for (const [index, createdBooking] of Object.entries(createdBookings)) {
|
||||
logger.debug("Assertion for Booking with index:", index, { createdBooking });
|
||||
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.ACCEPTED,
|
||||
recurringEventId: mockBookingData.recurringEventId,
|
||||
references: [
|
||||
{
|
||||
type: "daily_video",
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
type: "google_calendar",
|
||||
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectBookingCreatedWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: "integrations:daily",
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
//FIXME: File a bug - All recurring bookings seem to have the same URL. They should have same CalVideo URL which could mean that future recurring meetings would have already expired by the time they are needed.
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`,
|
||||
});
|
||||
}
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
recurrence: {
|
||||
...recurrence,
|
||||
count: recurringCountInRequest,
|
||||
},
|
||||
});
|
||||
|
||||
expect(emails.get().length).toBe(2);
|
||||
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, [
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
]);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
`should create successful bookings for the number of slots requested even if the last slot is already booked as long as first two slots are free
|
||||
1. Should create the same number of bookings as requested slots in the database
|
||||
2. Should send emails for the first booking only to the booker as well as organizer
|
||||
3. Should create a calendar event for every booking in the destination calendar
|
||||
3. Should trigger BOOKING_CREATED webhook for every booking
|
||||
`,
|
||||
async ({ emails }) => {
|
||||
const recurringCountInRequest = 4;
|
||||
|
||||
const handleRecurringEventBooking = (await import("@calcom/web/pages/api/book/recurring-event"))
|
||||
.handleRecurringEventBooking;
|
||||
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 recurrence = getRecurrence({
|
||||
type: "weekly",
|
||||
numberOfOccurrences: 3,
|
||||
});
|
||||
const plus1DateString = getDate({ dateIncrement: 1 }).dateString;
|
||||
const plus4DateString = getDate({ dateIncrement: 4 }).dateString;
|
||||
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,
|
||||
recurringEvent: recurrence,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: "booking-1-uid",
|
||||
eventTypeId: 1,
|
||||
userId: organizer.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus4DateString}T04:00:00.000Z`,
|
||||
endTime: `${plus4DateString}T04:30:00.000Z`,
|
||||
},
|
||||
],
|
||||
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/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:30:00.000Z`,
|
||||
recurringEventId: uuidv4(),
|
||||
recurringCount: recurringCountInRequest,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const numOfSlotsToBeBooked = 4;
|
||||
const { req, res } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: Array(numOfSlotsToBeBooked)
|
||||
.fill(mockBookingData)
|
||||
.map((mockBookingData, index) => {
|
||||
return {
|
||||
...mockBookingData,
|
||||
start: getPlusDayDate(mockBookingData.start, index).toISOString(),
|
||||
end: getPlusDayDate(mockBookingData.end, index).toISOString(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const createdBookings = await handleRecurringEventBooking(req, res);
|
||||
expect(createdBookings.length).toBe(numOfSlotsToBeBooked);
|
||||
for (const [index, createdBooking] of Object.entries(createdBookings)) {
|
||||
logger.debug("Assertion for Booking with index:", index, { createdBooking });
|
||||
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.ACCEPTED,
|
||||
recurringEventId: mockBookingData.recurringEventId,
|
||||
references: [
|
||||
{
|
||||
type: "daily_video",
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
type: "google_calendar",
|
||||
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectBookingCreatedWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: "integrations:daily",
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
//FIXME: File a bug - All recurring bookings seem to have the same URL. They should have same CalVideo URL which could mean that future recurring meetings would have already expired by the time they are needed.
|
||||
videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`,
|
||||
});
|
||||
}
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
recurrence: {
|
||||
...recurrence,
|
||||
count: recurringCountInRequest,
|
||||
},
|
||||
});
|
||||
|
||||
expect(emails.get().length).toBe(2);
|
||||
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, [
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
]);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
|
||||
function getRecurrence({
|
||||
type,
|
||||
numberOfOccurrences,
|
||||
}: {
|
||||
type: "weekly" | "monthly" | "yearly";
|
||||
numberOfOccurrences: number;
|
||||
}) {
|
||||
const freq = type === "yearly" ? 0 : type === "monthly" ? 1 : 2;
|
||||
return { freq, count: numberOfOccurrences, interval: 1 };
|
||||
}
|
||||
});
|
|
@ -14,6 +14,7 @@ import logger from "../logger";
|
|||
|
||||
const log = logger.getSubLogger({ prefix: ["[handlePaymentSuccess]"] });
|
||||
export async function handlePaymentSuccess(paymentId: number, bookingId: number) {
|
||||
log.debug(`handling payment success for bookingId ${bookingId}`);
|
||||
const { booking, user: userWithCredentials, evt, eventType } = await getBooking(bookingId);
|
||||
|
||||
if (booking.location) evt.location = booking.location;
|
||||
|
|
Loading…
Reference in New Issue
Block a user