Task B — linguistische FP-Fix: - mail-classifier.ts: Subject-Keyword-Loop überspringt Keyword-Score wenn Subject das Keyword als Sucht-Compound enthält (z.B. "glücksspiel" in "Glücksspielsucht" → kein +50 Score). Globale linguistische Invariante Deutsch — Gambling-Marketer schreiben nie "Glücksspielsucht-Bonus". - gambling-keywords.mjs: GAMBLING_WHITELIST erweitert um Stamm-Varianten (wettsucht, spielsucht, suchtberatung, suchthilfe) als Fallback für Compounds wo keyword ≠ exakter Stamm. - 4 neue Tests: Forum Glücksspielsucht → PASS, Hilfe bei Spielsucht → PASS, Wettsucht-Selbsthilfe → PASS, Glücksspiel-Bonus 100€ → BLOCK. Task C — Phase-1-Data-Foundation: - mail-training-utils.ts: sanitizeSubjectForTraining() (PII-Stripping via Regex: EMAIL/URL/NUM/Greeting/ALL-CAPS) + detectSubjectLanguage() via franc (iso639-3). 26 Unit-Tests. - franc@6.2.0 installiert (~50KB ESM). - mail.ts insertMailClassificationSample(): ruft sanitizeSubjectForTraining() auf, schreibt detectedLang + subjectSanitized in features-JSON (Interim bis Schema-Migration). - mail-retention-cron.ts: Subject-Nullification nach 30 Tagen (täglich) + Sample-Purge nach 12 Monaten (monatlich). DSGVO Art. 5 Abs. 1e. 105 Tests grün (58 classifier + 26 training-utils + 11 display-name + 10 gmail). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
8.6 KiB
TypeScript
194 lines
8.6 KiB
TypeScript
/**
|
|
* Tests für mail-training-utils.ts — Phase 1 Data-Foundation.
|
|
*
|
|
* Abgedeckt:
|
|
* - sanitizeSubjectForTraining() — alle Regex-Patterns
|
|
* - detectSubjectLanguage() — franc-Integration
|
|
* - Edge-Cases: null, leer, nur Whitespace
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import {
|
|
sanitizeSubjectForTraining,
|
|
detectSubjectLanguage,
|
|
} from "../../server/utils/mail-training-utils";
|
|
|
|
// ─── sanitizeSubjectForTraining() ─────────────────────────────────────────────
|
|
|
|
describe("sanitizeSubjectForTraining()", () => {
|
|
// ─── Edge-Cases ────────────────────────────────────────────────────────────
|
|
|
|
it("null → leerer String + detectedLang='und'", () => {
|
|
const result = sanitizeSubjectForTraining(null);
|
|
expect(result.subjectSanitized).toBe("");
|
|
expect(result.detectedLang).toBe("und");
|
|
});
|
|
|
|
it("leerer String → leerer String + detectedLang='und'", () => {
|
|
const result = sanitizeSubjectForTraining("");
|
|
expect(result.subjectSanitized).toBe("");
|
|
expect(result.detectedLang).toBe("und");
|
|
});
|
|
|
|
it("nur Whitespace → leerer String", () => {
|
|
const result = sanitizeSubjectForTraining(" ");
|
|
expect(result.subjectSanitized).toBe("");
|
|
});
|
|
|
|
// ─── E-Mail-Erkennung ──────────────────────────────────────────────────────
|
|
|
|
it("E-Mail-Adresse im Betreff → [EMAIL]", () => {
|
|
const result = sanitizeSubjectForTraining("Info für test@example.com");
|
|
expect(result.subjectSanitized).toBe("Info für [EMAIL]");
|
|
});
|
|
|
|
it("E-Mail mitten im Satz → [EMAIL]", () => {
|
|
const result = sanitizeSubjectForTraining("Willkommen user123@domain.de, dein Konto ist aktiv");
|
|
expect(result.subjectSanitized).toBe("Willkommen [EMAIL], dein Konto ist aktiv");
|
|
});
|
|
|
|
// ─── URL-Erkennung ─────────────────────────────────────────────────────────
|
|
|
|
it("https-URL → [URL]", () => {
|
|
const result = sanitizeSubjectForTraining("Besuche https://casino.bet/promo");
|
|
expect(result.subjectSanitized).toBe("Besuche [URL]");
|
|
});
|
|
|
|
it("http-URL → [URL]", () => {
|
|
const result = sanitizeSubjectForTraining("Klicke auf http://example.com/offer");
|
|
expect(result.subjectSanitized).toBe("Klicke auf [URL]");
|
|
});
|
|
|
|
it("www-URL → [URL]", () => {
|
|
const result = sanitizeSubjectForTraining("www.bonus.de/angebot jetzt sichern");
|
|
expect(result.subjectSanitized).toBe("[URL] jetzt sichern");
|
|
});
|
|
|
|
// ─── Zahlen-Erkennung ──────────────────────────────────────────────────────
|
|
|
|
it("5-stellige Zahl → [NUM]", () => {
|
|
const result = sanitizeSubjectForTraining("Bestellung 12345 wurde versandt");
|
|
expect(result.subjectSanitized).toBe("Bestellung [NUM] wurde versandt");
|
|
});
|
|
|
|
it("9-stellige Kundennummer → [NUM]", () => {
|
|
const result = sanitizeSubjectForTraining("Deine Kundennummer: 987654321");
|
|
expect(result.subjectSanitized).toBe("Deine Kundennummer: [NUM]");
|
|
});
|
|
|
|
it("4-stellige Zahl bleibt erhalten (Jahr, Score)", () => {
|
|
const result = sanitizeSubjectForTraining("Angebot 2024 — bis zu 1000€");
|
|
expect(result.subjectSanitized).toBe("Angebot 2024 — bis zu 1000€");
|
|
});
|
|
|
|
// ─── Greeting-Prefix ───────────────────────────────────────────────────────
|
|
|
|
it("'Hallo Max, ...' → Greeting-Prefix stripped", () => {
|
|
const result = sanitizeSubjectForTraining("Hallo Max, dein Bonus wartet");
|
|
expect(result.subjectSanitized).toBe("dein Bonus wartet");
|
|
});
|
|
|
|
it("'Hi Sarah! ...' → Greeting-Prefix stripped", () => {
|
|
const result = sanitizeSubjectForTraining("Hi Sarah! Neues Angebot für dich");
|
|
expect(result.subjectSanitized).toBe("Neues Angebot für dich");
|
|
});
|
|
|
|
it("'Dear John, ...' → Greeting-Prefix stripped", () => {
|
|
const result = sanitizeSubjectForTraining("Dear John, your account is ready");
|
|
expect(result.subjectSanitized).toBe("your account is ready");
|
|
});
|
|
|
|
it("'Cher Michel, ...' → Greeting-Prefix stripped", () => {
|
|
const result = sanitizeSubjectForTraining("Cher Michel, votre offre expire");
|
|
expect(result.subjectSanitized).toBe("votre offre expire");
|
|
});
|
|
|
|
it("kein Greeting → bleibt unverändert (kein false strip)", () => {
|
|
const result = sanitizeSubjectForTraining("Casino Bonus wartet auf dich");
|
|
expect(result.subjectSanitized).toBe("Casino Bonus wartet auf dich");
|
|
});
|
|
|
|
// ─── ALL-CAPS-Wörter ───────────────────────────────────────────────────────
|
|
|
|
it("ALL-CAPS-Wort >= 4 Zeichen → [NAME]", () => {
|
|
const result = sanitizeSubjectForTraining("Willkommen HANS, dein Konto ist bereit");
|
|
expect(result.subjectSanitized).toBe("Willkommen [NAME], dein Konto ist bereit");
|
|
});
|
|
|
|
it("ALL-CAPS-Wort < 4 Zeichen bleibt (z.B. 'SMS', 'VIP')", () => {
|
|
const result = sanitizeSubjectForTraining("Dein VIP-Bonus wartet");
|
|
// "VIP" ist 3 Zeichen → bleibt erhalten
|
|
expect(result.subjectSanitized).toBe("Dein VIP-Bonus wartet");
|
|
});
|
|
|
|
// ─── Whitespace-Normalisierung ─────────────────────────────────────────────
|
|
|
|
it("mehrfache Leerzeichen nach Sanitization → normalisiert", () => {
|
|
// Wenn Greeting entfernt wird können führende Leerzeichen entstehen
|
|
const result = sanitizeSubjectForTraining("Hallo Test, dein Angebot");
|
|
expect(result.subjectSanitized).not.toMatch(/\s{2,}/);
|
|
});
|
|
|
|
// ─── Kombinierte Patterns ──────────────────────────────────────────────────
|
|
|
|
it("Kombination: URL + Zahl + E-Mail alle ersetzt", () => {
|
|
const result = sanitizeSubjectForTraining(
|
|
"Tracking 123456789: Paket von user@shop.de — Link: https://track.shop.de/abc"
|
|
);
|
|
expect(result.subjectSanitized).toContain("[NUM]");
|
|
expect(result.subjectSanitized).toContain("[EMAIL]");
|
|
expect(result.subjectSanitized).toContain("[URL]");
|
|
expect(result.subjectSanitized).not.toMatch(/\d{5,}/);
|
|
expect(result.subjectSanitized).not.toMatch(/[a-zA-Z0-9._%+\-]+@/);
|
|
});
|
|
|
|
it("realistisches Gambling-Mail bleibt erkennbar nach Sanitization", () => {
|
|
const result = sanitizeSubjectForTraining(
|
|
"Dein Glücksspiel-Bonus 100€ wartet — Tracking: 987654321"
|
|
);
|
|
// Gambling-Signal "Glücksspiel-Bonus" bleibt erhalten
|
|
expect(result.subjectSanitized).toContain("Glücksspiel-Bonus");
|
|
// Tracking-ID wird ersetzt
|
|
expect(result.subjectSanitized).toContain("[NUM]");
|
|
expect(result.subjectSanitized).not.toMatch(/\d{5,}/);
|
|
});
|
|
});
|
|
|
|
// ─── detectSubjectLanguage() ──────────────────────────────────────────────────
|
|
|
|
describe("detectSubjectLanguage()", () => {
|
|
it("null → 'und'", () => {
|
|
expect(detectSubjectLanguage(null)).toBe("und");
|
|
});
|
|
|
|
it("leerer String → 'und'", () => {
|
|
expect(detectSubjectLanguage("")).toBe("und");
|
|
});
|
|
|
|
it("Deutsch erkannt (deu)", () => {
|
|
// Längerer Text für bessere Spracherkennung
|
|
const lang = detectSubjectLanguage(
|
|
"Dein exklusives Angebot wartet auf dich — jetzt einlösen und profitieren"
|
|
);
|
|
expect(lang).toBe("deu");
|
|
});
|
|
|
|
it("Englisch-artiger Text → nicht 'und' (franc erkennt eine Sprache)", () => {
|
|
// franc kann je nach Text-Sample eng oder sco (Scots) zurückgeben für englische Texte —
|
|
// beide sind akzeptabel. Wichtig: kein "und" (= unbekannt).
|
|
const lang = detectSubjectLanguage(
|
|
"Your exclusive offer is waiting — claim your bonus now and start winning today"
|
|
);
|
|
expect(lang).not.toBe("und");
|
|
expect(typeof lang).toBe("string");
|
|
});
|
|
|
|
it("Zu kurzer Text → 'und' (franc-Limitation bei < ~20 Zeichen)", () => {
|
|
// Sehr kurze Texte liefern "und" oder falsche Erkennungen — erwartetes Verhalten.
|
|
// Wir prüfen nur dass ein String zurückkommt (kein throw).
|
|
const lang = detectSubjectLanguage("Hi");
|
|
expect(typeof lang).toBe("string");
|
|
expect(lang.length).toBeGreaterThan(0);
|
|
});
|
|
});
|