import { beforeEach, describe, expect, it } from "vitest"; import { prisma } from "@/lib/db"; import { passwortHashen } from "@/lib/passwort-hash"; import { base32Dekodieren, totpCode, totpGeheimnisErzeugen, totpSchritt } from "@/lib/totp"; import { AnmeldeFehler, notfallAnmelden, oidcBenutzerUebernehmen, totpEinrichtungAbschliessen, } from "@/server/services/anmeldung"; import { datenbankLeeren } from "./hilfen/db"; /** Notfall-Admin mit Pflicht-TOTP und OIDC-Kontouebernahme gegen die Datenbank (#004). */ const PASSWORT = "notfall-test-passwort-123"; async function notfallAnlegen() { return prisma.benutzer.create({ data: { anmeldename: "notfall", name: "Notfall-Admin", rolle: "ADMIN", istNotfall: true, passwortHash: await passwortHashen(PASSWORT), }, }); } function codeZu(geheimnis: string, zeit: Date, versatz = 0) { return totpCode(base32Dekodieren(geheimnis), totpSchritt(zeit.getTime()) + versatz); } beforeEach(async () => { await datenbankLeeren(); }); describe("Notfall-Admin", () => { it("fuehrt beim ersten Mal zur TOTP-Einrichtung, nicht in die Anwendung", async () => { await notfallAnlegen(); const ergebnis = await notfallAnmelden("notfall", PASSWORT, ""); expect(ergebnis.art).toBe("totpEinrichten"); }); it("richtet TOTP ein und meldet danach nur mit gueltigem Code an", async () => { const benutzer = await notfallAnlegen(); const geheimnis = totpGeheimnisErzeugen(); const t0 = new Date("2026-10-12T10:00:00Z"); await expect(totpEinrichtungAbschliessen(benutzer.id, geheimnis, "000000", t0)).rejects.toThrow( AnmeldeFehler, ); await totpEinrichtungAbschliessen(benutzer.id, geheimnis, codeZu(geheimnis, t0), t0); const gespeichert = await prisma.benutzer.findUniqueOrThrow({ where: { id: benutzer.id } }); // Das Geheimnis liegt verschluesselt, nicht im Klartext. expect(Buffer.from(gespeichert.totpGeheimnis ?? []).toString("utf8")).not.toContain(geheimnis); const t1 = new Date(t0.getTime() + 60_000); await expect(notfallAnmelden("notfall", PASSWORT, "123456", t1)).rejects.toThrow(AnmeldeFehler); const ergebnis = await notfallAnmelden("notfall", PASSWORT, codeZu(geheimnis, t1), t1); expect(ergebnis.art).toBe("angemeldet"); // Derselbe Code ein zweites Mal: abgelehnt. await expect(notfallAnmelden("notfall", PASSWORT, codeZu(geheimnis, t1), t1)).rejects.toThrow( AnmeldeFehler, ); }); it("verraet nicht, ob Name oder Passwort falsch war", async () => { await notfallAnlegen(); const a = await notfallAnmelden("gibtsnicht", PASSWORT, "").catch((f: Error) => f.message); const b = await notfallAnmelden("notfall", "falsch-falsch-falsch", "").catch((f: Error) => f.message); expect(a).toBe(b); }); it("sperrt nach fuenf Fehlversuchen, auch fuer das richtige Passwort", async () => { await notfallAnlegen(); for (let i = 0; i < 5; i += 1) { await expect(notfallAnmelden("notfall", "falsch-falsch-falsch", "")).rejects.toThrow(); } await expect(notfallAnmelden("notfall", PASSWORT, "")).rejects.toThrow(/Zu viele Fehlversuche/); }); it("protokolliert Fehlversuche ohne Passwort", async () => { await notfallAnlegen(); await notfallAnmelden("notfall", "geheimes-falsches-pw", "").catch(() => undefined); const eintraege = await prisma.protokoll.findMany(); expect(eintraege.map((e) => e.aktion)).toContain("anmeldung.notfall.fehlgeschlagen"); expect( JSON.stringify(eintraege, (_, wert: unknown) => (typeof wert === "bigint" ? String(wert) : wert)), ).not.toContain("geheimes-falsches-pw"); }); }); describe("OIDC-Konto", () => { const basis = { sub: "sub-1", email: "florian@example.de", emailBestaetigt: true, name: "Florian", ziel: "/", erneut: false, }; it("legt das Konto an und nimmt die Rolle aus den Gruppen", async () => { const benutzer = await oidcBenutzerUebernehmen({ ...basis, gruppen: ["sshkv-operators"] }); expect(benutzer.rolle).toBe("OPERATOR"); const wieder = await oidcBenutzerUebernehmen({ ...basis, gruppen: ["sshkv-readers"] }); expect(wieder.id).toBe(benutzer.id); expect(wieder.rolle).toBe("LESER"); }); it("verweigert ohne sshkv-Gruppe", async () => { await expect(oidcBenutzerUebernehmen({ ...basis, gruppen: [] })).rejects.toMatchObject({ grund: "keine-gruppe", }); expect(await prisma.benutzer.count()).toBe(0); }); it("verknuepft nur ueber eine bestaetigte Adresse mit einem bestehenden Konto", async () => { await prisma.benutzer.create({ data: { email: basis.email, name: "Alt", rolle: "LESER" } }); await expect( oidcBenutzerUebernehmen({ ...basis, emailBestaetigt: false, gruppen: ["sshkv-admins"] }), ).rejects.toMatchObject({ grund: "konto-konflikt" }); }); });