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