Files
cal.diy/packages/features/bookings/lib/handleNewBooking/test/booking-validations.test.ts
T
Rodrigo Ehlers 66c5f810ea fix: remove singular query that was using slow OR query (#24715)
## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

Removes and replaces the singular query that was still using the slow OR query from prisma. It was mainly used on the public booking page and triggered after someone entered their email (600ms debounce delay).

## Visual Demo (For contributors especially)

Internal change only nothing changes on the UI or business logic.

#### Video Demo (if applicable):

N/A

#### Image Demo (if applicable):

N/A

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->

Basically the same as https://github.com/calcom/cal.com/pull/24298 since it touches relevant code.

1. Have a user that has the setting to prevent impersonation on.
2. Go to another users public booking page of any meeting.
3. Try to enter the email of the first user into the booking for.
4. Wait 1 second and make sure the button shows "Verify email".

## Checklist

<!-- Remove bullet points below that don't apply to you -->

- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my changes generate no new warnings
2025-10-27 17:11:05 +00:00

1142 lines
33 KiB
TypeScript

/**
* Booking Validation Specifications
* These specifications verify the business rules and validation behavior for booking creation
*/
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
import {
createBookingScenario,
TestData,
getOrganizer,
getBooker,
getScenarioData,
getGoogleCalendarCredential,
mockCalendarToHaveNoBusySlots,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
import { afterEach, beforeEach, vi } from "vitest";
import { describe, expect } from "vitest";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
import { getNewBookingHandler } from "./getNewBookingHandler";
vi.mock("@calcom/trpc/server/routers/viewer/auth/util", () => ({
verifyCodeUnAuthenticated: vi.fn(),
}));
const { mockFindManyByEmailsWithEmailVerificationSettings } = vi.hoisted(() => ({
mockFindManyByEmailsWithEmailVerificationSettings: vi.fn(),
}));
vi.mock("@calcom/features/users/repositories/UserRepository", async (importOriginal) => {
const actual = await importOriginal();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const OriginalUserRepository = (actual as any).UserRepository;
return {
...actual,
UserRepository: vi.fn().mockImplementation((prisma) => {
const realInstance = new OriginalUserRepository(prisma);
realInstance.findManyByEmailsWithEmailVerificationSettings =
mockFindManyByEmailsWithEmailVerificationSettings;
return realInstance;
}),
};
});
function addToBlacklistedEmails(emails: string[]) {
process.env.BLACKLISTED_GUEST_EMAILS = emails.join(",");
}
function resetBlacklistedEmails() {
delete process.env.BLACKLISTED_GUEST_EMAILS;
}
beforeEach(() => {
mockFindManyByEmailsWithEmailVerificationSettings.mockResolvedValue([]);
});
afterEach(() => {
resetBlacklistedEmails();
vi.clearAllMocks();
});
describe("Booking Validation Specifications", () => {
setupAndTeardown();
describe("Email Blacklist Validation", () => {
test("when email is in BLACKLISTED_GUEST_EMAILS, allow the user to book only if they are logged in with that email", async () => {
const handleNewBooking = getNewBookingHandler();
const blockedEmail = "organizer@example.com"; // Use organizer's email as the blocked one
const booker = getBooker({
email: blockedEmail,
name: "Organizer",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
addToBlacklistedEmails(["organizer@example.com", "spam@test.com"]);
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
// Non logged in user should not be able to book
await expect(
handleNewBooking({
bookingData: mockBookingData,
})
).rejects.toThrow(
"Attendee email has been blocked. Make sure to login as organizer@example.com to use this email for creating a booking."
);
// Should allow booking when the user who owns the blacklisted email is logged in
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
userId: 101, // Same as organizer who owns the blacklisted email
});
expect(createdBooking).toEqual(
expect.objectContaining({
id: expect.any(Number),
uid: expect.any(String),
status: BookingStatus.ACCEPTED,
})
);
});
test("prevents booking when blacklisted email is not verified in the system", async () => {
const handleNewBooking = getNewBookingHandler();
const blockedEmail = "blocked@example.com";
const booker = getBooker({
email: blockedEmail,
name: "Unverified User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: null,
});
// Mock environment variable for blacklisted emails
addToBlacklistedEmails(["blocked@example.com"]);
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
// Should prevent booking when blacklisted email has no verified user in database
await expect(
handleNewBooking({
bookingData: mockBookingData,
})
).rejects.toThrow("Cannot use this email to create the booking.");
});
});
describe("Active Bookings Limit Validation", () => {
test("allows booking when user is under their active booking limit", async () => {
vi.setSystemTime(new Date("2025-01-01"));
const plus1DateString = "2025-01-02";
const handleNewBooking = getNewBookingHandler();
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],
});
// Create test scenario with event type that has maxActiveBookingsPerBooker limit
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
// Two bookings allowed for the booker
maxActiveBookingsPerBooker: 2,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"]],
bookings: [
{
uid: "existing-booking-1",
eventTypeId: 1,
userId: organizer.id,
startTime: `${plus1DateString}T10:00:00.000Z`,
endTime: `${plus1DateString}T10:30:00.000Z`,
title: "Existing Booking",
status: BookingStatus.ACCEPTED,
// Booker already has a booking in future
attendees: [
{
email: booker.email,
},
],
},
],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
// Should allow booking when user has not reached their limit (1 booking < 2 limit)
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
});
expect(createdBooking).toEqual(
expect.objectContaining({
id: expect.any(Number),
uid: expect.any(String),
status: BookingStatus.ACCEPTED,
})
);
// Second booking should be rejected
await expect(
handleNewBooking({
bookingData: mockBookingData,
})
).rejects.toThrow("booker_limit_exceeded_error");
});
test("enforces booking limits with reschedule option when enabled", async () => {
vi.setSystemTime(new Date("2025-01-01"));
const plus1DateString = "2025-01-02";
const plus2DateString = "2025-01-03";
const handleNewBooking = getNewBookingHandler();
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],
});
// Create test scenario with event type that has reschedule option enabled
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
maxActiveBookingsPerBooker: 2,
maxActiveBookingPerBookerOfferReschedule: true,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"]],
bookings: [
{
uid: "existing-booking-1",
eventTypeId: 1,
userId: organizer.id,
startTime: `${plus1DateString}T10:00:00.000Z`,
endTime: `${plus1DateString}T10:30:00.000Z`,
title: "Existing Booking",
status: BookingStatus.ACCEPTED,
attendees: [
{
email: booker.email,
},
],
},
{
uid: "existing-booking-2",
eventTypeId: 1,
userId: organizer.id,
startTime: `${plus2DateString}T10:00:00.000Z`,
endTime: `${plus2DateString}T10:30:00.000Z`,
title: "Existing Booking",
status: BookingStatus.ACCEPTED,
attendees: [
{
email: booker.email,
},
],
},
],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
try {
await handleNewBooking({
bookingData: mockBookingData,
});
} catch (error) {
expect(error.message).toEqual("booker_limit_exceeded_error_reschedule");
expect(error.data).toEqual(
expect.objectContaining({
rescheduleUid: "existing-booking-1",
})
);
}
});
});
describe("User Email Verification Setting", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should block booking when main booker requires verification and is not logged in", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "user@example.com",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "user@example.com",
id: 201,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [userWithVerificationRequired],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
await expect(
handleNewBooking({
bookingData: mockBookingData,
})
).rejects.toThrow(
"Attendee email has been blocked. Make sure to login as user@example.com to use this email for creating a booking."
);
});
test("should allow booking when main booker requires verification but is logged in", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "user@example.com",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "user@example.com",
id: 201,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [userWithVerificationRequired],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
userId: 201,
});
expect(createdBooking).toEqual(
expect.objectContaining({
id: expect.any(Number),
uid: expect.any(String),
status: BookingStatus.ACCEPTED,
})
);
});
test("should create booking when main booker provides valid verification code", async () => {
const handleNewBooking = getNewBookingHandler();
const { verifyCodeUnAuthenticated } = await import("@calcom/trpc/server/routers/viewer/auth/util");
vi.mocked(verifyCodeUnAuthenticated).mockResolvedValue(true);
const booker = getBooker({
email: "user@example.com",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "user@example.com",
id: 201,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [userWithVerificationRequired],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
verificationCode: "valid-code-123",
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
});
expect(createdBooking).toEqual(
expect.objectContaining({
id: expect.any(Number),
uid: expect.any(String),
status: BookingStatus.ACCEPTED,
})
);
expect(verifyCodeUnAuthenticated).toHaveBeenCalledWith("user@example.com", "valid-code-123");
});
test("should require verification when secondary email of user with verification setting is used as main booker", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "secondary@example.com",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "primary@example.com",
id: 201,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
secondaryEmails: [{ email: "secondary@example.com", emailVerified: new Date() }],
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [userWithVerificationRequired],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
await expect(
handleNewBooking({
bookingData: mockBookingData,
})
).rejects.toThrow(
"Attendee email has been blocked. Make sure to login as secondary@example.com to use this email for creating a booking."
);
});
test("should filter out guest that requires verification", async () => {
const handleNewBooking = getNewBookingHandler();
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],
emailVerified: new Date(),
});
const guestWithVerification = getOrganizer({
name: "Guest",
email: "guest-with-verification@example.com",
id: 202,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [guestWithVerification],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
mockFindManyByEmailsWithEmailVerificationSettings.mockResolvedValue([
{
email: "guest-with-verification@example.com",
matchedEmail: "guest-with-verification@example.com",
requiresBookerEmailVerification: true,
},
]);
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
guests: ["guest-with-verification@example.com", "regular-guest@example.com"],
},
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
});
expect(createdBooking).toEqual(
expect.objectContaining({
id: expect.any(Number),
uid: expect.any(String),
status: BookingStatus.ACCEPTED,
})
);
const booking = await prismaMock.booking.findFirst({
where: { id: createdBooking.id },
include: { attendees: true },
});
const guestEmails = booking?.attendees.map((a) => a.email).filter((e) => e !== booker.email);
expect(guestEmails).toEqual(["regular-guest@example.com"]);
expect(guestEmails).not.toContain("guest-with-verification@example.com");
});
test("should filter out secondary email with verification when added as guest", async () => {
const handleNewBooking = getNewBookingHandler();
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],
emailVerified: new Date(),
});
const userWithSecondaryEmail = getOrganizer({
name: "User",
email: "primary@example.com",
id: 202,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
secondaryEmails: [{ email: "secondary@example.com", emailVerified: new Date() }],
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [userWithSecondaryEmail],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
mockFindManyByEmailsWithEmailVerificationSettings.mockResolvedValue([
{
email: "primary@example.com",
matchedEmail: "secondary@example.com",
requiresBookerEmailVerification: true,
},
]);
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
guests: ["secondary@example.com", "regular-guest@example.com"],
},
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
});
expect(createdBooking).toEqual(
expect.objectContaining({
id: expect.any(Number),
uid: expect.any(String),
status: BookingStatus.ACCEPTED,
})
);
const booking = await prismaMock.booking.findFirst({
where: { id: createdBooking.id },
include: { attendees: true },
});
const guestEmails = booking?.attendees.map((a) => a.email).filter((e) => e !== booker.email);
expect(guestEmails).toEqual(["regular-guest@example.com"]);
expect(guestEmails).not.toContain("secondary@example.com");
});
test("should match plus-addressed email to base email for verification check", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "user+tag@example.com",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "user@example.com",
id: 201,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [userWithVerificationRequired],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
await expect(
handleNewBooking({
bookingData: mockBookingData,
})
).rejects.toThrow("Attendee email has been blocked");
});
test("should filter only guests requiring verification from multiple guests", async () => {
const handleNewBooking = getNewBookingHandler();
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],
emailVerified: new Date(),
});
const guest1 = getOrganizer({
name: "Guest1",
email: "guest1-verify@example.com",
id: 202,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
const guest3 = getOrganizer({
name: "Guest3",
email: "guest3-verify@example.com",
id: 203,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [guest1, guest3],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
mockFindManyByEmailsWithEmailVerificationSettings.mockResolvedValue([
{
email: "guest1-verify@example.com",
matchedEmail: "guest1-verify@example.com",
requiresBookerEmailVerification: true,
},
{
email: "guest3-verify@example.com",
matchedEmail: "guest3-verify@example.com",
requiresBookerEmailVerification: true,
},
]);
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
guests: [
"guest1-verify@example.com",
"guest2-no-verify@example.com",
"guest3-verify@example.com",
"guest4-no-verify@example.com",
],
},
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
});
expect(createdBooking).toEqual(
expect.objectContaining({
id: expect.any(Number),
uid: expect.any(String),
status: BookingStatus.ACCEPTED,
})
);
const booking = await prismaMock.booking.findFirst({
where: { id: createdBooking.id },
include: { attendees: true },
});
const guestEmails = booking?.attendees.map((a) => a.email).filter((e) => e !== booker.email);
expect(guestEmails).toEqual(
expect.arrayContaining(["guest2-no-verify@example.com", "guest4-no-verify@example.com"])
);
expect(guestEmails).not.toContain("guest1-verify@example.com");
expect(guestEmails).not.toContain("guest3-verify@example.com");
expect(guestEmails?.length).toBe(2);
});
test("should throw error when invalid verification code is provided", async () => {
const handleNewBooking = getNewBookingHandler();
const { verifyCodeUnAuthenticated } = await import("@calcom/trpc/server/routers/viewer/auth/util");
vi.mocked(verifyCodeUnAuthenticated).mockResolvedValue(false);
const booker = getBooker({
email: "user@example.com",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "user@example.com",
id: 201,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
usersApartFromOrganizer: [userWithVerificationRequired],
apps: [TestData.apps["google-calendar"]],
})
);
await mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
verificationCode: "invalid-code",
},
});
await expect(
handleNewBooking({
bookingData: mockBookingData,
})
).rejects.toThrow("Invalid verification code");
expect(verifyCodeUnAuthenticated).toHaveBeenCalledWith("user@example.com", "invalid-code");
});
});
});