fix(abnahme): Healthcheck schweigt, zwei Konventionen praezisiert (#41)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #41.
This commit is contained in:
2026-10-04 14:12:28 +00:00
parent 9027229b55
commit 88d7db8c3d
12 changed files with 307 additions and 31 deletions
+24
View File
@@ -5,6 +5,30 @@ Alle nennenswerten Änderungen an Laufzettel werden in dieser Datei dokumentiert
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
## [Unveröffentlicht]
### Geändert
- Der Healthcheck `/api/gesundheit` nennt im Fehlerfall keinen Grund mehr. Der Pfad ist ohne
Anmeldung erreichbar, und eine Meldung von Prisma oder `pg` nennt Host, Port, Benutzernamen und
Datenbanknamen. Nach außen steht jetzt nur noch `{"status":"fehler"}` mit Status 503; die
Begründung geht ins Protokoll des Prozesses (#039)
- Die Konvention „nie auf rohe Hexwerte“ nennt ihre sechs begründeten Ausnahmen beim Namen —
Druckansichten, QR-Code und `themeColor` entscheiden nicht nach unserem Farbschema. Die Regel war
strenger formuliert als gemeint (#040)
- `CLAUDE.md` und `README.md` nennen den LDAP-Testcontainer als Teil des Tors vor dem Commit. Ohne
ihn überspringt die Suite **22 Tests** — den ganzen Verzeichnis-Abgleich —, sagt das auf stderr,
und der Lauf sieht trotzdem grün aus (#040)
- Im README fehlte `format:pruefen` in den Qualitätsschranken, obwohl `CLAUDE.md` es seit #010
verlangt (#040)
### Hinzugefügt
- `pnpm pruefe:doku` vergleicht die Schranken-Befehle in `CLAUDE.md` und `README.md` und verlangt von
beiden den LDAP-Container. Genau diese Abweichung war unbemerkt entstanden (#040)
- Fünf Tests für den Healthcheck, darunter einer, der belegt, dass die Begründung ins Protokoll
geschrieben und nicht bloß verschluckt wird (#039)
## [0.1.0] – 2026-09-26
### Hinzugefügt
+18
View File
@@ -176,9 +176,16 @@ speichern, neu laden, und nachsehen, ob es wirklich in der Datenbank steht.
## Vor jedem Commit
```bash
docker compose -f docker-compose.test.yml up -d --wait ldap
pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test && pnpm pruefe:doku
```
**Der LDAP-Container gehört in die erste Zeile.** Ohne ihn überspringt die Suite **22 Tests** — den
ganzen Verzeichnis-Abgleich (nachgezählt: 81 statt 59 in `tests/abgleich.test.ts` und
`tests/verzeichnis.test.ts`). Sie sagt das auf stderr, aber der Lauf sieht grün aus, und wer nur
diesem Abschnitt folgte, hat den Abgleich nie geprüft. In CI bricht sie stattdessen ab, absichtlich.
Aufgefallen bei der Abnahme zu #019.
`format:pruefen` gehört dazu, seit der Review in #010 darauf gestoßen ist: `lint-staged` formatiert
beim Commit automatisch nach, und das Ergebnis wäre ein Commit mit Änderungen, die niemand gesehen
hat. Lieber vorher sehen, was sich ändert.
@@ -198,6 +205,17 @@ Abschnitte mit zweistelliger Nummer (`01 · Vorbereitung`), die aus der Position
kommt und nie gespeichert wird. Druckansichten und Etiketten immer hell.
**Am Handy nie horizontales Scrollen** — bei 390 px geprüft.
**„Nie auf rohe Hexwerte" meint die Oberfläche.** Es gibt genau sechs begründete Ausnahmen, und sie
haben alle denselben Grund: dort entscheidet nicht unser Farbschema, sondern etwas außerhalb davon.
| Stelle | Wert | Warum kein Token |
|---|---|---|
| `src/app/drucken/lauf/[id]/page.tsx`, `src/app/drucken/etiketten/page.tsx` | `#ffffff` | Druck ist immer hell, unabhängig vom Erscheinungsbild der Anwendung |
| `src/lib/qr.ts` | `#000000ff` / `#ffffffff` | Ein QR-Code muss reines Schwarz auf reinem Weiß sein, sonst liest ihn kein Decoder |
| `src/app/layout.tsx` (`themeColor`), `src/app/manifest.ts` (zweimal) | `#0B0D10` | `themeColor` und das Web-App-Manifest vertragen keine CSS-Variablen |
Wer eine siebte hinzufügt, trägt sie hier ein — oder lässt es.
> Der folgende Block ist **nicht von uns**: `next dev` schreibt ihn bei jedem Start selbst in diese
> Datei und stellt ihn wieder her, wenn man ihn löscht. Er steht deshalb hier, auf Englisch, und
> wird mitcommittet, damit der Arbeitsbaum sauber bleibt.
+7 -2
View File
@@ -45,11 +45,16 @@ openssl rand -base64 48
### Qualitätsschranken
```bash
pnpm lint && pnpm typecheck && pnpm test && pnpm pruefe:doku
docker compose -f docker-compose.test.yml up -d --wait ldap # sonst fehlen 22 Tests
pnpm lint && pnpm typecheck && pnpm format:pruefen && pnpm test && pnpm pruefe:doku
```
Der LDAP-Container ist keine Zierde: ohne ihn überspringt die Suite den ganzen Verzeichnis-Abgleich
— sie sagt das auf stderr, aber der Lauf sieht grün aus. In CI bricht sie stattdessen ab.
Ein Pre-Commit-Hook erzwingt Lint und Format, ein Commit-Msg-Hook Conventional Commits.
Ende-zu-Ende-Tests: `pnpm test:e2e`.
Ende-zu-Ende-Tests: `pnpm test:e2e` (verlangt einen gebauten, laufenden Stand — siehe
`docs/anleitung/`).
## Betrieb mit Docker Compose
+1 -1
View File
@@ -249,7 +249,7 @@ Datei nicht neu. Änderungen an `APP_VERSION` oder `GIT_SHA` sind Build-Argument
|---|---|
| `APP_SECRET fehlt in der .env` beim `up` | Genau das. Der Stand bricht absichtlich ab, statt mit einem Standardwert zu starten. Gilt ebenso für `APP_URL` und `POSTGRES_PASSWORD`. |
| `web` startet nicht, `migrate` steht auf `exited (1)` | `docker compose logs migrate`. Meist erreicht die Migration die Datenbank nicht — oder du hast `POSTGRES_PASSWORD` nach dem ersten Start geändert. Das Passwort steckt dann noch im Volume. |
| `/api/gesundheit` antwortet mit 503 | Die Anwendung läuft, die Datenbank antwortet nicht. `docker compose ps` — ist `postgres` gesund? |
| `/api/gesundheit` antwortet mit 503 | Die Anwendung läuft, die Datenbank antwortet nicht. `docker compose ps` — ist `postgres` gesund? Die Antwort nennt absichtlich keinen Grund, weil der Pfad ohne Anmeldung erreichbar ist; der Grund steht in `docker compose logs web`. |
| Der Seed bricht mittendrin ab | Bis zur Abnahme von #019 fehlte dem Einmal-Dienst `APP_SECRET`, und das Bind-Passwort der Beispiel-Verzeichnisquelle ließ sich nicht verschlüsseln — abgebrochen wurde erst, nachdem die Vorlagen schon standen. Steht dein `docker-compose.yml` auf dem aktuellen Stand, ist das behoben; ein erneuter Lauf holt den Rest nach. |
| Nach dem Login wieder auf der Anmeldeseite | Das Cookie ist `Secure`, die Verbindung aber `http`. `APP_URL` auf `https://…` setzen und HTTPS über den Proxy anbieten. |
| Die QR-Codes zeigen auf `localhost:3000` | `APP_URL` ist nicht oder falsch gesetzt. Die Oberfläche warnt unter **Admin** und in der Etikettenansicht davor. Aufkleber, die so gedruckt wurden, sind wertlos — bevor du weiterdruckst, `APP_URL` richtigstellen und `docker compose up -d`. |
+5
View File
@@ -203,6 +203,11 @@ schlimmsten Fall, macht den Unterpfad aber nicht betriebsfähig.
antwortet `{"status":"ok"}` oder mit Status 503. Das ist der richtige Pfad für die Überwachung des
Proxys; `/` leitet ohne Cookie auf die Anmeldeseite um und sagt nichts über den Zustand.
Im Fehlerfall sagt er nur `{"status":"fehler"}` und **nicht, warum** — der Pfad ist ohne Anmeldung
erreichbar, und eine Datenbankmeldung nennt Host, Port, Benutzernamen und Datenbanknamen. Die
Begründung steht im Protokoll des Prozesses: `docker compose logs web` beziehungsweise
`journalctl -u laufzettel -n 50`.
**Die Kamera braucht HTTPS.** Der QR-Scanner in der Anwendung benutzt die Kamera des Geräts, und
Browser geben sie nur über eine gesicherte Verbindung frei (`localhost` ausgenommen). Ohne TLS
scannt am Handy niemand — das ist der praktische Grund, warum der Proxy nicht optional ist.
+50 -1
View File
@@ -22,11 +22,14 @@ als Unterpunkte mit Aufgabe und Dauer.
| #017 | M12 – Seed der sieben Vorlagen | 1 h 30 min |
| #018 | M13 – E2E-Tests, Anleitung, PDF | 2 h 15 min |
| #019 | M14 – Betriebswege und Release v0.1.0 | 5 h 25 min |
| | **Summe** | **35 h 20 min** |
| #025 | Healthcheck gibt die rohe Datenbankfehlermeldung heraus | 25 min |
| #026 | Zwei Konventionen präzisieren | 20 min |
| | **Summe** | **36 h 05 min** |
| Meilenstein | Tatsächlich | Status |
|---|---|---|
| v0.1.0 | 35 h 20 min | veröffentlicht |
| v0.2.0 | 45 min | in Arbeit |
---
@@ -977,6 +980,52 @@ als Unterpunkte mit Aufgabe und Dauer.
---
## 2026-09-26 · Sitzung 16 · 16:00–16:45
**Issues:** #025, #026 (beide aus der Abnahme zu #019)
**Version:** 0.2.0 (unveröffentlicht)
### Erledigt
- **`/api/gesundheit` verrät nichts mehr.** Der Pfad ist ohne Anmeldung erreichbar; eine Meldung von
Prisma oder `pg` nennt Host, Port, Benutzernamen und Datenbanknamen. Nach außen bleibt
`{"status":"fehler"}` mit 503, die Begründung geht ins Protokoll — dasselbe Muster, das
`src/server/actions/verzeichnis.ts` schon für unbekannte Ausnahmen benutzt
- **Fünf Tests** dafür, einer davon für die andere Hälfte der Zusage: dass die Begründung wirklich
**geschrieben** und nicht bloß verschluckt wird
- **Die Hexwert-Regel nennt ihre Ausnahmen.** Sechs Stellen, als Tabelle mit Begründung je Zeile
- **Der LDAP-Container steht jetzt im Tor**, in `CLAUDE.md` und im README — mit der gemessenen Zahl
- **`pnpm pruefe:doku` vergleicht die beiden Schranken-Blöcke** und verlangt von beiden den Container
### Entscheidungen
- **Die Regel wurde ergänzt, nicht der Code geändert.** Alle sechs Hexwerte haben denselben Grund:
dort entscheidet nicht unser Farbschema, sondern etwas außerhalb davon — Papier, ein QR-Decoder,
das Web-App-Manifest. Sie in eine Konstante zu ziehen hätte eine Abstraktion über drei
unverwandte Dinge gelegt und die Begründung versteckt. Die Tabelle in `CLAUDE.md` zeigt sie.
- **Der Hinweis auf den Container reicht nicht, er muss geprüft werden.** Zwei Dateien beschreiben
dasselbe Tor und waren auseinandergelaufen. Ein Satz in einer Konvention hält nur, solange ihn
jemand liest; die sechste Prüfung in `pruefe:doku` hält ihn, auch wenn niemand hinsieht.
### Probleme
- **Ich hätte „rund zwanzig Tests" in eine Konvention geschrieben.** Die Zahl stammte aus dem
Abnahmebericht (dort: 18), nicht aus einer Messung. Nachgezählt sind es **22** — mit Container 81,
ohne 59. Eine ungefähre Zahl in einer verbindlichen Datei ist derselbe Fehlertyp, den dieses
Projekt vierzehn Meilensteine lang gesammelt hat, nur kleiner.
- **Der Review hat eine Ausnahme übersehen.** Er nannte vier rohe Hexwerte, es sind sechs:
`src/lib/qr.ts` fehlte. Das Issue #026 hat die falsche Zahl von ihm übernommen; beim Abhaken wäre
ein richtiger Haken über falschem Text entstanden. Issue-Text korrigiert, dann abgehakt.
- **`docs/architektur.md` trägt die Gestaltungsregel nicht**, obwohl `CLAUDE.md` verlangt, dass
Änderungen dort mitgehen und das Issue es behauptete. Geprüft statt geglaubt.
### Nächster Schritt
- Phase 2 (#002) oder die vier verbliebenen v0.2.0-Issues: #020 Vorgesetzter, #021 stabiler
Schlüssel, #022 Schnellstart pflegen, #023 Protokoll-Ansicht. #024 wartet auf einen PVE-Host.
---
## Phase 1 abgeschlossen: was aus dem Plan wurde
**35 h 20 min** über vierzehn Meilensteine, geschätzt waren **148 h**. Der Faktor ist kein Verdienst,
+7 -7
View File
@@ -1,10 +1,10 @@
# #025 Healthcheck gibt die rohe Datenbankfehlermeldung heraus
**Status:** offen
**Status:** erledigt
**Im Tracker:** [#39](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/39)
**Meilenstein:** v0.2.0
**Labels:** phase-2, backend
**Geschätzt:** 1 h **Tatsächlich:** –
**Geschätzt:** 1 h **Tatsächlich:** 25 min
## Ziel
@@ -23,13 +23,13 @@ Aufgefallen beim Abnahme-Review zu [#019](019-betrieb-release.md).
## Aufgaben
- [ ] Nach außen nur `{ "status": "fehler" }` mit Status 503
- [ ] Die Begründung serverseitig protokollieren, damit sie beim Suchen nicht fehlt
- [ ] Prüfen, ob `docs/betrieb/` irgendwo die Meldung als Diagnosehilfe nennt — dann auf das Log
- [x] Nach außen nur `{ "status": "fehler" }` mit Status 503
- [x] Die Begründung serverseitig protokollieren, damit sie beim Suchen nicht fehlt
- [x] Prüfen, ob `docs/betrieb/` irgendwo die Meldung als Diagnosehilfe nennt — dann auf das Log
verweisen
## Akzeptanzkriterien
- [ ] Test: bei nicht erreichbarer Datenbank enthält die Antwort weder Host noch Port noch
- [x] Test: bei nicht erreichbarer Datenbank enthält die Antwort weder Host noch Port noch
Benutzernamen
- [ ] Test: der Statuscode bleibt 503, damit Docker und der Reverse Proxy weiter richtig entscheiden
- [x] Test: der Statuscode bleibt 503, damit Docker und der Reverse Proxy weiter richtig entscheiden
+22 -14
View File
@@ -1,22 +1,27 @@
# #026 Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind
**Status:** offen
**Status:** erledigt
**Im Tracker:** [#40](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/40)
**Meilenstein:** v0.2.0
**Labels:** phase-2, docs
**Geschätzt:** 1 h **Tatsächlich:** –
**Geschätzt:** 1 h **Tatsächlich:** 20 min
## Ziel
Zwei Sätze in `CLAUDE.md` (und gleichlautend in `docs/architektur.md`) beschreiben den Code nicht
genau. Beide fielen beim Abnahme-Review zu [#019](019-betrieb-release.md) auf.
Zwei Sätze in `CLAUDE.md` beschreiben den Code nicht genau. Beide fielen beim Abnahme-Review zu
[#019](019-betrieb-release.md) auf.
**Nachtrag aus der Umsetzung:** `docs/architektur.md` trägt die Gestaltungsregel gar nicht — sie
steht nur in `CLAUDE.md`. Und es sind **sechs** Ausnahmen, nicht vier: der Review hat `src/lib/qr.ts`
übersehen, wo der QR-Code reines Schwarz auf reinem Weiß braucht, damit ein Decoder ihn liest.
**1. Rohe Hexwerte.** Die Regel sagt „Komponenten greifen nur darauf zu, nie auf rohe Hexwerte“.
Tatsächlich stehen vier begründete Ausnahmen im Code: `#ffffff` in `drucken/lauf/[id]/page.tsx` und
`drucken/etiketten/page.tsx` (Druck ist immer hell, unabhängig vom Farbschema der Anwendung) sowie
`#0B0D10` in `layout.tsx` und `manifest.ts` (`themeColor` und das Manifest vertragen keine
CSS-Variablen). Die Regel ist strenger formuliert als gemeint — sie soll um diese Ausnahmen ergänzt
werden, nicht der Code geändert.
Tatsächlich stehen sechs begründete Ausnahmen im Code: `#ffffff` in `drucken/lauf/[id]/page.tsx` und
`drucken/etiketten/page.tsx` (Druck ist immer hell, unabhängig vom Farbschema der Anwendung),
`#000000ff`/`#ffffffff` in `src/lib/qr.ts` (ein QR-Code muss reines Schwarz auf reinem Weiß sein)
sowie `#0B0D10` in `layout.tsx` und zweimal in `manifest.ts` (`themeColor` und das Manifest
vertragen keine CSS-Variablen). Die Regel ist strenger formuliert als gemeint — sie soll um diese
Ausnahmen ergänzt werden, nicht der Code geändert.
**2. `pnpm test` ohne LDAP-Container.** „Vor jedem Commit“ in `CLAUDE.md` und „Qualitätsschranken“ im
README nennen `pnpm test`, aber nicht
@@ -24,14 +29,17 @@ README nennen `pnpm test`, aber nicht
Tests gegen das echte Verzeichnis — sie sagt das auf stderr, aber der Lauf sieht grün aus. Wer nur
dem dokumentierten Weg folgt, prüft den Verzeichnis-Abgleich nie. Ein Satz in beiden Dateien.
Nachgezählt sind es **22 Tests**: `tests/abgleich.test.ts` und `tests/verzeichnis.test.ts` ergeben
mit Container 81, ohne 59. Dabei fiel auf, dass im README zusätzlich `format:pruefen` fehlte.
## Aufgaben
- [ ] Ausnahme für rohe Hexwerte in `CLAUDE.md` und `docs/architektur.md` benennen, mit Begründung
- [ ] Den LDAP-Container in `CLAUDE.md` („Vor jedem Commit“) und `README.md` aufnehmen
- [ ] Prüfen, ob sich der Hinweis maschinell absichern lässt — `pruefe:doku` könnte verlangen, dass
- [x] Ausnahme für rohe Hexwerte in `CLAUDE.md` benennen, mit Begründung (in `docs/architektur.md` steht die Regel nicht)
- [x] Den LDAP-Container in `CLAUDE.md` („Vor jedem Commit“) und `README.md` aufnehmen
- [x] Prüfen, ob sich der Hinweis maschinell absichern lässt — `pruefe:doku` könnte verlangen, dass
jede in `CLAUDE.md` genannte Schranke auch den nötigen Vorlauf nennt
## Akzeptanzkriterien
- [ ] Die vier Hexwerte im Code stehen in Einklang mit der Regel, ohne den Code zu ändern
- [ ] Wer `CLAUDE.md` von oben nach unten befolgt, fährt die Suite mit LDAP-Container
- [x] Die sechs Hexwerte im Code stehen in Einklang mit der Regel, ohne den Code zu ändern
- [x] Wer `CLAUDE.md` von oben nach unten befolgt, fährt die Suite mit LDAP-Container
+3 -3
View File
@@ -29,6 +29,6 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #022 | [Schnellstart je Gerätetyp pflegen](022-schnellstart-pflegen.md) | offen | v0.2.0 | 3 h | – |
| #023 | [Protokoll ansehen](023-protokoll-ansicht.md) | offen | v0.2.0 | 4 h | – |
| #024 | [Proxmox-Einzeiler auf einem echten PVE-Host gegenprüfen](024-proxmox-echtlauf.md) | offen | v0.2.0 | 2 h | – |
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | offen | v0.2.0 | 1 h | – |
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | offen | v0.2.0 | 1 h | – |
| | **Summe** | | | **152 h** | **35 h 20 min** |
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min |
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
| | **Summe** | | | **152 h** | **36 h 05 min** |
+51
View File
@@ -8,6 +8,7 @@
* 3. Geschaetzte und tatsaechliche Zeiten stimmen zwischen Datei und Uebersicht.
* 4. Alle in der Anleitung verlinkten Bilder sind vorhanden.
* 5. Kein Markdown verweist auf eine Datei, die es nicht gibt.
* 6. Die Schranken-Befehle in CLAUDE.md und README.md stimmen ueberein.
*/
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { join, dirname, resolve } from "node:path";
@@ -134,6 +135,56 @@ function feld(text, name) {
}
}
// ---- 6. Die Schranken in CLAUDE.md und README.md -------------------------
//
// Zwei Dateien beschreiben dasselbe Tor, und sie sind auseinandergelaufen: im
// README fehlte `format:pruefen`, und beide nannten den LDAP-Container nicht, ohne
// den die Suite 22 Tests still ueberspringt. Beides fiel erst bei der Abnahme zu
// #019 auf -- ein gruener Lauf sieht genauso aus. Geprueft wird deshalb der
// Befehlsblock selbst, nicht seine Umgebung (#040).
{
/** Der erste eingezaeunte Block nach einer Ueberschrift. */
const blockNach = (text, ueberschrift) => {
const ab = text.indexOf(ueberschrift);
if (ab < 0) return null;
const treffer = text.slice(ab).match(/```(?:bash|sh)?\n([\s\S]*?)```/);
return treffer?.[1] ?? null;
};
const skripte = (block) => [...block.matchAll(/pnpm ([a-z:]+)/g)].map((m) => m[1]).sort();
const quellen = [
["CLAUDE.md", "## Vor jedem Commit"],
["README.md", "### Qualitätsschranken"],
];
const bloecke = quellen.map(([datei, ueberschrift]) => {
const block = blockNach(lies(datei), ueberschrift);
if (block === null) melde(`${datei}: kein Befehlsblock unter "${ueberschrift}" gefunden.`);
return [datei, block];
});
if (bloecke.every(([, block]) => block !== null)) {
const [[dateiA, blockA], [dateiB, blockB]] = bloecke;
const a = skripte(blockA);
const b = skripte(blockB);
if (a.join(" ") !== b.join(" ")) {
melde(
`Die Schranken laufen auseinander: ${dateiA} nennt [${a.join(", ")}], ` +
`${dateiB} nennt [${b.join(", ")}].`,
);
}
for (const [datei, block] of bloecke) {
if (!/docker compose -f docker-compose\.test\.yml up .*ldap/.test(block)) {
melde(
`${datei}: der Schranken-Block nennt den LDAP-Container nicht. ` +
`Ohne ihn ueberspringt die Suite den Verzeichnis-Abgleich, und der Lauf sieht gruen aus.`,
);
}
}
}
}
// ---- Ergebnis --------------------------------------------------------------
if (befunde.length > 0) {
console.error(`\nDokumentation und Code laufen auseinander (${befunde.length}):\n`);
+17 -3
View File
@@ -1,12 +1,26 @@
import { prisma } from "@/lib/db";
/** Healthcheck fuer Docker und den Reverse Proxy. Prueft, ob die Datenbank antwortet. */
/**
* Healthcheck fuer Docker und den Reverse Proxy. Prueft, ob die Datenbank antwortet.
*
* Dieser Pfad ist absichtlich ohne Anmeldung erreichbar — er steht in der
* Allowlist von `src/proxy.ts`, damit ein Healthcheck kein Cookie braucht.
* Deshalb geht die **Begruendung** eines Fehlers nicht hinaus: eine Meldung von
* Prisma oder `pg` nennt typischerweise Host, Port, Benutzernamen und
* Datenbanknamen. Nach aussen genuegt, **ob** es geht; warum nicht, steht im
* Protokoll des Prozesses (`docker compose logs web`,
* `journalctl -u laufzettel`). Dasselbe Muster wie in
* `src/server/actions/verzeichnis.ts` (#039).
*/
export async function GET(): Promise<Response> {
try {
await prisma.$queryRaw`SELECT 1`;
return Response.json({ status: "ok" });
} catch (fehler) {
const meldung = fehler instanceof Error ? fehler.message : "unbekannter Fehler";
return Response.json({ status: "fehler", meldung }, { status: 503 });
console.error(
"Healthcheck fehlgeschlagen:",
fehler instanceof Error ? `${fehler.name}: ${fehler.message}` : "unbekannter Fehler",
);
return Response.json({ status: "fehler" }, { status: 503 });
}
}
+102
View File
@@ -0,0 +1,102 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
/**
* Der Healthcheck (#039).
*
* Dieser Pfad ist der einzige, der ohne Anmeldung **und** mit Datenbankzugriff
* antwortet. Geprueft wird deshalb nicht, dass er funktioniert — das tun der
* Compose-Stand, `install.sh --test` und jeder e2e-Vorlauf —, sondern dass er im
* Fehlerfall **nichts verraet**. Die Datenbank wird dafuer ersetzt: eine echte
* Verbindung laesst sich nicht so kaputtmachen, dass die Meldung reproduzierbar
* Host, Port und Benutzernamen enthaelt.
*/
const abfrage = vi.fn();
vi.mock("@/lib/db", () => ({
prisma: {
get $queryRaw() {
return abfrage;
},
},
}));
/**
* So sieht die Meldung wirklich aus, wenn `pg` nicht durchkommt — sie nennt
* alles, was ein Angreifer ueber die Datenbank wissen will.
*/
const ECHTE_PG_MELDUNG =
"Can't reach database server at `db-prod.firma.local:5432`. " +
"Authentication failed against database server, the provided database credentials for " +
"`laufzettel_prod` are not valid. Database `laufzettel_betrieb` does not exist.";
const GEHEIMNISSE = [
"db-prod.firma.local",
"5432",
"laufzettel_prod",
"laufzettel_betrieb",
"Authentication failed",
];
async function antwort() {
// Nach jedem Zuruecksetzen der Module neu geholt, damit der Mock greift.
const { GET } = await import("@/app/api/gesundheit/route");
return GET();
}
describe("Healthcheck", () => {
let protokoll: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.resetModules();
abfrage.mockReset();
protokoll = vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
protokoll.mockRestore();
});
it("antwortet mit ok, wenn die Datenbank antwortet", async () => {
abfrage.mockResolvedValue([{ "1": 1 }]);
const a = await antwort();
expect(a.status).toBe(200);
await expect(a.json()).resolves.toEqual({ status: "ok" });
});
it("gibt im Fehlerfall weder Host noch Port noch Benutzernamen heraus", async () => {
abfrage.mockRejectedValue(new Error(ECHTE_PG_MELDUNG));
const a = await antwort();
const text = await a.text();
for (const geheimnis of GEHEIMNISSE) {
expect(text).not.toContain(geheimnis);
}
expect(JSON.parse(text)).toEqual({ status: "fehler" });
});
it("behaelt 503, damit Docker und der Reverse Proxy richtig entscheiden", async () => {
abfrage.mockRejectedValue(new Error(ECHTE_PG_MELDUNG));
expect((await antwort()).status).toBe(503);
});
it("schreibt die Begruendung ins Protokoll, statt sie zu verschlucken", async () => {
abfrage.mockRejectedValue(new Error(ECHTE_PG_MELDUNG));
await antwort();
expect(protokoll).toHaveBeenCalledTimes(1);
const geschrieben = protokoll.mock.calls[0]?.join(" ") ?? "";
expect(geschrieben).toContain("Healthcheck fehlgeschlagen");
// Ohne das waere die Meldung nur weg, nicht aufgehoben.
expect(geschrieben).toContain("db-prod.firma.local:5432");
});
it("nennt auch eine Ausnahme ohne Error-Klasse, ohne sie auszugeben", async () => {
abfrage.mockRejectedValue("nur ein Text");
const a = await antwort();
expect(a.status).toBe(503);
await expect(a.json()).resolves.toEqual({ status: "fehler" });
expect(protokoll.mock.calls[0]?.join(" ")).toContain("unbekannter Fehler");
});
});