Files
Laufzettel/tests/betriebsstand.test.ts
T
Florian WildeandClaude Opus 5 80d97467da
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
fix: Reviewbefunde aus der Durchsicht von Phase 2 (#036, #027)
Acht Meilensteine lagen ohne fremden Blick auf main. Ein Review mit eigener
Datenbank, Gegenproben und vollem Suitenlauf hat sie gegen die
Akzeptanzkriterien und gegen CLAUDE.md geprueft.

Die Kernregeln halten: fuenf von fuenf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde
lagen am Rand — und die drei wertvollsten waren Gegenproben, die gruen blieben.

Schwer:

- Der Link in der Mail "Verzeichnis-Abgleich fehlgeschlagen" war ein 404
  (/verwaltung/verzeichnis statt /admin/verzeichnis). Die eine Meldung, die an
  alle Administratoren geht, im Moment eines Ausfalls. Ein Test prueft jetzt
  jeden Meldungspfad gegen die vorhandenen Routen.
- Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt unter
  dem Platzhalter "wird ersetzt" — der Zweck des Formats ist, eine Vorlage in
  eine zweite Installation zu bringen. Bausteine werden jetzt aufgeloest.

Mittel:

- Der Anhang-Handler hatte keinen Test; die ganze Rechtepruefung liess sich
  durch einen hartcodierten ADMIN ersetzen, Suite gruen. Es ist die einzige
  Stelle, die rohe Dateibytes ausliefert. tests/anhang-route.test.ts faehrt ihn
  jetzt echt, acht Pruefungen.
- /api/scim in der Proxy-Allowlist stand in keinem Test. OEFFENTLICH ist
  exportiert, der Test vergleicht beide Richtungen: eine Liste, die nur in einer
  Richtung geprueft wird, laeuft dem Code hinterher.
- Ein offener Ereignisstrom ueberlebte Abmelden, Deaktivieren und
  Sitzungswiderruf. Der Herzschlag prueft die Sitzung jetzt nach. Dass es kein
  Test zeigte, lag an einer Attrappe `sitzung: {}` — eine Attrappe ohne die Form
  der Wirklichkeit prueft etwas anderes als die Wirklichkeit.
- README kannte MITARBEITER nicht und behauptete, unbekannte Benutzer wuerden
  angelegt. Wer auf v0.2.0 hebt und nur das README liest, haette genau diese
  Leute ausgesperrt.
- sicherung.md nannte einen Anhaenge-Pfad, den es nirgends gibt — als
  Rueckfallwert in einem Skript mit rm -rf. Jetzt bricht es ab statt zu raten.
- Attributnamen wurden nur in der Server Action geprueft, waehrend der
  Graph-Leser sich im Kommentar darauf berief.

Dazu #027: die Ende-zu-Ende-Ablaeufe raeumen jetzt zentral auf, vor und nach
dem Lauf, und ein struktureller Test bewacht es. Beim ersten echten Lauf seit
dem 26. September waren sieben Ablaeufe rot, vier Ursachen, keine ein
Produktfehler — unbemerkt, weil pnpm test:e2e in keinem Tor steht. Das ist jetzt
#038 und keine Fussnote.

Ein Befund des Reviews war falsch (der Zustands-Handler habe keinen Test; er hat
acht direkte Aufrufe). Nachgeprueft, bevor etwas geaendert wurde.

1454 Vitest-Tests (vorher 1432), 113 E2E-Ablaeufe gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-11 01:32:09 +02:00

153 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync, readdirSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { E2E_PRAEFIX } from "./e2e/aufraeumen";
import { parse } from "yaml";
/**
* Die Datenbank im Betriebsstand ist absichtlich von aussen nicht erreichbar.
* Diese Tests halten das fest, damit ein ports:-Mapping nicht versehentlich
* zurueckkommt. Siehe docs/betrieb/inbetriebnahme.md.
*/
function compose(datei: string): Record<string, never> {
return parse(readFileSync(datei, "utf8")) as Record<string, never>;
}
describe("Betriebsstand: docker-compose.yml", () => {
const stand = compose("docker-compose.yml") as unknown as {
services: Record<string, { ports?: unknown[]; networks?: string[] }>;
networks: Record<string, { internal?: boolean } | null>;
};
it("veroeffentlicht keinen Port der Datenbank", () => {
expect(stand.services["postgres"]?.ports).toBeUndefined();
});
it("haengt die Datenbank nur in das interne Netz", () => {
expect(stand.services["postgres"]?.networks).toEqual(["intern"]);
});
it("hat das interne Netz ohne Gateway", () => {
expect(stand.networks["intern"]?.internal).toBe(true);
});
it("laesst nur den Webdienst in das externe Netz", () => {
const aussen = Object.entries(stand.services)
.filter(([, dienst]) => dienst.networks?.includes("extern"))
.map(([name]) => name);
expect(aussen).toEqual(["web"]);
});
it("bindet den Webdienst standardmaessig nur auf localhost", () => {
expect(stand.services["web"]?.ports).toEqual(["${BIND_ADDR:-127.0.0.1}:${APP_PORT:-3000}:3000"]);
});
it("prueft die Gesundheit des Webdienstes ueber IPv4, nicht ueber localhost", () => {
// busybox-wget loest localhost zuerst nach ::1 auf, Next lauscht nur auf
// 0.0.0.0:3000. Mit localhost stand web dauerhaft auf (unhealthy), obwohl
// die App antwortete (#019).
const probe = (stand.services["web"] as unknown as { healthcheck?: { test?: string[] } }).healthcheck
?.test;
expect(probe).toEqual(["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/gesundheit"]);
});
it("hat ein Volume für die Anhänge – sonst sind sie beim nächsten Neuaufbau weg", () => {
/*
* Die Dateien der Anhaenge liegen im Dateisystem, in der Datenbank stehen
* nur die Metadaten (#032). Ohne Volume lebten sie im Container: ein
* `docker compose up --build` waere Datenverlust, und die Zeilen zeigten auf
* Dateien, die es nicht mehr gibt.
*/
const web = stand.services["web"] as unknown as {
volumes?: string[];
environment?: Record<string, string>;
};
expect(web.volumes).toContain("anhaenge:/daten/anhaenge");
expect(web.environment?.["ANHAENGE_PFAD"]).toBe("/daten/anhaenge");
expect(Object.keys((stand as unknown as { volumes: Record<string, unknown> }).volumes)).toContain(
"anhaenge",
);
});
it("nennt das Anhang-Volume in der Sicherungsanleitung", () => {
// Eine Sicherung, die nur `pg_dump` macht, verliert ab #032 Daten. Dass die
// Anleitung das sagt, ist Teil der Zusage – nicht nur das Volume selbst.
const sicherung = readFileSync("docs/betrieb/sicherung.md", "utf8");
expect(sicherung).toContain("anhaenge");
expect(sicherung).toMatch(/ANHAENGE_PFAD/);
});
it("bricht ohne Geheimnisse ab, statt mit Standardwerten zu starten", () => {
const roh = readFileSync("docker-compose.yml", "utf8");
expect(roh).toContain("${APP_SECRET:?");
expect(roh).toContain("${POSTGRES_PASSWORD:?");
expect(roh).toContain("${APP_URL:?");
});
});
describe("Die Ende-zu-Ende-Abläufe räumen auf", () => {
/*
* **Der Test, der die Lücke beim nächsten Meilenstein fängt** (#027).
*
* In der Entwicklungsdatenbank standen 105 Vorlagen mit Namen wie
* `ZZ E2E Editor 1790324192596 C`. Der Editor-Ablauf räumt seit #018 selbst
* auf — aber zwei Abläufe aus Phase 2 (`anhaenge`, `live`) legten Zeilen an
* und hatten keinen Nachlauf. Das fiel nicht auf, weil die Suite davon nichts
* sieht: Reste sind kein Testfehler, sie sind ein Schaden an allem, was
* danach in dieselbe Datenbank sieht.
*
* Geräumt wird jetzt zentral in `tests/e2e/aufraeumen.ts`, vor **und** nach
* dem Lauf. Damit das trägt, muss zweierlei gelten, und beides steht hier:
* jeder Ablauf benutzt das Präfix, und der Vor- und Nachlauf sind registriert.
*/
const ABLAEUFE = readdirSync("tests/e2e").filter((datei) => datei.endsWith(".spec.ts"));
it("findet überhaupt Abläufe – sonst prüft der Test darunter nichts", () => {
// Ohne diese Zeile wäre eine leere Liste ein grüner Test.
expect(ABLAEUFE.length).toBeGreaterThan(10);
});
it("benennt jede angelegte Zeile mit dem Präfix, an dem aufgeräumt wird", () => {
/*
* Geprüft wird die **Umkehrung**: ein Ablauf, der Daten anlegt, ohne das
* Präfix zu benutzen, ist von keinem Aufräumen erfasst. Erkennbar ist das
* daran, dass er ein Formularfeld „Name“ oder „Titel“ füllt.
*/
const ohnePraefix: string[] = [];
for (const datei of ABLAEUFE) {
const inhalt = readFileSync(`tests/e2e/${datei}`, "utf8");
const legtAn = /getByLabel\((?:"|')(?:Name|Titel|Bezeichnung)/u.test(inhalt);
if (legtAn && !inhalt.includes(E2E_PRAEFIX)) ohnePraefix.push(datei);
}
expect(
ohnePraefix,
`Diese Abläufe legen Zeilen an, ohne „${E2E_PRAEFIX}“ zu benutzen — das zentrale ` +
"Aufräumen findet sie nicht wieder.",
).toEqual([]);
});
it("hat Vorlauf und Nachlauf registriert", () => {
/*
* Beides, und das ist der Punkt: der Nachlauf läuft nach einem Testfehler
* und nach einem Zeitablauf, aber nicht nach `Strg+C`. Dann findet der
* Vorlauf des nächsten Durchgangs die Reste — und ein Vorlauf lässt sich
* nicht überspringen.
*/
const konfiguration = readFileSync("playwright.config.ts", "utf8");
expect(konfiguration).toContain("global-setup");
expect(konfiguration).toContain("global-teardown");
expect(readFileSync("tests/e2e/global-setup.ts", "utf8")).toContain("e2eAufraeumenMitBericht");
});
});
describe("Betriebsstand: Entwicklungsdatenbank", () => {
it("bindet nur auf localhost und auf einen eigenen, umstellbaren Port", () => {
const stand = compose("docker-compose.dev.yml") as unknown as {
services: Record<string, { ports?: string[] }>;
};
// Der Standard ist 5434, weil 5432 lokal und 5433 von einem anderen Projekt
// belegt sein koennen. Umstellbar ueber DEV_DB_PORT.
expect(stand.services["postgres"]?.ports).toEqual(["127.0.0.1:${DEV_DB_PORT:-5434}:5432"]);
});
});