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 { return parse(readFileSync(datei, "utf8")) as Record; } describe("Betriebsstand: docker-compose.yml", () => { const stand = compose("docker-compose.yml") as unknown as { services: Record; networks: Record; }; 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; }; expect(web.volumes).toContain("anhaenge:/daten/anhaenge"); expect(web.environment?.["ANHAENGE_PFAD"]).toBe("/daten/anhaenge"); expect(Object.keys((stand as unknown as { volumes: Record }).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; }; // 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"]); }); });