import { describe, test, expect, vi, beforeEach } from "vitest"; import { getBlockedUsersMap, isUserBlocked, checkWatchlistBlocking, type BlockableUser, type BlockingInfo, } from "./check-user-blocking"; vi.mock("@calcom/features/di/watchlist/containers/watchlist", () => ({ getWatchlistFeature: vi.fn(), })); const mockGlobalBlocking: { areBlocked: ReturnType } = { areBlocked: vi.fn(), }; const mockOrgBlocking: { areBlocked: ReturnType } = { areBlocked: vi.fn(), }; const mockWatchlistFeature: { globalBlocking: typeof mockGlobalBlocking; orgBlocking: typeof mockOrgBlocking; watchlist: Record; audit: Record; } = { globalBlocking: mockGlobalBlocking, orgBlocking: mockOrgBlocking, watchlist: {}, audit: {}, }; describe("check-user-blocking", () => { beforeEach(async () => { vi.clearAllMocks(); const { getWatchlistFeature } = await import("@calcom/features/di/watchlist/containers/watchlist"); vi.mocked(getWatchlistFeature).mockResolvedValue(mockWatchlistFeature as never); }); describe("getBlockedUsersMap", () => { describe("Empty input handling", () => { test("should return empty result for empty users array", async () => { const result = await getBlockedUsersMap([]); expect(result).toEqual({ blockingMap: new Map(), blockedCount: 0, lockedCount: 0, watchlistBlockedCount: 0, }); expect(mockGlobalBlocking.areBlocked).not.toHaveBeenCalled(); }); test("should skip users with empty emails", async () => { mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["valid@example.com", { isBlocked: false }]]) ); const users: BlockableUser[] = [ { email: "", locked: false }, { email: " ", locked: false }, { email: "valid@example.com", locked: false }, ]; const result = await getBlockedUsersMap(users); expect(result.blockedCount).toBe(0); expect(mockGlobalBlocking.areBlocked).toHaveBeenCalledWith(["valid@example.com"]); }); test("should return empty result if all users have empty emails", async () => { const users: BlockableUser[] = [ { email: "", locked: false }, { email: " ", locked: false }, ]; const result = await getBlockedUsersMap(users); expect(result).toEqual({ blockingMap: new Map(), blockedCount: 0, lockedCount: 0, watchlistBlockedCount: 0, }); expect(mockGlobalBlocking.areBlocked).not.toHaveBeenCalled(); }); }); describe("Locked user blocking", () => { test("should block locked users without calling watchlist", async () => { const users: BlockableUser[] = [ { email: "locked@example.com", locked: true }, { email: "unlocked@example.com", locked: false }, ]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["unlocked@example.com", { isBlocked: false }]]) ); const result = await getBlockedUsersMap(users); expect(result.blockedCount).toBe(1); expect(result.lockedCount).toBe(1); expect(result.watchlistBlockedCount).toBe(0); const lockedInfo = result.blockingMap.get("locked@example.com"); expect(lockedInfo).toEqual({ isBlocked: true, reason: "locked" }); // Should only check unlocked user in watchlist expect(mockGlobalBlocking.areBlocked).toHaveBeenCalledWith(["unlocked@example.com"]); }); test("should not call watchlist if all users are locked", async () => { const users: BlockableUser[] = [ { email: "locked1@example.com", locked: true }, { email: "locked2@example.com", locked: true }, ]; const result = await getBlockedUsersMap(users); expect(result.blockedCount).toBe(2); expect(result.lockedCount).toBe(2); expect(result.watchlistBlockedCount).toBe(0); expect(mockGlobalBlocking.areBlocked).not.toHaveBeenCalled(); }); test("should correctly count multiple locked users", async () => { const users: BlockableUser[] = [ { email: "locked1@example.com", locked: true }, { email: "locked2@example.com", locked: true }, { email: "locked3@example.com", locked: true }, ]; const result = await getBlockedUsersMap(users); expect(result.lockedCount).toBe(3); expect(result.blockedCount).toBe(3); }); }); describe("Watchlist blocking", () => { test("should block users on global watchlist", async () => { const users: BlockableUser[] = [ { email: "blocked@spam.com", locked: false }, { email: "clean@example.com", locked: false }, ]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([ ["blocked@spam.com", { isBlocked: true }], ["clean@example.com", { isBlocked: false }], ]) ); const result = await getBlockedUsersMap(users); expect(result.blockedCount).toBe(1); expect(result.lockedCount).toBe(0); expect(result.watchlistBlockedCount).toBe(1); const blockedInfo = result.blockingMap.get("blocked@spam.com"); expect(blockedInfo).toEqual({ isBlocked: true, reason: "watchlist" }); const cleanInfo = result.blockingMap.get("clean@example.com"); expect(cleanInfo).toEqual({ isBlocked: false }); }); test("should check org-level blocking when organizationId is provided", async () => { const users: BlockableUser[] = [{ email: "user@example.com", locked: false }]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["user@example.com", { isBlocked: false }]]) ); mockOrgBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await getBlockedUsersMap(users, 123); expect(result.blockedCount).toBe(1); expect(result.watchlistBlockedCount).toBe(1); expect(mockOrgBlocking.areBlocked).toHaveBeenCalledWith(["user@example.com"], 123); }); test("should not check org-level blocking when organizationId is not provided", async () => { const users: BlockableUser[] = [{ email: "user@example.com", locked: false }]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["user@example.com", { isBlocked: false }]]) ); await getBlockedUsersMap(users); expect(mockOrgBlocking.areBlocked).not.toHaveBeenCalled(); }); test("should not check org-level blocking when organizationId is null", async () => { const users: BlockableUser[] = [{ email: "user@example.com", locked: false }]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["user@example.com", { isBlocked: false }]]) ); await getBlockedUsersMap(users, null); expect(mockOrgBlocking.areBlocked).not.toHaveBeenCalled(); }); }); describe("Combined blocking (locked + watchlist)", () => { test("should correctly count both locked and watchlist-blocked users", async () => { const users: BlockableUser[] = [ { email: "locked@example.com", locked: true }, { email: "watchlist-blocked@spam.com", locked: false }, { email: "clean@example.com", locked: false }, ]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([ ["watchlist-blocked@spam.com", { isBlocked: true }], ["clean@example.com", { isBlocked: false }], ]) ); const result = await getBlockedUsersMap(users); expect(result.blockedCount).toBe(2); expect(result.lockedCount).toBe(1); expect(result.watchlistBlockedCount).toBe(1); }); test("should prioritize locked status over watchlist (locked users not checked in watchlist)", async () => { const users: BlockableUser[] = [ { email: "locked-and-would-be-blocked@spam.com", locked: true }, { email: "clean@example.com", locked: false }, ]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["clean@example.com", { isBlocked: false }]]) ); const result = await getBlockedUsersMap(users); // The locked user's email should NOT be sent to watchlist check expect(mockGlobalBlocking.areBlocked).toHaveBeenCalledWith(["clean@example.com"]); expect(mockGlobalBlocking.areBlocked).not.toHaveBeenCalledWith( expect.arrayContaining(["locked-and-would-be-blocked@spam.com"]) ); // But the locked user should still be marked as blocked const lockedInfo = result.blockingMap.get("locked-and-would-be-blocked@spam.com"); expect(lockedInfo).toEqual({ isBlocked: true, reason: "locked" }); }); }); describe("Email normalization", () => { test("should normalize emails to lowercase", async () => { const users: BlockableUser[] = [{ email: "USER@EXAMPLE.COM", locked: false }]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["user@example.com", { isBlocked: false }]]) ); const result = await getBlockedUsersMap(users); expect(result.blockingMap.has("user@example.com")).toBe(true); expect(result.blockingMap.has("USER@EXAMPLE.COM")).toBe(false); }); test("should trim whitespace from emails", async () => { const users: BlockableUser[] = [{ email: " user@example.com ", locked: false }]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["user@example.com", { isBlocked: false }]]) ); const result = await getBlockedUsersMap(users); expect(result.blockingMap.has("user@example.com")).toBe(true); }); }); }); describe("isUserBlocked", () => { test("should return true for blocked user", () => { const blockingMap = new Map([ ["blocked@example.com", { isBlocked: true, reason: "locked" }], ]); expect(isUserBlocked("blocked@example.com", blockingMap)).toBe(true); }); test("should return false for non-blocked user", () => { const blockingMap = new Map([["clean@example.com", { isBlocked: false }]]); expect(isUserBlocked("clean@example.com", blockingMap)).toBe(false); }); test("should return false for user not in map", () => { const blockingMap = new Map(); expect(isUserBlocked("unknown@example.com", blockingMap)).toBe(false); }); test("should return false for empty email", () => { const blockingMap = new Map([ ["blocked@example.com", { isBlocked: true, reason: "locked" }], ]); expect(isUserBlocked("", blockingMap)).toBe(false); expect(isUserBlocked(" ", blockingMap)).toBe(false); }); test("should normalize email before lookup", () => { const blockingMap = new Map([ ["blocked@example.com", { isBlocked: true, reason: "locked" }], ]); expect(isUserBlocked("BLOCKED@EXAMPLE.COM", blockingMap)).toBe(true); expect(isUserBlocked(" blocked@example.com ", blockingMap)).toBe(true); }); }); describe("checkWatchlistBlocking", () => { test("should batch check all emails against global watchlist", async () => { const emails = ["user1@example.com", "user2@example.com"]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([ ["user1@example.com", { isBlocked: false }], ["user2@example.com", { isBlocked: true }], ]) ); const result = await checkWatchlistBlocking(emails); expect(mockGlobalBlocking.areBlocked).toHaveBeenCalledWith(emails); expect(result.get("user1@example.com")).toBe(false); expect(result.get("user2@example.com")).toBe(true); }); test("should combine global and org blocking results", async () => { const emails = ["user@example.com"]; mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); mockOrgBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await checkWatchlistBlocking(emails, 123); expect(result.get("user@example.com")).toBe(true); }); test("should block if either global or org blocks", async () => { const emails = ["global-blocked@example.com", "org-blocked@example.com", "clean@example.com"]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([ ["global-blocked@example.com", { isBlocked: true }], ["org-blocked@example.com", { isBlocked: false }], ["clean@example.com", { isBlocked: false }], ]) ); mockOrgBlocking.areBlocked.mockResolvedValue( new Map([ ["global-blocked@example.com", { isBlocked: false }], ["org-blocked@example.com", { isBlocked: true }], ["clean@example.com", { isBlocked: false }], ]) ); const result = await checkWatchlistBlocking(emails, 123); expect(result.get("global-blocked@example.com")).toBe(true); expect(result.get("org-blocked@example.com")).toBe(true); expect(result.get("clean@example.com")).toBe(false); }); test("should normalize emails in result map", async () => { const emails = ["USER@EXAMPLE.COM"]; mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await checkWatchlistBlocking(emails); expect(result.get("user@example.com")).toBe(true); }); }); });