Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ describe("handleNewBooking - Spam Detection", () => {
"should block booking when domain is in global watchlist and return decoy response",
async () => {
const handleNewBooking = getNewBookingHandler();
const blockedDomain = "@globalspammydomain.com";
const blockedEmail = `user${blockedDomain}`;
const blockedDomain = "globalspammydomain.com";
const blockedEmail = `user@${blockedDomain}`;

const booker = getBooker({
email: blockedEmail,
Expand Down Expand Up @@ -496,8 +496,8 @@ describe("handleNewBooking - Spam Detection", () => {
"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}`;
const blockedDomain = "spammydomain.com";
const blockedEmail = `user@${blockedDomain}`;

// Create organization with a team
const org = await createOrganization({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ export const checkIfFreeEmailDomain = async (params: CheckFreeEmailDomainParams)

try {
const emailDomain = extractDomainFromEmail(email);
const domainWithoutAt = emailDomain.slice(1);

// If there's no email domain return as if it was a free email domain
if (!domainWithoutAt) return true;
if (!emailDomain) return true;

// Gmail and Outlook are one of the most common email domains so we don't need to check the domains list
if (domainWithoutAt === "gmail.com" || domainWithoutAt === "outlook.com") return true;
if (emailDomain === "gmail.com" || emailDomain === "outlook.com") return true;

const watchlist = await getWatchlistFeature();
return await watchlist.globalBlocking.isFreeEmailDomain(domainWithoutAt);
return await watchlist.globalBlocking.isFreeEmailDomain(emailDomain);
} catch (err) {
log?.error(err);
// If normalization fails, treat as free email domain for safety
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ describe("GlobalBlockingService", () => {
expect(result.reason).toBe(WatchlistType.EMAIL);
expect(result.watchlistEntry).toEqual(mockEntry);
expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("blocked@example.com");
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com");
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("example.com");
});

test("should return blocked when domain matches", async () => {
const mockEntry = {
id: "456",
type: WatchlistType.DOMAIN,
value: "@spam.com",
value: "spam.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: true,
Expand All @@ -72,7 +72,7 @@ describe("GlobalBlockingService", () => {
expect(result.reason).toBe(WatchlistType.DOMAIN);
expect(result.watchlistEntry).toEqual(mockEntry);
expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("user@spam.com");
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@spam.com");
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("spam.com");
});

test("should return not blocked when no matches", async () => {
Expand All @@ -93,7 +93,7 @@ describe("GlobalBlockingService", () => {
await service.isBlocked("USER@EXAMPLE.COM");

expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("user@example.com");
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com");
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("example.com");
});

test("should check both email and domain in parallel", async () => {
Expand Down Expand Up @@ -126,7 +126,7 @@ describe("GlobalBlockingService", () => {
const domainEntry = {
id: "456",
type: WatchlistType.DOMAIN,
value: "@example.com",
value: "example.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: true,
Expand All @@ -151,7 +151,7 @@ describe("GlobalBlockingService", () => {
const mockEntry = {
id: "789",
type: WatchlistType.DOMAIN,
value: "@yahoo.com",
value: "yahoo.com",
description: null,
action: WatchlistAction.REPORT,
isGlobal: true,
Expand All @@ -165,7 +165,7 @@ describe("GlobalBlockingService", () => {
const result = await service.isFreeEmailDomain("yahoo.com");

expect(result).toBe(true);
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@yahoo.com");
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("yahoo.com");
});

test("should return false when domain is not in free email list", async () => {
Expand All @@ -174,23 +174,23 @@ describe("GlobalBlockingService", () => {
const result = await service.isFreeEmailDomain("corporatedomain.com");

expect(result).toBe(false);
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@corporatedomain.com");
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("corporatedomain.com");
});

test("should normalize domain before checking", async () => {
vi.mocked(mockGlobalRepo.findFreeEmailDomain).mockResolvedValue(null);

await service.isFreeEmailDomain("GMAIL.COM");

expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@gmail.com");
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("gmail.com");
});

test("should handle domain with @ prefix", async () => {
vi.mocked(mockGlobalRepo.findFreeEmailDomain).mockResolvedValue(null);

await service.isFreeEmailDomain("@hotmail.com");

expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@hotmail.com");
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("hotmail.com");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ describe("OrganizationBlockingService", () => {
email: "blocked@example.com",
organizationId: ORGANIZATION_ID,
});
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORGANIZATION_ID);
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORGANIZATION_ID);
});

test("should return blocked when domain matches for organization", async () => {
const mockEntry = {
id: "456",
type: WatchlistType.DOMAIN,
value: "@competitor.com",
value: "competitor.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: false,
Expand Down Expand Up @@ -99,7 +99,7 @@ describe("OrganizationBlockingService", () => {
email: "user@example.com",
organizationId: ORGANIZATION_ID,
});
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORGANIZATION_ID);
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORGANIZATION_ID);
});

test("should check both email and domain in parallel", async () => {
Expand Down Expand Up @@ -132,7 +132,7 @@ describe("OrganizationBlockingService", () => {
const domainEntry = {
id: "456",
type: WatchlistType.DOMAIN,
value: "@example.com",
value: "example.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: false,
Expand Down Expand Up @@ -165,7 +165,7 @@ describe("OrganizationBlockingService", () => {
email: "test@example.com",
organizationId: ORG_A,
});
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORG_A);
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORG_A);
expect(mockOrgRepo.findBlockedEmail).not.toHaveBeenCalledWith({
email: "test@example.com",
organizationId: ORG_B,
Expand Down
10 changes: 5 additions & 5 deletions packages/features/watchlist/lib/service/WatchlistService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe("WatchlistService", () => {
const mockEntry = {
id: "456",
type: WatchlistType.DOMAIN,
value: "@spam.com",
value: "spam.com",
description: "Spam domain",
action: WatchlistAction.BLOCK,
isGlobal: true,
Expand All @@ -107,7 +107,7 @@ describe("WatchlistService", () => {

expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({
type: WatchlistType.DOMAIN,
value: "@spam.com", // Normalized with @ prefix
value: "spam.com", // Normalized without @ prefix
description: "Spam domain",
action: WatchlistAction.BLOCK,
source: undefined,
Expand Down Expand Up @@ -172,7 +172,7 @@ describe("WatchlistService", () => {
const mockEntry = {
id: "free-1",
type: WatchlistType.DOMAIN,
value: "@gmail.com",
value: "gmail.com",
description: null,
action: WatchlistAction.REPORT,
isGlobal: true,
Expand All @@ -193,7 +193,7 @@ describe("WatchlistService", () => {

expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({
type: WatchlistType.DOMAIN,
value: "@gmail.com",
value: "gmail.com",
description: undefined,
action: WatchlistAction.REPORT,
source: WatchlistSource.FREE_DOMAIN_POLICY,
Expand Down Expand Up @@ -326,7 +326,7 @@ describe("WatchlistService", () => {
{
id: "2",
type: WatchlistType.DOMAIN,
value: "@competitor.com",
value: "competitor.com",
description: null,
action: WatchlistAction.BLOCK,
isGlobal: false,
Expand Down
28 changes: 14 additions & 14 deletions packages/features/watchlist/lib/utils/normalization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ describe("normalization", () => {

describe("normalizeDomain", () => {
test("should normalize basic domain", () => {
expect(normalizeDomain("Example.COM")).toBe("@example.com");
expect(normalizeDomain("@Domain.ORG")).toBe("@domain.org");
expect(normalizeDomain(" sub.domain.net ")).toBe("@sub.domain.net");
expect(normalizeDomain("Example.COM")).toBe("example.com");
expect(normalizeDomain("@Domain.ORG")).toBe("domain.org");
expect(normalizeDomain(" sub.domain.net ")).toBe("sub.domain.net");
});

test("should preserve full domain including subdomains", () => {
expect(normalizeDomain("mail.google.com")).toBe("@mail.google.com");
expect(normalizeDomain("sub.domain.example.org")).toBe("@sub.domain.example.org");
expect(normalizeDomain("mail.google.com")).toBe("mail.google.com");
expect(normalizeDomain("sub.domain.example.org")).toBe("sub.domain.example.org");
});

test("should handle multi-level TLDs correctly", () => {
expect(normalizeDomain("example.co.uk")).toBe("@example.co.uk");
expect(normalizeDomain("mail.example.co.uk")).toBe("@mail.example.co.uk");
expect(normalizeDomain("company.com.au")).toBe("@company.com.au");
expect(normalizeDomain("example.co.uk")).toBe("example.co.uk");
expect(normalizeDomain("mail.example.co.uk")).toBe("mail.example.co.uk");
expect(normalizeDomain("company.com.au")).toBe("company.com.au");
});

test("should throw on invalid domains", () => {
Expand All @@ -43,14 +43,14 @@ describe("normalization", () => {

describe("extractDomainFromEmail", () => {
test("should extract and normalize domain from email", () => {
expect(extractDomainFromEmail("user@Example.COM")).toBe("@example.com");
expect(extractDomainFromEmail("test@sub.domain.org")).toBe("@sub.domain.org");
expect(extractDomainFromEmail("user@mail.google.com")).toBe("@mail.google.com");
expect(extractDomainFromEmail("user@Example.COM")).toBe("example.com");
expect(extractDomainFromEmail("test@sub.domain.org")).toBe("sub.domain.org");
expect(extractDomainFromEmail("user@mail.google.com")).toBe("mail.google.com");
});

test("should handle multi-level TLDs", () => {
expect(extractDomainFromEmail("user@example.co.uk")).toBe("@example.co.uk");
expect(extractDomainFromEmail("admin@mail.company.com.au")).toBe("@mail.company.com.au");
expect(extractDomainFromEmail("user@example.co.uk")).toBe("example.co.uk");
expect(extractDomainFromEmail("admin@mail.company.com.au")).toBe("mail.company.com.au");
});

test("should throw on invalid emails", () => {
Expand Down Expand Up @@ -78,7 +78,7 @@ describe("normalization", () => {
describe("Edge cases and consistency", () => {
test("should handle international domains", () => {
// Note: In a real implementation, you might want to handle punycode
expect(normalizeDomain("münchen.de")).toBe("@münchen.de");
expect(normalizeDomain("münchen.de")).toBe("münchen.de");
});

test("should be consistent across multiple calls", () => {
Expand Down
10 changes: 5 additions & 5 deletions packages/features/watchlist/lib/utils/normalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ export function normalizeEmail(email: string): string {
* Rules applied:
* 1. Convert to lowercase
* 2. Trim whitespace
* 3. Ensure proper @ prefix for domain entries
* 3. Remove @ prefix if present
*
* Note: Domains are stored AS-IS with @ prefix (e.g., @mail.google.com, @example.co.uk)
* Note: Domains are stored without @ prefix (e.g., mail.google.com, example.co.uk)
* No subdomain stripping is performed to avoid multi-level TLD issues.
* If you want to block subdomains separately, create separate entries.
*
* @param domain - Raw domain (with or without @ prefix)
* @returns Normalized domain with @ prefix
* @returns Normalized domain without @ prefix
*/
export function normalizeDomain(domain: string): string {
let normalized = domain.trim().toLowerCase();
Expand All @@ -54,14 +54,14 @@ export function normalizeDomain(domain: string): string {
throw new Error(`Invalid domain format: ${domain}`);
}

return `@${normalized}`;
return normalized;
}

/**
* Extracts and normalizes domain from an email address
*
* @param email - Email address
* @returns Normalized domain with @ prefix
* @returns Normalized domain without @ prefix
*/
export function extractDomainFromEmail(email: string): string {
const normalizedEmail = normalizeEmail(email);
Expand Down
Loading