fix: Support domains without @ prefix in watchlist (#24476)
* fix: Support domains without @ prefix in watchlist * fix: free email domain check
This commit is contained in:
+4
-4
@@ -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,
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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 () => {
|
||||
@@ -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 () => {
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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 () => {
|
||||
@@ -174,7 +174,7 @@ 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 () => {
|
||||
@@ -182,7 +182,7 @@ describe("GlobalBlockingService", () => {
|
||||
|
||||
await service.isFreeEmailDomain("GMAIL.COM");
|
||||
|
||||
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@gmail.com");
|
||||
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("gmail.com");
|
||||
});
|
||||
|
||||
test("should handle domain with @ prefix", async () => {
|
||||
@@ -190,7 +190,7 @@ describe("GlobalBlockingService", () => {
|
||||
|
||||
await service.isFreeEmailDomain("@hotmail.com");
|
||||
|
||||
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@hotmail.com");
|
||||
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("hotmail.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
@@ -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 () => {
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -326,7 +326,7 @@ describe("WatchlistService", () => {
|
||||
{
|
||||
id: "2",
|
||||
type: WatchlistType.DOMAIN,
|
||||
value: "@competitor.com",
|
||||
value: "competitor.com",
|
||||
description: null,
|
||||
action: WatchlistAction.BLOCK,
|
||||
isGlobal: false,
|
||||
|
||||
@@ -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", () => {
|
||||
@@ -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", () => {
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user