import { describe, expect, it, vi, beforeEach } from "vitest"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { shouldSkipAttendeeEmailWithSettings, fetchOrganizationEmailSettings } from "./email-manager"; import AttendeeScheduledEmail from "./templates/attendee-scheduled-email"; const mockGetEmailSettings = vi.fn(); vi.mock("@calcom/features/organizations/repositories/OrganizationSettingsRepository", () => ({ OrganizationSettingsRepository: vi.fn().mockImplementation(function () { return { getEmailSettings: mockGetEmailSettings, }; }), })); vi.mock("@calcom/prisma", () => ({ prisma: {}, })); // Mock dependencies for AttendeeScheduledEmail tests vi.mock("./lib/generateIcsFile", () => ({ default: vi.fn(() => "mock-ical-content"), GenerateIcsRole: { ATTENDEE: "ATTENDEE", }, })); vi.mock("./src/renderEmail", () => ({ default: vi.fn(() => Promise.resolve("mock-email")), })); vi.mock("@calcom/lib/getReplyToHeader", () => ({ getReplyToHeader: vi.fn(() => ({})), })); vi.mock("@calcom/lib/CalEventParser", () => ({ getRichDescription: vi.fn(() => "mock-description"), })); vi.mock("./templates/_base-email", () => { return { default: class MockBaseEmail { getMailerOptions() { return { from: "test@cal.com" }; } }, }; }); describe("shouldSkipAttendeeEmailWithSettings", () => { beforeEach(() => { vi.clearAllMocks(); }); describe.each([ ["confirmation", "disableAttendeeConfirmationEmail"], ["cancellation", "disableAttendeeCancellationEmail"], ["rescheduled", "disableAttendeeRescheduledEmail"], ["request", "disableAttendeeRequestEmail"], ["reassigned", "disableAttendeeReassignedEmail"], ["awaiting_payment", "disableAttendeeAwaitingPaymentEmail"], ["reschedule_request", "disableAttendeeRescheduleRequestEmail"], ["location_change", "disableAttendeeLocationChangeEmail"], ["new_event", "disableAttendeeNewEventEmail"], ] as const)("Email type: %s", (emailType, settingKey) => { it(`should skip email when organization has ${settingKey} enabled`, async () => { const orgSettings = { disableAttendeeConfirmationEmail: settingKey === "disableAttendeeConfirmationEmail", disableAttendeeCancellationEmail: settingKey === "disableAttendeeCancellationEmail", disableAttendeeRescheduledEmail: settingKey === "disableAttendeeRescheduledEmail", disableAttendeeRequestEmail: settingKey === "disableAttendeeRequestEmail", disableAttendeeReassignedEmail: settingKey === "disableAttendeeReassignedEmail", disableAttendeeAwaitingPaymentEmail: settingKey === "disableAttendeeAwaitingPaymentEmail", disableAttendeeRescheduleRequestEmail: settingKey === "disableAttendeeRescheduleRequestEmail", disableAttendeeLocationChangeEmail: settingKey === "disableAttendeeLocationChangeEmail", disableAttendeeNewEventEmail: settingKey === "disableAttendeeNewEventEmail", }; const result = shouldSkipAttendeeEmailWithSettings(undefined, orgSettings, emailType); expect(result).toBe(true); }); it(`should send email when organization has ${settingKey} disabled`, async () => { const orgSettings = { disableAttendeeConfirmationEmail: false, disableAttendeeCancellationEmail: false, disableAttendeeRescheduledEmail: false, disableAttendeeRequestEmail: false, disableAttendeeReassignedEmail: false, disableAttendeeAwaitingPaymentEmail: false, disableAttendeeRescheduleRequestEmail: false, disableAttendeeLocationChangeEmail: false, disableAttendeeNewEventEmail: false, }; const result = shouldSkipAttendeeEmailWithSettings(undefined, orgSettings, emailType); expect(result).toBe(false); }); }); describe("Metadata fallback", () => { it("should skip email when metadata has disableStandardEmails.all.attendee enabled", () => { const metadata: EventTypeMetadata = { disableStandardEmails: { all: { attendee: true, }, }, }; const result = shouldSkipAttendeeEmailWithSettings(metadata, null, "confirmation"); expect(result).toBe(true); }); }); describe("Priority: organization settings override metadata", () => { it("should skip email when org setting is enabled even if metadata allows", () => { const orgSettings = { disableAttendeeConfirmationEmail: true, disableAttendeeCancellationEmail: false, disableAttendeeRescheduledEmail: false, disableAttendeeRequestEmail: false, disableAttendeeReassignedEmail: false, disableAttendeeAwaitingPaymentEmail: false, disableAttendeeRescheduleRequestEmail: false, disableAttendeeLocationChangeEmail: false, disableAttendeeNewEventEmail: false, }; const metadata: EventTypeMetadata = { disableStandardEmails: { confirmation: { attendee: false, }, }, }; const result = shouldSkipAttendeeEmailWithSettings(metadata, orgSettings, "confirmation"); expect(result).toBe(true); }); }); describe("Edge cases", () => { it("should send email when organizationSettings is null", () => { const result = shouldSkipAttendeeEmailWithSettings(undefined, null, "confirmation"); expect(result).toBe(false); }); it("should send email when emailType is undefined", () => { const orgSettings = { disableAttendeeConfirmationEmail: true, disableAttendeeCancellationEmail: false, disableAttendeeRescheduledEmail: false, disableAttendeeRequestEmail: false, disableAttendeeReassignedEmail: false, disableAttendeeAwaitingPaymentEmail: false, disableAttendeeRescheduleRequestEmail: false, disableAttendeeLocationChangeEmail: false, disableAttendeeNewEventEmail: false, }; const result = shouldSkipAttendeeEmailWithSettings(undefined, orgSettings, undefined); expect(result).toBe(false); }); it("should send email when metadata is undefined and org settings are disabled", () => { const orgSettings = { disableAttendeeConfirmationEmail: false, disableAttendeeCancellationEmail: false, disableAttendeeRescheduledEmail: false, disableAttendeeRequestEmail: false, disableAttendeeReassignedEmail: false, disableAttendeeAwaitingPaymentEmail: false, disableAttendeeRescheduleRequestEmail: false, disableAttendeeLocationChangeEmail: false, disableAttendeeNewEventEmail: false, }; const result = shouldSkipAttendeeEmailWithSettings(undefined, orgSettings, "confirmation"); expect(result).toBe(false); }); }); }); describe("AttendeeScheduledEmail - Privacy fix for seated events", () => { const createMockPerson = (name: string, email: string): Person => ({ name, email, timeZone: "America/New_York", language: { translate: vi.fn((key: string) => key), locale: "en", }, }); const createMockCalendarEvent = ( options: { seatsPerTimeSlot?: number | null; seatsShowAttendees?: boolean | null; attendees?: Person[]; } = {} ): CalendarEvent => { const attendees = options.attendees || [ createMockPerson("Alice", "alice@example.com"), createMockPerson("Bob", "bob@example.com"), createMockPerson("Charlie", "charlie@example.com"), ]; return { title: "Test Event", type: "Test Event Type", startTime: "2024-01-01T10:00:00Z", endTime: "2024-01-01T11:00:00Z", organizer: createMockPerson("Organizer", "organizer@example.com"), attendees, seatsPerTimeSlot: options.seatsPerTimeSlot ?? null, seatsShowAttendees: options.seatsShowAttendees ?? null, } as CalendarEvent; }; describe("Privacy: seatsShowAttendees setting", () => { it("should filter attendees to only recipient when seatsShowAttendees is false for seated events", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 5, seatsShowAttendees: false, }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient); // Should only contain the recipient expect(email.calEvent.attendees).toHaveLength(1); expect(email.calEvent.attendees[0].email).toBe(recipient.email); expect(email.calEvent.attendees[0].name).toBe(recipient.name); }); it("should include all attendees when seatsShowAttendees is true for seated events", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 5, seatsShowAttendees: true, }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient); // Should contain all attendees expect(email.calEvent.attendees).toHaveLength(3); expect(email.calEvent.attendees.map((a) => a.email)).toEqual([ "alice@example.com", "bob@example.com", "charlie@example.com", ]); }); it("should filter attendees when seatsShowAttendees is null for seated events (defaults to false)", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 5, seatsShowAttendees: null, }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient); // Should only contain the recipient (null defaults to false) expect(email.calEvent.attendees).toHaveLength(1); expect(email.calEvent.attendees[0].email).toBe(recipient.email); }); it("should include all attendees for non-seated events regardless of seatsShowAttendees", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: null, seatsShowAttendees: false, // This shouldn't matter for non-seated events }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient); // Should contain all attendees (non-seated events always show all) expect(email.calEvent.attendees).toHaveLength(3); }); }); describe("Explicit showAttendees parameter", () => { it("should use explicit showAttendees=true parameter even when seatsShowAttendees is false", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 5, seatsShowAttendees: false, }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient, true); // Should contain all attendees because explicit parameter overrides expect(email.calEvent.attendees).toHaveLength(3); }); it("should use explicit showAttendees=false parameter even when seatsShowAttendees is true", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 5, seatsShowAttendees: true, }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient, false); // Should only contain recipient because explicit parameter overrides expect(email.calEvent.attendees).toHaveLength(1); expect(email.calEvent.attendees[0].email).toBe(recipient.email); }); }); describe("Edge cases", () => { it("should handle single attendee correctly when filtering", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 5, seatsShowAttendees: false, attendees: [createMockPerson("Solo", "solo@example.com")], }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient); expect(email.calEvent.attendees).toHaveLength(1); expect(email.calEvent.attendees[0].email).toBe("solo@example.com"); }); it("should not mutate original calEvent when filtering attendees", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 5, seatsShowAttendees: false, }); const originalAttendeesCount = calEvent.attendees.length; const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient); // Original calEvent should remain unchanged expect(calEvent.attendees).toHaveLength(originalAttendeesCount); // Email's calEvent should be filtered expect(email.calEvent.attendees).toHaveLength(1); // They should be different objects (cloned) expect(email.calEvent).not.toBe(calEvent); }); it("should use same calEvent reference when not filtering (performance optimization)", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 5, seatsShowAttendees: true, }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient); // Should use same reference when not filtering expect(email.calEvent).toBe(calEvent); }); }); describe("Real-world scenarios", () => { it("should protect privacy for paid seated events with sharing disabled", () => { // This is the reported bug scenario const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 10, seatsShowAttendees: false, // Privacy setting disabled attendees: [ createMockPerson("Customer 1", "customer1@example.com"), createMockPerson("Customer 2", "customer2@example.com"), createMockPerson("Customer 3", "customer3@example.com"), ], }); const recipient = calEvent.attendees[1]; // Customer 2 const email = new AttendeeScheduledEmail(calEvent, recipient); // Customer 2 should only see their own information expect(email.calEvent.attendees).toHaveLength(1); expect(email.calEvent.attendees[0].email).toBe("customer2@example.com"); expect(email.calEvent.attendees[0].name).toBe("Customer 2"); // Should not contain other customers' information expect(email.calEvent.attendees.some((a) => a.email === "customer1@example.com")).toBe(false); expect(email.calEvent.attendees.some((a) => a.email === "customer3@example.com")).toBe(false); }); it("should allow sharing when explicitly enabled for seated events", () => { const calEvent = createMockCalendarEvent({ seatsPerTimeSlot: 10, seatsShowAttendees: true, // Sharing enabled attendees: [ createMockPerson("Attendee 1", "attendee1@example.com"), createMockPerson("Attendee 2", "attendee2@example.com"), ], }); const recipient = calEvent.attendees[0]; const email = new AttendeeScheduledEmail(calEvent, recipient); // Should see all attendees when sharing is enabled expect(email.calEvent.attendees).toHaveLength(2); expect(email.calEvent.attendees.map((a) => a.email)).toEqual([ "attendee1@example.com", "attendee2@example.com", ]); }); }); });