Skip to content

Commit

Permalink
test: Add collective scheduling tests (calcom#11670)
Browse files Browse the repository at this point in the history
  • Loading branch information
hariombalhara authored Oct 10, 2023
1 parent 1456e2d commit 2faf24f
Show file tree
Hide file tree
Showing 20 changed files with 2,330 additions and 813 deletions.
299 changes: 214 additions & 85 deletions apps/web/test/utils/bookingScenario/bookingScenario.ts

Large diffs are not rendered by default.

69 changes: 55 additions & 14 deletions apps/web/test/utils/bookingScenario/expects.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(
Expand All @@ -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({
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -584,27 +624,28 @@ 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[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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(
Expand All @@ -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));
}
Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/appStoreMetaData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/getNormalizedAppMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 10 additions & 2 deletions packages/app-store/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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
Expand All @@ -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 */
Expand Down
27 changes: 17 additions & 10 deletions packages/core/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 19 additions & 5 deletions packages/core/getUserAvailability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -25,6 +26,7 @@ import type {

import { getBusyTimes, getBusyTimesForLimitChecks } from "./getBusyTimes";

const log = logger.getChildLogger({ prefix: ["getUserAvailability"] });
const availabilitySchema = z
.object({
dateFrom: stringToDayjs,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/videoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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) }));
Expand Down
3 changes: 1 addition & 2 deletions packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
Original file line number Diff line number Diff line change
@@ -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");
});
Loading

0 comments on commit 2faf24f

Please sign in to comment.