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:
|
* Testet:
|
||||||
* - getPlanLimits returnt strukturiertes { web, mail } Objekt
|
* - getPlanLimits returnt customDomains als ZAHL (kein { web, mail } Objekt mehr)
|
||||||
* - countActiveCustomDomainsSplit returnt korrekte Split-Counts (Unit ohne DB)
|
* - 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 Domain-Pattern → mail_domain
|
||||||
* - POST-Body-Compat: { pattern, kind: 'mail' } mit Display-Name-Pattern → 400 INVALID_MAIL_DOMAIN (v1.0)
|
* - 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: { 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
|
* - POST-Body-Compat: volle Adresse (local@domain.tld) → local-part gestripped, gespeichert als mail_domain
|
||||||
* - Submit eines mail_domain → erlaubt (gleich wie web)
|
* - 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.
|
* 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 ────────────────────────────────────────────────────────
|
// ─── Plan-Limits Shape ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("getPlanLimits — customDomains ist strukturiertes Objekt", () => {
|
describe("getPlanLimits — customDomains ist ZAHL (gemeinsamer Slot-Pool)", () => {
|
||||||
it("Free: customDomains = { web: 5, mail: 5 }", () => {
|
it("Pro: customDomains = 10", () => {
|
||||||
const limits = getPlanLimits("free");
|
|
||||||
expect(limits.customDomains).toEqual({ web: 5, mail: 5 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Pro: customDomains = { web: 5, mail: 5 }", () => {
|
|
||||||
const limits = getPlanLimits("pro");
|
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");
|
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");
|
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");
|
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", () => {
|
it("Legacy 'free' → Pro limits (customDomains = 10, kein free-Tier mehr)", () => {
|
||||||
const val = PLAN_LIMITS.free.customDomains;
|
const limits = getPlanLimits("free");
|
||||||
expect(typeof val).not.toBe("number");
|
expect(limits.customDomains).toBe(10);
|
||||||
expect(typeof val).toBe("object");
|
});
|
||||||
expect(typeof val.web).toBe("number");
|
|
||||||
expect(typeof val.mail).toBe("number");
|
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 ────────────────────────────────────
|
// ─── DomainSubmission type-Kopier-Semantik ────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -275,9 +315,8 @@ describe("countActiveCustomDomainsSplit — Slot-Counting-Semantik (Dokumentatio
|
|||||||
*/
|
*/
|
||||||
describe("DomainSubmission.type — Type-Kopier-Semantik beim Submit", () => {
|
describe("DomainSubmission.type — Type-Kopier-Semantik beim Submit", () => {
|
||||||
it("type='web' aus CustomDomain → DomainSubmission.type='web'", () => {
|
it("type='web' aus CustomDomain → DomainSubmission.type='web'", () => {
|
||||||
// Spiegelt die Logik in submitDomainForReview: domain.type wird in Submission kopiert
|
|
||||||
const customDomainType = "web";
|
const customDomainType = "web";
|
||||||
const submissionType = customDomainType; // direkte Zuweisung im DB-Layer
|
const submissionType = customDomainType;
|
||||||
expect(submissionType).toBe("web");
|
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)", () => {
|
it("type='web' ist Default falls CustomDomain fehlt (Backfill-Semantik)", () => {
|
||||||
// Backfill-Migration: Rows ohne JOIN-Match behalten DEFAULT 'web'
|
|
||||||
const fallback = "web";
|
const fallback = "web";
|
||||||
expect(fallback).toBe("web");
|
expect(fallback).toBe("web");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Community-Vote-Post-Text für mail_domain enthält Hinweis auf Mail-Absender", () => {
|
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: string = "mail_domain";
|
||||||
const type = "mail_domain";
|
|
||||||
const domain = "mailing.casino-affiliate.com";
|
const domain = "mailing.casino-affiliate.com";
|
||||||
const postContent =
|
const postContent =
|
||||||
type === "mail_domain"
|
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", () => {
|
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 domain = "casino.de";
|
||||||
const postContent =
|
const postContent =
|
||||||
type === "mail_domain"
|
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)", () => {
|
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.
|
// 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.
|
// 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:
|
const type: string = "mail_display_name";
|
||||||
// if (existing.type === 'mail_display_name') → 400 DISPLAY_NAME_NOT_SUBMITTABLE
|
|
||||||
const type = "mail_display_name";
|
|
||||||
const isSubmittable = type !== "mail_display_name";
|
const isSubmittable = type !== "mail_display_name";
|
||||||
expect(isSubmittable).toBe(false);
|
expect(isSubmittable).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("type='mail_domain' ist submittable (gleich wie web)", () => {
|
it("type='mail_domain' ist submittable (gleich wie web)", () => {
|
||||||
const type = "mail_domain";
|
const type: string = "mail_domain";
|
||||||
const isSubmittable = type !== "mail_display_name";
|
const isSubmittable = type !== "mail_display_name";
|
||||||
expect(isSubmittable).toBe(true);
|
expect(isSubmittable).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("type='web' ist submittable", () => {
|
it("type='web' ist submittable", () => {
|
||||||
const type = "web";
|
const type: string = "web";
|
||||||
const isSubmittable = type !== "mail_display_name";
|
const isSubmittable = type !== "mail_display_name";
|
||||||
expect(isSubmittable).toBe(true);
|
expect(isSubmittable).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user