Files
Keith WilliamsGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Rodrigo EhlersDhairyashil ShindeRodrigo EhlersCarina Wollendorfer
64297f027c feat: add user-specific email verification setting (#24298)
* feat: add user-specific email verification setting

Add requiresBookerEmailVerification boolean field to User model that allows
users to protect their email from impersonation during bookings.

When enabled, anyone attempting to book using the protected user's email
address (as booker or guest) must complete email verification and be logged
in as that email owner.

Key changes:
- Add requiresBookerEmailVerification field to User schema
- Create settings toggle in /settings/my-account/general
- Update checkIfBookerEmailIsBlocked to check booker's account setting
- Update guest filtering in handleNewBooking and addGuests handlers
- Add i18n translations for new setting
- Check both primary and verified secondary emails

Additional fixes:
- Replace 'any' types with proper Prisma and zod types in user.ts
- Fix member role type in sessionMiddleware.ts
- Fix avatar URL generation bug in sessionMiddleware.ts

These type fixes were necessary to resolve pre-commit lint warnings that
were blocking the commit.

Co-Authored-By: [email protected] <[email protected]>

* fix: address PR review comments

- Remove unrelated Watchlist index drops from migration
- Add missing Watchlist indexes to schema.prisma to fix drift
- Refactor checkIfBookerEmailIsBlocked to throw ErrorWithCode
- Move HttpError handling to handleNewBooking caller layer

Addresses review comments on PR #24298

Co-Authored-By: [email protected] <[email protected]>

* refactor: move Prisma queries to UserRepository and remove unrelated Watchlist changes

- Add findByEmailWithEmailVerificationSetting method to UserRepository
- Add findManyByEmailsWithEmailVerificationSettings method to UserRepository
- Refactor checkIfUserEmailVerificationRequired handler to use UserRepository
- Refactor addGuests handler to use UserRepository
- Remove unrelated Watchlist schema indices (organizationId/isGlobal, source)
- Remove unrelated WatchlistAudit unique constraint on id

Addresses review comments on PR #24298

Co-Authored-By: [email protected] <[email protected]>

* fix: better error codes + use repo

* Updated db query with manully written one using UNION (#24430)

* fix: resolve usage of deprecated secondary email in return value

* fix: type errors from refactors

* fix: address CodeRabbit PR review comments

- Add NOT NULL constraint to requiresBookerEmailVerification migration
- Dedupe guest input by base email to handle plus-addressing correctly
- Compare attendees by base email instead of raw strings
- Send emails only to filtered uniqueGuests (not all guests)
- Improve error logging with actual error details

Co-Authored-By: [email protected] <[email protected]>

* fix: indices added by mistake

Co-authored-by: Carina Wollendorfer <[email protected]>

* chore: update label of setting

* fix: return matched email for guests

* chore: remove whitespace

* test: add comprehensive email verification tests

- Add 9 test scenarios covering user email verification setting
- Test main booker verification (logged in/out, with/without code)
- Test secondary email verification as main booker and guest
- Test guest filtering when verification is required
- Test plus-addressed email handling
- Test multiple guests with mixed verification requirements
- Test invalid verification code error handling
- Update bookingScenario helper to support requiresBookerEmailVerification and secondaryEmails

Co-Authored-By: [email protected] <[email protected]>

* fix: correct guest placement in test mock data

Move guests array from top-level booking data into responses object
to match expected structure in getBookingData.ts which looks for
responses.guests (line 74).

Fixes three failing tests:
- should filter out guest that requires verification
- should filter out secondary email with verification when added as guest
- should filter only guests requiring verification from multiple guests

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Rodrigo Ehlers <[email protected]>
Co-authored-by: Dhairyashil Shinde <[email protected]>
Co-authored-by: Rodrigo Ehlers <[email protected]>
Co-authored-by: Carina Wollendorfer <[email protected]>
2025-10-15 11:36:03 +00:00

1146 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, mockFindByEmailWithEmailVerificationSetting } =
vi.hoisted(() => ({
mockFindManyByEmailsWithEmailVerificationSettings: vi.fn(),
mockFindByEmailWithEmailVerificationSetting: 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;
realInstance.findByEmailWithEmailVerificationSetting = mockFindByEmailWithEmailVerificationSetting;
return realInstance;
}),
};
});
function addToBlacklistedEmails(emails: string[]) {
process.env.BLACKLISTED_GUEST_EMAILS = emails.join(",");
}
function resetBlacklistedEmails() {
delete process.env.BLACKLISTED_GUEST_EMAILS;
}
beforeEach(() => {
mockFindManyByEmailsWithEmailVerificationSettings.mockResolvedValue([]);
mockFindByEmailWithEmailVerificationSetting.mockResolvedValue(null);
});
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 = "[email protected]"; // Use organizer's email as the blocked one
const booker = getBooker({
email: blockedEmail,
name: "Organizer",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
addToBlacklistedEmails(["[email protected]", "[email protected]"]);
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 [email protected] 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 = "[email protected]";
const booker = getBooker({
email: blockedEmail,
name: "Unverified User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: null,
});
// Mock environment variable for blacklisted emails
addToBlacklistedEmails(["[email protected]"]);
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: "[email protected]",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
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: "[email protected]",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
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: "[email protected]",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "[email protected]",
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 [email protected] 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: "[email protected]",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "[email protected]",
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: "[email protected]",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "[email protected]",
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("[email protected]", "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: "[email protected]",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "[email protected]",
id: 201,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
secondaryEmails: [{ email: "[email protected]", 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 [email protected] to use this email for creating a booking."
);
});
test("should filter out guest that requires verification", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "[email protected]",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const guestWithVerification = getOrganizer({
name: "Guest",
email: "[email protected]",
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: "[email protected]",
matchedEmail: "[email protected]",
requiresBookerEmailVerification: true,
},
]);
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
guests: ["[email protected]", "[email protected]"],
},
},
});
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(["[email protected]"]);
expect(guestEmails).not.toContain("[email protected]");
});
test("should filter out secondary email with verification when added as guest", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "[email protected]",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithSecondaryEmail = getOrganizer({
name: "User",
email: "[email protected]",
id: 202,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
secondaryEmails: [{ email: "[email protected]", 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: "[email protected]",
matchedEmail: "[email protected]",
requiresBookerEmailVerification: true,
},
]);
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
guests: ["[email protected]", "[email protected]"],
},
},
});
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(["[email protected]"]);
expect(guestEmails).not.toContain("[email protected]");
});
test("should match plus-addressed email to base email for verification check", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "[email protected]",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "[email protected]",
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: "[email protected]",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const guest1 = getOrganizer({
name: "Guest1",
email: "[email protected]",
id: 202,
schedules: [TestData.schedules.IstWorkHours],
emailVerified: new Date(),
requiresBookerEmailVerification: true,
});
const guest3 = getOrganizer({
name: "Guest3",
email: "[email protected]",
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: "[email protected]",
matchedEmail: "[email protected]",
requiresBookerEmailVerification: true,
},
{
email: "[email protected]",
matchedEmail: "[email protected]",
requiresBookerEmailVerification: true,
},
]);
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
guests: [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
],
},
},
});
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(["[email protected]", "[email protected]"])
);
expect(guestEmails).not.toContain("[email protected]");
expect(guestEmails).not.toContain("[email protected]");
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: "[email protected]",
name: "User",
});
const organizer = getOrganizer({
name: "Organizer",
email: "[email protected]",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
emailVerified: new Date(),
});
const userWithVerificationRequired = getOrganizer({
name: "User",
email: "[email protected]",
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("[email protected]", "invalid-code");
});
});
});