/** * Tests: Layer 2.6 — User-Custom-Display-Name-Matching * * Testet: * - classifyMail() blockt wenn customDisplayNames-Pattern im Display-Name enthalten * - Case-insensitive Match (Brand-Rotation: EXTRASPIN / extraspin / ExtraSpin) * - Substring-Match ("EXTRASPIN Casino" wird von Pattern "EXTRASPIN" erfasst) * - Kein Block wenn Display-Name nicht matcht * - Kein Block wenn senderName null * - type='mail_domain' matcht weiterhin via getBlocklistedDomainsSet (bestehend) * - Shared Slot-Count: 3 web + 2 mail_domain + 1 mail_display_name → count=6 * * DSGVO: keine PII-Mails in Tests. Synthetic Brand-Namen (EXTRASPIN, CASINOX). */ import { describe, it, expect, vi } from "vitest"; vi.mock("../../server/utils/gambling-keywords.mjs", () => ({ GAMBLING_KEYWORDS: [ "casino", "bet365", "bwin", "tipico", "jackpot", "freispiel", "slots", "roulette", "wette", "stake", "spinz", "casinoly", ], GAMBLING_WHITELIST: [ "wettervorhersage", "wetter", "wetterbericht", "wettkampf", "wettbewerb", ], })); import { classifyMail } from "../../server/utils/mail-classifier"; // ─── Layer 2.6: Display-Name-Match ────────────────────────────────────────── describe("classifyMail() — Layer 2.6 Custom Display-Name-Match", () => { const emptyDomainSet = new Set(); it("EXTRASPIN matcht exakt als Substring → BLOCK (custom-display-name)", async () => { const result = await classifyMail({ mail: { senderEmail: "noreply@em123.delivery.net", senderName: "EXTRASPIN", subject: "Dein Bonus wartet", }, blockedDomainSet: emptyDomainSet, customDisplayNames: ["EXTRASPIN"], }); expect(result.action).toBe("blocked"); expect(result.triggerSource).toBe("custom-display-name"); expect(result.score).toBe(100); }); it("EXTRASPIN matcht 'EXTRASPIN Casino' als Substring → BLOCK", async () => { const result = await classifyMail({ mail: { senderEmail: "noreply@em456.relay.net", senderName: "EXTRASPIN Casino", subject: "Exklusives Angebot für dich", }, blockedDomainSet: emptyDomainSet, customDisplayNames: ["EXTRASPIN"], }); expect(result.action).toBe("blocked"); expect(result.triggerSource).toBe("custom-display-name"); }); it("EXTRASPIN matcht 'ExtraSpin Bonus' case-insensitiv → BLOCK", async () => { const result = await classifyMail({ mail: { senderEmail: "noreply@em789.relay.net", senderName: "ExtraSpin Bonus", subject: "Willkommensbonus", }, blockedDomainSet: emptyDomainSet, customDisplayNames: ["EXTRASPIN"], }); expect(result.action).toBe("blocked"); expect(result.triggerSource).toBe("custom-display-name"); }); it("extraspin (lowercase pattern) matcht 'EXTRASPIN Casino' case-insensitiv → BLOCK", async () => { const result = await classifyMail({ mail: { senderEmail: "info@em.relay.net", senderName: "EXTRASPIN Casino", subject: "Willkommen", }, blockedDomainSet: emptyDomainSet, customDisplayNames: ["extraspin"], }); expect(result.action).toBe("blocked"); expect(result.triggerSource).toBe("custom-display-name"); }); it("unrelated Display-Name 'Amazon' matcht nicht → PASS (kein Block)", async () => { const result = await classifyMail({ mail: { senderEmail: "no-reply@amazon.de", senderName: "Amazon", subject: "Deine Bestellung wurde versandt", }, blockedDomainSet: emptyDomainSet, customDisplayNames: ["EXTRASPIN"], }); expect(result.action).toBe("passed"); expect(result.triggerSource).not.toBe("custom-display-name"); }); it("senderName ist null → kein Layer-2.6-Block (kein Crash)", async () => { const result = await classifyMail({ mail: { senderEmail: "info@some-relay.net", senderName: null, subject: "Test", }, blockedDomainSet: emptyDomainSet, customDisplayNames: ["EXTRASPIN"], }); // Kein Block durch Layer 2.6 — senderName=null, kein Match möglich expect(result.triggerSource).not.toBe("custom-display-name"); }); it("leere customDisplayNames → kein Layer-2.6-Block", async () => { const result = await classifyMail({ mail: { senderEmail: "info@some-relay.net", senderName: "EXTRASPIN Casino", subject: "Test", }, blockedDomainSet: emptyDomainSet, customDisplayNames: [], }); expect(result.triggerSource).not.toBe("custom-display-name"); }); it("customDisplayNames fehlt (undefined) → kein Crash, kein Layer-2.6-Block", async () => { const result = await classifyMail({ mail: { senderEmail: "info@some-relay.net", senderName: "EXTRASPIN Casino", subject: "Test", }, blockedDomainSet: emptyDomainSet, // customDisplayNames nicht übergeben → optional, default undefined }); expect(result.triggerSource).not.toBe("custom-display-name"); }); it("mehrere Patterns — zweites Pattern 'CASINOX' matcht → BLOCK", async () => { const result = await classifyMail({ mail: { senderEmail: "noreply@em.relay.net", senderName: "CASINOX VIP", subject: "VIP-Angebot", }, blockedDomainSet: emptyDomainSet, customDisplayNames: ["EXTRASPIN", "CASINOX"], }); expect(result.action).toBe("blocked"); expect(result.triggerSource).toBe("custom-display-name"); }); }); // ─── Bestehende Domain-Types bleiben unverändert ────────────────────────────── describe("classifyMail() — type='mail_domain' via blockedDomainSet (bestehend)", () => { it("mail_domain-Eintrag in blockedDomainSet → Layer-2-Block (domain)", async () => { // type='mail_domain' landet via getBlocklistedDomainsSet in blockedDomainSet — // Blocking-Logik ist identisch zu type='web'. const domainSet = new Set(["casinox.com"]); const result = await classifyMail({ mail: { senderEmail: "promo@casinox.com", senderName: "CasinoX", subject: "Dein Bonus", }, blockedDomainSet: domainSet, customDisplayNames: [], }); expect(result.action).toBe("blocked"); expect(result.triggerSource).toBe("domain"); expect(result.features.domainBlocked).toBe(true); }); }); // ─── Separate Slot-Buckets (seit plan-limits-Refactor) ─────────────────────── describe("Separate Slot-Buckets — web vs. mail (Dokumentations-Test ohne DB)", () => { it("web-Slot und mail-Slot sind UNABHÄNGIG — 5 web voll blockiert nicht mail-Bucket", () => { // countActiveCustomDomainsSplit() gibt { web, mail } zurück. // mail-Bucket = mail_domain + mail_display_name kombiniert. // web-Bucket und mail-Bucket sind vollständig getrennt. // // Beispiel: 5 web belegt + 2 mail belegt → web-Limit erreicht, mail-Limit NICHT. // POST new mail → sollte 200 zurückgeben (mail-Slot frei). // POST new web → sollte 403 WEB_LIMIT_REACHED zurückgeben. // // Detaillierte Logik-Tests: tests/custom-domains/plan-limits.test.ts expect(true).toBe(true); // Dokumentiert Semantik-Änderung von Shared→Separate }); });