Vier Meilensteine fuer v0.3.0, 34 h geschaetzt: bedingte Schritte und Abschnitte (M23), Vier-Augen-Freigabe (M24), PDF-Uebergabeprotokoll (M25), weitere Schritt-Typen (M26). Die Reihenfolge ist begruendet: M23 zuerst, weil es als einziges am Datenmodell und an der Abschlussregel arbeitet; M26 zuletzt, weil UNTERSCHRIFT erst im PDF einen Zweck hat. Jedes Issue nennt die Entscheidungen, die zu treffen sind, mit Empfehlung und Begruendung. Bei M23 ist die entscheidende: eine Bedingung auf einer Variable liesse sich beim Start festschreiben wie bei #031, eine auf einem Schrittergebnis nicht — die Antwort entsteht erst waehrend der Arbeit. Beim Nummernvergeben einen Fund: vier Codestellen, sechs Changelog-Zeilen und der Doku-Pruefer nannten dreistellig die Tracker-Nummern 39 und 40 — gemeint waren die Issue-Dateien 025 und 026. Solange es diese Dateien nicht gab, fiel es niemandem auf; mit Phase 3 waere es eine falsche Spur geworden, und zwar in Kommentaren, die eine Codestelle begruenden. Behoben, und pruefe:doku prueft es ab jetzt: jeder dreistellige Verweis muss auf eine Issue-Datei zeigen. Gegenprobe mit einer erfundenen Nummer schlaegt an. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
/**
|
|
* Der Healthcheck (#025).
|
|
*
|
|
* 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");
|
|
});
|
|
});
|