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 = { "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/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\//); }); });