test: Geldpfade absichern und Go-Live-Pruefung ehrlich machen
Die beiden Dateien, in denen Geld und Lagerbestand zusammenlaufen, hatten bisher keinen einzigen Test. Dazu meldete die Go-Live-Pruefung gruene Haken fuer Werte, die gar nicht gesetzt waren. Tests fuer api/checkout/route.ts (10 Faelle) Torwaechter (fremder Origin, Turnstile, Rate-Limit), deaktiviertes Produkt, Preis 0, ausverkaufte Ware, Sitzungslebensdauer — und die beiden Regressionen: scheitert die Stripe-Sitzung, wird die Bestellung aufgeloest und OHNE orderNumber geantwortet; laesst sich die Sitzungs-ID nicht speichern, wird die Sitzung geschlossen. Per Mutationstest geprueft, dass der zweite Fall ohne den Fix rot wird. Tests fuer api/checkout/webhook/route.ts (12 Faelle) Signaturpruefung, verzoegerte Zahlung (SEPA), Auslieferung samt Rechnung und Anhang, mehrfach zugestelltes Ereignis (Gutschein und Rechnungsnummer duerfen sich nicht wiederholen), Zahlung auf eine stornierte Bestellung, Betragsabweichung, abgelaufene Sitzung, fehlgeschlagener Einzug, volle und teilweise Erstattung, Rueckbuchung. Datenbank und Stripe sind in beiden Dateien vollstaendig ersetzt. Go-Live-Pruefung: falsch-positive Haken beseitigt Regeln, die einen Wert voraussetzen, bestanden bei fehlendem Wert einfach — eine leere Zeichenkette enthaelt nun einmal kein "example.com". Der Bericht meldete deshalb "NEXT_PUBLIC_SITE_URL ist die echte Domain" als bestanden, direkt neben dem Blocker "NEXT_PUBLIC_SITE_URL gesetzt". Ohne jeden Stripe-Schluessel galt sogar der Live-Modus als bestaetigt. Das ist die gefaehrlichste Fehlmeldung, die eine solche Pruefung machen kann: sie beruhigt ueber genau den Punkt, der offen ist. mustWhenSet/shouldWhenSet lassen Folgeregeln bei fehlendem Wert entfallen. Ausserdem sagt der Bericht jetzt ausdruecklich, dass er die Umgebung prueft, in der er LAEUFT. Lokal meldet er Blocker, die in der ConfigMap des Clusters laengst erledigt sind; wer das ein paarmal erlebt, liest ueber den Bericht hinweg. Das README nennt den Aufruf im Pod. tsc: 0 Fehler. vitest: 316/316 gruen (zuvor 294). eslint: 0 Warnungen. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
1fa8dc9ce2
commit
dbf146c19f
4 files changed
+732
-12
No files matched your search
+58
-12
@@ -43,6 +43,33 @@ function should(label: string, ok: boolean, hint: string): void {
|
||||
const value = (name: string): string => (process.env[name] ?? "").trim();
|
||||
const isSet = (name: string): boolean => value(name).length > 0;
|
||||
|
||||
/**
|
||||
* Eine Regel, die einen GESETZTEN Wert voraussetzt.
|
||||
*
|
||||
* Ohne diese Weiche meldete der Bericht gruene Haken fuer Variablen, die gar
|
||||
* nicht existieren: "NEXT_PUBLIC_SITE_URL ist die echte Domain, kein
|
||||
* Platzhalter" stand als bestanden da, weil eine leere Zeichenkette nun einmal
|
||||
* kein "example.com" enthaelt — direkt neben dem Blocker
|
||||
* "NEXT_PUBLIC_SITE_URL gesetzt". Dasselbe galt fuer den Test auf einen
|
||||
* sk_test-Schluessel: ohne jeden Stripe-Schluessel galt der Live-Modus als
|
||||
* bestaetigt.
|
||||
*
|
||||
* Das ist die gefaehrlichste Art von Fehlmeldung, die eine Go-Live-Pruefung
|
||||
* machen kann — sie beruhigt ueber genau den Punkt, der offen ist. Fehlt der
|
||||
* Wert, entfaellt die Folgeregel hier stillschweigend; gemeldet wird er
|
||||
* ohnehin schon von der Regel, die seine blosse Existenz prueft.
|
||||
*/
|
||||
function mustWhenSet(name: string, label: string, ok: (raw: string) => boolean, hint: string): void {
|
||||
if (!isSet(name)) return;
|
||||
must(label, ok(value(name)), hint);
|
||||
}
|
||||
|
||||
/** Wie mustWhenSet, nur als Warnung statt als Blocker. */
|
||||
function shouldWhenSet(name: string, label: string, ok: (raw: string) => boolean, hint: string): void {
|
||||
if (!isSet(name)) return;
|
||||
should(label, ok(value(name)), hint);
|
||||
}
|
||||
|
||||
// --- Sitzungs-/Zugangsgeheimnisse -----------------------------------------
|
||||
const sessionSecret = value("SESSION_SECRET");
|
||||
must(
|
||||
@@ -50,9 +77,10 @@ must(
|
||||
sessionSecret.length >= 32,
|
||||
"Signiert die Login-Cookies. Erzeugen mit: openssl rand -base64 32"
|
||||
);
|
||||
must(
|
||||
mustWhenSet(
|
||||
"SESSION_SECRET",
|
||||
"SESSION_SECRET ist nicht mehr der Beispielwert",
|
||||
!sessionSecret.includes("change-me"),
|
||||
(raw) => !raw.includes("change-me"),
|
||||
"Der Wert aus .env.example ist oeffentlich bekannt — jede Sitzung waere faelschbar."
|
||||
);
|
||||
|
||||
@@ -75,14 +103,16 @@ must(
|
||||
siteUrl.length > 0,
|
||||
"Einzige Quelle fuer Sitemap, Canonical-Tags, JSON-LD und alle Links in E-Mails (src/lib/url.ts)."
|
||||
);
|
||||
must(
|
||||
mustWhenSet(
|
||||
"NEXT_PUBLIC_SITE_URL",
|
||||
"NEXT_PUBLIC_SITE_URL nutzt HTTPS und nicht localhost",
|
||||
siteUrl.startsWith("https://") && !siteUrl.includes("localhost"),
|
||||
(raw) => raw.startsWith("https://") && !raw.includes("localhost"),
|
||||
"Bei HTTP oder localhost landen genau diese Adressen in Bestellbestaetigungen und in der Sitemap."
|
||||
);
|
||||
must(
|
||||
mustWhenSet(
|
||||
"NEXT_PUBLIC_SITE_URL",
|
||||
"NEXT_PUBLIC_SITE_URL ist die echte Domain, kein Platzhalter",
|
||||
!siteUrl.includes("example.com") && !siteUrl.includes("example.org"),
|
||||
(raw) => !raw.includes("example.com") && !raw.includes("example.org"),
|
||||
'Die echte Domain steht in k8s/01-configmap.yaml (NEXT_PUBLIC_SITE_URL) und k8s/06-ingress.yaml (host/tls.hosts) und muss an beiden Stellen dieselbe sein. Steht hier eine Beispiel-Domain, zeigen Sitemap, Canonical-Tags und jeder Link in jeder Bestellbestaetigung auf eine fremde Domain.'
|
||||
);
|
||||
|
||||
@@ -97,9 +127,10 @@ must(
|
||||
isSet("STRIPE_WEBHOOK_SECRET"),
|
||||
'Ohne Webhook-Secret wird keine Zahlung bestaetigt: Bestellungen bleiben auf "NEU" stehen, es gibt keine Rechnung und keine Bestaetigungsmail.'
|
||||
);
|
||||
must(
|
||||
mustWhenSet(
|
||||
"STRIPE_SECRET_KEY",
|
||||
"Stripe laeuft im Live-Modus (kein sk_test-Schluessel)",
|
||||
!value("STRIPE_SECRET_KEY").startsWith("sk_test"),
|
||||
(raw) => !raw.startsWith("sk_test"),
|
||||
"Mit einem Test-Schluessel zahlt niemand echtes Geld — Bestellungen sehen erfolgreich aus, ohne dass Umsatz entsteht."
|
||||
);
|
||||
|
||||
@@ -109,9 +140,12 @@ must(
|
||||
isSet("SMTP_HOST") && isSet("SMTP_USER") && isSet("SMTP_PASSWORD"),
|
||||
"Ohne SMTP werden E-Mails nur ins Server-Log geschrieben (siehe src/lib/mail.ts): keine Bestellbestaetigung, keine Versandmeldung, keine Passwort-Zuruecksetzung."
|
||||
);
|
||||
should(
|
||||
// Ohne SMTP_FROM greift der Standardabsender aus lib/mail.ts — deshalb nur
|
||||
// pruefen, wenn tatsaechlich ein eigener Wert hinterlegt ist.
|
||||
shouldWhenSet(
|
||||
"SMTP_FROM",
|
||||
"SMTP_FROM zeigt auf eine eigene Domain",
|
||||
!value("SMTP_FROM").includes("example.com"),
|
||||
(raw) => !raw.includes("example.com"),
|
||||
"Absenderadressen fremder Domains landen regelmaessig im Spam-Ordner."
|
||||
);
|
||||
|
||||
@@ -126,9 +160,10 @@ must(
|
||||
vatRate.length > 0 && Number.isInteger(vatRateNumber) && vatRateNumber >= 0 && vatRateNumber <= 100,
|
||||
"Wird auf jeder Rechnung ausgewiesen. 7 = ermaessigt (Honig), 19 = voll, 0 = Kleinunternehmerregelung nach § 19 UStG. Ohne gueltigen Wert rechnet die Anwendung mit 7% weiter — fuer Kerzen zu niedrig, fuer Kleinunternehmer schlicht falsch."
|
||||
);
|
||||
should(
|
||||
shouldWhenSet(
|
||||
"VAT_RATE_PERCENT",
|
||||
"Umsatzsteuersatz ist ein in Deutschland ueblicher Wert",
|
||||
["0", "7", "19"].includes(vatRate),
|
||||
(raw) => ["0", "7", "19"].includes(raw),
|
||||
`Eingetragen ist "${vatRate}". Ueblich sind 0 (§ 19 UStG), 7 (ermaessigt) oder 19 (voll) — ein anderer Wert ist moeglich, aber pruefenswert.`
|
||||
);
|
||||
|
||||
@@ -295,6 +330,17 @@ function report(): void {
|
||||
// --- Ausgabe ---------------------------------------------------------------
|
||||
const line = "─".repeat(72);
|
||||
console.log(`\n${line}\nGo-Live-Pruefung Wabenhain\n${line}`);
|
||||
// Ohne diesen Hinweis wird der Bericht regelmaessig falsch gelesen: auf einem
|
||||
// Entwicklungsrechner meldet er Blocker fuer Werte, die in der ConfigMap des
|
||||
// Clusters laengst richtig stehen (NEXT_PUBLIC_SITE_URL, SMTP_FROM,
|
||||
// VAT_RATE_PERCENT). Wer das ein paarmal erlebt, sieht ueber den Bericht
|
||||
// hinweg — und damit auch ueber den einen Blocker, der echt ist.
|
||||
console.log(
|
||||
`
|
||||
Geprueft wird die Umgebung, in der dieses Skript LAEUFT. Fuer eine Aussage
|
||||
ueber den Live-Betrieb im Pod ausfuehren, nicht auf dem eigenen Rechner —
|
||||
siehe README, Abschnitt "Vor dem Live-Gang".`
|
||||
);
|
||||
console.log(`\n✔ Bestanden: ${passed.length}`);
|
||||
for (const item of passed) console.log(` ✔ ${item}`);
|
||||
|
||||
|
||||
Reference in new issue
Block a user