2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
# Unit and Integration Tests
Make sure you have copied .env.test.example to .env.test
You can run all jest tests as
`yarn test`
You can run tests matching specific description by following command
`yarn test -t getSchedule`
Tip: Use `--watchAll` flag to run tests on every change

View File

@@ -0,0 +1,15 @@
# Set the version of docker compose to use
version: "3.9"
# The containers that compose the project
services:
db:
image: postgres:13
restart: always
container_name: integration-tests-prisma
ports:
- "5433:5432"
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
POSTGRES_DB: tests

View File

@@ -0,0 +1,31 @@
// my-test.ts
import { test as base } from "vitest";
import { getTestEmails } from "@calcom/lib/testEmails";
import { getTestSMS } from "@calcom/lib/testSMS";
export interface Fixtures {
emails: ReturnType<typeof getEmailsFixture>;
sms: ReturnType<typeof getSMSFixture>;
}
export const test = base.extend<Fixtures>({
emails: async ({}, use) => {
await use(getEmailsFixture());
},
sms: async ({}, use) => {
await use(getSMSFixture());
},
});
function getEmailsFixture() {
return {
get: getTestEmails,
};
}
function getSMSFixture() {
return {
get: getTestSMS,
};
}

View File

@@ -0,0 +1,291 @@
import { getSampleUserInSession } from "../utils/bookingScenario/getSampleUserInSession";
import { setupAndTeardown } from "../utils/bookingScenario/setupAndTeardown";
import {
createBookingScenario,
getGoogleCalendarCredential,
TestData,
getOrganizer,
getBooker,
getScenarioData,
getMockBookingAttendee,
getDate,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { expectBookingRequestRescheduledEmails } from "@calcom/web/test/utils/bookingScenario/expects";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { describe } from "vitest";
import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import type { TRequestRescheduleInputSchema } from "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.schema";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { test } from "@calcom/web/test/fixtures/fixtures";
export type CustomNextApiRequest = NextApiRequest & Request;
export type CustomNextApiResponse = NextApiResponse & Response;
describe("Handler: requestReschedule", () => {
setupAndTeardown();
describe("User Event Booking", () => {
test(`should be able to request-reschedule for a user booking
1. RequestReschedule emails go to both attendee and the person requesting the reschedule`, async ({
emails,
}) => {
const { requestRescheduleHandler } = await import(
"@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler"
);
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 bookingUid = "MOCKED_BOOKING_UID";
const eventTypeSlug = "event-type-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,
slug: eventTypeSlug,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
uid: bookingUid,
eventTypeId: 1,
userId: 101,
status: BookingStatus.ACCEPTED,
startTime: `${plus1DateString}T05:00:00.000Z`,
endTime: `${plus1DateString}T05:15:00.000Z`,
attendees: [
getMockBookingAttendee({
id: 2,
name: booker.name,
email: booker.email,
// Booker's locale when the fresh booking happened earlier
locale: "hi",
// Booker's timezone when the fresh booking happened earlier
timeZone: "Asia/Kolkata",
noShow: false,
}),
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
const loggedInUser = {
organizationId: null,
id: 101,
username: "reschedule-requester",
name: "Reschedule Requester",
email: "reschedule-requester@example.com",
};
await requestRescheduleHandler(
getTrpcHandlerData({
user: loggedInUser,
input: {
bookingId: bookingUid,
rescheduleReason: "",
},
})
);
expectBookingRequestRescheduledEmails({
booking: {
uid: bookingUid,
},
booker,
organizer: organizer,
loggedInUser,
emails,
bookNewTimePath: `/${organizer.username}/${eventTypeSlug}`,
});
});
});
describe("Team Event Booking", () => {
test(`should be able to request-reschedule for a team event booking
1. RequestReschedule emails go to both attendee and the person requesting the reschedule`, async ({
emails,
}) => {
const { requestRescheduleHandler } = await import(
"@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler"
);
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
teams: [
{
membership: {
accepted: true,
},
team: {
id: 1,
name: "Team 1",
slug: "team-1",
},
},
],
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const bookingUid = "MOCKED_BOOKING_UID";
const eventTypeSlug = "event-type-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,
slug: eventTypeSlug,
slotInterval: 45,
teamId: 1,
schedulingType: SchedulingType.COLLECTIVE,
length: 45,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
uid: bookingUid,
eventTypeId: 1,
userId: 101,
status: BookingStatus.ACCEPTED,
startTime: `${plus1DateString}T05:00:00.000Z`,
endTime: `${plus1DateString}T05:15:00.000Z`,
attendees: [
getMockBookingAttendee({
id: 2,
name: booker.name,
email: booker.email,
// Booker's locale when the fresh booking happened earlier
locale: "hi",
// Booker's timezone when the fresh booking happened earlier
timeZone: "Asia/Kolkata",
noShow: false,
}),
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
const loggedInUser = {
organizationId: null,
id: 101,
username: "reschedule-requester",
name: "Reschedule Requester",
email: "reschedule-requester@example.com",
};
await requestRescheduleHandler(
getTrpcHandlerData({
user: loggedInUser,
input: {
bookingId: bookingUid,
rescheduleReason: "",
},
})
);
expectBookingRequestRescheduledEmails({
booking: {
uid: bookingUid,
},
booker,
organizer: organizer,
loggedInUser,
emails,
bookNewTimePath: "/team/team-1/event-type-1",
});
});
test.todo("Verify that the email should go to organizer as well as the team members");
});
});
function getTrpcHandlerData({
input,
user,
}: {
input: TRequestRescheduleInputSchema;
user: Partial<Omit<NonNullable<TrpcSessionUser>, "id" | "email" | "username">> &
Pick<NonNullable<TrpcSessionUser>, "id" | "email" | "username">;
}) {
return {
ctx: {
user: {
...getSampleUserInSession(),
...user,
avatarUrl: user.avatarUrl || null,
profile: {
upId: "",
id: 1,
name: "",
avatarUrl: "",
startTime: 0,
endTime: 0,
username: user.username || "",
organizationId: null,
organization: null,
bufferTime: 5,
avatar: null,
},
} as unknown as NonNullable<TrpcSessionUser>,
},
input: input,
};
}

View File

@@ -0,0 +1,15 @@
module.exports = (path, options) => {
// Call the defaultResolver, so we leverage its cache, error handling, etc.
return options.defaultResolver(path, {
...options,
// Use packageFilter to process parsed `package.json` before the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb)
packageFilter: (pkg) => {
// See https://github.com/microsoft/accessibility-insights-web/blob/40416a4ae6b91baf43102f58e069eff787de4de2/src/tests/common/resolver.js
if (pkg.name === "uuid" || pkg.name === "nanoid") {
delete pkg["exports"];
delete pkg["module"];
}
return pkg;
},
});
};

View File

@@ -0,0 +1,6 @@
// This is a workaround for https://github.com/jsdom/jsdom/issues/2524#issuecomment-902027138
// See https://github.com/microsoft/accessibility-insights-web/blob/40416a4ae6b91baf43102f58e069eff787de4de2/src/tests/unit/jest-setup.ts
const { TextEncoder, TextDecoder } = require("util");
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;

View File

@@ -0,0 +1,115 @@
import { expect, it } from "vitest";
import { availabilityAsString } from "@calcom/lib/availability";
it("correctly handles 1 day", async () => {
const availability = {
id: 1,
userId: 2,
eventTypeId: 3,
days: [1],
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1,
profileId: null,
};
const result = availabilityAsString(availability, {
locale: "en",
hour12: true,
});
expect(replaceUnicodeSpace(result)).toBe("Mon, 9:00 AM - 5:00 PM");
});
it("correctly handles all days", async () => {
const availability = {
id: 1,
userId: 2,
eventTypeId: 3,
days: [1, 2, 3, 4, 5, 6, 7],
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1,
profileId: null,
};
const result = availabilityAsString(availability, {
locale: "en",
hour12: true,
});
expect(replaceUnicodeSpace(result)).toBe("Mon - Sun, 9:00 AM - 5:00 PM");
});
it("correctly handles staggered days", async () => {
const availability = {
id: 1,
userId: 2,
eventTypeId: 3,
days: [1, 3, 5, 7],
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1,
profileId: null,
};
const result = availabilityAsString(availability, {
locale: "en",
hour12: true,
});
expect(replaceUnicodeSpace(result)).toBe("Mon, Wed, Fri, Sun, 9:00 AM - 5:00 PM");
});
it("correctly produces days and times - 12 hours", async () => {
const availability = {
id: 1,
userId: 2,
eventTypeId: 3,
days: [1, 2, 3],
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1,
profileId: null,
};
const result = availabilityAsString(availability, {
locale: "en",
hour12: true,
});
expect(replaceUnicodeSpace(result)).toBe("Mon - Wed, 9:00 AM - 5:00 PM");
});
it("correctly produces days and times - 24 hours", async () => {
const availability = {
id: 1,
userId: 2,
eventTypeId: 3,
days: [1, 2, 3],
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1,
profileId: null,
};
const result = availabilityAsString(availability, {
locale: "en",
hour12: false,
});
expect(replaceUnicodeSpace(result)).toBe("Mon - Wed, 09:00 - 17:00");
});
// INFO: This is because on GitHub, the international date formatting
// produces Unicode characters. Instead of using line for line code from the
// availability.ts file, opted for this instead.
const replaceUnicodeSpace = (string: string) => {
return string.replace(/\u202f/g, " ");
};

View File

@@ -0,0 +1,106 @@
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
import { describe, expect, it } from "vitest";
import dayjs from "@calcom/dayjs";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { checkBookingLimits, checkBookingLimit } from "@calcom/lib/server";
import type { IntervalLimit } from "@calcom/types/Calendar";
type Mockdata = {
id: number;
startDate: Date;
bookingLimits: IntervalLimit;
};
const MOCK_DATA: Mockdata = {
id: 1,
startDate: dayjs("2022-09-30T09:00:00+01:00").toDate(),
bookingLimits: {
PER_DAY: 1,
},
};
describe("Check Booking Limits Tests", () => {
it("Should return no errors", async () => {
prismaMock.booking.count.mockResolvedValue(0);
expect(
checkBookingLimits(MOCK_DATA.bookingLimits, MOCK_DATA.startDate, MOCK_DATA.id)
).resolves.toBeTruthy();
});
it("Should throw an error", async () => {
// Mock there being two a day
prismaMock.booking.count.mockResolvedValue(2);
expect(
checkBookingLimits(MOCK_DATA.bookingLimits, MOCK_DATA.startDate, MOCK_DATA.id)
).rejects.toThrowError();
});
it("Should pass with multiple booking limits", async () => {
prismaMock.booking.count.mockResolvedValue(0);
expect(
checkBookingLimits(
{
PER_DAY: 1,
PER_WEEK: 2,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).resolves.toBeTruthy();
});
it("Should pass with multiple booking limits with one undefined", async () => {
prismaMock.booking.count.mockResolvedValue(0);
expect(
checkBookingLimits(
{
PER_DAY: 1,
PER_WEEK: undefined,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).resolves.toBeTruthy();
});
it("Should handle mutiple limits correctly", async () => {
prismaMock.booking.count.mockResolvedValue(1);
expect(
checkBookingLimit({
key: "PER_DAY",
limitingNumber: 2,
eventStartDate: MOCK_DATA.startDate,
eventId: MOCK_DATA.id,
})
).resolves.not.toThrow();
prismaMock.booking.count.mockResolvedValue(3);
expect(
checkBookingLimit({
key: "PER_WEEK",
limitingNumber: 2,
eventStartDate: MOCK_DATA.startDate,
eventId: MOCK_DATA.id,
})
).rejects.toThrowError();
});
});
describe("Booking limit validation", () => {
it("Should validate a correct limit", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 3, PER_MONTH: 5 })).toBe(true);
});
it("Should invalidate an incorrect limit", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 9, PER_MONTH: 5 })).toBe(false);
});
it("Should validate a correct limit with 'gaps' ", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 9, PER_YEAR: 25 })).toBe(true);
});
it("Should validate a correct limit with equal values ", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 1, PER_YEAR: 1 })).toBe(true);
});
it("Should validate a correct with empty", () => {
expect(validateIntervalLimitOrder({})).toBe(true);
});
});

View File

@@ -0,0 +1,115 @@
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
import { describe, expect, it } from "vitest";
import dayjs from "@calcom/dayjs";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { checkDurationLimit, checkDurationLimits } from "@calcom/lib/server";
type MockData = {
id: number;
startDate: Date;
};
const MOCK_DATA: MockData = {
id: 1,
startDate: dayjs("2022-09-30T09:00:00+01:00").toDate(),
};
// Path: apps/web/test/lib/checkDurationLimits.ts
describe("Check Duration Limits Tests", () => {
it("Should return no errors if limit is not reached", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 0 }]);
await expect(
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
).resolves.toBeTruthy();
});
it("Should throw an error if limit is reached", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
await expect(
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
).rejects.toThrowError();
});
it("Should pass with multiple duration limits", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
await expect(
checkDurationLimits(
{
PER_DAY: 60,
PER_WEEK: 120,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).resolves.toBeTruthy();
});
it("Should pass with multiple duration limits with one undefined", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
await expect(
checkDurationLimits(
{
PER_DAY: 60,
PER_WEEK: undefined,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).resolves.toBeTruthy();
});
it("Should return no errors if limit is not reached with multiple bookings", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
await expect(
checkDurationLimits(
{
PER_DAY: 90,
PER_WEEK: 120,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).resolves.toBeTruthy();
});
it("Should throw an error if one of the limit is reached with multiple bookings", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 90 }]);
await expect(
checkDurationLimits(
{
PER_DAY: 60,
PER_WEEK: 120,
},
MOCK_DATA.startDate,
MOCK_DATA.id
)
).rejects.toThrowError();
});
});
// Path: apps/web/test/lib/checkDurationLimits.ts
describe("Check Duration Limit Tests", () => {
it("Should return no busyTimes and no error if limit is not reached", async () => {
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
await expect(
checkDurationLimit({
key: "PER_DAY",
limitingNumber: 90,
eventStartDate: MOCK_DATA.startDate,
eventId: MOCK_DATA.id,
})
).resolves.toBeUndefined();
});
});
describe("Duration limit validation", () => {
it("Should validate limit where ranges have ascending values", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 30, PER_MONTH: 60 })).toBe(true);
});
it("Should invalidate limit where ranges does not have a strict ascending values", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 60, PER_WEEK: 30 })).toBe(false);
});
it("Should validate a correct limit with 'gaps'", () => {
expect(validateIntervalLimitOrder({ PER_DAY: 60, PER_YEAR: 120 })).toBe(true);
});
it("Should validate empty limit", () => {
expect(validateIntervalLimitOrder({})).toBe(true);
});
});

View File

@@ -0,0 +1,123 @@
import { expect, it, beforeAll, vi } from "vitest";
import { getAggregateWorkingHours } from "@calcom/core/getAggregateWorkingHours";
beforeAll(() => {
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
});
const HAWAII_AND_NEWYORK_TEAM = [
{
timeZone: "America/Detroit", // GMT -4 per 22th of Aug, 2022
workingHours: [{ userId: 1, days: [1, 2, 3, 4, 5], startTime: 780, endTime: 1260 }],
busy: [],
dateOverrides: [],
datesOutOfOffice: {},
},
{
timeZone: "Pacific/Honolulu", // GMT -10 per 22th of Aug, 2022
workingHours: [
{ userId: 1, days: [3, 4, 5], startTime: 0, endTime: 360 },
{ userId: 2, days: [6], startTime: 0, endTime: 180 },
{ userId: 3, days: [2, 3, 4], startTime: 780, endTime: 1439 },
{ userId: 4, days: [5], startTime: 780, endTime: 1439 },
],
busy: [],
dateOverrides: [],
datesOutOfOffice: {},
},
];
/* TODO: Make this test more "professional" */
it("Sydney and Shiraz can live in harmony 🙏", async () => {
expect(getAggregateWorkingHours(HAWAII_AND_NEWYORK_TEAM, "COLLECTIVE")).toMatchInlineSnapshot(`
[
{
"days": [
3,
4,
5,
],
"endTime": 360,
"startTime": 780,
},
{
"days": [
6,
],
"endTime": 180,
"startTime": 0,
"userId": 2,
},
{
"days": [
2,
3,
4,
],
"endTime": 1260,
"startTime": 780,
},
{
"days": [
5,
],
"endTime": 1260,
"startTime": 780,
},
]
`);
expect(getAggregateWorkingHours(HAWAII_AND_NEWYORK_TEAM, "ROUND_ROBIN")).toMatchInlineSnapshot(`
[
{
"days": [
1,
2,
3,
4,
5,
],
"endTime": 1260,
"startTime": 780,
"userId": 1,
},
{
"days": [
3,
4,
5,
],
"endTime": 360,
"startTime": 0,
"userId": 1,
},
{
"days": [
6,
],
"endTime": 180,
"startTime": 0,
"userId": 2,
},
{
"days": [
2,
3,
4,
],
"endTime": 1439,
"startTime": 780,
"userId": 3,
},
{
"days": [
5,
],
"endTime": 1439,
"startTime": 780,
"userId": 4,
},
]
`);
});

View File

@@ -0,0 +1,71 @@
import type { Availability } from "@prisma/client";
import { expect, it, beforeAll, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import { getAvailabilityFromSchedule } from "@calcom/lib/availability";
beforeAll(() => {
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
});
//parse "hh:mm-hh:mm" into <Availability> object
const parseWorkingHours = (workingHours: string) => {
const times = workingHours.split("-").map((time) => dayjs(time, "hh:mm").toDate());
return { start: times[0], end: times[1] };
};
const p = parseWorkingHours;
// mocked working hours
const fulltimeWH = p("09:00-17:00");
const morningWH = p("09:00-12:00");
const afternoonWH = p("13:00-17:00");
it("should return an empty availability array when received an empty schedule", async () => {
const schedule = [[]];
expect(getAvailabilityFromSchedule(schedule)).toStrictEqual([]);
});
it("should return availability for all workable days from 9:00 to 17:00", async () => {
const schedule = [[], [fulltimeWH], [fulltimeWH], [fulltimeWH], [fulltimeWH], [fulltimeWH], []];
const expected = [
{
days: [1, 2, 3, 4, 5],
startTime: fulltimeWH.start,
endTime: fulltimeWH.end,
},
] as Availability[];
expect(getAvailabilityFromSchedule(schedule)).toStrictEqual(expected);
});
it("should return the available days grouped by the available time slots", async () => {
const schedule = [
[],
[afternoonWH],
[afternoonWH],
[morningWH, afternoonWH],
[fulltimeWH],
[morningWH],
[],
];
const expected = [
{
days: [1, 2, 3],
startTime: afternoonWH.start,
endTime: afternoonWH.end,
},
{
days: [3, 5],
startTime: morningWH.start,
endTime: morningWH.end,
},
{
days: [4],
startTime: fulltimeWH.start,
endTime: fulltimeWH.end,
},
] as Availability[];
expect(getAvailabilityFromSchedule(schedule)).toStrictEqual(expected);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
import { diff } from "jest-diff";
import { expect } from "vitest";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
export const expectedSlotsForSchedule = {
IstWorkHours: {
interval: {
"1hr": {
allPossibleSlotsStartingAt430: [
"04:30:00.000Z",
"05:30:00.000Z",
"06:30:00.000Z",
"07:30:00.000Z",
"08:30:00.000Z",
"09:30:00.000Z",
"10:30:00.000Z",
"11:30:00.000Z",
],
allPossibleSlotsStartingAt4: [
"04:00:00.000Z",
"05:00:00.000Z",
"06:00:00.000Z",
"07:00:00.000Z",
"08:00:00.000Z",
"09:00:00.000Z",
"10:00:00.000Z",
"11:00:00.000Z",
],
},
},
},
};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers<R> {
toHaveTimeSlots(expectedSlots: string[], date: { dateString: string; doExactMatch?: boolean }): R;
/**
* Explicitly checks if the date is disabled and fails if date is marked as OOO
*/
toHaveDateDisabled(date: { dateString: string }): R;
}
}
}
expect.extend({
toHaveTimeSlots(
schedule: { slots: Record<string, Slot[]> },
expectedSlots: string[],
{ dateString, doExactMatch }: { dateString: string; doExactMatch: boolean }
) {
if (!schedule.slots[`${dateString}`]) {
return {
pass: false,
message: () => `has no timeslots for ${dateString}`,
};
}
const expectedSlotHasFullTimestamp = expectedSlots[0].split("-").length === 3;
if (
!schedule.slots[`${dateString}`]
.map((slot) => slot.time)
.every((actualSlotTime, index) => {
const expectedSlotTime = expectedSlotHasFullTimestamp
? expectedSlots[index]
: `${dateString}T${expectedSlots[index]}`;
return expectedSlotTime === actualSlotTime;
})
) {
return {
pass: false,
message: () =>
`has incorrect timeslots for ${dateString}.\n\r ${diff(
expectedSlots.map((expectedSlot) => {
if (expectedSlotHasFullTimestamp) {
return expectedSlot;
}
return `${dateString}T${expectedSlot}`;
}),
schedule.slots[`${dateString}`].map((slot) => slot.time)
)}`,
};
}
if (doExactMatch) {
return {
pass: expectedSlots.length === schedule.slots[`${dateString}`].length,
message: () =>
`number of slots don't match for ${dateString}. Expected ${expectedSlots.length} but got ${
schedule.slots[`${dateString}`].length
}`,
};
}
return {
pass: true,
message: () => "has correct timeslots ",
};
},
toHaveDateDisabled(schedule: { slots: Record<string, Slot[]> }, { dateString }: { dateString: string }) {
// Frontend requires that the date must not be set for that date to be shown as disabled.Because weirdly, if an empty array is provided the date itself isn't shown which we don't want
if (!schedule.slots[`${dateString}`]) {
return {
pass: true,
message: () => `is not disabled for ${dateString}`,
};
}
if (schedule.slots[`${dateString}`].length === 0) {
return {
pass: false,
message: () => `is all day OOO for ${dateString}.`,
};
}
return {
pass: false,
message: () => `has timeslots for ${dateString}`,
};
},
});
export { expect } from "vitest";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
import prismock from "../../../../../tests/libs/__mocks__/prisma";
import { vi, beforeEach, afterEach } from "vitest";
const cleanup = async () => {
await prismock.eventType.deleteMany();
await prismock.user.deleteMany();
await prismock.schedule.deleteMany();
await prismock.selectedCalendar.deleteMany();
await prismock.credential.deleteMany();
await prismock.booking.deleteMany();
await prismock.app.deleteMany();
vi.useRealTimers();
};
export function setupAndTeardown() {
beforeEach(async () => {
await cleanup();
});
afterEach(async () => {
await cleanup();
});
}

View File

@@ -0,0 +1,16 @@
import { getDate } from "../../utils/bookingScenario/bookingScenario";
import { vi } from "vitest";
export function timeTravelToTheBeginningOfToday({ utcOffsetInHours = 0 }: { utcOffsetInHours: number }) {
const timeInTheUtcOffsetInHours = 24 - utcOffsetInHours;
const timeInTheUtcOffsetInMinutes = timeInTheUtcOffsetInHours * 60;
const hours = Math.floor(timeInTheUtcOffsetInMinutes / 60);
const hoursString = hours < 10 ? `0${hours}` : `${hours}`;
const minutes = timeInTheUtcOffsetInMinutes % 60;
const minutesString = minutes < 10 ? `0${minutes}` : `${minutes}`;
const { dateString: yesterdayDateString } = getDate({ dateIncrement: -1 });
console.log({ yesterdayDateString, hours, minutes });
vi.setSystemTime(`${yesterdayDateString}T${hoursString}:${minutesString}:00.000Z`);
}

View File

@@ -0,0 +1,96 @@
import { expect, beforeEach, afterEach, it, vi, describe } from "vitest";
import { filterByCities, addCitiesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
const cityData = [
{
city: "San Francisco",
timezone: "America/Argentina/Cordoba",
},
{
city: "Sao Francisco do Sul",
timezone: "America/Sao_Paulo",
},
{
city: "San Francisco de Macoris",
timezone: "America/Santo_Domingo",
},
{
city: "San Francisco Gotera",
timezone: "America/El_Salvador",
},
{
city: "San Francisco",
timezone: "America/Los_Angeles",
},
];
const option = {
value: "America/Los_Angeles",
label: "(GMT-8:00) San Francisco",
offset: -8,
abbrev: "PST",
altName: "Pacific Standard Time",
};
describe("getTimezone", () => {
beforeEach(() => {
vi.useFakeTimers().setSystemTime(new Date("2020-01-01"));
});
afterEach(() => {
vi.runOnlyPendingTimers();
vi.useRealTimers();
});
it("should return empty array for an empty string", () => {
expect(filterByCities("", cityData)).toMatchInlineSnapshot(`[]`);
});
it("should filter cities for a valid city name", () => {
expect(filterByCities("San Francisco", cityData)).toMatchInlineSnapshot(`
[
{
"city": "San Francisco",
"timezone": "America/Argentina/Cordoba",
},
{
"city": "San Francisco de Macoris",
"timezone": "America/Santo_Domingo",
},
{
"city": "San Francisco Gotera",
"timezone": "America/El_Salvador",
},
{
"city": "San Francisco",
"timezone": "America/Los_Angeles",
},
]
`);
});
it("should return appropriate timezone(s) for a given city name array", () => {
expect(addCitiesToDropdown(cityData)).toMatchInlineSnapshot(`
{
"America/Argentina/Cordoba": "San Francisco",
"America/El_Salvador": "San Francisco Gotera",
"America/Los_Angeles": "San Francisco",
"America/Santo_Domingo": "San Francisco de Macoris",
"America/Sao_Paulo": "Sao Francisco do Sul",
}
`);
});
it("should render city name as option label if cityData is not empty", () => {
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -8:00"`);
vi.setSystemTime(new Date("2020-06-01"));
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -7:00"`);
});
it("should return timezone as option label if cityData is empty", () => {
expect(handleOptionLabel(option, [])).toMatchInlineSnapshot(`"America/Los Angeles GMT -8:00"`);
vi.setSystemTime(new Date("2020-06-01"));
expect(handleOptionLabel(option, [])).toMatchInlineSnapshot(`"America/Los Angeles GMT -7:00"`);
});
});

View File

@@ -0,0 +1,155 @@
import { expect, it, vi, beforeAll } from "vitest";
import dayjs from "@calcom/dayjs";
import { getWorkingHours } from "@calcom/lib/availability";
beforeAll(() => {
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
});
it("correctly translates Availability (UTC+0) to UTC workingHours", async () => {
expect(
getWorkingHours({ timeZone: "GMT" }, [
{
days: [0],
startTime: new Date(Date.UTC(2021, 11, 16, 23)),
endTime: new Date(Date.UTC(2021, 11, 16, 23, 59)),
},
])
).toStrictEqual([
{
days: [0],
endTime: 1439,
startTime: 1380,
},
]);
});
it("correctly translates Availability in a positive UTC offset (Pacific/Auckland) to UTC workingHours", async () => {
// Take note that (Pacific/Auckland) is UTC+12 on 2021-06-20, NOT +13 like the other half of the year.
expect(
getWorkingHours({ timeZone: "Pacific/Auckland" }, [
{
days: [1],
startTime: new Date(Date.UTC(2021, 11, 16, 0)),
endTime: new Date(Date.UTC(2021, 11, 16, 23, 59)),
},
])
).toStrictEqual([
{
days: [1],
endTime: 719,
startTime: 0,
},
{
days: [0],
endTime: 1439,
startTime: 720, // 0 (midnight) - 12 * 60 (DST)
},
]);
});
it("correctly translates Availability in a negative UTC offset (Pacific/Midway) to UTC workingHours", async () => {
// Take note that (Pacific/Midway) is UTC-12 on 2021-06-20, NOT +13 like the other half of the year.
expect(
getWorkingHours({ timeZone: "Pacific/Midway" }, [
{
days: [1],
startTime: new Date(Date.UTC(2021, 11, 16, 0)),
endTime: new Date(Date.UTC(2021, 11, 16, 23, 59)),
},
])
).toStrictEqual([
{
days: [2],
endTime: 659,
startTime: 0,
},
{
days: [1],
endTime: 1439,
startTime: 660,
},
]);
});
it("can do the same with UTC offsets", async () => {
// Take note that (Pacific/Midway) is UTC-12 on 2021-06-20, NOT +13 like the other half of the year.
expect(
getWorkingHours({ utcOffset: dayjs().tz("Pacific/Midway").utcOffset() }, [
{
days: [1],
startTime: new Date(Date.UTC(2021, 11, 16, 0)),
endTime: new Date(Date.UTC(2021, 11, 16, 23, 59)),
},
])
).toStrictEqual([
{
days: [2],
endTime: 659,
startTime: 0,
},
{
days: [1],
endTime: 1439,
startTime: 660,
},
]);
});
it("can also shift UTC into other timeZones", async () => {
// UTC+0 time with 23:00 - 23:59 (Sunday) and 00:00 - 16:00 (Monday) when cast into UTC+1 should become 00:00 = 17:00 (Monday)
expect(
getWorkingHours({ utcOffset: -60 }, [
{
days: [0],
startTime: new Date(Date.UTC(2021, 11, 16, 23)),
endTime: new Date(Date.UTC(2021, 11, 16, 23, 59)),
},
{
days: [1],
startTime: new Date(Date.UTC(2021, 11, 17, 0)),
endTime: new Date(Date.UTC(2021, 11, 17, 16)),
},
])
).toStrictEqual([
// TODO: Maybe the desired result is 0-1020 as a single entry, but this requires some post-processing to merge. It may work as is so leaving this as now.
{
days: [1],
endTime: 59,
startTime: 0,
},
{
days: [1],
endTime: 1020,
startTime: 60,
},
]);
// And the other way around; UTC+0 time with 00:00 - 1:00 (Monday) and 21:00 - 24:00 (Sunday) when cast into UTC-1 should become 20:00 = 24:00 (Sunday)
expect(
getWorkingHours({ utcOffset: 60 }, [
{
days: [0],
startTime: new Date(Date.UTC(2021, 11, 16, 21)),
endTime: new Date(Date.UTC(2021, 11, 16, 23, 59)),
},
{
days: [1],
startTime: new Date(Date.UTC(2021, 11, 17, 0)),
endTime: new Date(Date.UTC(2021, 11, 17, 1)),
},
])
).toStrictEqual([
// TODO: Maybe the desired result is 1200-1439 as a single entry, but this requires some post-processing to merge. It may work as is so leaving this as now.
{
days: [0],
endTime: 1379,
startTime: 1200,
},
{
days: [0],
endTime: 1439,
startTime: 1380,
},
]);
});

View File

@@ -0,0 +1,470 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
import type { EventType } from "@prisma/client";
import { describe, expect, it, vi } from "vitest";
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
import { buildEventType } from "@calcom/lib/test/builder";
import type { Prisma } from "@calcom/prisma/client";
import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/prisma/zod";
const mockFindFirstEventType = (data?: Partial<CompleteEventType>) => {
const eventType = buildEventType(data as Partial<EventType>);
// const { scheduleId, destinationCalendar, ...restEventType } = eventType;
prismaMock.eventType.findFirst.mockResolvedValue(eventType as EventType);
return eventType;
};
vi.mock("@calcom/emails/email-manager", () => {
return {
sendSlugReplacementEmail: () => ({}),
};
});
vi.mock("@calcom/lib/server/i18n", () => {
return {
getTranslation: (key: string) => key,
};
});
describe("handleChildrenEventTypes", () => {
describe("Shortcircuits", () => {
it("Returns message 'No managed event type'", async () => {
mockFindFirstEventType();
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [], team: { name: "" } },
children: [],
updatedEventType: { schedulingType: null, slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(result.newUserIds).toEqual(undefined);
expect(result.oldUserIds).toEqual(undefined);
expect(result.deletedUserIds).toEqual(undefined);
expect(result.deletedExistentEventTypes).toEqual(undefined);
expect(result.message).toBe("No managed event type");
});
it("Returns message 'No managed event metadata'", async () => {
mockFindFirstEventType({
metadata: {},
locations: [],
});
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [], team: { name: "" } },
children: [],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(result.newUserIds).toEqual(undefined);
expect(result.oldUserIds).toEqual(undefined);
expect(result.deletedUserIds).toEqual(undefined);
expect(result.deletedExistentEventTypes).toEqual(undefined);
expect(result.message).toBe("No managed event metadata");
});
it("Returns message 'Missing event type'", async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.eventType.findFirst.mockImplementation(() => {
return new Promise((resolve) => {
resolve(null);
});
});
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [], team: { name: "" } },
children: [],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(result.newUserIds).toEqual(undefined);
expect(result.oldUserIds).toEqual(undefined);
expect(result.deletedUserIds).toEqual(undefined);
expect(result.deletedExistentEventTypes).toEqual(undefined);
expect(result.message).toBe("Missing event type");
});
});
describe("Happy paths", () => {
it("Adds new users", async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const {
schedulingType,
id,
teamId,
timeZone,
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [], team: { name: "" } },
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } }],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...evType,
parentId: 1,
users: { connect: [{ id: 4 }] },
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
userId: 4,
},
});
expect(result.newUserIds).toEqual([4]);
expect(result.oldUserIds).toEqual([]);
expect(result.deletedUserIds).toEqual([]);
expect(result.deletedExistentEventTypes).toEqual(undefined);
});
it("Updates old users", async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const {
schedulingType,
id,
teamId,
timeZone,
locations,
parentId,
userId,
scheduleId,
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
});
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } }],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: "somestring",
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {
bookingLimits: undefined,
},
});
const { profileId, ...rest } = evType;
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...rest,
locations: [],
scheduleId: null,
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
hashedLink: { create: { link: expect.any(String) } },
},
where: {
userId_parentId: {
userId: 4,
parentId: 1,
},
},
});
expect(result.newUserIds).toEqual([]);
expect(result.oldUserIds).toEqual([4]);
expect(result.deletedUserIds).toEqual([]);
expect(result.deletedExistentEventTypes).toEqual(undefined);
});
it("Deletes old users", async () => {
mockFindFirstEventType({ users: [], metadata: { managedEventConfig: {} }, locations: [] });
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
children: [],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(result.newUserIds).toEqual([]);
expect(result.oldUserIds).toEqual([]);
expect(result.deletedUserIds).toEqual([4]);
expect(result.deletedExistentEventTypes).toEqual(undefined);
});
it("Adds new users and updates/delete old users", async () => {
mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
});
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }, { userId: 1 }], team: { name: "" } },
children: [
{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } },
{ hidden: false, owner: { id: 5, name: "", email: "", eventTypeSlugs: [] } },
],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
// Have been called
expect(result.newUserIds).toEqual([5]);
expect(result.oldUserIds).toEqual([4]);
expect(result.deletedUserIds).toEqual([1]);
expect(result.deletedExistentEventTypes).toEqual(undefined);
});
});
describe("Slug conflicts", () => {
it("Deletes existent event types for new users added", async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const {
schedulingType,
id,
teamId,
timeZone,
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [], team: { name: "" } },
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: ["something"] } }],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...evType,
parentId: 1,
users: { connect: [{ id: 4 }] },
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: undefined,
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
userId: 4,
workflows: undefined,
},
});
expect(result.newUserIds).toEqual([4]);
expect(result.oldUserIds).toEqual([]);
expect(result.deletedUserIds).toEqual([]);
expect(result.deletedExistentEventTypes).toEqual([123]);
});
it("Deletes existent event types for old users updated", async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const {
schedulingType,
id,
teamId,
timeZone,
locations,
parentId,
userId,
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
});
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: ["something"] } }],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {
length: 30,
},
});
const { profileId, ...rest } = evType;
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...rest,
locations: [],
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
},
where: {
userId_parentId: {
userId: 4,
parentId: 1,
},
},
});
expect(result.newUserIds).toEqual([]);
expect(result.oldUserIds).toEqual([4]);
expect(result.deletedUserIds).toEqual([]);
expect(result.deletedExistentEventTypes).toEqual([123]);
});
});
describe("Workflows", () => {
it("Links workflows to new and existing assigned members", async () => {
const {
schedulingType: _schedulingType,
id: _id,
teamId: _teamId,
locations: _locations,
timeZone: _timeZone,
parentId: _parentId,
userId: _userId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
workflows: [
{
workflowId: 11,
} as CompleteWorkflowsOnEventTypes,
],
});
prismaMock.$transaction.mockResolvedValue([{ id: 2 }]);
await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
children: [
{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } },
{ hidden: false, owner: { id: 5, name: "", email: "", eventTypeSlugs: [] } },
],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
profileId: null,
updatedValues: {},
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: undefined,
locations: [],
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
parentId: 1,
userId: 5,
users: {
connect: [
{
id: 5,
},
],
},
workflows: {
create: [{ workflowId: 11 }],
},
},
});
const { profileId, ...rest } = evType;
if ("workflows" in rest) delete rest.workflows;
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...rest,
locations: [],
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
hashedLink: undefined,
},
where: {
userId_parentId: {
userId: 4,
parentId: 1,
},
},
});
expect(prismaMock.workflowsOnEventTypes.upsert).toHaveBeenCalledWith({
create: {
eventTypeId: 2,
workflowId: 11,
},
update: {},
where: {
workflowId_eventTypeId: {
eventTypeId: 2,
workflowId: 11,
},
},
});
});
});
});

View File

@@ -0,0 +1,150 @@
import { it, expect, describe, beforeAll } from "vitest";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getSubdomainRegExp } = require("../../getSubdomainRegExp");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { match, pathToRegexp } = require("next/dist/compiled/path-to-regexp");
type MatcherRes = (path: string) => { params: Record<string, string> };
let orgUserTypeRouteMatch: MatcherRes;
let orgUserRouteMatch: MatcherRes;
beforeAll(async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
process.env.NEXT_PUBLIC_WEBAPP_URL = "http://example.com";
const {
orgUserRoutePath,
orgUserTypeRoutePath,
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require("../../pagesAndRewritePaths");
orgUserTypeRouteMatch = match(orgUserTypeRoutePath);
orgUserRouteMatch = match(orgUserRoutePath);
console.log({
regExps: {
orgUserTypeRouteMatch: pathToRegexp(orgUserTypeRoutePath),
orgUserRouteMatch: pathToRegexp(orgUserRoutePath),
},
});
});
describe("next.config.js - Org Rewrite", () => {
const orgHostRegExp = (subdomainRegExp: string) =>
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.
new RegExp(`^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`);
describe("Host matching based on NEXT_PUBLIC_WEBAPP_URL", () => {
it("https://app.cal.com", () => {
const subdomainRegExp = getSubdomainRegExp("https://app.cal.com");
expect(orgHostRegExp(subdomainRegExp).exec("app.cal.com")).toEqual(null);
expect(orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups?.orgSlug).toEqual("company");
expect(orgHostRegExp(subdomainRegExp).exec("org.cal.com")?.groups?.orgSlug).toEqual("org");
expect(orgHostRegExp(subdomainRegExp).exec("localhost:3000")).toEqual(null);
});
it("app.cal.com", () => {
const subdomainRegExp = getSubdomainRegExp("app.cal.com");
expect(orgHostRegExp(subdomainRegExp).exec("app.cal.com")).toEqual(null);
expect(orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups?.orgSlug).toEqual("company");
});
it("https://calcom.app.company.com", () => {
const subdomainRegExp = getSubdomainRegExp("https://calcom.app.company.com");
expect(orgHostRegExp(subdomainRegExp).exec("calcom.app.company.com")).toEqual(null);
expect(orgHostRegExp(subdomainRegExp).exec("acme.calcom.app.company.com")?.groups?.orgSlug).toEqual(
"acme"
);
});
it("https://calcom.example.com", () => {
const subdomainRegExp = getSubdomainRegExp("https://calcom.example.com");
expect(orgHostRegExp(subdomainRegExp).exec("calcom.example.com")).toEqual(null);
expect(orgHostRegExp(subdomainRegExp).exec("acme.calcom.example.com")?.groups?.orgSlug).toEqual("acme");
// The following also matches which causes anything other than the domain in NEXT_PUBLIC_WEBAPP_URL to give 404
expect(orgHostRegExp(subdomainRegExp).exec("some-other.company.com")?.groups?.orgSlug).toEqual(
"some-other"
);
});
it("Should ignore Vercel preview URLs", () => {
const subdomainRegExp = getSubdomainRegExp("https://cal-xxxxxxxx-cal.vercel.app");
expect(
orgHostRegExp(subdomainRegExp).exec("https://cal-xxxxxxxx-cal.vercel.app")
).toMatchInlineSnapshot("null");
expect(orgHostRegExp(subdomainRegExp).exec("cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot(
"null"
);
});
});
describe("Rewrite", () => {
it("booking pages", () => {
expect(orgUserTypeRouteMatch("/user/type")?.params).toContain({
user: "user",
type: "type",
});
// User slug starting with 404(which is a page route) will work
expect(orgUserTypeRouteMatch("/404a/def")?.params).toEqual({
user: "404a",
type: "def",
});
// Team Page won't match - There is no /team prefix required for Org team event pages
expect(orgUserTypeRouteMatch("/team/abc")).toEqual(false);
expect(orgUserTypeRouteMatch("/abc")).toEqual(false);
expect(orgUserRouteMatch("/abc")?.params).toContain({
user: "abc",
});
// Tests that something that starts with 'd' which could accidentally match /d route is correctly identified as a booking page
expect(orgUserRouteMatch("/designer")?.params).toContain({
user: "designer",
});
// Tests that something that starts with 'apps' which could accidentally match /apps route is correctly identified as a booking page
expect(orgUserRouteMatch("/apps-conflict-possibility")?.params).toContain({
user: "apps-conflict-possibility",
});
// Tests that something that starts with '_next' which could accidentally match /_next route is correctly identified as a booking page
expect(orgUserRouteMatch("/_next-candidate")?.params).toContain({
user: "_next-candidate",
});
// Tests that something that starts with 'public' which could accidentally match /public route is correctly identified as a booking page
expect(orgUserRouteMatch("/public-person")?.params).toContain({
user: "public-person",
});
});
it("Non booking pages", () => {
expect(orgUserTypeRouteMatch("/_next/def")).toEqual(false);
expect(orgUserTypeRouteMatch("/public/def")).toEqual(false);
expect(orgUserRouteMatch("/_next/")).toEqual(false);
expect(orgUserRouteMatch("/public/")).toEqual(false);
expect(orgUserRouteMatch("/event-types/")).toEqual(false);
expect(orgUserTypeRouteMatch("/event-types/")).toEqual(false);
expect(orgUserRouteMatch("/event-types/?abc=1")).toEqual(false);
expect(orgUserTypeRouteMatch("/event-types/?abc=1")).toEqual(false);
expect(orgUserRouteMatch("/event-types")).toEqual(false);
expect(orgUserTypeRouteMatch("/event-types")).toEqual(false);
expect(orgUserRouteMatch("/event-types?abc=1")).toEqual(false);
expect(orgUserTypeRouteMatch("/event-types?abc=1")).toEqual(false);
expect(orgUserTypeRouteMatch("/john/avatar.png")).toEqual(false);
expect(orgUserTypeRouteMatch("/cancel/abcd")).toEqual(false);
expect(orgUserTypeRouteMatch("/success/abcd")).toEqual(false);
expect(orgUserRouteMatch("/forms/xdsdf-sd")).toEqual(false);
expect(orgUserRouteMatch("/router?form=")).toEqual(false);
});
});
});

View File

@@ -0,0 +1,10 @@
import { expect, it } from "vitest";
import { parseZone } from "@calcom/lib/parse-zone";
const EXPECTED_DATE_STRING = "2021-06-20T11:59:59+02:00";
it("has the right utcOffset regardless of the local timeZone", async () => {
expect(parseZone(EXPECTED_DATE_STRING)?.utcOffset()).toEqual(120);
expect(parseZone(EXPECTED_DATE_STRING)?.format()).toEqual(EXPECTED_DATE_STRING);
});

View File

@@ -0,0 +1,193 @@
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
import { expect, it } from "vitest";
import { getLuckyUser } from "@calcom/lib/server";
import { buildUser } from "@calcom/lib/test/builder";
it("can find lucky user with maximize availability", async () => {
const user1 = buildUser({
id: 1,
username: "test1",
name: "Test User 1",
email: "test@example.com",
bookings: [
{
createdAt: new Date("2022-01-25T05:30:00.000Z"),
},
{
createdAt: new Date("2022-01-25T06:30:00.000Z"),
},
],
});
const user2 = buildUser({
id: 2,
username: "test2",
name: "Test User 2",
email: "tes2t@example.com",
bookings: [
{
createdAt: new Date("2022-01-25T04:30:00.000Z"),
},
],
});
const users = [user1, user2];
// TODO: we may be able to use native prisma generics somehow?
prismaMock.user.findMany.mockResolvedValue(users);
prismaMock.booking.findMany.mockResolvedValue([]);
await expect(
getLuckyUser("MAXIMIZE_AVAILABILITY", {
availableUsers: users,
eventTypeId: 1,
})
).resolves.toStrictEqual(users[1]);
});
it("can find lucky user with maximize availability and priority ranking", async () => {
const user1 = buildUser({
id: 1,
username: "test1",
name: "Test User 1",
email: "test@example.com",
priority: 2,
bookings: [
{
createdAt: new Date("2022-01-25T05:30:00.000Z"),
},
{
createdAt: new Date("2022-01-25T06:30:00.000Z"),
},
],
});
const user2 = buildUser({
id: 2,
username: "test2",
name: "Test User 2",
email: "tes2t@example.com",
bookings: [
{
createdAt: new Date("2022-01-25T04:30:00.000Z"),
},
],
});
const users = [user1, user2];
// TODO: we may be able to use native prisma generics somehow?
prismaMock.user.findMany.mockResolvedValue(users);
prismaMock.booking.findMany.mockResolvedValue([]);
const test = await getLuckyUser("MAXIMIZE_AVAILABILITY", {
availableUsers: users,
eventTypeId: 1,
});
// both users have medium priority (one user has no priority set, default to medium) so pick least recently booked
await expect(
getLuckyUser("MAXIMIZE_AVAILABILITY", {
availableUsers: users,
eventTypeId: 1,
})
).resolves.toStrictEqual(users[1]);
const userLowest = buildUser({
id: 1,
username: "test1",
name: "Test User 1",
email: "test@example.com",
priority: 0,
bookings: [
{
createdAt: new Date("2022-01-25T03:30:00.000Z"),
},
],
});
const userMedium = buildUser({
id: 2,
username: "test2",
name: "Test User 2",
email: "tes2t@example.com",
priority: 2,
bookings: [
{
createdAt: new Date("2022-01-25T04:30:00.000Z"),
},
],
});
const userHighest = buildUser({
id: 2,
username: "test2",
name: "Test User 2",
email: "tes2t@example.com",
priority: 4,
bookings: [
{
createdAt: new Date("2022-01-25T05:30:00.000Z"),
},
],
});
const usersWithPriorities = [userLowest, userMedium, userHighest];
// TODO: we may be able to use native prisma generics somehow?
prismaMock.user.findMany.mockResolvedValue(usersWithPriorities);
prismaMock.booking.findMany.mockResolvedValue([]);
// pick the user with the highest priority
await expect(
getLuckyUser("MAXIMIZE_AVAILABILITY", {
availableUsers: usersWithPriorities,
eventTypeId: 1,
})
).resolves.toStrictEqual(usersWithPriorities[2]);
const userLow = buildUser({
id: 1,
username: "test1",
name: "Test User 1",
email: "test@example.com",
priority: 0,
bookings: [
{
createdAt: new Date("2022-01-25T02:30:00.000Z"),
},
],
});
const userHighLeastRecentBooking = buildUser({
id: 2,
username: "test2",
name: "Test User 2",
email: "tes2t@example.com",
priority: 3,
bookings: [
{
createdAt: new Date("2022-01-25T03:30:00.000Z"),
},
],
});
const userHighRecentBooking = buildUser({
id: 3,
username: "test3",
name: "Test User 3",
email: "test3t@example.com",
priority: 3,
bookings: [
{
createdAt: new Date("2022-01-25T04:30:00.000Z"),
},
],
});
const usersWithSamePriorities = [userLow, userHighLeastRecentBooking, userHighRecentBooking];
// TODO: we may be able to use native prisma generics somehow?
prismaMock.user.findMany.mockResolvedValue(usersWithSamePriorities);
prismaMock.booking.findMany.mockResolvedValue([]);
// pick the least recently booked user of the two with the highest priority
await expect(
getLuckyUser("MAXIMIZE_AVAILABILITY", {
availableUsers: usersWithSamePriorities,
eventTypeId: 1,
})
).resolves.toStrictEqual(usersWithSamePriorities[1]);
});

View File

@@ -0,0 +1,88 @@
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
import type { Payment, Prisma, PaymentOption, Booking } from "@prisma/client";
import { v4 as uuidv4 } from "uuid";
import "vitest-fetch-mock";
import { sendAwaitingPaymentEmail } from "@calcom/emails";
import logger from "@calcom/lib/logger";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
export function getMockPaymentService() {
function createPaymentLink(/*{ paymentUid, name, email, date }*/) {
return "http://mock-payment.example.com/";
}
const paymentUid = uuidv4();
const externalId = uuidv4();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
class MockPaymentService implements IAbstractPaymentService {
// TODO: We shouldn't need to implement adding a row to Payment table but that's a requirement right now.
// We should actually delegate table creation to the core app. Here, only the payment app specific logic should come
async create(
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
bookingId: Booking["id"],
userId: Booking["userId"],
username: string | null,
bookerName: string | null,
bookerEmail: string,
paymentOption: PaymentOption
) {
const paymentCreateData = {
id: 1,
uid: paymentUid,
appId: null,
bookingId,
// booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
fee: 10,
success: true,
refunded: false,
data: {},
externalId,
paymentOption,
amount: payment.amount,
currency: payment.currency,
};
const paymentData = prismaMock.payment.create({
data: paymentCreateData,
});
logger.silly("Created mock payment", JSON.stringify({ paymentData }));
return paymentData;
}
async afterPayment(
event: CalendarEvent,
booking: {
user: { email: string | null; name: string | null; timeZone: string } | null;
id: number;
startTime: { toISOString: () => string };
uid: string;
},
paymentData: Payment
): Promise<void> {
// TODO: App implementing PaymentService is supposed to send email by itself at the moment.
await sendAwaitingPaymentEmail({
...event,
paymentInfo: {
link: createPaymentLink(/*{
paymentUid: paymentData.uid,
name: booking.user?.name,
email: booking.user?.email,
date: booking.startTime.toISOString(),
}*/),
paymentOption: paymentData.paymentOption || "ON_BOOKING",
amount: paymentData.amount,
currency: paymentData.currency,
},
});
}
}
return {
paymentUid,
externalId,
MockPaymentService,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
import { createMocks } from "node-mocks-http";
import type {
CustomNextApiRequest,
CustomNextApiResponse,
} from "@calcom/features/bookings/lib/handleNewBooking/test/fresh-booking.test";
export function createMockNextJsRequest(...args: Parameters<typeof createMocks>) {
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
import { getDate } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import type { SchedulingType } from "@calcom/prisma/client";
export const DEFAULT_TIMEZONE_BOOKER = "Asia/Kolkata";
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: DEFAULT_TIMEZONE_BOOKER,
language: "en",
user: "teampro",
metadata: {},
hasHashedBookingLink: false,
hashedLink: null,
};
}
export function getMockRequestDataForBooking({
data,
}: {
data: Partial<ReturnType<typeof getBasicMockRequestDataForBooking>> & {
eventTypeId: number;
user?: string;
rescheduleUid?: string;
bookingUid?: string;
recurringEventId?: string;
recurringCount?: number;
schedulingType?: SchedulingType;
responses: {
email: string;
name: string;
location: { optionValue: ""; value: string };
smsReminderNumber?: string;
};
};
}) {
return {
...getBasicMockRequestDataForBooking(),
...data,
};
}

View File

@@ -0,0 +1,7 @@
import type z from "zod";
import type { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
export function getMockRequestDataForCancelBooking(data: z.infer<typeof schemaBookingCancelParams>) {
return data;
}

View File

@@ -0,0 +1,46 @@
import { UserPermissionRole } from "@calcom/prisma/client";
import { IdentityProvider } from "@calcom/prisma/enums";
export const getSampleUserInSession = function () {
return {
locale: "",
avatar: "",
organization: {
isOrgAdmin: false,
metadata: null,
id: 1,
requestedSlug: null,
},
profile: null,
defaultScheduleId: null,
name: "",
defaultBookerLayouts: null,
timeZone: "Asia/Kolkata",
selectedCalendars: [],
destinationCalendar: null,
emailVerified: new Date(),
allowDynamicBooking: false,
bio: "",
weekStart: "",
startTime: 0,
endTime: 0,
bufferTime: 0,
hideBranding: false,
timeFormat: 12,
twoFactorEnabled: false,
identityProvider: IdentityProvider.CAL,
brandColor: "#292929",
darkBrandColor: "#fafafa",
metadata: null,
role: UserPermissionRole.USER,
disableImpersonation: false,
organizationId: null,
theme: "",
appTheme: "",
createdDate: new Date(),
trialEndsAt: new Date(),
completedOnboarding: false,
allowSEOIndexing: false,
receiveMonthlyDigestEmail: false,
};
};

View File

@@ -0,0 +1,36 @@
import {
enableEmailFeature,
mockNoTranslations,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { beforeEach, afterEach } from "vitest";
export function setupAndTeardown() {
beforeEach(() => {
// Required to able to generate token in email in some cases
//@ts-expect-error - It is a readonly variable
process.env.CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui";
//@ts-expect-error - It is a readonly variable
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";
// Ensure that Rate Limiting isn't enforced for tests
delete process.env.UNKEY_ROOT_KEY;
mockNoTranslations();
// mockEnableEmailFeature();
enableEmailFeature();
globalThis.testEmails = [];
fetchMock.resetMocks();
});
afterEach(() => {
//@ts-expect-error - It is a readonly variable
delete process.env.CALENDSO_ENCRYPTION_KEY;
//@ts-expect-error - It is a readonly variable
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";
});
}

View File

@@ -0,0 +1,81 @@
import { createOrganization } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import type { TestFunction } from "vitest";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { test } from "@calcom/web/test/fixtures/fixtures";
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
const WEBSITE_PROTOCOL = new URL(WEBSITE_URL).protocol;
const _testWithAndWithoutOrg = (
description: Parameters<typeof testWithAndWithoutOrg>[0],
fn: Parameters<typeof testWithAndWithoutOrg>[1],
timeout: Parameters<typeof testWithAndWithoutOrg>[2],
mode: "only" | "skip" | "run" = "run"
) => {
const t = mode === "only" ? test.only : mode === "skip" ? test.skip : test;
t(
`${description} - With org`,
async ({ emails, sms, meta, task, onTestFailed, expect, skip }) => {
const org = await createOrganization({
name: "Test Org",
slug: "testorg",
});
await fn({
meta,
task,
onTestFailed,
expect,
emails,
sms,
skip,
org: {
organization: org,
urlOrigin: `${WEBSITE_PROTOCOL}//${org.slug}.cal.local:3000`,
},
});
},
timeout
);
t(
`${description}`,
async ({ emails, sms, meta, task, onTestFailed, expect, skip }) => {
await fn({
emails,
sms,
meta,
task,
onTestFailed,
expect,
skip,
org: null,
});
},
timeout
);
};
export const testWithAndWithoutOrg = (
description: string,
fn: TestFunction<
Fixtures & {
org: {
organization: { id: number | null };
urlOrigin?: string;
} | null;
}
>,
timeout?: number
) => {
_testWithAndWithoutOrg(description, fn, timeout, "run");
};
testWithAndWithoutOrg.only = ((description, fn, timeout) => {
_testWithAndWithoutOrg(description, fn, timeout, "only");
}) as typeof _testWithAndWithoutOrg;
testWithAndWithoutOrg.skip = ((description, fn, timeout) => {
_testWithAndWithoutOrg(description, fn, timeout, "skip");
}) as typeof _testWithAndWithoutOrg;