From d2a54e29509b61d15092ffd4bfed538dbe2335a5 Mon Sep 17 00:00:00 2001 From: Romit <85230081+romitg2@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:16:45 +0530 Subject: [PATCH 1/3] refactor: remove dead workflow runtime config (#29028) --- apps/web/vercel.json | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/apps/web/vercel.json b/apps/web/vercel.json index 007507b6d356d1..6194c43f85d44d 100644 --- a/apps/web/vercel.json +++ b/apps/web/vercel.json @@ -29,18 +29,5 @@ "schedule": "*/5 * * * *" } ], - "functions": { - "app/api/cron/workflows/scheduleEmailReminders/route.ts": { - "maxDuration": 800 - }, - "app/api/cron/workflows/scheduleSMSReminders/route.ts": { - "maxDuration": 800 - }, - "app/api/cron/workflows/scheduleWhatsappReminders/route.ts": { - "maxDuration": 800 - }, - "pages/api/trpc/workflows/[trpc].ts": { - "maxDuration": 800 - } - } + "functions": {} } From de1ffb08d683cd4ea249b1240108874aeea14c6e Mon Sep 17 00:00:00 2001 From: Romit <85230081+romitg2@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:18:02 +0530 Subject: [PATCH 2/3] cleanup(test): remove org admin event type integration coverage (#29016) --- .../lib/getEventTypeById.integration-test.ts | 265 ------------------ 1 file changed, 265 deletions(-) diff --git a/packages/features/eventtypes/lib/getEventTypeById.integration-test.ts b/packages/features/eventtypes/lib/getEventTypeById.integration-test.ts index ad5d27dc34b66b..5c6ea320e5595d 100644 --- a/packages/features/eventtypes/lib/getEventTypeById.integration-test.ts +++ b/packages/features/eventtypes/lib/getEventTypeById.integration-test.ts @@ -26,22 +26,13 @@ describe("getRawEventType", () => { const createdResources: { eventTypes: number[]; users: number[]; - teams: number[]; - memberships: number[]; - profiles: number[]; } = { eventTypes: [], users: [], - teams: [], - memberships: [], - profiles: [], }; - // Helper functions to create test data const createTestUser = async (overrides?: { - organizationId?: number; username?: string; - withProfile?: boolean; }) => { const timestamp = Date.now() + Math.random(); const username = overrides?.username ?? `testuser-${timestamp}`; @@ -49,76 +40,12 @@ describe("getRawEventType", () => { data: { username, email: `testuser-${timestamp}@example.com`, - organizationId: overrides?.organizationId, - ...(overrides?.withProfile && - overrides.organizationId && { - profiles: { - create: { - organizationId: overrides.organizationId, - uid: username, - username, - }, - }, - }), }, }); createdResources.users.push(user.id); return user; }; - const createTestOrganization = async (overrides?: { isPlatform?: boolean }) => { - const timestamp = Date.now() + Math.random(); - const team = await prisma.team.create({ - data: { - name: `Test Organization ${timestamp}`, - slug: `test-org-${timestamp}`, - isOrganization: true, - isPlatform: overrides?.isPlatform ?? false, - }, - }); - createdResources.teams.push(team.id); - return team; - }; - - const createTestTeam = async (parentId?: number) => { - const timestamp = Date.now() + Math.random(); - const team = await prisma.team.create({ - data: { - name: `Test Team ${timestamp}`, - slug: `test-team-${timestamp}`, - parentId: parentId ?? null, - }, - }); - createdResources.teams.push(team.id); - return team; - }; - - const createTestOrgAdmin = async (organizationId: number) => { - const timestamp = Date.now() + Math.random(); - const user = await prisma.user.create({ - data: { - username: `orgadmin-${timestamp}`, - email: `orgadmin-${timestamp}@example.com`, - organizationId, - }, - }); - createdResources.users.push(user.id); - return user; - }; - - const createTestTeamMember = async (teamId: number, userId: number) => { - const membership = await prisma.membership.create({ - data: { - teamId, - userId, - role: "MEMBER", - accepted: true, - }, - }); - createdResources.memberships.push(membership.id); - return membership; - }; - const createTestEventType = async (userId: number, overrides?: { slug?: string; title?: string }) => { const timestamp = Date.now() + Math.random(); const eventType = await prisma.eventType.create({ @@ -139,56 +66,18 @@ describe("getRawEventType", () => { return eventType; }; - const createTestTeamEventType = async (teamId: number) => { - const timestamp = Date.now() + Math.random(); - const eventType = await prisma.eventType.create({ - data: { - title: `Team Event ${timestamp}`, - slug: `team-event-${timestamp}`, - length: 30, - teamId, - }, - include: { - team: true, - users: true, - }, - }); - createdResources.eventTypes.push(eventType.id); - return eventType; - }; - beforeEach(() => { mockNoTranslations(); - // Reset tracking arrays createdResources.eventTypes = []; createdResources.users = []; - createdResources.teams = []; - createdResources.memberships = []; - createdResources.profiles = []; }); afterEach(async () => { - // Clean up in reverse order to avoid foreign key violations if (createdResources.eventTypes.length > 0) { await prisma.eventType.deleteMany({ where: { id: { in: createdResources.eventTypes } }, }); } - if (createdResources.memberships.length > 0) { - await prisma.membership.deleteMany({ - where: { id: { in: createdResources.memberships } }, - }); - } - if (createdResources.profiles.length > 0) { - await prisma.profile.deleteMany({ - where: { id: { in: createdResources.profiles } }, - }); - } - if (createdResources.teams.length > 0) { - await prisma.team.deleteMany({ - where: { id: { in: createdResources.teams } }, - }); - } if (createdResources.users.length > 0) { await prisma.user.deleteMany({ where: { id: { in: createdResources.users } }, @@ -270,158 +159,4 @@ describe("getRawEventType", () => { expect(result).toBeNull(); }); }); - - describe("Organization admin access", () => { - test("should fetch team event type when user is org admin and is a team member", async () => { - const organization = await createTestOrganization(); - const team = await createTestTeam(organization.id); - const orgAdmin = await createTestOrgAdmin(organization.id); - await createTestTeamMember(team.id, orgAdmin.id); - const eventType = await createTestTeamEventType(team.id); - - const result = await getRawEventType({ - userId: orgAdmin.id, - eventTypeId: eventType.id, - isUserOrganizationAdmin: true, - currentOrganizationId: organization.id, - prisma: prisma as unknown as PrismaClient, - }); - - expect(result).toBeDefined(); - expect(result?.id).toBe(eventType.id); - expect(result?.title).toContain("Team Event"); - expect(result?.teamId).toBe(team.id); - }); - - test("should return null when org admin tries to access event type from different org", async () => { - const org1 = await createTestOrganization(); - const org2 = await createTestOrganization(); - const team1 = await createTestTeam(org1.id); - const org2Admin = await createTestOrgAdmin(org2.id); - const eventType = await createTestTeamEventType(team1.id); - - const result = await getRawEventType({ - userId: org2Admin.id, - eventTypeId: eventType.id, - isUserOrganizationAdmin: true, - currentOrganizationId: org2.id, - prisma: prisma as unknown as PrismaClient, - }); - - expect(result).toBeNull(); - }); - - test("should fallback to regular user access when org admin flag is true but no organizationId", async () => { - const user = await createTestUser(); - const eventType = await createTestEventType(user.id, { title: "Regular User Event" }); - - const result = await getRawEventType({ - userId: user.id, - eventTypeId: eventType.id, - isUserOrganizationAdmin: true, - currentOrganizationId: null, - prisma: prisma as unknown as PrismaClient, - }); - - expect(result).toBeDefined(); - expect(result?.id).toBe(eventType.id); - expect(result?.userId).toBe(user.id); - }); - }); - - describe("when user is platform organization admin", () => { - test("should access any team event type within the platform organization", async () => { - const platformOrg = await createTestOrganization({ isPlatform: true }); - const orgSubTeam = await createTestTeam(platformOrg.id); - const platformAdmin = await createTestOrgAdmin(platformOrg.id); - const teamEvent = await createTestTeamEventType(orgSubTeam.id); - - const result = await getRawEventType({ - userId: platformAdmin.id, - eventTypeId: teamEvent.id, - isUserOrganizationAdmin: true, - currentOrganizationId: platformOrg.id, - prisma: prisma as unknown as PrismaClient, - }); - - expect(result).toBeDefined(); - expect(result?.id).toBe(teamEvent.id); - expect(result?.teamId).toBe(orgSubTeam.id); - }); - - test("should access user event types within the platform organization", async () => { - const platformOrg = await createTestOrganization({ isPlatform: true }); - const platformAdmin = await createTestOrgAdmin(platformOrg.id); - const orgUser = await createTestUser({ organizationId: platformOrg.id, withProfile: true }); - const userEvent = await createTestEventType(orgUser.id, { title: "Platform User Event" }); - - const result = await getRawEventType({ - userId: platformAdmin.id, - eventTypeId: userEvent.id, - isUserOrganizationAdmin: true, - currentOrganizationId: platformOrg.id, - prisma: prisma as unknown as PrismaClient, - }); - - expect(result).toBeDefined(); - expect(result?.id).toBe(userEvent.id); - expect(result?.userId).toBe(orgUser.id); - }); - }); - - describe("when user is non-platform organization admin", () => { - test("should access regular team event when admin is a team member", async () => { - const regularOrg = await createTestOrganization({ isPlatform: false }); - const standaloneTeam = await createTestTeam(); - const orgAdmin = await createTestOrgAdmin(regularOrg.id); - await createTestTeamMember(standaloneTeam.id, orgAdmin.id); - const teamEvent = await createTestTeamEventType(standaloneTeam.id); - - const result = await getRawEventType({ - userId: orgAdmin.id, - eventTypeId: teamEvent.id, - isUserOrganizationAdmin: true, - currentOrganizationId: regularOrg.id, - prisma: prisma as unknown as PrismaClient, - }); - - expect(result).toBeDefined(); - expect(result?.id).toBe(teamEvent.id); - expect(result?.teamId).toBe(standaloneTeam.id); - }); - - test("should not access regular team event when admin is not a team member", async () => { - const regularOrg = await createTestOrganization({ isPlatform: false }); - const standaloneTeam = await createTestTeam(); - const orgAdmin = await createTestOrgAdmin(regularOrg.id); - const teamEvent = await createTestTeamEventType(standaloneTeam.id); - - const result = await getRawEventType({ - userId: orgAdmin.id, - eventTypeId: teamEvent.id, - isUserOrganizationAdmin: true, - currentOrganizationId: regularOrg.id, - prisma: prisma as unknown as PrismaClient, - }); - - expect(result).toBeNull(); - }); - - test("should not access organization sub-team event when admin is not a team member", async () => { - const regularOrg = await createTestOrganization({ isPlatform: false }); - const orgSubTeam = await createTestTeam(regularOrg.id); - const orgAdmin = await createTestOrgAdmin(regularOrg.id); - const subTeamEvent = await createTestTeamEventType(orgSubTeam.id); - - const result = await getRawEventType({ - userId: orgAdmin.id, - eventTypeId: subTeamEvent.id, - isUserOrganizationAdmin: true, - currentOrganizationId: regularOrg.id, - prisma: prisma as unknown as PrismaClient, - }); - - expect(result).toBeNull(); - }); - }); }); From c2c95b371a691a5db042db7705f7708dbe62ce96 Mon Sep 17 00:00:00 2001 From: Romit <85230081+romitg2@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:43:57 +0530 Subject: [PATCH 3/3] cleanup(test): remove org booking integration coverage (#29019) --- .../accepted-action.integration-test.ts | 257 ---------- .../attendee-added-action.integration-test.ts | 122 ----- .../cancelled-action.integration-test.ts | 125 ----- .../created-action.integration-test.ts | 359 ------------- ...ocation-changed-action.integration-test.ts | 173 ------- ...no-show-updated-action.integration-test.ts | 320 ------------ .../test/spam-booking.integration-test.ts | 485 ------------------ 7 files changed, 1841 deletions(-) delete mode 100644 packages/features/booking-audit/lib/service/__tests__/accepted-action.integration-test.ts delete mode 100644 packages/features/booking-audit/lib/service/__tests__/attendee-added-action.integration-test.ts delete mode 100644 packages/features/booking-audit/lib/service/__tests__/cancelled-action.integration-test.ts delete mode 100644 packages/features/booking-audit/lib/service/__tests__/created-action.integration-test.ts delete mode 100644 packages/features/booking-audit/lib/service/__tests__/location-changed-action.integration-test.ts delete mode 100644 packages/features/booking-audit/lib/service/__tests__/no-show-updated-action.integration-test.ts diff --git a/packages/features/booking-audit/lib/service/__tests__/accepted-action.integration-test.ts b/packages/features/booking-audit/lib/service/__tests__/accepted-action.integration-test.ts deleted file mode 100644 index a46c9026086080..00000000000000 --- a/packages/features/booking-audit/lib/service/__tests__/accepted-action.integration-test.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { BookingStatus } from "@calcom/prisma/enums"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container"; -import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container"; -import { makeUserActor } from "../../makeActor"; -import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer"; -import type { BookingAuditViewerService } from "../BookingAuditViewerService"; -import { - cleanupTestData, - createTestBooking, - createTestEventType, - createTestMembership, - createTestOrganization, - createTestUser, - enableFeatureForOrganization, -} from "./integration-utils"; - -describe("Accepted Action Integration", () => { - let bookingAuditTaskConsumer: BookingAuditTaskConsumer; - let bookingAuditViewerService: BookingAuditViewerService; - - let testData: { - owner: { id: number; uuid: string; email: string }; - attendee: { id: number; email: string }; - organization: { id: number }; - eventType: { id: number }; - booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus }; - }; - const additionalBookingUids: string[] = []; - - beforeEach(async () => { - bookingAuditTaskConsumer = getBookingAuditTaskConsumer(); - bookingAuditViewerService = getBookingAuditViewerService(); - - const owner = await createTestUser({ name: "Test Audit User" }); - const organization = await createTestOrganization(); - await createTestMembership(owner.id, organization.id); - await enableFeatureForOrganization(organization.id, "booking-audit"); - const eventType = await createTestEventType(owner.id); - const attendee = await createTestUser({ name: "Test Attendee" }); - - const booking = await createTestBooking(owner.id, eventType.id, { - status: BookingStatus.PENDING, - attendees: [ - { - email: attendee.email, - name: attendee.name || "Test Attendee", - timeZone: "UTC", - }, - ], - }); - - testData = { - owner: { id: owner.id, uuid: owner.uuid, email: owner.email }, - attendee: { id: attendee.id, email: attendee.email }, - organization: { id: organization.id }, - eventType: { id: eventType.id }, - booking: { - uid: booking.uid, - startTime: booking.startTime, - endTime: booking.endTime, - status: booking.status, - }, - }; - }); - - afterEach(async () => { - for (const bookingUid of additionalBookingUids) { - await cleanupTestData({ - bookingUid, - }); - } - additionalBookingUids.length = 0; // Clear the array for next test - - if (!testData) return; - - await cleanupTestData({ - bookingUid: testData.booking?.uid, - userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [], - attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [], - eventTypeId: testData.eventType?.id, - organizationId: testData.organization?.id, - userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined), - featureSlug: "booking-audit", - }); - }); - - describe("when single booking is accepted", () => { - it("should create audit record and retrieve it with correct data formatting", async () => { - const actor = makeUserActor(testData.owner.uuid); - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "ACCEPTED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED }, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.bookingUid).toBe(testData.booking.uid); - expect(result.auditLogs).toHaveLength(1); - - const auditLog = result.auditLogs[0]; - expect(auditLog.bookingUid).toBe(testData.booking.uid); - expect(auditLog.action).toBe("ACCEPTED"); - expect(auditLog.type).toBe("RECORD_UPDATED"); - - const displayData = auditLog.displayJson as Record; - expect(displayData).toBeDefined(); - expect(displayData.previousStatus).toBe(BookingStatus.PENDING); - expect(displayData.newStatus).toBe(BookingStatus.ACCEPTED); - }); - - it("should enrich actor information with user details from database", async () => { - const actor = makeUserActor(testData.owner.uuid); - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "ACCEPTED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED }, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - const auditLog = result.auditLogs[0]; - expect(auditLog.actor.displayName).toBe("Test Audit User"); - expect(auditLog.actor.displayEmail).toBe(testData.owner.email); - expect(auditLog.actor.userUuid).toBe(testData.owner.uuid); - }); - }); - - describe("when multiple bookings are accepted in bulk", () => { - it("should create audit records for all bookings with same operation ID", async () => { - const booking2 = await createTestBooking(testData.owner.id, testData.eventType.id, { - status: BookingStatus.PENDING, - attendees: [ - { - email: testData.attendee.email, - name: "Test Attendee", - timeZone: "UTC", - }, - ], - }); - additionalBookingUids.push(booking2.uid); - - const booking3 = await createTestBooking(testData.owner.id, testData.eventType.id, { - status: BookingStatus.PENDING, - attendees: [ - { - email: testData.attendee.email, - name: "Test Attendee", - timeZone: "UTC", - }, - ], - }); - additionalBookingUids.push(booking3.uid); - - const actor = makeUserActor(testData.owner.uuid); - const operationId = `bulk-op-${Date.now()}`; - const timestamp = Date.now(); - - await bookingAuditTaskConsumer.processBulkAuditTask( - { - isBulk: true, - bookings: [ - { - bookingUid: testData.booking.uid, - data: { - status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED }, - }, - }, - { - bookingUid: booking2.uid, - data: { - status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED }, - }, - }, - { - bookingUid: booking3.uid, - data: { - status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED }, - }, - }, - ], - actor, - action: "ACCEPTED", - source: "WEBAPP", - operationId, - timestamp, - organizationId: testData.organization.id, - }, - "test-task-id" - ); - - const result1 = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - const result2 = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: booking2.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - const result3 = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: booking3.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result1.auditLogs).toHaveLength(1); - expect(result2.auditLogs).toHaveLength(1); - expect(result3.auditLogs).toHaveLength(1); - - expect(result1.auditLogs[0].action).toBe("ACCEPTED"); - expect(result2.auditLogs[0].action).toBe("ACCEPTED"); - expect(result3.auditLogs[0].action).toBe("ACCEPTED"); - - // Verify all bookings share the same operationId - expect(result1.auditLogs[0].operationId).toBe(operationId); - expect(result2.auditLogs[0].operationId).toBe(operationId); - expect(result3.auditLogs[0].operationId).toBe(operationId); - }); - }); -}); diff --git a/packages/features/booking-audit/lib/service/__tests__/attendee-added-action.integration-test.ts b/packages/features/booking-audit/lib/service/__tests__/attendee-added-action.integration-test.ts deleted file mode 100644 index 144d3564a3e6f3..00000000000000 --- a/packages/features/booking-audit/lib/service/__tests__/attendee-added-action.integration-test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, expect, it, beforeEach, afterEach } from "vitest"; - -import type { BookingStatus } from "@calcom/prisma/enums"; -import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer"; -import type { BookingAuditViewerService } from "../BookingAuditViewerService"; -import { makeUserActor } from "../../makeActor"; -import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container"; -import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container"; -import { - createTestUser, - createTestOrganization, - createTestMembership, - createTestEventType, - createTestBooking, - enableFeatureForOrganization, - cleanupTestData, -} from "./integration-utils"; - -describe("Attendee Added Action Integration", () => { - let bookingAuditTaskConsumer: BookingAuditTaskConsumer; - let bookingAuditViewerService: BookingAuditViewerService; - - let testData: { - owner: { id: number; uuid: string; email: string }; - attendee: { id: number; email: string }; - organization: { id: number }; - eventType: { id: number }; - booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus }; - }; - - beforeEach(async () => { - bookingAuditTaskConsumer = getBookingAuditTaskConsumer(); - bookingAuditViewerService = getBookingAuditViewerService(); - - const owner = await createTestUser({ name: "Test Audit User" }); - const organization = await createTestOrganization(); - await createTestMembership(owner.id, organization.id); - await enableFeatureForOrganization(organization.id, "booking-audit"); - const eventType = await createTestEventType(owner.id); - const attendee = await createTestUser({ name: "Test Attendee" }); - - const booking = await createTestBooking(owner.id, eventType.id, { - attendees: [ - { - email: attendee.email, - name: attendee.name || "Test Attendee", - timeZone: "UTC", - }, - ], - }); - - testData = { - owner: { id: owner.id, uuid: owner.uuid, email: owner.email }, - attendee: { id: attendee.id, email: attendee.email }, - organization: { id: organization.id }, - eventType: { id: eventType.id }, - booking: { - uid: booking.uid, - startTime: booking.startTime, - endTime: booking.endTime, - status: booking.status, - }, - }; - }); - - afterEach(async () => { - if (!testData) return; - - await cleanupTestData({ - bookingUid: testData.booking?.uid, - userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [], - attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [], - eventTypeId: testData.eventType?.id, - organizationId: testData.organization?.id, - userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined), - featureSlug: "booking-audit", - }); - }); - - describe("when guests are added to a booking", () => { - it("should create audit record with correct added attendees data", async () => { - const actor = makeUserActor(testData.owner.uuid); - const newGuestEmails = ["guest1@example.com", "guest2@example.com"]; - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "ATTENDEE_ADDED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - added: newGuestEmails, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.bookingUid).toBe(testData.booking.uid); - expect(result.auditLogs).toHaveLength(1); - - const auditLog = result.auditLogs[0]; - expect(auditLog.bookingUid).toBe(testData.booking.uid); - expect(auditLog.action).toBe("ATTENDEE_ADDED"); - expect(auditLog.type).toBe("RECORD_UPDATED"); - - const displayData = auditLog.displayJson as { addedAttendees: string[] }; - expect(displayData).toBeDefined(); - expect(displayData.addedAttendees).toEqual(newGuestEmails); - - expect(auditLog.actor.displayName).toBe("Test Audit User"); - expect(auditLog.actor.displayEmail).toBe(testData.owner.email); - expect(auditLog.actor.userUuid).toBe(testData.owner.uuid); - }); - }); -}); diff --git a/packages/features/booking-audit/lib/service/__tests__/cancelled-action.integration-test.ts b/packages/features/booking-audit/lib/service/__tests__/cancelled-action.integration-test.ts deleted file mode 100644 index b52f81a474e1ac..00000000000000 --- a/packages/features/booking-audit/lib/service/__tests__/cancelled-action.integration-test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { describe, expect, it, beforeEach, afterEach } from "vitest"; - -import { BookingStatus } from "@calcom/prisma/enums"; - -import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer"; -import type { BookingAuditViewerService } from "../BookingAuditViewerService"; -import { makeUserActor } from "../../makeActor"; -import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container"; -import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container"; -import { - createTestUser, - createTestOrganization, - createTestMembership, - createTestEventType, - createTestBooking, - enableFeatureForOrganization, - cleanupTestData, -} from "./integration-utils"; - -describe("Cancelled Action Integration", () => { - let bookingAuditTaskConsumer: BookingAuditTaskConsumer; - let bookingAuditViewerService: BookingAuditViewerService; - - let testData: { - owner: { id: number; uuid: string; email: string }; - attendee: { id: number; email: string }; - organization: { id: number }; - eventType: { id: number }; - booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus }; - }; - - beforeEach(async () => { - bookingAuditTaskConsumer = getBookingAuditTaskConsumer(); - bookingAuditViewerService = getBookingAuditViewerService(); - - const owner = await createTestUser({ name: "Test Audit User" }); - const organization = await createTestOrganization(); - await createTestMembership(owner.id, organization.id); - await enableFeatureForOrganization(organization.id, "booking-audit"); - const eventType = await createTestEventType(owner.id); - const attendee = await createTestUser({ name: "Test Attendee" }); - - const booking = await createTestBooking(owner.id, eventType.id, { - attendees: [ - { - email: attendee.email, - name: attendee.name || "Test Attendee", - timeZone: "UTC", - }, - ], - }); - - testData = { - owner: { id: owner.id, uuid: owner.uuid, email: owner.email }, - attendee: { id: attendee.id, email: attendee.email }, - organization: { id: organization.id }, - eventType: { id: eventType.id }, - booking: { - uid: booking.uid, - startTime: booking.startTime, - endTime: booking.endTime, - status: booking.status, - }, - }; - }); - - afterEach(async () => { - if (!testData) return; - - await cleanupTestData({ - bookingUid: testData.booking?.uid, - userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [], - attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [], - eventTypeId: testData.eventType?.id, - organizationId: testData.organization?.id, - userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined), - featureSlug: "booking-audit", - }); - }); - - describe("when booking is cancelled", () => { - it("should create audit record with cancellation details and retrieve it correctly", async () => { - const actor = makeUserActor(testData.owner.uuid); - const cancellationReason = "Schedule conflict"; - const cancelledBy = "owner"; - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "CANCELLED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - cancellationReason, - cancelledBy, - status: { - old: BookingStatus.ACCEPTED, - new: BookingStatus.CANCELLED, - }, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.auditLogs).toHaveLength(1); - const auditLog = result.auditLogs[0]; - - expect(auditLog.action).toBe("CANCELLED"); - expect(auditLog.type).toBe("RECORD_UPDATED"); - - const displayData = auditLog.displayJson as Record; - expect(displayData.cancellationReason).toBe(cancellationReason); - expect(displayData.cancelledBy).toBe(cancelledBy); - expect(displayData.previousStatus).toBe(BookingStatus.ACCEPTED); - expect(displayData.newStatus).toBe(BookingStatus.CANCELLED); - }); - }); -}); diff --git a/packages/features/booking-audit/lib/service/__tests__/created-action.integration-test.ts b/packages/features/booking-audit/lib/service/__tests__/created-action.integration-test.ts deleted file mode 100644 index 10a47d53dee5eb..00000000000000 --- a/packages/features/booking-audit/lib/service/__tests__/created-action.integration-test.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { describe, expect, it, beforeEach, afterEach } from "vitest"; - -import type { BookingStatus } from "@calcom/prisma/enums"; -import prisma from "@calcom/prisma"; -import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer"; -import type { BookingAuditViewerService } from "../BookingAuditViewerService"; -import { makeUserActor } from "../../makeActor"; -import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container"; -import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container"; -import { - createTestUser, - createTestOrganization, - createTestMembership, - createTestEventType, - createTestBooking, - enableFeatureForOrganization, - cleanupTestData, -} from "./integration-utils"; - -describe("Created Action Integration", () => { - let bookingAuditTaskConsumer: BookingAuditTaskConsumer; - let bookingAuditViewerService: BookingAuditViewerService; - - let testData: { - owner: { id: number; uuid: string; email: string }; - attendee: { id: number; email: string }; - organization: { id: number }; - eventType: { id: number }; - booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus }; - }; - - beforeEach(async () => { - bookingAuditTaskConsumer = getBookingAuditTaskConsumer(); - bookingAuditViewerService = getBookingAuditViewerService(); - - const owner = await createTestUser({ name: "Test Audit User" }); - const organization = await createTestOrganization(); - await createTestMembership(owner.id, organization.id); - await enableFeatureForOrganization(organization.id, "booking-audit"); - const eventType = await createTestEventType(owner.id); - const attendee = await createTestUser({ name: "Test Attendee" }); - - const booking = await createTestBooking(owner.id, eventType.id, { - attendees: [ - { - email: attendee.email, - name: attendee.name || "Test Attendee", - timeZone: "UTC", - }, - ], - }); - - testData = { - owner: { id: owner.id, uuid: owner.uuid, email: owner.email }, - attendee: { id: attendee.id, email: attendee.email }, - organization: { id: organization.id }, - eventType: { id: eventType.id }, - booking: { - uid: booking.uid, - startTime: booking.startTime, - endTime: booking.endTime, - status: booking.status, - }, - }; - }); - - afterEach(async () => { - if (!testData) return; - - await cleanupTestData({ - bookingUid: testData.booking?.uid, - userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [], - attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [], - eventTypeId: testData.eventType?.id, - organizationId: testData.organization?.id, - userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined), - featureSlug: "booking-audit", - }); - }); - - describe("when single booking is created", () => { - it("should create audit record and retrieve it with correct data formatting", async () => { - const actor = makeUserActor(testData.owner.uuid); - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "CREATED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - startTime: testData.booking.startTime.getTime(), - endTime: testData.booking.endTime.getTime(), - status: testData.booking.status, - hostUserUuid: testData.owner.uuid, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.bookingUid).toBe(testData.booking.uid); - expect(result.auditLogs).toHaveLength(1); - - const auditLog = result.auditLogs[0]; - expect(auditLog.bookingUid).toBe(testData.booking.uid); - expect(auditLog.action).toBe("CREATED"); - expect(auditLog.type).toBe("RECORD_CREATED"); - - const displayData = auditLog.displayJson as Record; - expect(displayData).toBeDefined(); - expect(displayData.startTime).toBeDefined(); - expect(typeof displayData.startTime).toBe("string"); - expect(displayData.startTime).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); - - expect(displayData.endTime).toBeDefined(); - expect(typeof displayData.endTime).toBe("string"); - expect(displayData.endTime).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); - - expect(displayData.status).toBe(testData.booking.status); - }); - - it("should enrich actor information with user details from database", async () => { - const actor = makeUserActor(testData.owner.uuid); - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "CREATED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - startTime: testData.booking.startTime.getTime(), - endTime: testData.booking.endTime.getTime(), - status: testData.booking.status, - hostUserUuid: testData.owner.uuid, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - const auditLog = result.auditLogs[0]; - expect(auditLog.actor.displayName).toBe("Test Audit User"); - expect(auditLog.actor.displayEmail).toBe(testData.owner.email); - expect(auditLog.actor.userUuid).toBe(testData.owner.uuid); - }); - - it("should include impersonator details when context has impersonatedBy", async () => { - // Create a second user to act as impersonator - const impersonator = await createTestUser({ name: "Admin Impersonator" }); - - const actor = makeUserActor(testData.owner.uuid); - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "CREATED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - startTime: testData.booking.startTime.getTime(), - endTime: testData.booking.endTime.getTime(), - status: testData.booking.status, - hostUserUuid: testData.owner.uuid, - }, - timestamp: Date.now(), - context: { - impersonatedBy: impersonator.uuid, - }, - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.auditLogs).toHaveLength(1); - - const auditLog = result.auditLogs[0]; - expect(auditLog.impersonatedBy).toBeDefined(); - expect(auditLog.impersonatedBy?.displayName).toBe("Admin Impersonator"); - expect(auditLog.impersonatedBy?.displayEmail).toBe(impersonator.email); - - // Cleanup impersonator user - await prisma.user.delete({ where: { id: impersonator.id } }); - }); - - it.skip("should deny access to unauthorized users viewing audit logs", async () => { - const actor = makeUserActor(testData.owner.uuid); - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "CREATED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - startTime: testData.booking.startTime.getTime(), - endTime: testData.booking.endTime.getTime(), - status: testData.booking.status, - hostUserUuid: testData.owner.uuid, - }, - timestamp: Date.now(), - }); - - const ownerResult = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - expect(ownerResult.auditLogs).toHaveLength(1); - - const unauthorizedUserId = 999999; - const unauthorizedEmail = "unauthorized@example.com"; - - await expect( - bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: unauthorizedUserId, - userEmail: unauthorizedEmail, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }) - ).rejects.toThrow(); - }); - }); - - describe("when multiple bookings are created in bulk", () => { - it("should create audit records for all bookings with same operation ID", async () => { - const booking2 = await createTestBooking(testData.owner.id, testData.eventType.id, { - attendees: [ - { - email: testData.attendee.email, - name: "Test Attendee", - timeZone: "UTC", - }, - ], - }); - - const booking3 = await createTestBooking(testData.owner.id, testData.eventType.id, { - attendees: [ - { - email: testData.attendee.email, - name: "Test Attendee", - timeZone: "UTC", - }, - ], - }); - - const actor = makeUserActor(testData.owner.uuid); - const operationId = `bulk-op-${Date.now()}`; - const timestamp = Date.now(); - - await bookingAuditTaskConsumer.processBulkAuditTask( - { - isBulk: true, - bookings: [ - { - bookingUid: testData.booking.uid, - data: { - startTime: testData.booking.startTime.getTime(), - endTime: testData.booking.endTime.getTime(), - status: testData.booking.status, - hostUserUuid: testData.owner.uuid, - }, - }, - { - bookingUid: booking2.uid, - data: { - startTime: booking2.startTime.getTime(), - endTime: booking2.endTime.getTime(), - status: booking2.status, - hostUserUuid: testData.owner.uuid, - }, - }, - { - bookingUid: booking3.uid, - data: { - startTime: booking3.startTime.getTime(), - endTime: booking3.endTime.getTime(), - status: booking3.status, - hostUserUuid: testData.owner.uuid, - }, - }, - ], - actor, - action: "CREATED", - source: "WEBAPP", - operationId, - timestamp, - organizationId: testData.organization.id, - }, - "test-task-id" - ); - - const result1 = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - const result2 = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: booking2.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - const result3 = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: booking3.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result1.auditLogs).toHaveLength(1); - expect(result2.auditLogs).toHaveLength(1); - expect(result3.auditLogs).toHaveLength(1); - - expect(result1.auditLogs[0].action).toBe("CREATED"); - expect(result2.auditLogs[0].action).toBe("CREATED"); - expect(result3.auditLogs[0].action).toBe("CREATED"); - - // Verify all bookings share the same operationId - expect(result1.auditLogs[0].operationId).toBe(operationId); - expect(result2.auditLogs[0].operationId).toBe(operationId); - expect(result3.auditLogs[0].operationId).toBe(operationId); - - await cleanupTestData({ - bookingUid: booking2.uid, - }); - await cleanupTestData({ - bookingUid: booking3.uid, - }); - }); - }); -}); diff --git a/packages/features/booking-audit/lib/service/__tests__/location-changed-action.integration-test.ts b/packages/features/booking-audit/lib/service/__tests__/location-changed-action.integration-test.ts deleted file mode 100644 index 8defc96763da23..00000000000000 --- a/packages/features/booking-audit/lib/service/__tests__/location-changed-action.integration-test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { describe, expect, it, beforeEach, afterEach } from "vitest"; - -import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer"; -import type { BookingAuditViewerService } from "../BookingAuditViewerService"; -import { makeUserActor } from "../../makeActor"; -import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container"; -import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container"; -import { - createTestUser, - createTestOrganization, - createTestMembership, - createTestEventType, - createTestBooking, - enableFeatureForOrganization, - cleanupTestData, -} from "./integration-utils"; -import { BookingStatus } from "@calcom/prisma/enums"; - -describe("Location Changed Action Integration", () => { - let bookingAuditTaskConsumer: BookingAuditTaskConsumer; - let bookingAuditViewerService: BookingAuditViewerService; - - let testData: { - owner: { id: number; uuid: string; email: string }; - attendee: { id: number; email: string }; - organization: { id: number }; - eventType: { id: number }; - booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus }; - }; - - beforeEach(async () => { - bookingAuditTaskConsumer = getBookingAuditTaskConsumer(); - bookingAuditViewerService = getBookingAuditViewerService(); - - // Setup basic test data using Test Data Builder pattern - const owner = await createTestUser({ name: "Location Audit User" }); - const organization = await createTestOrganization(); - await createTestMembership(owner.id, organization.id); - await enableFeatureForOrganization(organization.id, "booking-audit"); - const eventType = await createTestEventType(owner.id); - const attendee = await createTestUser({ name: "Test Attendee" }); - - const booking = await createTestBooking(owner.id, eventType.id, { - attendees: [ - { - email: attendee.email, - name: attendee.name || "Test Attendee", - timeZone: "UTC", - }, - ], - }); - - testData = { - owner: { id: owner.id, uuid: owner.uuid, email: owner.email }, - attendee: { id: attendee.id, email: attendee.email }, - organization: { id: organization.id }, - eventType: { id: eventType.id }, - booking: { - uid: booking.uid, - startTime: booking.startTime, - endTime: booking.endTime, - status: booking.status, - }, - }; - }); - - afterEach(async () => { - if (!testData) return; - - await cleanupTestData({ - bookingUid: testData.booking?.uid, - userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [], - attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [], - eventTypeId: testData.eventType?.id, - organizationId: testData.organization?.id, - userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined), - featureSlug: "booking-audit", - }); - }); - - describe("when location is changed", () => { - it("should create audit record and retrieve it with correct location data", async () => { - const actor = makeUserActor(testData.owner.uuid); - const operationId = `op-${Date.now()}`; - - // Simulate LOCATION_CHANGED action - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "LOCATION_CHANGED", - source: "WEBAPP", - operationId, - data: { - location: { - old: "Zoom", - new: "Google Meet", - }, - }, - timestamp: Date.now(), - }); - - // Retrieve logs - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.bookingUid).toBe(testData.booking.uid); - - // Find the specific log we just created - const auditLog = result.auditLogs.find((log) => log.operationId === operationId); - expect(auditLog).toBeDefined(); - - if (!auditLog) throw new Error("Audit log not found"); - - expect(auditLog.action).toBe("LOCATION_CHANGED"); - // The type identifier from the service - expect(auditLog.type).toBe("RECORD_UPDATED"); - - expect(auditLog.displayJson).toBeNull(); - - // Verify the title params are correct - expect(auditLog.actionDisplayTitle).toBeDefined(); - expect(auditLog.actionDisplayTitle.key).toBe("booking_audit_action.location_changed_from_to"); - expect(auditLog.actionDisplayTitle.params).toEqual({ - fromLocation: "Zoom", - toLocation: "Google Meet", - }); - }); - - it("should handle location change where old value was null", async () => { - const actor = makeUserActor(testData.owner.uuid); - const operationId = `op-initial-${Date.now()}`; - - // Simulate setting location for the first time (old is null) - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "LOCATION_CHANGED", - source: "WEBAPP", - operationId, - data: { - location: { - old: null, - new: "In Person", - }, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - const auditLog = result.auditLogs.find((log) => log.operationId === operationId); - expect(auditLog).toBeDefined(); - - expect(auditLog!.displayJson).toBeNull(); - - expect(auditLog!.actionDisplayTitle.params).toEqual({ - fromLocation: "No location defined", - toLocation: "In Person", - }); - }); - }); -}); diff --git a/packages/features/booking-audit/lib/service/__tests__/no-show-updated-action.integration-test.ts b/packages/features/booking-audit/lib/service/__tests__/no-show-updated-action.integration-test.ts deleted file mode 100644 index 9d3303225ec4eb..00000000000000 --- a/packages/features/booking-audit/lib/service/__tests__/no-show-updated-action.integration-test.ts +++ /dev/null @@ -1,320 +0,0 @@ -import type { BookingStatus } from "@calcom/prisma/enums"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container"; -import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container"; -import { makeUserActor } from "../../makeActor"; -import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer"; -import type { BookingAuditViewerService } from "../BookingAuditViewerService"; -import { - cleanupTestData, - createTestBooking, - createTestEventType, - createTestMembership, - createTestOrganization, - createTestUser, - enableFeatureForOrganization, -} from "./integration-utils"; - -describe("No-Show Updated Action Integration", () => { - let bookingAuditTaskConsumer: BookingAuditTaskConsumer; - let bookingAuditViewerService: BookingAuditViewerService; - - let testData: { - owner: { id: number; uuid: string; email: string }; - attendee: { id: number; email: string }; - organization: { id: number }; - eventType: { id: number }; - booking: { id: number; uid: string; startTime: Date; endTime: Date; status: BookingStatus }; - }; - - const additionalAttendeeEmails: string[] = []; - - beforeEach(async () => { - bookingAuditTaskConsumer = getBookingAuditTaskConsumer(); - bookingAuditViewerService = getBookingAuditViewerService(); - - const owner = await createTestUser({ name: "Test Host User" }); - const organization = await createTestOrganization(); - await createTestMembership(owner.id, organization.id); - await enableFeatureForOrganization(organization.id, "booking-audit"); - const eventType = await createTestEventType(owner.id); - const attendee = await createTestUser({ name: "Test Attendee" }); - - const booking = await createTestBooking(owner.id, eventType.id, { - attendees: [ - { - email: attendee.email, - name: attendee.name || "Test Attendee", - timeZone: "UTC", - }, - ], - }); - - testData = { - owner: { id: owner.id, uuid: owner.uuid, email: owner.email }, - attendee: { id: attendee.id, email: attendee.email }, - organization: { id: organization.id }, - eventType: { id: eventType.id }, - booking: { - id: booking.id, - uid: booking.uid, - startTime: booking.startTime, - endTime: booking.endTime, - status: booking.status, - }, - }; - }); - - afterEach(async () => { - if (!testData) return; - - await cleanupTestData({ - bookingUid: testData.booking?.uid, - userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [], - attendeeEmails: [ - ...(testData.attendee?.email ? [testData.attendee.email] : []), - ...additionalAttendeeEmails, - ], - eventTypeId: testData.eventType?.id, - organizationId: testData.organization?.id, - userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined), - featureSlug: "booking-audit", - }); - additionalAttendeeEmails.length = 0; - }); - - describe("when host is marked as no-show", () => { - it("should create audit record with host field containing userUuid and noShow", async () => { - const actor = makeUserActor(testData.owner.uuid); - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "NO_SHOW_UPDATED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - // New schema: host contains userUuid and noShow change - host: { - userUuid: testData.owner.uuid, - noShow: { old: null, new: true }, - }, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.bookingUid).toBe(testData.booking.uid); - expect(result.auditLogs).toHaveLength(1); - - const auditLog = result.auditLogs[0]; - expect(auditLog.bookingUid).toBe(testData.booking.uid); - expect(auditLog.action).toBe("NO_SHOW_UPDATED"); - expect(auditLog.type).toBe("RECORD_UPDATED"); - - const displayData = auditLog.displayJson as Record; - expect(displayData).toBeDefined(); - expect(displayData.hostNoShow).toBe(true); - expect(displayData.previousHostNoShow).toBe(null); - }); - }); - - describe("when attendees are marked as no-show", () => { - it("should create audit record with attendeesNoShow array", async () => { - const actor = makeUserActor(testData.owner.uuid); - - const attendeesNoShow = [{ attendeeEmail: testData.attendee.email, noShow: { old: null, new: true } }]; - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "NO_SHOW_UPDATED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - attendeesNoShow, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.bookingUid).toBe(testData.booking.uid); - expect(result.auditLogs).toHaveLength(1); - - const auditLog = result.auditLogs[0]; - expect(auditLog.action).toBe("NO_SHOW_UPDATED"); - expect(auditLog.type).toBe("RECORD_UPDATED"); - - const displayData = auditLog.displayJson as Record; - expect(displayData).toBeDefined(); - expect(displayData.attendeesNoShow).toBeDefined(); - - const storedAttendeesNoShow = displayData.attendeesNoShow as Array<{ - attendeeEmail: string; - noShow: { old: boolean | null; new: boolean }; - }>; - expect(storedAttendeesNoShow).toHaveLength(1); - expect(storedAttendeesNoShow[0].attendeeEmail).toBe(testData.attendee.email); - expect(storedAttendeesNoShow[0].noShow.old).toBe(null); - expect(storedAttendeesNoShow[0].noShow.new).toBe(true); - }); - - it("should handle multiple attendees marked as no-show", async () => { - const { prisma } = await import("@calcom/prisma"); - const secondAttendeeEmail = `second-attendee-${Date.now()}@example.com`; - additionalAttendeeEmails.push(secondAttendeeEmail); - await prisma.attendee.create({ - data: { - email: secondAttendeeEmail, - name: "Second Attendee", - timeZone: "UTC", - bookingId: testData.booking.id, - }, - }); - - const actor = makeUserActor(testData.owner.uuid); - - const attendeesNoShow = [ - { attendeeEmail: testData.attendee.email, noShow: { old: null, new: true } }, - { attendeeEmail: secondAttendeeEmail, noShow: { old: false, new: true } }, - ]; - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "NO_SHOW_UPDATED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: { - attendeesNoShow, - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.auditLogs).toHaveLength(1); - - const displayData = result.auditLogs[0].displayJson as Record; - const storedAttendeesNoShow = displayData.attendeesNoShow as Array<{ - attendeeEmail: string; - noShow: { old: boolean | null; new: boolean }; - }>; - - expect(storedAttendeesNoShow).toHaveLength(2); - const firstAttendee = storedAttendeesNoShow.find((a) => a.attendeeEmail === testData.attendee.email); - const secondAttendeeData = storedAttendeesNoShow.find((a) => a.attendeeEmail === secondAttendeeEmail); - expect(firstAttendee?.noShow).toEqual({ old: null, new: true }); - expect(secondAttendeeData?.noShow).toEqual({ old: false, new: true }); - }); - }); - - describe("when both host and attendees are marked as no-show", () => { - it("should create single audit record with both host and attendeesNoShow fields", async () => { - const actor = makeUserActor(testData.owner.uuid); - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "NO_SHOW_UPDATED", - source: "API_V2", - operationId: `op-${Date.now()}`, - data: { - host: { - userUuid: testData.owner.uuid, - noShow: { old: null, new: true }, - }, - attendeesNoShow: [{ attendeeEmail: testData.attendee.email, noShow: { old: null, new: true } }], - }, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.auditLogs).toHaveLength(1); - - const auditLog = result.auditLogs[0]; - expect(auditLog.action).toBe("NO_SHOW_UPDATED"); - expect(auditLog.source).toBe("API_V2"); - - const displayData = auditLog.displayJson as Record; - - expect(displayData.hostNoShow).toBe(true); - expect(displayData.previousHostNoShow).toBe(null); - - const storedAttendeesNoShow = displayData.attendeesNoShow as Array<{ - attendeeEmail: string; - noShow: { old: boolean | null; new: boolean }; - }>; - expect(storedAttendeesNoShow).toHaveLength(1); - expect(storedAttendeesNoShow[0].attendeeEmail).toBe(testData.attendee.email); - expect(storedAttendeesNoShow[0].noShow).toEqual({ old: null, new: true }); - }); - }); - - describe("schema validation with array format", () => { - it("should accept attendeesNoShow data with array format", async () => { - const actor = makeUserActor(testData.owner.uuid); - - const dataWithArrayFormat = { - attendeesNoShow: [{ attendeeEmail: testData.attendee.email, noShow: { old: null, new: true } }], - }; - - await bookingAuditTaskConsumer.onBookingAction({ - bookingUid: testData.booking.uid, - actor, - action: "NO_SHOW_UPDATED", - source: "WEBAPP", - operationId: `op-${Date.now()}`, - data: dataWithArrayFormat, - timestamp: Date.now(), - }); - - const result = await bookingAuditViewerService.getAuditLogsForBooking({ - bookingUid: testData.booking.uid, - userId: testData.owner.id, - userEmail: testData.owner.email, - userTimeZone: "UTC", - organizationId: testData.organization.id, - }); - - expect(result.auditLogs).toHaveLength(1); - - const displayData = result.auditLogs[0].displayJson as Record; - expect(displayData.attendeesNoShow).toBeDefined(); - - const storedAttendeesNoShow = displayData.attendeesNoShow as Array<{ - attendeeEmail: string; - noShow: { old: boolean | null; new: boolean }; - }>; - expect(storedAttendeesNoShow).toHaveLength(1); - expect(storedAttendeesNoShow[0].attendeeEmail).toBe(testData.attendee.email); - }); - }); -}); diff --git a/packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts b/packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts index e699aae7a629e3..670f2035efa635 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts @@ -8,7 +8,6 @@ import { mockCalendarToHaveNoBusySlots, mockCalendarToCrashOnGetAvailability, BookingLocations, - createOrganization, } from "@calcom/testing/lib/bookingScenario/bookingScenario"; import { getMockRequestDataForBooking } from "@calcom/testing/lib/bookingScenario/getMockRequestDataForBooking"; import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTeardown"; @@ -54,22 +53,6 @@ const createGlobalWatchlistEntry = async (overrides: { }); }; -const createOrganizationWatchlistEntry = async ( - organizationId: number, - overrides: { - type: WatchlistType; - value: string; - action: "BLOCK" | "REPORT"; - } -) => { - return createTestWatchlistEntry({ - type: overrides.type, - value: overrides.value, - action: overrides.action, - organizationId, - }); -}; - const expectDecoyBookingResponse = (booking: Record) => { expect(booking).toHaveProperty("isShortCircuitedBooking", true); expect(booking).toHaveProperty("uid"); @@ -402,472 +385,4 @@ describe("handleNewBooking - Spam Detection", () => { ); }); - describe("Organization Watchlist Blocking:", () => { - test( - "should block booking when email is in organization watchlist and return decoy response", - async () => { - const handleNewBooking = getNewBookingHandler(); - const blockedEmail = "org-spammer@example.com"; - - // Create organization with a team - const org = await createOrganization({ - name: "Test Org", - slug: "test-org", - withTeam: true, - }); - - const booker = getBooker({ - email: blockedEmail, - name: "Org Blocked Booker", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - organizationId: org.id, - }); - - await createOrganizationWatchlistEntry(org.id, { - type: WatchlistType.EMAIL, - value: blockedEmail, - action: "BLOCK", - }); - - // Use the child team ID for the eventType - const teamId = org.children && org.children.length > 0 ? org.children[0].id : null; - - await createBookingScenario( - getScenarioData( - { - eventTypes: [ - { - id: 1, - slotInterval: 30, - length: 30, - teamId, - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }, - { id: org.id } - ) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", { - create: { - id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", - }, - }); - - const mockBookingData = getMockRequestDataForBooking({ - data: { - user: organizer.username, - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.CalVideo }, - }, - }, - }); - - const createdBooking = await handleNewBooking({ - bookingData: mockBookingData, - }); - - expectDecoyBookingResponse(createdBooking); - expect(createdBooking.attendees[0].email).toBe(blockedEmail); - await expectNoBookingInDatabase(blockedEmail); - }, - timeout - ); - - test( - "should block booking when domain is in organization watchlist and return decoy response", - async () => { - const handleNewBooking = getNewBookingHandler(); - const blockedDomain = "spammydomain.com"; - const blockedEmail = `user@${blockedDomain}`; - - // Create organization with a team - const org = await createOrganization({ - name: "Test Org", - slug: "test-org", - withTeam: true, - }); - - const booker = getBooker({ - email: blockedEmail, - name: "Domain Blocked Booker", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - organizationId: org.id, - }); - - await createOrganizationWatchlistEntry(org.id, { - type: WatchlistType.DOMAIN, - value: blockedDomain, - action: "BLOCK", - }); - - // Use the child team ID for the eventType - const teamId = org.children && org.children.length > 0 ? org.children[0].id : null; - - await createBookingScenario( - getScenarioData( - { - eventTypes: [ - { - id: 1, - slotInterval: 30, - length: 30, - teamId, - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }, - { id: org.id } - ) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", { - create: { - id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", - }, - }); - - const mockBookingData = getMockRequestDataForBooking({ - data: { - user: organizer.username, - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.CalVideo }, - }, - }, - }); - - const createdBooking = await handleNewBooking({ - bookingData: mockBookingData, - }); - - expectDecoyBookingResponse(createdBooking); - expect(createdBooking.attendees[0].email).toBe(blockedEmail); - await expectNoBookingInDatabase(blockedEmail); - }, - timeout - ); - - test( - "should NOT block booking when email is in a different organization's watchlist", - async () => { - const handleNewBooking = getNewBookingHandler(); - const blockedEmail = "different-org-user@example.com"; - - // Create two different organizations with teams - const organizerOrg = await createOrganization({ - name: "Organizer Org", - slug: "organizer-org", - withTeam: true, - }); - - const differentOrg = await createOrganization({ - name: "Different Org", - slug: "different-org", - withTeam: true, - }); - - const booker = getBooker({ - email: blockedEmail, - name: "Booker", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - organizationId: organizerOrg.id, - }); - - // Block email in a DIFFERENT organization - await createOrganizationWatchlistEntry(differentOrg.id, { - type: WatchlistType.EMAIL, - value: blockedEmail, - action: "BLOCK", - }); - - // Use the child team ID for the eventType - const teamId = - organizerOrg.children && organizerOrg.children.length > 0 ? organizerOrg.children[0].id : null; - - await createBookingScenario( - getScenarioData( - { - eventTypes: [ - { - id: 1, - slotInterval: 30, - length: 30, - teamId, - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }, - { id: organizerOrg.id } - ) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", { - create: { - id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", - }, - }); - - const mockBookingData = getMockRequestDataForBooking({ - data: { - user: organizer.username, - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.CalVideo }, - }, - }, - }); - - const createdBooking = await handleNewBooking({ - bookingData: mockBookingData, - }); - - // Should NOT be a decoy response - booking should succeed - expect(createdBooking).not.toHaveProperty("isShortCircuitedBooking"); - expect(createdBooking.status).toBe(BookingStatus.ACCEPTED); - expect(createdBooking.id).not.toBe(0); - expect(createdBooking.attendees[0].email).toBe(blockedEmail); - }, - timeout - ); - - test( - "should block booking for managed event when email is in organization watchlist", - async () => { - const handleNewBooking = getNewBookingHandler(); - const blockedEmail = "managed-event-spammer@example.com"; - - // Create organization with a team - const org = await createOrganization({ - name: "Managed Event Org", - slug: "managed-event-org", - withTeam: true, - }); - - const booker = getBooker({ - email: blockedEmail, - name: "Managed Event Booker", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - organizationId: org.id, - }); - - await createOrganizationWatchlistEntry(org.id, { - type: WatchlistType.EMAIL, - value: blockedEmail, - action: "BLOCK", - }); - - // Get the child team ID - const teamId = org.children && org.children.length > 0 ? org.children[0].id : null; - - // Create a parent event type and a managed (child) event type - await createBookingScenario( - getScenarioData( - { - eventTypes: [ - // Parent event type - { - id: 1, - slotInterval: 30, - length: 30, - teamId, - users: [ - { - id: 101, - }, - ], - }, - // Managed event type (child) - has parent but no teamId - { - id: 2, - slotInterval: 30, - length: 30, - parent: { id: 1 }, // References parent event type - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }, - { id: org.id } - ) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", { - create: { - id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", - }, - }); - - // Try to book the managed event (id: 2) - const mockBookingData = getMockRequestDataForBooking({ - data: { - user: organizer.username, - eventTypeId: 2, // Booking the managed event - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.CalVideo }, - }, - }, - }); - - const createdBooking = await handleNewBooking({ - bookingData: mockBookingData, - }); - - // Should return a decoy response since email is blocked in the organization - expectDecoyBookingResponse(createdBooking); - expect(createdBooking.attendees[0].email).toBe(blockedEmail); - await expectNoBookingInDatabase(blockedEmail); - }, - timeout - ); - - test( - "should block booking for user event in organization when email is in organization watchlist", - async () => { - const handleNewBooking = getNewBookingHandler(); - const blockedEmail = "user-event-spammer@example.com"; - - // Create organization - const org = await createOrganization({ - name: "User Event Org", - slug: "user-event-org", - withTeam: false, - }); - - const booker = getBooker({ - email: blockedEmail, - name: "User Event Booker", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - organizationId: org.id, - }); - - await createOrganizationWatchlistEntry(org.id, { - type: WatchlistType.EMAIL, - value: blockedEmail, - action: "BLOCK", - }); - - // Create a user event (no teamId) but with a profile linking to the organization - await createBookingScenario( - getScenarioData( - { - eventTypes: [ - { - id: 1, - slotInterval: 30, - length: 30, - // User Event Type has userId set - userId: 101, - }, - ], - organizer, - apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }, - { id: org.id } - ) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", { - create: { - id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", - }, - }); - - const mockBookingData = getMockRequestDataForBooking({ - data: { - user: organizer.username, - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.CalVideo }, - }, - }, - }); - - const createdBooking = await handleNewBooking({ - bookingData: mockBookingData, - }); - - // Should return a decoy response since email is blocked in the organization - expectDecoyBookingResponse(createdBooking); - expect(createdBooking.attendees[0].email).toBe(blockedEmail); - await expectNoBookingInDatabase(blockedEmail); - }, - timeout - ); - }); });