import { WatchlistAction, WatchlistSource, WatchlistType } from "@calcom/prisma/enums"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { AdminWatchlistOperationsService } from "./AdminWatchlistOperationsService"; vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ sendEmailVerification: vi.fn().mockResolvedValue(undefined), })); vi.mock("@calcom/prisma", () => ({ default: {}, prisma: {}, })); import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; function createMockWatchlistRepo() { return { findEntryWithAuditAndReports: vi.fn(), deleteEntry: vi.fn().mockResolvedValue(undefined), findEntriesByIds: vi.fn().mockResolvedValue([]), bulkDeleteEntries: vi.fn().mockResolvedValue({ deleted: 0 }), createEntry: vi.fn(), createEntryIfNotExists: vi.fn(), checkExists: vi.fn(), findAllEntriesWithLatestAudit: vi.fn(), createEntryFromReport: vi.fn(), findOrgAndGlobalEntries: vi.fn(), }; } function createMockBookingReportRepo() { return { findPendingSystemReportsByEmail: vi.fn(), findPendingSystemReportsByDomain: vi.fn(), bulkLinkGlobalWatchlistWithSystemStatus: vi.fn(), dismissSystemReportsByEmail: vi.fn(), }; } function createMockUserRepo() { return { unlockByEmail: vi.fn().mockResolvedValue(null), }; } function makeEntry(overrides: Record = {}) { return { id: "entry-1", type: WatchlistType.EMAIL, value: "test@example.com", action: WatchlistAction.BLOCK, description: null, organizationId: null, isGlobal: true, source: WatchlistSource.SIGNUP, lastUpdatedAt: new Date(), ...overrides, }; } describe("AdminWatchlistOperationsService", () => { let service: AdminWatchlistOperationsService; let mockWatchlistRepo: ReturnType; let mockBookingReportRepo: ReturnType; let mockUserRepo: ReturnType; beforeEach(() => { vi.clearAllMocks(); mockWatchlistRepo = createMockWatchlistRepo(); mockBookingReportRepo = createMockBookingReportRepo(); mockUserRepo = createMockUserRepo(); service = new AdminWatchlistOperationsService({ watchlistRepo: mockWatchlistRepo as never, bookingReportRepo: mockBookingReportRepo as never, userRepo: mockUserRepo as never, }); }); describe("deleteWatchlistEntry - auto-unlock", () => { test("should unlock user and send verification email when deleting SIGNUP email entry", async () => { const entry = makeEntry({ source: WatchlistSource.SIGNUP, type: WatchlistType.EMAIL }); mockWatchlistRepo.findEntryWithAuditAndReports.mockResolvedValue({ entry, auditHistory: [] }); mockUserRepo.unlockByEmail.mockResolvedValue({ email: "test@example.com", username: "testuser" }); await service.deleteWatchlistEntry({ entryId: "entry-1", userId: 1 }); expect(mockWatchlistRepo.deleteEntry).toHaveBeenCalledWith("entry-1", 1); expect(mockUserRepo.unlockByEmail).toHaveBeenCalledWith({ email: "test@example.com" }); expect(sendEmailVerification).toHaveBeenCalledWith({ email: "test@example.com", username: "testuser", }); }); test("should not unlock user when deleting MANUAL source entry", async () => { const entry = makeEntry({ source: WatchlistSource.MANUAL, type: WatchlistType.EMAIL }); mockWatchlistRepo.findEntryWithAuditAndReports.mockResolvedValue({ entry, auditHistory: [] }); await service.deleteWatchlistEntry({ entryId: "entry-1", userId: 1 }); expect(mockWatchlistRepo.deleteEntry).toHaveBeenCalledWith("entry-1", 1); expect(mockUserRepo.unlockByEmail).not.toHaveBeenCalled(); expect(sendEmailVerification).not.toHaveBeenCalled(); }); test("should not unlock user when deleting SIGNUP domain entry", async () => { const entry = makeEntry({ source: WatchlistSource.SIGNUP, type: WatchlistType.DOMAIN, value: "example.com", }); mockWatchlistRepo.findEntryWithAuditAndReports.mockResolvedValue({ entry, auditHistory: [] }); await service.deleteWatchlistEntry({ entryId: "entry-1", userId: 1 }); expect(mockUserRepo.unlockByEmail).not.toHaveBeenCalled(); }); test("should not send verification email when user is not locked", async () => { const entry = makeEntry({ source: WatchlistSource.SIGNUP, type: WatchlistType.EMAIL }); mockWatchlistRepo.findEntryWithAuditAndReports.mockResolvedValue({ entry, auditHistory: [] }); mockUserRepo.unlockByEmail.mockResolvedValue(null); await service.deleteWatchlistEntry({ entryId: "entry-1", userId: 1 }); expect(mockUserRepo.unlockByEmail).toHaveBeenCalledWith({ email: "test@example.com" }); expect(sendEmailVerification).not.toHaveBeenCalled(); }); test("should not throw if unlock fails", async () => { const entry = makeEntry({ source: WatchlistSource.SIGNUP, type: WatchlistType.EMAIL }); mockWatchlistRepo.findEntryWithAuditAndReports.mockResolvedValue({ entry, auditHistory: [] }); mockUserRepo.unlockByEmail.mockRejectedValue(new Error("db error")); const result = await service.deleteWatchlistEntry({ entryId: "entry-1", userId: 1 }); expect(result.success).toBe(true); }); }); describe("bulkDeleteWatchlistEntries - auto-unlock", () => { test("should unlock users for SIGNUP email entries in bulk delete", async () => { const entries = [ { id: "e1", isGlobal: true, organizationId: null, source: WatchlistSource.SIGNUP, type: WatchlistType.EMAIL, value: "user1@example.com", }, { id: "e2", isGlobal: true, organizationId: null, source: WatchlistSource.MANUAL, type: WatchlistType.EMAIL, value: "user2@example.com", }, ]; mockWatchlistRepo.findEntriesByIds.mockResolvedValue(entries); mockWatchlistRepo.bulkDeleteEntries.mockResolvedValue({ deleted: 2 }); mockUserRepo.unlockByEmail.mockResolvedValue({ email: "user1@example.com", username: "user1" }); await service.bulkDeleteWatchlistEntries({ entryIds: ["e1", "e2"], userId: 1 }); expect(mockUserRepo.unlockByEmail).toHaveBeenCalledTimes(1); expect(mockUserRepo.unlockByEmail).toHaveBeenCalledWith({ email: "user1@example.com" }); expect(sendEmailVerification).toHaveBeenCalledWith({ email: "user1@example.com", username: "user1", }); }); test("should not unlock users for non-SIGNUP entries in bulk delete", async () => { const entries = [ { id: "e1", isGlobal: true, organizationId: null, source: WatchlistSource.MANUAL, type: WatchlistType.EMAIL, value: "user@example.com", }, ]; mockWatchlistRepo.findEntriesByIds.mockResolvedValue(entries); mockWatchlistRepo.bulkDeleteEntries.mockResolvedValue({ deleted: 1 }); await service.bulkDeleteWatchlistEntries({ entryIds: ["e1"], userId: 1 }); expect(mockUserRepo.unlockByEmail).not.toHaveBeenCalled(); }); test("should handle multiple SIGNUP entries in bulk delete", async () => { const entries = [ { id: "e1", isGlobal: true, organizationId: null, source: WatchlistSource.SIGNUP, type: WatchlistType.EMAIL, value: "user1@example.com", }, { id: "e2", isGlobal: true, organizationId: null, source: WatchlistSource.SIGNUP, type: WatchlistType.EMAIL, value: "user2@example.com", }, ]; mockWatchlistRepo.findEntriesByIds.mockResolvedValue(entries); mockWatchlistRepo.bulkDeleteEntries.mockResolvedValue({ deleted: 2 }); mockUserRepo.unlockByEmail .mockResolvedValueOnce({ email: "user1@example.com", username: "user1" }) .mockResolvedValueOnce({ email: "user2@example.com", username: "user2" }); await service.bulkDeleteWatchlistEntries({ entryIds: ["e1", "e2"], userId: 1 }); expect(mockUserRepo.unlockByEmail).toHaveBeenCalledTimes(2); expect(sendEmailVerification).toHaveBeenCalledTimes(2); }); }); });