Files
proxdeploy/tests/zugang.test.ts
T
florian.wildeandClaude Opus 5.5 6d6b0ff121 feat(auth): Anmeldung per OIDC mit Rollen aus Gruppen
Authorization Code mit PKCE, State und Nonce über openid-client 6.8.8; die Rolle kommt bei jeder Anmeldung aus dem Gruppen-Claim. Lokale Konten werden nie über den Namen übernommen, verknüpfen geht nur angemeldet. Getestet gegen einen Mock-IdP inklusive gefälschtem State, falscher Nonce und fremder Audience.

Closes #016

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:57:50 +02:00

71 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import { describe, expect, it } from "vitest";
import { istOeffentlich, OEFFENTLICH } from "@/proxy";
/**
* Strukturelle Prüfung: jede Seite und jeder Route Handler prüft sein Recht selbst.
* Ausnahmen stehen hier namentlich mit Begründung – ein neuer Handler ohne Prüfung soll
* jemanden zwingen, sie aufzuschreiben.
*/
const AUSNAHMEN: Record<string, string> = {
"src/app/api/v1/auth/anmelden/route.ts": "Anmeldung selbst",
"src/app/api/v1/auth/token/route.ts":
"proxdeploy login: Passwort gegen Token – prüft das Passwort selbst (mit Sperre)",
"src/app/api/v1/auth/abmelden/route.ts": "Abmelden muss ohne gültige Sitzung gehen",
"src/app/api/v1/auth/oidc/start/route.ts":
"SSO-Start ist der Weg zur Anmeldung; Verknüpfen prüft die Sitzung selbst",
"src/app/api/v1/auth/oidc/rueckruf/route.ts":
"Rückkehr vom IdP; prüft State, PKCE, Nonce und ID-Token selbst",
"src/app/api/v1/gesundheit/route.ts": "Healthcheck, verrät nur die Version",
"src/app/anmelden/page.tsx": "Anmeldeseite",
"src/app/kein-recht/page.tsx": "zeigt nur einen Hinweis",
};
function dateien(verzeichnis: string): string[] {
return readdirSync(verzeichnis).flatMap((name) => {
const pfad = join(verzeichnis, name);
return statSync(pfad).isDirectory() ? dateien(pfad) : [pfad];
});
}
const wurzel = process.cwd();
const kandidaten = dateien(join(wurzel, "src/app"))
.map((pfad) => relative(wurzel, pfad))
.filter((pfad) => pfad.endsWith("/page.tsx") || pfad.endsWith("/route.ts"));
describe("Zugangsprüfung", () => {
it.each(kandidaten.filter((pfad) => !(pfad in AUSNAHMEN)))("%s prüft sein Recht", (pfad) => {
const quelltext = readFileSync(join(wurzel, pfad), "utf8");
expect(quelltext).toMatch(/seiteSchuetzen\(|rechtErforderlich\(/);
});
it("jede Ausnahme gibt es noch", () => {
for (const pfad of Object.keys(AUSNAHMEN)) expect(kandidaten).toContain(pfad);
});
it("öffentliche Pfade sind genau die bekannten", () => {
expect(OEFFENTLICH).toEqual([
"/anmelden",
"/api/v1/auth",
"/api/v1/gesundheit",
"/brand",
"/manifest.webmanifest",
]);
expect(istOeffentlich("/api/v1/auth/anmelden")).toBe(true);
expect(istOeffentlich("/api/v1/authentifizieren")).toBe(false);
expect(istOeffentlich("/server")).toBe(false);
});
});
describe("Client-Komponenten", () => {
const clientDateien = dateien(join(wurzel, "src"))
.filter((pfad) => /\.tsx?$/.test(pfad) && !pfad.includes("/generated/"))
.filter((pfad) => /^["']use client["']/m.test(readFileSync(pfad, "utf8")));
it.each(clientDateien.map((p) => relative(wurzel, p)))("%s importiert nichts aus src/server", (pfad) => {
// Auch reine Typimporte nicht: sonst wandert beim nächsten Umbau doch Servercode ins Browser-Bündel.
expect(readFileSync(join(wurzel, pfad), "utf8")).not.toMatch(/from ["']@\/server\//);
});
});