Files
calendar/packages/features/watchlist/lib/service/WatchlistService.test.ts
T
Hariom BalharaandGitHub 37cec89bf6 fix: Support domains without @ prefix in watchlist (#24476)
* fix: Support domains without @ prefix in watchlist

* fix: free email domain check
2025-10-15 13:42:02 +00:00

374 lines
11 KiB
TypeScript

import { describe, test, expect, vi, beforeEach } from "vitest";
import { WatchlistType, WatchlistAction, WatchlistSource } from "@calcom/prisma/enums";
import type {
IGlobalWatchlistRepository,
IOrganizationWatchlistRepository,
} from "../interface/IWatchlistRepositories";
import { WatchlistService } from "./WatchlistService";
const mockGlobalRepo: IGlobalWatchlistRepository = {
findBlockedEmail: vi.fn(),
findBlockedDomain: vi.fn(),
findFreeEmailDomain: vi.fn(),
createEntry: vi.fn(),
updateEntry: vi.fn(),
deleteEntry: vi.fn(),
findById: vi.fn(),
listBlockedEntries: vi.fn(),
};
const mockOrgRepo: IOrganizationWatchlistRepository = {
findBlockedEmail: vi.fn(),
findBlockedDomain: vi.fn(),
createEntry: vi.fn(),
updateEntry: vi.fn(),
deleteEntry: vi.fn(),
findById: vi.fn(),
listBlockedEntries: vi.fn(),
listAllOrganizationEntries: vi.fn(),
};
const mockLogger = {
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
};
describe("WatchlistService", () => {
let service: WatchlistService;
beforeEach(() => {
vi.clearAllMocks();
service = new WatchlistService({
globalRepo: mockGlobalRepo,
orgRepo: mockOrgRepo,
logger: mockLogger as never,
});
});
describe("createEntry", () => {
test("should create global entry with normalized email", async () => {
const mockEntry = {
id: "123",
type: WatchlistType.EMAIL,
value: "test@example.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: true,
organizationId: null,
source: WatchlistSource.MANUAL,
lastUpdatedAt: new Date(),
};
vi.mocked(mockGlobalRepo.createEntry).mockResolvedValue(mockEntry);
const result = await service.createEntry({
type: WatchlistType.EMAIL,
value: "TEST@EXAMPLE.COM", // Not normalized
action: WatchlistAction.BLOCK,
isGlobal: true,
});
expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({
type: WatchlistType.EMAIL,
value: "test@example.com", // Normalized by service
description: undefined,
action: WatchlistAction.BLOCK,
source: undefined,
});
expect(result).toEqual(mockEntry);
});
test("should create global entry with normalized domain", async () => {
const mockEntry = {
id: "456",
type: WatchlistType.DOMAIN,
value: "spam.com",
description: "Spam domain",
action: WatchlistAction.BLOCK,
isGlobal: true,
organizationId: null,
source: WatchlistSource.MANUAL,
lastUpdatedAt: new Date(),
};
vi.mocked(mockGlobalRepo.createEntry).mockResolvedValue(mockEntry);
const result = await service.createEntry({
type: WatchlistType.DOMAIN,
value: "SPAM.COM", // Not normalized, no @ prefix
description: "Spam domain",
action: WatchlistAction.BLOCK,
isGlobal: true,
});
expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({
type: WatchlistType.DOMAIN,
value: "spam.com", // Normalized without @ prefix
description: "Spam domain",
action: WatchlistAction.BLOCK,
source: undefined,
});
expect(result).toEqual(mockEntry);
});
test("should create organization entry", async () => {
const mockEntry = {
id: "789",
type: WatchlistType.EMAIL,
value: "competitor@example.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: false,
organizationId: 123,
source: WatchlistSource.MANUAL,
lastUpdatedAt: new Date(),
};
vi.mocked(mockOrgRepo.createEntry).mockResolvedValue(mockEntry);
const result = await service.createEntry({
type: WatchlistType.EMAIL,
value: "COMPETITOR@EXAMPLE.COM",
action: WatchlistAction.BLOCK,
isGlobal: false,
organizationId: 123,
});
expect(mockOrgRepo.createEntry).toHaveBeenCalledWith(123, {
type: WatchlistType.EMAIL,
value: "competitor@example.com",
description: undefined,
action: WatchlistAction.BLOCK,
source: undefined,
});
expect(result).toEqual(mockEntry);
});
test("should throw when organizationId missing for non-global entry", async () => {
await expect(
service.createEntry({
type: WatchlistType.EMAIL,
value: "test@example.com",
action: WatchlistAction.BLOCK,
isGlobal: false,
// organizationId missing!
})
).rejects.toThrow("organizationId is required for organization-scoped entries");
expect(mockLogger.error).toHaveBeenCalledWith(
"organizationId required for non-global entry",
expect.objectContaining({
type: WatchlistType.EMAIL,
value: "test@example.com",
})
);
});
test("should create entry with FREE_DOMAIN_POLICY source", async () => {
const mockEntry = {
id: "free-1",
type: WatchlistType.DOMAIN,
value: "gmail.com",
description: null,
action: WatchlistAction.REPORT,
isGlobal: true,
organizationId: null,
source: WatchlistSource.FREE_DOMAIN_POLICY,
lastUpdatedAt: new Date(),
};
vi.mocked(mockGlobalRepo.createEntry).mockResolvedValue(mockEntry);
const result = await service.createEntry({
type: WatchlistType.DOMAIN,
value: "gmail.com",
action: WatchlistAction.REPORT,
isGlobal: true,
source: WatchlistSource.FREE_DOMAIN_POLICY,
});
expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({
type: WatchlistType.DOMAIN,
value: "gmail.com",
description: undefined,
action: WatchlistAction.REPORT,
source: WatchlistSource.FREE_DOMAIN_POLICY,
});
expect(result).toEqual(mockEntry);
});
});
describe("updateEntry", () => {
test("should update entry with normalized email", async () => {
const mockEntry = {
id: "123",
type: WatchlistType.EMAIL,
value: "updated@example.com",
description: "Updated description",
action: WatchlistAction.ALERT,
isGlobal: true,
organizationId: null,
source: WatchlistSource.MANUAL,
lastUpdatedAt: new Date(),
};
vi.mocked(mockGlobalRepo.updateEntry).mockResolvedValue(mockEntry);
const result = await service.updateEntry("123", {
type: WatchlistType.EMAIL,
value: "UPDATED@EXAMPLE.COM", // Not normalized
description: "Updated description",
action: WatchlistAction.ALERT,
});
expect(mockGlobalRepo.updateEntry).toHaveBeenCalledWith("123", {
type: WatchlistType.EMAIL,
value: "updated@example.com", // Normalized
description: "Updated description",
action: WatchlistAction.ALERT,
source: undefined,
});
expect(result).toEqual(mockEntry);
});
test("should update entry without value change", async () => {
const mockEntry = {
id: "123",
type: WatchlistType.EMAIL,
value: "test@example.com",
description: "New description",
action: WatchlistAction.BLOCK,
isGlobal: true,
organizationId: null,
source: WatchlistSource.MANUAL,
lastUpdatedAt: new Date(),
};
vi.mocked(mockGlobalRepo.updateEntry).mockResolvedValue(mockEntry);
const result = await service.updateEntry("123", {
type: WatchlistType.EMAIL, // Required for normalization
description: "New description",
});
expect(mockGlobalRepo.updateEntry).toHaveBeenCalledWith("123", {
type: WatchlistType.EMAIL,
description: "New description",
action: undefined,
source: undefined,
});
expect(result).toEqual(mockEntry);
});
});
describe("deleteEntry", () => {
test("should delete entry by id", async () => {
vi.mocked(mockGlobalRepo.deleteEntry).mockResolvedValue();
await service.deleteEntry("123");
expect(mockGlobalRepo.deleteEntry).toHaveBeenCalledWith("123");
});
});
describe("getEntry", () => {
test("should get entry by id", async () => {
const mockEntry = {
id: "123",
type: WatchlistType.EMAIL,
value: "test@example.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: true,
organizationId: null,
source: WatchlistSource.MANUAL,
lastUpdatedAt: new Date(),
};
vi.mocked(mockGlobalRepo.findById).mockResolvedValue(mockEntry);
const result = await service.getEntry("123");
expect(mockGlobalRepo.findById).toHaveBeenCalledWith("123");
expect(result).toEqual(mockEntry);
});
test("should return null when entry not found", async () => {
vi.mocked(mockGlobalRepo.findById).mockResolvedValue(null);
const result = await service.getEntry("nonexistent");
expect(result).toBeNull();
});
});
describe("listAllSystemEntries", () => {
test("should return combined global and org entries", async () => {
const globalEntries = [
{
id: "1",
type: WatchlistType.EMAIL,
value: "global@example.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: true,
organizationId: null,
source: WatchlistSource.MANUAL,
lastUpdatedAt: new Date(),
},
];
const orgEntries = [
{
id: "2",
type: WatchlistType.DOMAIN,
value: "competitor.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: false,
organizationId: 123,
source: WatchlistSource.MANUAL,
lastUpdatedAt: new Date(),
},
];
vi.mocked(mockGlobalRepo.listBlockedEntries).mockResolvedValue(globalEntries);
vi.mocked(mockOrgRepo.listAllOrganizationEntries).mockResolvedValue(orgEntries);
const result = await service.listAllSystemEntries();
expect(mockGlobalRepo.listBlockedEntries).toHaveBeenCalled();
expect(mockOrgRepo.listAllOrganizationEntries).toHaveBeenCalled();
expect(result).toHaveLength(2);
expect(result).toEqual([...globalEntries, ...orgEntries]);
});
test("should return empty array when no entries exist", async () => {
vi.mocked(mockGlobalRepo.listBlockedEntries).mockResolvedValue([]);
vi.mocked(mockOrgRepo.listAllOrganizationEntries).mockResolvedValue([]);
const result = await service.listAllSystemEntries();
expect(result).toEqual([]);
});
test("should fetch global and org entries in parallel", async () => {
const globalPromise = Promise.resolve([]);
const orgPromise = Promise.resolve([]);
vi.mocked(mockGlobalRepo.listBlockedEntries).mockReturnValue(globalPromise);
vi.mocked(mockOrgRepo.listAllOrganizationEntries).mockReturnValue(orgPromise);
await service.listAllSystemEntries();
// Verify both repository methods are invoked (implementation uses Promise.all)
expect(mockGlobalRepo.listBlockedEntries).toHaveBeenCalled();
expect(mockOrgRepo.listAllOrganizationEntries).toHaveBeenCalled();
});
});
});