/* Unit tests for partners covering ReferralPartnerSchema v4; RFC-1277 display switches + publication predicates; loadReferralPartners lifecycle classification. Does not test route generation or HTTP — loader/resolver purity only. RFC-1198: created. RFC-1222: rewritten for schema v2 — lifecycle sets, consent union (none|document|channel), active⇒consent and retired⇒retiredAt refinements, slugContainsNameToken; v1 field rejection asserted (forward-only). RFC-1258: v3 coverage — statement/basis/scope/validUntil/website/photo/relationToClientWorld consent gates, active⇒validUntil, locale membership; fixtures carry validUntil. RFC-1277: review fixes: JSON-twin honors display.*; drop orphaned landing: copy facts; heroAbout requires statement gate; CSS class + dead ghost CTA; v6 wording in validators/docs; REVIEW_TOKENS word boundaries; schema-gate tests (81+73 green) */ import { describe, expect, test } from "vitest"; import { referralBasisVisible, referralFitsVisible, referralHeroAboutVisible, referralLinksVisible, referralPhotoVisible, referralStatementVisible, } from "../copy.ts"; import { buildReferralPartnerDoc } from "../disclosure-json.ts"; import { loadActiveReferralPartners, loadReferralLandingConfig, loadReferralPartners, REF_PARAM_PATTERN, REFERRER_MAX_LENGTH, ReferralPartnerSchema, resolveReferralPartner, RESERVED_REFERRAL_SLUGS, slugContainsNameToken, type ReferralPartner, } from "../partners.ts"; const ACTIVE: ReferralPartner = { slug: "ostpost", displayName: "Luba Kemenova", compensation: "referral-fee", locales: ["de", "uk"], status: "active", consent: { method: "document", ref: "trust/consents/ostpost", grantedAt: "2026-10-01" }, validUntil: "2027-04-01", }; const PENDING: ReferralPartner = { ...ACTIVE, status: "pending", consent: { method: "none" }, }; const RETIRED: ReferralPartner = { ...ACTIVE, status: "retired", retiredAt: "2026-12-31", }; function yamlOf(body: string): Record { return { "/src/content/referral-partners/partners.yaml": body }; } const partnerYaml = (extra = "") => `partners: - slug: "ostpost" displayName: "Luba Kemenova" compensation: "referral-fee" locales: ["de", "uk"] status: "active" consent: { method: "document", ref: "trust/consents/ostpost", grantedAt: "2026-10-01" } validUntil: "2027-04-01" ${extra}`; describe("ReferralPartnerSchema v4", () => { test("accepts a fully-specified active partner", () => { expect(ReferralPartnerSchema.parse(ACTIVE)).toEqual(ACTIVE); }); test("accepts a fully-specified v3 partner with voice, basis, scope, and consents (RFC-1258)", () => { const p = { ...ACTIVE, role: { de: "Coach", uk: "Коуч" }, relationToClientWorld: { de: "Ich begleite Betriebe, die sichtbar werden wollen.", uk: "Супроводжую бізнеси, які хочуть бути помітними.", }, statement: { de: "Ich habe die Bedingungen schriftlich gelesen.", uk: "Я читала умови в письмовій формі.", }, statementConsent: { method: "channel" as const, channel: "email" as const, grantedAt: "2026-10-05", }, statementConfirmedAt: "2026-10-05", basis: [ { claim: { de: "AGB gelesen", uk: "Прочитала AGB" }, verifiedBy: "recommender" as const, date: "2026-10-03", }, ], scope: { fits: [{ de: "Handwerk", uk: "Ремесло" }], doesNotFit: [{ de: "Onlineshops", uk: "Інтернет-магазини" }], }, website: "https://example.com", websiteConsent: { method: "channel" as const, channel: "email" as const, grantedAt: "2026-10-05", }, photo: "/r/assets/ostpost/portrait.jpg", photoConsent: { method: "document" as const, ref: "trust/consents/ostpost-photo", grantedAt: "2026-10-05", }, }; expect(ReferralPartnerSchema.parse(p)).toEqual(p); }); test("accepts a v4 partner with isClient, instagram, approval, and display (RFC-1277)", () => { const p = { ...ACTIVE, statement: { de: "Ich empfehle.", uk: "Я рекомендую." }, statementConsent: { method: "channel" as const, channel: "email" as const, grantedAt: "2026-10-05", }, statementConfirmedAt: "2026-10-05", isClient: false, instagram: "https://www.instagram.com/ostpost/", instagramConsent: { method: "channel" as const, channel: "email" as const, grantedAt: "2026-10-05", }, approval: { approvedOn: "2026-10-14", evidence: "mail-2026-10-14" }, display: { statement: true, basis: false, fits: true }, }; expect(ReferralPartnerSchema.parse(p)).toEqual(p); }); test("accepts pending with consent.method none", () => { expect(ReferralPartnerSchema.parse(PENDING)).toEqual(PENDING); }); test("accepts channel consent", () => { const p = { ...ACTIVE, consent: { method: "channel", channel: "telegram", grantedAt: "2026-10-01" }, }; expect(ReferralPartnerSchema.parse(p)).toEqual(p); }); test.each([ ["active with consent.method none", { ...ACTIVE, consent: { method: "none" } }], [ "active with consent lacking grantedAt", { ...ACTIVE, consent: { method: "document", ref: "x" } }, ], ["retired without retiredAt", { ...ACTIVE, status: "retired" }], ["pending carrying retiredAt", { ...PENDING, retiredAt: "2026-12-31" }], ["legacy v1 field publicNameConsentRef", { ...ACTIVE, publicNameConsentRef: "c" }], ["legacy v1 field active", { ...ACTIVE, active: true }], [ "consent with unknown key", { ...ACTIVE, consent: { method: "none", grantedAt: "2026-10-01" } }, ], ["role locale outside locales", { ...ACTIVE, role: { fr: "Rôle" } }], ["active without validUntil (RFC-1258)", { ...ACTIVE, validUntil: undefined }], [ "statement without statementConsent (RFC-1258)", { ...ACTIVE, statement: { de: "Ich empfehle." } }, ], [ "statement without statementConfirmedAt (RFC-1258)", { ...ACTIVE, statement: { de: "Ich empfehle." }, statementConsent: ACTIVE.consent }, ], [ "statement on pending partner (RFC-1258)", { ...PENDING, statement: { de: "Ich empfehle." }, statementConsent: ACTIVE.consent, statementConfirmedAt: "2026-10-01", }, ], [ "statementConsent without statement (RFC-1258)", { ...ACTIVE, statementConsent: ACTIVE.consent }, ], [ "basis item without date (RFC-1258)", { ...ACTIVE, statement: { de: "Ich empfehle." }, statementConsent: ACTIVE.consent, statementConfirmedAt: "2026-10-01", basis: [{ claim: { de: "AGB gelesen" }, verifiedBy: "recommender" }], }, ], [ "basis without statement (RFC-1258)", { ...ACTIVE, basis: [{ claim: { de: "AGB gelesen" }, verifiedBy: "recommender", date: "2026-10-01" }], }, ], [ "scope without statement (RFC-1258)", { ...ACTIVE, scope: { fits: [{ de: "Handwerk" }], doesNotFit: [{ de: "Shops" }] } }, ], ["website without websiteConsent (RFC-1258)", { ...ACTIVE, website: "https://example.com" }], ["photo without photoConsent (RFC-1258)", { ...ACTIVE, photo: "luba" }], [ "relationToClientWorld without statementConsent (RFC-1258)", { ...ACTIVE, relationToClientWorld: { de: "Ich begleite Betriebe." } }, ], [ "statement locale outside locales (RFC-1258)", { ...ACTIVE, statement: { fr: "Je recommande." }, statementConsent: ACTIVE.consent, statementConfirmedAt: "2026-10-01", }, ], [ "instagram without instagramConsent (RFC-1277)", { ...ACTIVE, instagram: "https://www.instagram.com/ostpost/" }, ], [ "instagramConsent without instagram (RFC-1277)", { ...ACTIVE, instagramConsent: ACTIVE.consent }, ], [ "instagram on pending partner (RFC-1277)", { ...PENDING, instagram: "https://www.instagram.com/ostpost/", instagramConsent: ACTIVE.consent, }, ], ["isClient without statement (RFC-1277)", { ...ACTIVE, isClient: false }], [ "approval without statement (RFC-1277)", { ...ACTIVE, approval: { approvedOn: "2026-10-14", evidence: "mail-1" } }, ], ["display with unknown key (RFC-1277)", { ...ACTIVE, display: { unknown: true } }], ])("rejects %s", (_label, partner) => { const result = ReferralPartnerSchema.safeParse(partner); expect( result.success, "v4 schema must reject this shape — check ReferralPartnerSchema superRefine/strict members", ).toBe(false); }); test.each([ ["too short", "ab"], ["too long", "a".repeat(33)], ["uppercase", "Anna"], ["leading dash", "-anna"], ["double dash", "an--na"], ["reserved", "admin"], ])("rejects invalid slug: %s", (_label, slug) => { expect(ReferralPartnerSchema.safeParse({ ...ACTIVE, slug }).success).toBe(false); }); }); describe("RFC-1277 display switches + publication predicates", () => { const VOICED: ReferralPartner = { ...ACTIVE, relationToClientWorld: { de: "Ich begleite Betriebe.", uk: "Супроводжую бізнеси." }, statement: { de: "Ich empfehle.", uk: "Я рекомендую." }, statementConsent: { method: "channel", channel: "email", grantedAt: "2026-10-05" }, statementConfirmedAt: "2026-10-05", basis: [ { claim: { de: "AGB gelesen", uk: "Прочитала AGB" }, verifiedBy: "recommender", date: "2026-10-03", }, ], scope: { fits: [{ de: "Handwerk" }], doesNotFit: [{ de: "Shops" }] }, website: "https://example.com", websiteConsent: { method: "channel", channel: "email", grantedAt: "2026-10-05" }, instagram: "https://www.instagram.com/ostpost/", instagramConsent: { method: "channel", channel: "email", grantedAt: "2026-10-05" }, photo: "/r/assets/ostpost/portrait.jpg", photoConsent: { method: "document", ref: "x", grantedAt: "2026-10-05" }, }; test("all predicates publish when every display flag is absent (default = shown)", () => { expect(referralStatementVisible(VOICED)).toBe(true); expect(referralBasisVisible(VOICED)).toBe(true); expect(referralFitsVisible(VOICED)).toBe(true); expect(referralHeroAboutVisible(VOICED)).toBe(true); expect(referralLinksVisible(VOICED)).toBe(true); expect(referralPhotoVisible(VOICED)).toBe(true); }); test("display.statement:false hides statement AND the blocks that ride its gate", () => { const p = { ...VOICED, display: { statement: false } }; expect(referralStatementVisible(p)).toBe(false); expect(referralBasisVisible(p)).toBe(false); expect(referralFitsVisible(p)).toBe(false); expect(referralHeroAboutVisible(p)).toBe(false); // links/photo carry their own switches — untouched. expect(referralLinksVisible(p)).toBe(true); expect(referralPhotoVisible(p)).toBe(true); }); test("per-block switches hide independently without deleting data", () => { const p = { ...VOICED, display: { fits: false, links: false } }; expect(referralFitsVisible(p)).toBe(false); expect(referralLinksVisible(p)).toBe(false); expect(referralStatementVisible(p)).toBe(true); expect(referralBasisVisible(p)).toBe(true); expect(p.scope).toBeDefined(); expect(p.websiteConsent).toBeDefined(); }); test("a display flag can never reveal — absent data stays hidden", () => { const bare: ReferralPartner = { ...ACTIVE, display: { statement: true, fits: true } }; expect(referralStatementVisible(bare)).toBe(false); expect(referralFitsVisible(bare)).toBe(false); }); test("the /r/.json twin honors the same display gates", () => { const doc = buildReferralPartnerDoc(VOICED, {}); expect(doc).toMatchObject({ status: "active", statement: VOICED.statement, basis: VOICED.basis, scope: VOICED.scope, }); const hidden = buildReferralPartnerDoc({ ...VOICED, display: { fits: false } }, {}); expect(hidden).toMatchObject({ status: "active", statement: VOICED.statement }); expect(hidden).not.toHaveProperty("scope"); const noStatement = buildReferralPartnerDoc({ ...VOICED, display: { statement: false } }, {}); expect(noStatement).not.toHaveProperty("statement"); expect(noStatement).not.toHaveProperty("statementConfirmedAt"); expect(noStatement).not.toHaveProperty("basis"); expect(noStatement).not.toHaveProperty("scope"); }); }); describe("loadReferralPartners lifecycle classification", () => { test("classifies pending/active/retired into separate sets", () => { const raw = `partners: - slug: "ostpost" displayName: "Luba Kemenova" compensation: "referral-fee" locales: ["de"] status: "pending" consent: { method: "none" } - slug: "markt-blick" displayName: "Maria I." compensation: "steward" locales: ["de"] status: "active" consent: { method: "channel", channel: "email", grantedAt: "2026-09-15" } validUntil: "2027-03-15" - slug: "alt-werk" displayName: "Peter P." compensation: "none" locales: ["de"] status: "retired" retiredAt: "2026-08-01" consent: { method: "document", ref: "trust/consents/alt-werk", grantedAt: "2026-01-10" } `; const sets = loadReferralPartners(yamlOf(raw)); expect(sets.pending.map((p) => p.slug)).toEqual(["ostpost"]); expect(sets.active.map((p) => p.slug)).toEqual(["markt-blick"]); expect(sets.retired.map((p) => p.slug)).toEqual(["alt-werk"]); }); test("loadActiveReferralPartners returns only the active set", () => { const raw = `partners: - slug: "ostpost" displayName: "Luba Kemenova" compensation: "referral-fee" locales: ["de"] status: "pending" consent: { method: "none" } `; expect(loadActiveReferralPartners(yamlOf(raw))).toEqual([]); }); test("returns empty sets for missing file", () => { expect(loadReferralPartners({})).toEqual({ active: [], retired: [], pending: [], paused: [] }); }); test("rejects duplicate slugs — the whole file fails schema validation", () => { const warns: { code: string; data: Record }[] = []; const dup = partnerYaml(` - slug: "ostpost" displayName: "Other" compensation: "none" locales: ["de"] status: "pending" consent: { method: "none" } `); const sets = loadReferralPartners(yamlOf(dup), (c, d) => warns.push({ code: c, data: d })); expect(sets.active).toEqual([]); expect(warns.map((w) => w.code)).toContain("referral.partners.invalid"); expect(warns[0]?.data.detail).toContain("duplicate partner slug"); }); test("warns and skips a malformed file instead of throwing", () => { const warns: string[] = []; expect( loadReferralPartners({ "partners.yaml": "partners: [{broken" }, (c) => warns.push(c)), ).toEqual({ active: [], retired: [], pending: [], paused: [] }); expect(warns).toContain("referral.partners.invalid"); }); test("a legacy v1 file invalidates the whole registry (forward-only)", () => { const warns: { code: string; data: Record }[] = []; const legacy = `partners: - slug: "ostpost" displayName: "Luba Kemenova" compensation: "referral-fee" locales: ["de"] active: true publicNameConsentRef: "consent-1" `; const sets = loadReferralPartners(yamlOf(legacy), (c, d) => warns.push({ code: c, data: d })); expect(sets.active).toEqual([]); expect(warns.map((w) => w.code)).toContain("referral.partners.invalid"); }); }); describe("loadReferralLandingConfig", () => { test("returns the authored landing block verbatim", () => { const raw = `landing: rewardPhrases: referral-fee: { de: "70 €", uk: "70 €" } stewardRole: { de: "Markt-Steward" } agbAnchors: { de: { b2b: "sec-1-4" } } ${partnerYaml()}`; const config = loadReferralLandingConfig({ "partners.yaml": raw }); expect(config?.stewardRole?.de).toBe("Markt-Steward"); expect(config?.rewardPhrases?.["referral-fee"]?.uk).toBe("70 €"); expect(config?.agbAnchors?.de?.b2b).toBe("sec-1-4"); }); test("returns null when the block is absent or malformed", () => { expect(loadReferralLandingConfig(yamlOf(partnerYaml()))).toBeNull(); expect( loadReferralLandingConfig({ "partners.yaml": "landing: { bogus: 1 }\npartners: []" }), ).toBeNull(); }); }); describe("resolveReferralPartner", () => { const partners = [ACTIVE]; test("resolves an exact slug", () => { expect(resolveReferralPartner("ostpost", partners)).toEqual(ACTIVE); }); test("trims and lower-cases before matching", () => { expect(resolveReferralPartner(" OSTPOST ", partners)).toEqual(ACTIVE); }); test("never resolves a pending or retired slug", () => { expect(resolveReferralPartner("ostpost", [PENDING])).toBeNull(); expect(resolveReferralPartner("ostpost", [RETIRED])).toBeNull(); }); test.each([ ["empty", ""], ["whitespace", " "], ["unknown slug", "boris"], ["over-length", "a".repeat(REFERRER_MAX_LENGTH + 1)], ["free text", "mein Nachbar hat empfohlen"], ])("returns null for %s", (_label, input) => { expect(resolveReferralPartner(input, partners)).toBeNull(); }); }); describe("slugContainsNameToken (slug-PII guard)", () => { test("flags a slug carrying a displayName token", () => { expect(slugContainsNameToken("luba-kemenova", "Luba Kemenova")).toBe(true); expect(slugContainsNameToken("luba-tipps", "Luba Kemenova")).toBe(true); }); test("accepts a pseudonymous slug", () => { expect(slugContainsNameToken("ostpost", "Luba Kemenova")).toBe(false); }); test("ignores short tokens and substring-only overlap", () => { expect(slugContainsNameToken("marktplatz-ost", "Max Os")).toBe(false); expect(slugContainsNameToken("annate", "Anna Beispiel")).toBe(false); }); }); describe("patterns", () => { test("REF_PARAM_PATTERN bounds ?ref values to slug-like charset", () => { expect(REF_PARAM_PATTERN.test("ostpost")).toBe(true); expect(REF_PARAM_PATTERN.test("a-b-c")).toBe(true); expect(REF_PARAM_PATTERN.test("ab")).toBe(false); expect(REF_PARAM_PATTERN.test("anna