Files
cal-diy-oidc/apps/api/v1/test/lib/bookings/_post.test.ts
T

745 lines
25 KiB
TypeScript

import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, beforeEach } from "vitest";
import dayjs from "@calcom/dayjs";
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook, buildUser } from "@calcom/lib/test/builder";
import { prisma } from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import { CreationSource, BookingStatus } from "@calcom/prisma/enums";
import handler from "../../../pages/api/bookings/_post";
vi.mock("@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB", () => ({
getEventTypesFromDB: vi.fn(),
}));
const mockEventTypeData = {
eventType: {
id: 1,
title: "Test Event",
profile: { organizationId: null },
users: [{ id: 1, email: "test@example.com", credentials: [] }],
hosts: [],
customInputs: [],
recurringEvent: null,
length: 15,
slug: "test-event",
price: 0,
currency: "USD",
requiresConfirmation: false,
disableGuests: false,
minimumBookingNotice: 120,
beforeEventBuffer: 0,
afterEventBuffer: 0,
seatsPerTimeSlot: null,
seatsShowAttendees: false,
schedulingType: null,
periodType: "UNLIMITED",
periodStartDate: null,
periodEndDate: null,
periodDays: null,
periodCountCalendarDays: false,
locations: [],
metadata: {},
successRedirectUrl: null,
description: null,
team: null,
owner: { id: 1, email: "test@example.com", credentials: [] },
},
users: [{ id: 1, email: "test@example.com", credentials: [] }],
allCredentials: [],
destinationCalendar: null,
};
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/features/webhooks/lib/sendOrSchedulePayload", () => ({
default: vi.fn().mockResolvedValue({}),
}));
const mockFindOriginalRescheduledBooking = vi.fn();
vi.mock("@calcom/features/bookings/repositories/BookingRepository", () => ({
BookingRepository: vi.fn().mockImplementation(function() { return {
findOriginalRescheduledBooking: mockFindOriginalRescheduledBooking,
}; }),
}));
vi.mock("@calcom/features/watchlist/operations/check-if-users-are-blocked.controller", () => ({
checkIfUsersAreBlocked: vi.fn().mockResolvedValue(false),
}));
vi.mock("@calcom/features/watchlist/operations/filter-blocked-users.controller", () => ({
filterBlockedUsers: vi.fn().mockImplementation(async (users) => ({
eligibleUsers: users,
blockedCount: 0,
})),
}));
vi.mock("@calcom/features/di/containers/QualifiedHosts", () => ({
getQualifiedHostsService: vi.fn().mockReturnValue({
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
qualifiedRRHosts: [],
allFallbackRRHosts: [],
fixedHosts: [],
}),
}),
}));
vi.mock("@calcom/features/bookings/lib/EventManager", () => ({
default: vi.fn().mockImplementation(function() {
return {
reschedule: vi.fn().mockResolvedValue({
results: [],
referencesToCreate: [],
}),
create: vi.fn().mockResolvedValue({
results: [],
referencesToCreate: [],
}),
update: vi.fn().mockResolvedValue({
results: [],
referencesToCreate: [],
}),
createAllCalendarEvents: vi.fn().mockResolvedValue([]),
updateAllCalendarEvents: vi.fn().mockResolvedValue([]),
deleteEventsAndMeetings: vi.fn().mockResolvedValue([]),
};
}),
placeholderCreatedEvent: {
results: [],
referencesToCreate: [],
},
}));
vi.mock("@calcom/lib/availability", () => ({
getUserAvailability: vi.fn().mockResolvedValue([
{
start: new Date("1970-01-01T09:00:00.000Z"),
end: new Date("1970-01-01T17:00:00.000Z"),
},
]),
getAvailableSlots: vi.fn().mockResolvedValue([
{
time: new Date().toISOString(),
attendees: 1,
bookingUid: null,
users: [2],
},
]),
}));
vi.mock("@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers", () => ({
ensureAvailableUsers: vi.fn().mockImplementation(async (eventType) => {
return eventType.users || [{ id: 2, email: "test@example.com", name: "Test User", isFixed: false }];
}),
}));
vi.mock("@calcom/features/profile/repositories/ProfileRepository", () => ({
ProfileRepository: {
findManyForUser: vi.fn().mockResolvedValue([]),
buildPersonalProfileFromUser: vi.fn().mockReturnValue({
id: null,
upId: "usr-2",
username: "test-user",
organizationId: null,
organization: null,
}),
},
}));
vi.mock("@calcom/features/flags/features.repository", () => ({
FeaturesRepository: vi.fn().mockImplementation(function() { return {
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false),
checkIfTeamHasFeature: vi.fn().mockResolvedValue(false),
}; }),
}));
vi.mock("@calcom/features/webhooks/lib/getWebhooks", () => ({
default: vi.fn().mockResolvedValue([]),
}));
vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflows", () => ({
getAllWorkflows: vi.fn().mockResolvedValue([]),
workflowSelect: {},
}));
vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType", () => ({
getAllWorkflowsFromEventType: vi.fn().mockResolvedValue([]),
}));
vi.mock("@calcom/i18n/server", () => {
const mockT = (key: string, options?: any) => {
if (key === "event_between_users") {
return `${options?.eventName} between ${options?.host} and ${options?.attendeeName}`;
}
if (key === "scheduler") {
return "Scheduler";
}
if (key === "google_meet_warning") {
return "Google Meet warning";
}
return key;
};
return {
getTranslation: vi.fn().mockResolvedValue(mockT),
t: mockT,
};
});
describe("POST /api/bookings - eventTypeId validation", () => {
test("String eventTypeId should return 400", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
eventTypeId: "invalid-string",
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
message: "Bad request, eventTypeId must be a number",
})
);
});
test("Number eventTypeId should not trigger validation error", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
eventTypeId: 123,
},
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(
buildEventType({
id: 123,
profileId: null,
locations: [{ type: "integrations:daily" }],
})
);
await handler(req, res);
const statusCode = res._getStatusCode();
const responseData = JSON.parse(res._getData());
if (statusCode === 400) {
expect(responseData.message).not.toBe("Bad request, eventTypeId must be a number");
}
});
});
describe("POST /api/bookings", () => {
beforeEach(() => {
vi.clearAllMocks();
mockFindOriginalRescheduledBooking.mockResolvedValue(null);
(getEventTypesFromDB as any).mockResolvedValue(mockEventTypeData.eventType);
});
describe("Errors", () => {
test("Missing required data", async () => {
(getEventTypesFromDB as any).mockRejectedValue(new Error(ErrorCode.RequestBodyInvalid));
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
eventTypeId: 2,
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
message: ErrorCode.RequestBodyInvalid,
})
);
});
test("Invalid eventTypeId", async () => {
(getEventTypesFromDB as any).mockRejectedValue(new Error(ErrorCode.EventTypeNotFound));
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
title: "test",
eventTypeId: 999,
startTime: dayjs().add(1, "day").toDate(),
endTime: dayjs().add(1, "day").add(15, "minutes").toDate(),
},
prisma,
});
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
message: ErrorCode.EventTypeNotFound,
})
);
});
test.skip("Missing recurringCount", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "test",
start: dayjs().add(1, "day").format(),
end: dayjs().add(1, "day").add(15, "minutes").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
});
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
message: expect.stringContaining("recurringCount"),
})
);
});
test.skip("Invalid recurringCount", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "test",
start: dayjs().add(1, "day").format(),
end: dayjs().add(1, "day").add(15, "minutes").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
recurringCount: 15,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
});
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
message: expect.stringContaining("recurringCount"),
})
);
});
test.skip("No available users", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "test",
start: dayjs().add(1, "day").format(),
end: dayjs().add(1, "day").add(1, "hour").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
});
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
message: "Invalid event length",
})
);
});
});
describe("Success", () => {
describe("Regular event-type", () => {
let createdBooking: Booking;
test("Creates one single booking", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "test",
start: dayjs().add(1, "day").format(),
end: dayjs().add(1, "day").add(15, "minutes").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
});
prismaMock.booking.findMany.mockResolvedValue([]);
const mockBooking = buildBooking({
id: 1,
uid: "test-booking-uid",
title: "Test Event",
startTime: new Date(dayjs().add(1, "day").format()),
endTime: new Date(dayjs().add(1, "day").add(15, "minutes").format()),
eventTypeId: buildEventType({
id: 2,
profileId: null,
}).id,
oneTimePassword: null,
creationSource: "API_V1",
});
prismaMock.$transaction.mockImplementation(async (callback) => {
const mockTx = {
booking: {
create: prismaMock.booking.create.mockResolvedValue(mockBooking),
update: vi.fn().mockResolvedValue({}),
},
app_RoutingForms_FormResponse: {
update: vi.fn().mockResolvedValue({}),
},
};
return await callback(prismaMock);
});
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
createdBooking = JSON.parse(res._getData());
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
});
test("Reschedule created booking", async () => {
const originalBooking = buildBooking({
uid: "original-booking-uid",
userId: 4,
eventTypeId: buildEventType({
id: 2,
locations: [{ type: "integrations:daily" }],
}).id,
references: [],
});
mockFindOriginalRescheduledBooking.mockResolvedValue(originalBooking);
prismaMock.booking.findUnique.mockResolvedValue({
...originalBooking,
status: "CANCELLED",
});
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "testReschedule",
start: dayjs().add(2, "day").format(),
end: dayjs().add(2, "day").add(15, "minutes").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
rescheduleUid: "original-booking-uid",
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
});
prismaMock.booking.findMany.mockResolvedValue([]);
const mockBooking = buildBooking({
id: 2,
uid: "test-reschedule-uid",
title: "Test Reschedule Event",
startTime: new Date(dayjs().add(2, "day").format()),
endTime: new Date(dayjs().add(2, "day").add(15, "minutes").format()),
eventTypeId: buildEventType({
id: 2,
profileId: null,
locations: [{ type: "integrations:daily" }],
}).id,
oneTimePassword: null,
fromReschedule: "original-booking-uid",
});
prismaMock.$transaction.mockImplementation(async (callback) => {
const mockTx = {
booking: {
create: prismaMock.booking.create.mockResolvedValue(mockBooking),
update: vi.fn().mockResolvedValue({}),
},
app_RoutingForms_FormResponse: {
update: vi.fn().mockResolvedValue({}),
},
};
return await callback(prismaMock);
});
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
const rescheduledBooking = JSON.parse(res._getData()) as Booking;
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
expect(rescheduledBooking.fromReschedule).toEqual("original-booking-uid");
const previousBooking = await prisma.booking.findUnique({
where: { uid: "original-booking-uid" },
});
expect(previousBooking?.status).toBe(BookingStatus.CANCELLED);
});
test("Creates source as api_v1", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "test",
start: dayjs().add(1, "day").format(),
end: dayjs().add(1, "day").add(15, "minutes").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
});
prismaMock.booking.findMany.mockResolvedValue([]);
const mockBooking = buildBooking({
id: 3,
uid: "test-api-v1-uid",
title: "Test API V1 Event",
startTime: new Date(dayjs().add(1, "day").format()),
endTime: new Date(dayjs().add(1, "day").add(15, "minutes").format()),
eventTypeId: buildEventType({
id: 2,
profileId: null,
}).id,
oneTimePassword: null,
creationSource: "API_V1",
});
prismaMock.$transaction.mockImplementation(async (callback) => {
const mockTx = {
booking: {
create: prismaMock.booking.create.mockResolvedValue(mockBooking),
update: vi.fn().mockResolvedValue({}),
},
app_RoutingForms_FormResponse: {
update: vi.fn().mockResolvedValue({}),
},
};
return await callback(prismaMock);
});
await handler(req, res);
createdBooking = JSON.parse(res._getData());
expect(createdBooking.creationSource).toEqual(CreationSource.API_V1);
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
});
});
describe("Recurring event-type", () => {
test.skip("Creates multiple bookings", async () => {
const recurringDates = Array.from(Array(12).keys()).map((i) => ({
start: dayjs().add(1, "day").add(i, "week").toISOString(),
end: dayjs().add(1, "day").add(i, "week").add(15, "minutes").toISOString(),
}));
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
title: "test",
eventTypeId: 2,
start: recurringDates[0].start,
end: recurringDates[0].end,
timeZone: "UTC",
language: "en",
responses: {
name: "Test User",
email: "test@example.com",
location: { optionValue: "", value: "integrations:daily" },
},
metadata: {},
recurringCount: 12,
recurringEventId: "test-recurring-event-id",
allRecurringDates: recurringDates,
isFirstRecurringSlot: true,
numSlotsToCheckForAvailability: 12,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
});
const mockBookings = Array.from(Array(12).keys()).map((i) =>
buildBooking({ id: i + 1, uid: `recurring-booking-${i}` })
);
prismaMock.booking.create.mockResolvedValue(buildBooking({ id: 1, uid: "test-booking-uid" }));
prismaMock.$transaction.mockImplementation(async (callback) => {
return await callback(prismaMock);
});
prismaMock.webhook.findMany.mockResolvedValue([]);
await handler(req, res);
const data = JSON.parse(res._getData());
expect(prismaMock.$transaction).toHaveBeenCalled();
expect(res._getStatusCode()).toBe(200);
expect(data.message).toEqual("Bookings created successfully.");
expect(data.bookings.length).toEqual(12);
});
});
test.skip("Notifies multiple bookings", async () => {
const recurringDates = Array.from(Array(12).keys()).map((i) => ({
start: dayjs().add(1, "day").add(i, "week").toISOString(),
end: dayjs().add(1, "day").add(i, "week").add(15, "minutes").toISOString(),
}));
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
title: "test",
eventTypeId: 2,
start: recurringDates[0].start,
end: recurringDates[0].end,
timeZone: "UTC",
language: "en",
responses: {
name: "Test User",
email: "test@example.com",
location: { optionValue: "", value: "integrations:daily" },
},
metadata: {},
recurringCount: 12,
recurringEventId: "test-recurring-event-id",
allRecurringDates: recurringDates,
isFirstRecurringSlot: true,
numSlotsToCheckForAvailability: 12,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
});
const createdAt = new Date();
const mockBookings = Array.from(Array(12).keys()).map((i) =>
buildBooking({ id: i + 1, uid: `webhook-booking-${i}`, createdAt })
);
prismaMock.booking.create.mockResolvedValue(buildBooking({ id: 1, uid: "test-booking-uid" }));
prismaMock.$transaction.mockImplementation(async (callback) => {
return await callback(prismaMock);
});
const mockedWebhooks = [
buildWebhook({
subscriberUrl: "http://mockedURL1.com",
createdAt,
eventTypeId: 1,
secret: "secret1",
}),
buildWebhook({
subscriberUrl: "http://mockedURL2.com",
createdAt,
eventTypeId: 2,
secret: "secret2",
}),
];
prismaMock.webhook.findMany.mockResolvedValue(mockedWebhooks);
await handler(req, res);
const data = JSON.parse(res._getData());
expect(sendPayload).toHaveBeenCalledTimes(24);
expect(data.message).toEqual("Bookings created successfully.");
expect(data.bookings.length).toEqual(12);
});
});
});