Files
Laufzettel/tests/gesundheit.test.ts
T
florian.wilde 88d7db8c3d
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
fix(abnahme): Healthcheck schweigt, zwei Konventionen praezisiert (#41)
2026-10-04 14:12:28 +00:00

103 lines
3.3 KiB
TypeScript

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");
});
});