Recurring booking implementation (#167)
* Recurring booking implementation * Tweaks to expected texts in tests * Applied feedback Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||
* https://jestjs.io/docs/en/configuration.html
|
||||
*/
|
||||
|
||||
const config = {
|
||||
clearMocks: true,
|
||||
coverageDirectory: "./coverage",
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ["pages/api/**/*.ts"],
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
// coverageProvider: "babel",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
coverageReporters: ["json", "text", "lcov", "clover"],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 50,
|
||||
functions: 40,
|
||||
branches: 50,
|
||||
statements: 50,
|
||||
},
|
||||
},
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
errorOnDeprecated: true,
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
maxWorkers: "50%",
|
||||
|
||||
moduleNameMapper: {
|
||||
"^@lib/(.*)$": "<rootDir>/lib/$1",
|
||||
"^@api/(.*)$": "<rootDir>/pages/api/$1",
|
||||
},
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
slowTestThreshold: 0.1,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
};
|
||||
export default config;
|
||||
@@ -16,6 +16,7 @@ const schemaBookingCreateParams = z
|
||||
title: z.string(),
|
||||
startTime: z.date().or(z.string()),
|
||||
endTime: z.date().or(z.string()),
|
||||
recurringCount: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { BookingResponse, BookingsResponse } from "@lib/types";
|
||||
import { schemaBookingReadPublic } from "@lib/validations/booking";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings:
|
||||
* get:
|
||||
* summary: Find all bookings
|
||||
* operationId: listBookings
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No bookings were found
|
||||
*/
|
||||
async function handler(
|
||||
{ userId, isAdmin, prisma }: NextApiRequest,
|
||||
res: NextApiResponse<BookingsResponse | BookingResponse>
|
||||
) {
|
||||
const args: Prisma.BookingFindManyArgs = isAdmin ? {} : { where: { userId } };
|
||||
const data = await prisma.booking.findMany(args);
|
||||
const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking));
|
||||
if (!bookings) throw new HttpError({ statusCode: 401, message: "No Bookings were found" });
|
||||
res.status(200).json({ bookings });
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
@@ -0,0 +1,168 @@
|
||||
import { HttpError } from "@/../../packages/lib/http-error";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import z from "zod";
|
||||
|
||||
import { BookingResponse, BookingsResponse } from "@calcom/api/lib/types";
|
||||
import sendPayload from "@calcom/api/lib/utils/sendPayload";
|
||||
import getWebhooks from "@calcom/api/lib/utils/webhookSubscriptions";
|
||||
import { schemaBookingCreateBodyParams, schemaBookingReadPublic } from "@calcom/api/lib/validations/booking";
|
||||
import { schemaEventTypeReadPublic } from "@calcom/api/lib/validations/event-type";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings:
|
||||
* post:
|
||||
* summary: Creates a new booking
|
||||
* operationId: addBooking
|
||||
* requestBody:
|
||||
* description: Edit an existing booking related to one of your event-types
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* title:
|
||||
* type: string
|
||||
* example: 15min
|
||||
* startTime:
|
||||
* type: string
|
||||
* example: 1970-01-01T17:00:00.000Z
|
||||
* endTime:
|
||||
* type: string
|
||||
* example: 1970-01-01T17:00:00.000Z
|
||||
* recurringCount:
|
||||
* type: number
|
||||
* example: 8
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Booking(s) created successfully.
|
||||
* 400:
|
||||
* description: |
|
||||
* Message | Cause
|
||||
* :--|:--
|
||||
* Booking body is invalid| Missing property on booking entity.
|
||||
* Invalid eventTypeId| The provided eventTypeId does not exist.
|
||||
* Missing recurringCount| The eventType is recurring, and no recurringCount was passed.
|
||||
* Invalid recurringCount| The provided recurringCount is greater than the eventType recurring config
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function handler(
|
||||
{ body, userId, isAdmin, prisma }: NextApiRequest,
|
||||
res: NextApiResponse<BookingsResponse | BookingResponse>
|
||||
) {
|
||||
const booking = schemaBookingCreateBodyParams.parse(body);
|
||||
if (!isAdmin) {
|
||||
booking.userId = userId;
|
||||
}
|
||||
const eventTypeDb = await prisma.eventType.findUnique({
|
||||
where: { id: booking.eventTypeId },
|
||||
});
|
||||
const eventType = schemaEventTypeReadPublic.parse(eventTypeDb);
|
||||
let bookings: z.infer<typeof schemaBookingReadPublic>[];
|
||||
if (!eventType) throw new HttpError({ statusCode: 400, message: "Could not create new booking" });
|
||||
if (eventType.recurringEvent) {
|
||||
console.log("Event type has recurring configuration");
|
||||
if (!booking.recurringCount) throw new HttpError({ statusCode: 400, message: "Missing recurringCount" });
|
||||
if (eventType.recurringEvent.count && booking.recurringCount > eventType?.recurringEvent.count) {
|
||||
throw new HttpError({ statusCode: 400, message: "Invalid recurringCount" });
|
||||
}
|
||||
// Event type is recurring, ceating each booking
|
||||
const recurringEventId = uuidv4();
|
||||
const allBookings = await Promise.all(
|
||||
Array.from(Array(booking.recurringCount).keys()).map(async () => {
|
||||
return await prisma.booking.create({
|
||||
data: {
|
||||
uid: uuidv4(),
|
||||
recurringEventId,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
title: booking.title,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
userId: booking.userId,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
bookings = allBookings.map((book) => schemaBookingReadPublic.parse(book));
|
||||
} else {
|
||||
// Event type not recurring, ceating as single one
|
||||
const data = await prisma.booking.create({
|
||||
data: {
|
||||
uid: uuidv4(),
|
||||
eventTypeId: booking.eventTypeId,
|
||||
title: booking.title,
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
userId: booking.userId,
|
||||
},
|
||||
});
|
||||
bookings = [schemaBookingReadPublic.parse(data)];
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
bookings.map(async (booking) => {
|
||||
const evt = {
|
||||
type: eventType?.title || booking.title,
|
||||
title: booking.title,
|
||||
description: "",
|
||||
additionalNotes: "",
|
||||
customInputs: {},
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
name: "",
|
||||
email: "",
|
||||
timeZone: "",
|
||||
language: {
|
||||
locale: "en",
|
||||
},
|
||||
},
|
||||
attendees: [],
|
||||
location: "",
|
||||
destinationCalendar: null,
|
||||
hideCalendar: false,
|
||||
uid: booking.uid,
|
||||
metadata: {},
|
||||
};
|
||||
console.log(`evt: ${evt}`);
|
||||
|
||||
// Send Webhook call if hooked to BOOKING_CREATED
|
||||
const triggerEvent = WebhookTriggerEvents.BOOKING_CREATED;
|
||||
console.log(`Trigger Event: ${triggerEvent}`);
|
||||
const subscriberOptions = {
|
||||
userId,
|
||||
eventTypeId: booking.eventTypeId as number,
|
||||
triggerEvent,
|
||||
};
|
||||
console.log(`subscriberOptions: ${subscriberOptions}`);
|
||||
|
||||
const subscribers = await getWebhooks(subscriberOptions, prisma);
|
||||
console.log(`subscribers: ${subscribers}`);
|
||||
const bookingId = booking?.id;
|
||||
await Promise.all(
|
||||
subscribers.map((sub) =>
|
||||
sendPayload(triggerEvent, new Date().toISOString(), sub, {
|
||||
...evt,
|
||||
bookingId,
|
||||
})
|
||||
)
|
||||
);
|
||||
console.log("All promises resolved! About to send the response");
|
||||
})
|
||||
);
|
||||
|
||||
if (bookings.length > 1) {
|
||||
res.status(201).json({ bookings, message: "Bookings created successfully." });
|
||||
} else {
|
||||
res.status(201).json({ booking: bookings[0], message: "Booking created successfully." });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
+7
-167
@@ -1,170 +1,10 @@
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "@lib/helpers/withMiddleware";
|
||||
import { BookingResponse, BookingsResponse } from "@lib/types";
|
||||
import sendPayload from "@lib/utils/sendPayload";
|
||||
import getWebhooks from "@lib/utils/webhookSubscriptions";
|
||||
import { schemaBookingCreateBodyParams, schemaBookingReadPublic } from "@lib/validations/booking";
|
||||
import { schemaEventTypeReadPublic } from "@lib/validations/event-type";
|
||||
|
||||
async function createOrlistAllBookings(
|
||||
{ method, body, userId, isAdmin, prisma }: NextApiRequest,
|
||||
res: NextApiResponse<BookingsResponse | BookingResponse>
|
||||
) {
|
||||
if (method === "GET") {
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings:
|
||||
* get:
|
||||
* summary: Find all bookings
|
||||
* operationId: listBookings
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No bookings were found
|
||||
*/
|
||||
if (!isAdmin) {
|
||||
const data = await prisma.booking.findMany({ where: { userId } });
|
||||
const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking));
|
||||
if (bookings) res.status(200).json({ bookings });
|
||||
else {
|
||||
(error: Error) =>
|
||||
res.status(404).json({
|
||||
message: "No Bookings were found",
|
||||
error,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const data = await prisma.booking.findMany();
|
||||
const bookings = data.map((booking) => schemaBookingReadPublic.parse(booking));
|
||||
if (bookings) res.status(200).json({ bookings });
|
||||
else {
|
||||
(error: Error) =>
|
||||
res.status(404).json({
|
||||
message: "No Bookings were found",
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (method === "POST") {
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings:
|
||||
* post:
|
||||
* summary: Creates a new booking
|
||||
* operationId: addBooking
|
||||
* requestBody:
|
||||
* description: Edit an existing booking related to one of your event-types
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* title:
|
||||
* type: string
|
||||
* example: 15min
|
||||
* startTime:
|
||||
* type: string
|
||||
* example: 1970-01-01T17:00:00.000Z
|
||||
* endTime:
|
||||
* type: string
|
||||
* example: 1970-01-01T17:00:00.000Z
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, booking created
|
||||
* 400:
|
||||
* description: Bad request. Booking body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
const safe = schemaBookingCreateBodyParams.safeParse(body);
|
||||
if (!safe.success) {
|
||||
console.log(safe.error);
|
||||
res.status(400).json({ message: "Bad request. Booking body is invalid." });
|
||||
return;
|
||||
}
|
||||
if (!isAdmin) {
|
||||
safe.data.userId = userId;
|
||||
}
|
||||
const data = await prisma.booking.create({ data: { uid: uuidv4(), ...safe.data } });
|
||||
const booking = schemaBookingReadPublic.parse(data);
|
||||
|
||||
if (booking) {
|
||||
const eventType = await prisma.eventType
|
||||
.findUnique({ where: { id: booking.eventTypeId as number } })
|
||||
.then((data) => schemaEventTypeReadPublic.parse(data))
|
||||
.catch((e: Error) => {
|
||||
console.error(`Event type with ID: ${booking.eventTypeId} not found`, e);
|
||||
});
|
||||
console.log(`eventType: ${eventType}`);
|
||||
const evt = {
|
||||
type: eventType?.title || booking.title,
|
||||
title: booking.title,
|
||||
description: "",
|
||||
additionalNotes: "",
|
||||
customInputs: {},
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
name: "",
|
||||
email: "",
|
||||
timeZone: "",
|
||||
language: {
|
||||
locale: "en",
|
||||
},
|
||||
},
|
||||
attendees: [],
|
||||
location: "",
|
||||
destinationCalendar: null,
|
||||
hideCalendar: false,
|
||||
uid: booking.uid,
|
||||
metadata: {},
|
||||
};
|
||||
console.log(`evt: ${evt}`);
|
||||
|
||||
// Send Webhook call if hooked to BOOKING_CREATED
|
||||
const triggerEvent = WebhookTriggerEvents.BOOKING_CREATED;
|
||||
console.log(`Trigger Event: ${triggerEvent}`);
|
||||
const subscriberOptions = {
|
||||
userId,
|
||||
eventTypeId: booking.eventTypeId as number,
|
||||
triggerEvent,
|
||||
};
|
||||
console.log(`subscriberOptions: ${subscriberOptions}`);
|
||||
|
||||
const subscribers = await getWebhooks(subscriberOptions, prisma);
|
||||
console.log(`subscribers: ${subscribers}`);
|
||||
const bookingId = booking?.id;
|
||||
const promises = subscribers.map((sub) =>
|
||||
sendPayload(triggerEvent, new Date().toISOString(), sub, {
|
||||
...evt,
|
||||
bookingId,
|
||||
}).catch((e) => {
|
||||
console.error(`Error executing webhook for event: ${triggerEvent}, URL: ${sub.subscriberUrl}`, e);
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
console.log("All promises resolved! About to send the response");
|
||||
res.status(201).json({ booking, message: "Booking created successfully" });
|
||||
} else
|
||||
(error: Error) => {
|
||||
console.log(error);
|
||||
res.status(400).json({
|
||||
message: "Could not create new booking",
|
||||
error,
|
||||
});
|
||||
};
|
||||
} else res.status(405).json({ message: `Method ${method} not allowed` });
|
||||
}
|
||||
|
||||
export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllBookings);
|
||||
export default withMiddleware("HTTP_GET_OR_POST")(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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 _post`
|
||||
|
||||
Tip: Use `--watchAll` flag to run tests on every change
|
||||
@@ -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
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
@@ -0,0 +1,215 @@
|
||||
import { Booking, WebhookTriggerEvents } from "@prisma/client";
|
||||
import { Request, Response } from "express";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
|
||||
import sendPayload from "@calcom/api/lib/utils/sendPayload";
|
||||
import handler from "@calcom/api/pages/api/bookings/_post";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { buildEventType, buildWebhook, buildBooking } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { prismaMock } from "../../../../../tests/config/singleton";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
jest.mock("@calcom/api/lib/utils/sendPayload");
|
||||
|
||||
describe("POST /api/bookings", () => {
|
||||
describe("Errors", () => {
|
||||
test("Missing required data", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {},
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(400);
|
||||
expect(JSON.parse(res._getData())).toEqual(
|
||||
expect.objectContaining({
|
||||
message: "Booking body is invalid.",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("Invalid eventTypeId", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "test",
|
||||
eventTypeId: 2,
|
||||
startTime: dayjs().toDate(),
|
||||
endTime: dayjs().add(1, "day").toDate(),
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
//prismaMock.eventType.findUnique.mockResolvedValue(null);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(400);
|
||||
expect(JSON.parse(res._getData())).toEqual(
|
||||
expect.objectContaining({
|
||||
message: "Invalid eventTypeId.",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("Missing recurringCount", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "test",
|
||||
eventTypeId: 2,
|
||||
startTime: dayjs().toDate(),
|
||||
endTime: dayjs().add(1, "day").toDate(),
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||
buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } })
|
||||
);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(400);
|
||||
expect(JSON.parse(res._getData())).toEqual(
|
||||
expect.objectContaining({
|
||||
message: "Missing recurringCount.",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("Invalid recurringCount", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "test",
|
||||
eventTypeId: 2,
|
||||
startTime: dayjs().toDate(),
|
||||
endTime: dayjs().add(1, "day").toDate(),
|
||||
recurringCount: 15,
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||
buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } })
|
||||
);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(400);
|
||||
expect(JSON.parse(res._getData())).toEqual(
|
||||
expect.objectContaining({
|
||||
message: "Invalid recurringCount.",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success", () => {
|
||||
describe("Regular event-type", () => {
|
||||
test("Creates one single booking", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "test",
|
||||
eventTypeId: 2,
|
||||
startTime: dayjs().toDate(),
|
||||
endTime: dayjs().add(1, "day").toDate(),
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(buildEventType());
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Recurring event-type", () => {
|
||||
test("Creates multiple bookings", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "test",
|
||||
eventTypeId: 2,
|
||||
startTime: dayjs().toDate(),
|
||||
endTime: dayjs().add(1, "day").toDate(),
|
||||
recurringCount: 12,
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||
buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } })
|
||||
);
|
||||
|
||||
Array.from(Array(12).keys()).map(async () => {
|
||||
prismaMock.booking.create.mockResolvedValue(buildBooking());
|
||||
});
|
||||
|
||||
prismaMock.webhook.findMany.mockResolvedValue([]);
|
||||
|
||||
await handler(req, res);
|
||||
const data = JSON.parse(res._getData());
|
||||
|
||||
expect(prismaMock.booking.create).toHaveBeenCalledTimes(12);
|
||||
expect(res._getStatusCode()).toBe(201);
|
||||
expect(data.message).toEqual("Bookings created successfully.");
|
||||
expect(data.bookings.length).toEqual(12);
|
||||
});
|
||||
});
|
||||
test("Notifies multiple bookings", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
title: "test",
|
||||
eventTypeId: 2,
|
||||
startTime: dayjs().toDate(),
|
||||
endTime: dayjs().add(1, "day").toDate(),
|
||||
recurringCount: 12,
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||
buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } })
|
||||
);
|
||||
|
||||
const createdAt = new Date();
|
||||
Array.from(Array(12).keys()).map(async () => {
|
||||
prismaMock.booking.create.mockResolvedValue(buildBooking({ createdAt }));
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user