Files
ProxLedger/tests/auth.test.ts
T
florian.wildeandClaude Opus 5 2c2f842d20 fix(auth): Sitzungs-Cookie auch ueber http nutzbar, Anmeldung als Formular (#013)
Das Cookie erhielt im Produktionsbau immer Secure und wurde ueber http vom
Browser verworfen – nach der Anmeldung landete jeder Klick wieder auf der
Anmeldeseite. Massgeblich ist jetzt das Schema in APP_URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 23:41:01 +02:00

168 lines
5.8 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { hash } from "@node-rs/argon2";
import { cookieOptionen, sitzungErzeugen, sitzungLesen } from "@/lib/sitzung";
import { AnmeldeFehler, anmeldeRateLimit, mitOidcAnmelden, mitPasswortAnmelden } from "@/server/auth/benutzer";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
const GEHEIMNIS = "test-geheimnis-mit-mindestens-32-zeichen!!";
describe("Sitzungen", () => {
beforeEach(() => {
process.env.APP_SECRET = GEHEIMNIS;
});
it("erzeugt ein Token und liest es wieder", async () => {
const token = await sitzungErzeugen({
benutzerId: "b1",
email: "admin@example.de",
name: "Florian",
herkunft: "passwort",
});
const sitzung = await sitzungLesen(token);
expect(sitzung).toEqual({
benutzerId: "b1",
email: "admin@example.de",
name: "Florian",
herkunft: "passwort",
});
});
it("weist ein verändertes oder fremd signiertes Token ab", async () => {
const token = await sitzungErzeugen({
benutzerId: "b1",
email: "admin@example.de",
name: "Florian",
herkunft: "passwort",
});
expect(await sitzungLesen(`${token}x`)).toBeNull();
process.env.APP_SECRET = "ein-ganz-anderes-geheimnis-mit-32-zeichen!";
expect(await sitzungLesen(token)).toBeNull();
});
it("gibt bei fehlendem Token null zurück", async () => {
expect(await sitzungLesen(undefined)).toBeNull();
expect(await sitzungLesen("")).toBeNull();
});
it("verlangt einen ausreichend langen Schlüssel", async () => {
process.env.APP_SECRET = "zu-kurz";
await expect(
sitzungErzeugen({ benutzerId: "b1", email: "a@b.de", name: "A", herkunft: "passwort" }),
).rejects.toThrow(/APP_SECRET/);
});
it("setzt Secure nur, wenn das Dashboard über HTTPS läuft", () => {
const vorher = process.env.APP_URL;
process.env.APP_URL = "https://proxledger.example.de";
expect(cookieOptionen().secure).toBe(true);
// Produktiv gebaut, aber über http erreichbar: sonst verwirft der Browser das Cookie
// und die Anmeldung landet sofort wieder auf der Anmeldeseite.
process.env.APP_URL = "http://192.168.1.50:3000";
expect(cookieOptionen().secure).toBe(false);
process.env.APP_URL = vorher;
expect(cookieOptionen().httpOnly).toBe(true);
expect(cookieOptionen().sameSite).toBe("lax");
});
});
describe.runIf(datenbankVerfuegbar)("Anmeldung", () => {
beforeEach(async () => {
await datenbankLeeren();
anmeldeRateLimit.zuruecksetzen();
process.env.APP_SECRET = GEHEIMNIS;
});
afterAll(async () => {
await testPrisma.$disconnect();
});
async function admin(passwort = "geheim1234") {
return testPrisma.benutzer.create({
data: {
email: "admin@example.de",
name: "Florian",
passwortHash: await hash(passwort),
},
});
}
it("meldet mit richtigem Passwort an und vermerkt den Zeitpunkt", async () => {
const benutzer = await admin();
const angemeldet = await mitPasswortAnmelden("admin@example.de", "geheim1234", "1.2.3.4");
expect(angemeldet.id).toBe(benutzer.id);
const danach = await testPrisma.benutzer.findUniqueOrThrow({ where: { id: benutzer.id } });
expect(danach.letzterLoginAm).not.toBeNull();
const protokoll = await testPrisma.auditEintrag.findFirst({ where: { aktion: "anmeldung.erfolgreich" } });
expect(protokoll).not.toBeNull();
});
it("lehnt falsches Passwort ab und protokolliert den Versuch", async () => {
await admin();
await expect(mitPasswortAnmelden("admin@example.de", "falsch", "1.2.3.4")).rejects.toThrow(AnmeldeFehler);
const protokoll = await testPrisma.auditEintrag.findFirst({ where: { aktion: "anmeldung.fehlgeschlagen" } });
expect(protokoll).not.toBeNull();
});
it("nennt bei unbekannter Adresse denselben Grund wie bei falschem Passwort", async () => {
await admin();
const unbekannt = await mitPasswortAnmelden("niemand@example.de", "x", "1.2.3.4").catch((f) => f);
const falsch = await mitPasswortAnmelden("admin@example.de", "falsch", "1.2.3.4").catch((f) => f);
expect(unbekannt.message).toBe(falsch.message);
});
it("lehnt deaktivierte Konten ab", async () => {
const benutzer = await admin();
await testPrisma.benutzer.update({ where: { id: benutzer.id }, data: { aktiv: false } });
await expect(mitPasswortAnmelden("admin@example.de", "geheim1234", "1.2.3.4")).rejects.toThrow(
AnmeldeFehler,
);
});
it("bremst nach fünf Fehlversuchen", async () => {
await admin();
for (let i = 0; i < 5; i++) {
await mitPasswortAnmelden("admin@example.de", "falsch", "9.9.9.9").catch(() => undefined);
}
await expect(mitPasswortAnmelden("admin@example.de", "geheim1234", "9.9.9.9")).rejects.toThrow(
/Zu viele Versuche/,
);
});
it("verknüpft eine OIDC-Anmeldung mit dem vorhandenen Konto", async () => {
const benutzer = await admin();
const angemeldet = await mitOidcAnmelden("subject-1", "admin@example.de", "Florian Wilde");
expect(angemeldet.id).toBe(benutzer.id);
expect(angemeldet.oidcSubject).toBe("subject-1");
expect(await testPrisma.benutzer.count()).toBe(1);
});
it("legt bei unbekannter OIDC-Anmeldung ein Konto an", async () => {
const angemeldet = await mitOidcAnmelden("subject-2", "Neu@example.de", "Neue Person");
expect(angemeldet.email).toBe("neu@example.de");
expect(angemeldet.passwortHash).toBeNull();
expect(await testPrisma.benutzer.count()).toBe(1);
});
it("erkennt dieselbe Subject-ID wieder, auch wenn sich die E-Mail ändert", async () => {
const erst = await mitOidcAnmelden("subject-3", "alt@example.de", "Person");
const wieder = await mitOidcAnmelden("subject-3", "neu@example.de", "Person");
expect(wieder.id).toBe(erst.id);
expect(await testPrisma.benutzer.count()).toBe(1);
});
});