test(custom-domains): plan-limits-Test auf gemeinsamen Slot-Pool umgeschrieben
Der Test prüfte die alte Per-Bucket-Logik (web/mail getrennt) + Free-Tier.
Angepasst an den 10/20-Pool-Refactor: customDomains als Zahl, Pro 10 /
Legend 20, gemeinsamer web+mail-Pool, LIMIT_REACHED, GET liefert
{ items, count, limit }. Free-Tier-Fälle entfernt. 45/45 grün.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
834e6efffc
commit
38a74dd1ad
@ -1,14 +1,18 @@
|
||||
/**
|
||||
* Tests: Custom-Domain Plan-Limits (separate web/mail buckets)
|
||||
* Tests: Custom-Domain Plan-Limits (gemeinsamer Slot-Pool web + mail)
|
||||
*
|
||||
* Testet:
|
||||
* - getPlanLimits returnt strukturiertes { web, mail } Objekt
|
||||
* - countActiveCustomDomainsSplit returnt korrekte Split-Counts (Unit ohne DB)
|
||||
* - getPlanLimits returnt customDomains als ZAHL (kein { web, mail } Objekt mehr)
|
||||
* - Pro = 10, Legend = 20 — ein gemeinsamer Pool
|
||||
* - Legacy-Plan-Namen: premium → Legend, standard/free/etc. → Pro
|
||||
* - PLAN_LIMITS hat keinen 'free'-Eintrag mehr (nur pro + legend)
|
||||
* - Slot-Limit-Prüfung: currentCount >= limit → LIMIT_REACHED (kein Bucket-Split)
|
||||
* - POST-Body-Compat: { pattern, kind: 'mail' } mit Domain-Pattern → mail_domain
|
||||
* - POST-Body-Compat: { pattern, kind: 'mail' } mit Display-Name-Pattern → 400 INVALID_MAIL_DOMAIN (v1.0)
|
||||
* - POST-Body-Compat: { domain, type: 'mail_display_name' } → 400 DISPLAY_NAME_NOT_SUPPORTED (v1.0)
|
||||
* - POST-Body-Compat: volle Adresse (local@domain.tld) → local-part gestripped, gespeichert als mail_domain
|
||||
* - Submit eines mail_domain → erlaubt (gleich wie web)
|
||||
* - GET /api/custom-domains gibt { items, count, limit } zurück (Zahlen, kein Bucket-Split)
|
||||
*
|
||||
* DSGVO: keine PII. Synthetic Brand-Namen und Test-Domains.
|
||||
*/
|
||||
@ -17,38 +21,150 @@ import { getPlanLimits, PLAN_LIMITS } from "../../server/utils/plan-features";
|
||||
|
||||
// ─── Plan-Limits Shape ────────────────────────────────────────────────────────
|
||||
|
||||
describe("getPlanLimits — customDomains ist strukturiertes Objekt", () => {
|
||||
it("Free: customDomains = { web: 5, mail: 5 }", () => {
|
||||
const limits = getPlanLimits("free");
|
||||
expect(limits.customDomains).toEqual({ web: 5, mail: 5 });
|
||||
});
|
||||
|
||||
it("Pro: customDomains = { web: 5, mail: 5 }", () => {
|
||||
describe("getPlanLimits — customDomains ist ZAHL (gemeinsamer Slot-Pool)", () => {
|
||||
it("Pro: customDomains = 10", () => {
|
||||
const limits = getPlanLimits("pro");
|
||||
expect(limits.customDomains).toEqual({ web: 5, mail: 5 });
|
||||
expect(limits.customDomains).toBe(10);
|
||||
});
|
||||
|
||||
it("Legend: customDomains = { web: 10, mail: 10 }", () => {
|
||||
it("Legend: customDomains = 20", () => {
|
||||
const limits = getPlanLimits("legend");
|
||||
expect(limits.customDomains).toEqual({ web: 10, mail: 10 });
|
||||
expect(limits.customDomains).toBe(20);
|
||||
});
|
||||
|
||||
it("Legacy 'premium' → Legend limits", () => {
|
||||
it("Legacy 'premium' → Legend limits (customDomains = 20)", () => {
|
||||
const limits = getPlanLimits("premium");
|
||||
expect(limits.customDomains).toEqual({ web: 10, mail: 10 });
|
||||
expect(limits.customDomains).toBe(20);
|
||||
});
|
||||
|
||||
it("Legacy 'standard' → Pro limits", () => {
|
||||
it("Legacy 'standard' → Pro limits (customDomains = 10)", () => {
|
||||
const limits = getPlanLimits("standard");
|
||||
expect(limits.customDomains).toEqual({ web: 5, mail: 5 });
|
||||
expect(limits.customDomains).toBe(10);
|
||||
});
|
||||
|
||||
it("PLAN_LIMITS.free.customDomains hat keine 'number'-Shape mehr", () => {
|
||||
const val = PLAN_LIMITS.free.customDomains;
|
||||
expect(typeof val).not.toBe("number");
|
||||
expect(typeof val).toBe("object");
|
||||
expect(typeof val.web).toBe("number");
|
||||
expect(typeof val.mail).toBe("number");
|
||||
it("Legacy 'free' → Pro limits (customDomains = 10, kein free-Tier mehr)", () => {
|
||||
const limits = getPlanLimits("free");
|
||||
expect(limits.customDomains).toBe(10);
|
||||
});
|
||||
|
||||
it("Unbekannter Plan-String → Pro limits (sicherer Fallback)", () => {
|
||||
const limits = getPlanLimits("whatever");
|
||||
expect(limits.customDomains).toBe(10);
|
||||
});
|
||||
|
||||
it("customDomains ist typeof 'number' — kein Objekt mehr", () => {
|
||||
expect(typeof getPlanLimits("pro").customDomains).toBe("number");
|
||||
expect(typeof getPlanLimits("legend").customDomains).toBe("number");
|
||||
});
|
||||
|
||||
it("PLAN_LIMITS hat keinen 'free'-Eintrag", () => {
|
||||
expect("free" in PLAN_LIMITS).toBe(false);
|
||||
});
|
||||
|
||||
it("PLAN_LIMITS.pro.customDomains = 10", () => {
|
||||
expect(PLAN_LIMITS.pro.customDomains).toBe(10);
|
||||
});
|
||||
|
||||
it("PLAN_LIMITS.legend.customDomains = 20", () => {
|
||||
expect(PLAN_LIMITS.legend.customDomains).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Slot-Pool-Logik ──────────────────────────────────────────────────────────
|
||||
|
||||
describe("Slot-Pool-Logik — EIN gemeinsamer Pool, kein Bucket-Split", () => {
|
||||
it("10 domains (web + mail gemischt) bei Pro → voll (currentCount >= limit)", () => {
|
||||
const limit = getPlanLimits("pro").customDomains; // 10
|
||||
const currentCount = 10; // z.B. 7 web + 3 mail_domain
|
||||
expect(currentCount >= limit).toBe(true);
|
||||
});
|
||||
|
||||
it("9 domains bei Pro → noch ein Slot frei", () => {
|
||||
const limit = getPlanLimits("pro").customDomains; // 10
|
||||
const currentCount = 9;
|
||||
expect(currentCount >= limit).toBe(false);
|
||||
});
|
||||
|
||||
it("20 domains bei Legend → voll", () => {
|
||||
const limit = getPlanLimits("legend").customDomains; // 20
|
||||
const currentCount = 20;
|
||||
expect(currentCount >= limit).toBe(true);
|
||||
});
|
||||
|
||||
it("19 domains bei Legend → noch ein Slot frei", () => {
|
||||
const limit = getPlanLimits("legend").customDomains; // 20
|
||||
const currentCount = 19;
|
||||
expect(currentCount >= limit).toBe(false);
|
||||
});
|
||||
|
||||
it("web + mail_domain summieren sich im gleichen Pool — kein separater Bucket", () => {
|
||||
const limit = getPlanLimits("pro").customDomains; // 10
|
||||
const webCount = 7;
|
||||
const mailCount = 3;
|
||||
const totalCount = webCount + mailCount; // wie countActiveCustomDomains liefert
|
||||
expect(totalCount >= limit).toBe(true);
|
||||
});
|
||||
|
||||
it("7 web + 2 mail → 9 gesamt, noch Slot frei bei Pro", () => {
|
||||
const limit = getPlanLimits("pro").customDomains; // 10
|
||||
const totalCount = 7 + 2; // 9
|
||||
expect(totalCount >= limit).toBe(false);
|
||||
});
|
||||
|
||||
it("LIMIT_REACHED-Fehlercode (kein WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED mehr)", () => {
|
||||
// Spiegelt POST-Handler: bei currentCount >= limit → error: 'LIMIT_REACHED'
|
||||
const errorCode = "LIMIT_REACHED";
|
||||
expect(errorCode).not.toBe("WEB_LIMIT_REACHED");
|
||||
expect(errorCode).not.toBe("MAIL_LIMIT_REACHED");
|
||||
expect(errorCode).toBe("LIMIT_REACHED");
|
||||
});
|
||||
|
||||
it("LIMIT_REACHED-Response hat { error, resource, current, limit } — kein 'bucket'-Feld", () => {
|
||||
// Spiegelt die data-Shape aus index.post.ts bei Limit-Überschreitung
|
||||
const limitData: {
|
||||
error: string;
|
||||
resource: string;
|
||||
current: number;
|
||||
limit: number;
|
||||
bucket?: string;
|
||||
} = {
|
||||
error: "LIMIT_REACHED",
|
||||
resource: "custom_domains",
|
||||
current: 10,
|
||||
limit: 10,
|
||||
};
|
||||
expect(limitData.bucket).toBeUndefined();
|
||||
expect(limitData.resource).toBe("custom_domains");
|
||||
expect(typeof limitData.current).toBe("number");
|
||||
expect(typeof limitData.limit).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── GET /api/custom-domains Response-Shape ───────────────────────────────────
|
||||
|
||||
describe("GET /api/custom-domains — Response-Shape { items, count, limit }", () => {
|
||||
it("Response hat count (Zahl) statt counts:{web,mail}", () => {
|
||||
// Spiegelt index.get.ts: return { items, count, limit }
|
||||
const mockResponse: { items: unknown[]; count: number; limit: number } = {
|
||||
items: [],
|
||||
count: 3,
|
||||
limit: 10,
|
||||
};
|
||||
expect(typeof mockResponse.count).toBe("number");
|
||||
expect(typeof mockResponse.limit).toBe("number");
|
||||
// Kein splits mehr
|
||||
expect("counts" in mockResponse).toBe(false);
|
||||
expect("limits" in mockResponse).toBe(false);
|
||||
});
|
||||
|
||||
it("limit aus getPlanLimits('pro').customDomains = 10 — was GET zurückgibt", () => {
|
||||
const limit = getPlanLimits("pro").customDomains;
|
||||
expect(limit).toBe(10);
|
||||
});
|
||||
|
||||
it("limit aus getPlanLimits('legend').customDomains = 20 — was GET zurückgibt", () => {
|
||||
const limit = getPlanLimits("legend").customDomains;
|
||||
expect(limit).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
@ -191,82 +307,6 @@ describe("Body-Compat-Mapping — kind='mail' Pattern-Analyse (v1.0: nur Domain-
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Slot-Bucket-Logik ────────────────────────────────────────────────────────
|
||||
|
||||
describe("Slot-Bucket-Logik — welcher Bucket pro Type", () => {
|
||||
it("type='web' → bucket='web'", () => {
|
||||
const bucket = "web" === "web" ? "web" : "mail";
|
||||
expect(bucket).toBe("web");
|
||||
});
|
||||
|
||||
it("type='mail_domain' → bucket='mail'", () => {
|
||||
const type = "mail_domain";
|
||||
const bucket: "web" | "mail" = type === "web" ? "web" : "mail";
|
||||
expect(bucket).toBe("mail");
|
||||
});
|
||||
|
||||
it("type='mail_display_name' → bucket='mail'", () => {
|
||||
const type = "mail_display_name";
|
||||
const bucket: "web" | "mail" = type === "web" ? "web" : "mail";
|
||||
expect(bucket).toBe("mail");
|
||||
});
|
||||
|
||||
it("5 mail_domain + 0 mail_display_name = 5 mail-Slots belegt (kein Platz für 6tes bei Free)", () => {
|
||||
const freeMailLimit = PLAN_LIMITS.free.customDomains.mail; // 5
|
||||
const mailCount = 5; // 5 mail_domain belegt
|
||||
expect(mailCount >= freeMailLimit).toBe(true);
|
||||
});
|
||||
|
||||
it("5 mail belegt + neues web → kein MAIL_LIMIT_REACHED (web-Slot separat)", () => {
|
||||
const freeWebLimit = PLAN_LIMITS.free.customDomains.web; // 5
|
||||
const freeMailLimit = PLAN_LIMITS.free.customDomains.mail; // 5
|
||||
const mailCount = 5;
|
||||
const webCount = 0;
|
||||
// Mail voll, aber web-Slot noch frei
|
||||
expect(webCount >= freeWebLimit).toBe(false); // web nicht voll
|
||||
expect(mailCount >= freeMailLimit).toBe(true); // mail voll
|
||||
});
|
||||
|
||||
it("5 web + 5 mail belegt → beide Buckets voll (Free-Plan exhausted)", () => {
|
||||
const limits = getPlanLimits("free");
|
||||
const webCount = 5;
|
||||
const mailCount = 5;
|
||||
expect(webCount >= limits.customDomains.web).toBe(true);
|
||||
expect(mailCount >= limits.customDomains.mail).toBe(true);
|
||||
});
|
||||
|
||||
it("legacy mail_display_name-Rows (falls vorhanden) zählen weiterhin in mail-Bucket", () => {
|
||||
// v1.0: mail_display_name kann nicht mehr über POST /api/custom-domains erstellt werden.
|
||||
// Etwaige Legacy-Rows (aus alten Tests oder manuellem Insert) zählen dennoch korrekt
|
||||
// im mail-Bucket via countActiveCustomDomainsSplit — da sie type='mail_display_name' haben.
|
||||
// DB-Typ bleibt im Schema erhalten für triviale v1.1-Reaktivierung.
|
||||
const mailDomainCount = 3;
|
||||
const mailDisplayNameCount = 2; // Legacy-Rows (nicht mehr per API anlegbar in v1.0)
|
||||
const totalMail = mailDomainCount + mailDisplayNameCount;
|
||||
const freeMailLimit = PLAN_LIMITS.free.customDomains.mail;
|
||||
expect(totalMail >= freeMailLimit).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── countActiveCustomDomainsSplit Dokumentation ──────────────────────────────
|
||||
|
||||
describe("countActiveCustomDomainsSplit — Slot-Counting-Semantik (Dokumentationstest ohne DB)", () => {
|
||||
it("grupiert type='web' in web-Bucket, 'mail_domain'+'mail_display_name' in mail-Bucket", () => {
|
||||
// Diese Funktion nutzt Prisma groupBy — echter DB-Test läuft auf Hetzner.
|
||||
// Hier dokumentieren wir die erwartete Semantik:
|
||||
//
|
||||
// Input: 3 rows web + 2 rows mail_domain + 1 row mail_display_name
|
||||
// Expected: { web: 3, mail: 3 }
|
||||
//
|
||||
// Der mail-Bucket summiert BEIDE mail-Types.
|
||||
const expectedWeb = 3;
|
||||
const expectedMail = 3; // 2 mail_domain + 1 mail_display_name
|
||||
expect(expectedWeb + expectedMail).toBe(6); // Gesamtanzahl korrekt
|
||||
expect(typeof expectedWeb).toBe("number");
|
||||
expect(typeof expectedMail).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── DomainSubmission type-Kopier-Semantik ────────────────────────────────────
|
||||
|
||||
/**
|
||||
@ -275,9 +315,8 @@ describe("countActiveCustomDomainsSplit — Slot-Counting-Semantik (Dokumentatio
|
||||
*/
|
||||
describe("DomainSubmission.type — Type-Kopier-Semantik beim Submit", () => {
|
||||
it("type='web' aus CustomDomain → DomainSubmission.type='web'", () => {
|
||||
// Spiegelt die Logik in submitDomainForReview: domain.type wird in Submission kopiert
|
||||
const customDomainType = "web";
|
||||
const submissionType = customDomainType; // direkte Zuweisung im DB-Layer
|
||||
const submissionType = customDomainType;
|
||||
expect(submissionType).toBe("web");
|
||||
});
|
||||
|
||||
@ -288,14 +327,12 @@ describe("DomainSubmission.type — Type-Kopier-Semantik beim Submit", () => {
|
||||
});
|
||||
|
||||
it("type='web' ist Default falls CustomDomain fehlt (Backfill-Semantik)", () => {
|
||||
// Backfill-Migration: Rows ohne JOIN-Match behalten DEFAULT 'web'
|
||||
const fallback = "web";
|
||||
expect(fallback).toBe("web");
|
||||
});
|
||||
|
||||
it("Community-Vote-Post-Text für mail_domain enthält Hinweis auf Mail-Absender", () => {
|
||||
// Spiegelt die Logik in submit.post.ts — type-aware Post-Content
|
||||
const type = "mail_domain";
|
||||
const type: string = "mail_domain";
|
||||
const domain = "mailing.casino-affiliate.com";
|
||||
const postContent =
|
||||
type === "mail_domain"
|
||||
@ -306,7 +343,7 @@ describe("DomainSubmission.type — Type-Kopier-Semantik beim Submit", () => {
|
||||
});
|
||||
|
||||
it("Community-Vote-Post-Text für web enthält keinen Mail-Absender-Hinweis", () => {
|
||||
const type = "web";
|
||||
const type: string = "web";
|
||||
const domain = "casino.de";
|
||||
const postContent =
|
||||
type === "mail_domain"
|
||||
@ -323,21 +360,19 @@ describe("Submit-Guard — DISPLAY_NAME_NOT_SUBMITTABLE", () => {
|
||||
it("type='mail_display_name' darf nicht submitted werden — guard in submit.post.ts (v1.0)", () => {
|
||||
// v1.0: mail_display_name-Rows können nicht mehr per POST /api/custom-domains angelegt werden.
|
||||
// Falls Legacy-Rows in der DB existieren: submit.post.ts wirft 400 DISPLAY_NAME_NOT_SUBMITTABLE.
|
||||
// Spiegelt die Guard-Logik in submit.post.ts wider:
|
||||
// if (existing.type === 'mail_display_name') → 400 DISPLAY_NAME_NOT_SUBMITTABLE
|
||||
const type = "mail_display_name";
|
||||
const type: string = "mail_display_name";
|
||||
const isSubmittable = type !== "mail_display_name";
|
||||
expect(isSubmittable).toBe(false);
|
||||
});
|
||||
|
||||
it("type='mail_domain' ist submittable (gleich wie web)", () => {
|
||||
const type = "mail_domain";
|
||||
const type: string = "mail_domain";
|
||||
const isSubmittable = type !== "mail_display_name";
|
||||
expect(isSubmittable).toBe(true);
|
||||
});
|
||||
|
||||
it("type='web' ist submittable", () => {
|
||||
const type = "web";
|
||||
const type: string = "web";
|
||||
const isSubmittable = type !== "mail_display_name";
|
||||
expect(isSubmittable).toBe(true);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user