Workstream 11: Tests & Sicherheitshärtung (Phase 7)
Beweist die Auth-Gating-Garantie und härtet das System ab (Definition of Done #1, #2, #3, #7, #8): - Routen-Manifest (tests/e2e/routes.manifest.ts) als einzige Quelle der Wahrheit; anonyme Seite -> Redirect /login, anonyme API -> 401. - Kritische auth-gating.spec.ts: genau ein Fall je Manifest-Eintrag, ohne Daten-Leak. - Driftschutz (routes.manifest.spec.ts + tests/unit/routes-manifest.test.ts): keine ungetestete neue Route unter src/app/**. - Default-Deny-Beweis für Server Actions (server-actions-guard.spec.ts + tests/unit/server-actions-guard.test.ts): jede "use server"-Funktion ruft als erste Anweisung einen Guard; Login-Actions per Allowlist ausgenommen. - Wiederverwendbare reine Scanner unter tests/support (route-scan, guard-scan) — offline lauffähig, in Vitest und Playwright geteilt. - rbac-scoping, search-eta, login-ratelimit, security-headers Specs (gegen geseedeten Server; in der Sandbox deferred, per test.skip abgesichert). - global-setup (Migration + Seed) und auth.setup (Login je Konto -> storageState); Playwright-Projekte setup -> chromium verdrahtet. - src/lib/security/headers.test.ts: statischer Beleg für CSP, HSTS, X-Frame-Options DENY, nosniff, Permissions-Policy. - vitest.config.ts: Coverage-Schwellen (>=90 %) für src/lib/search + src/lib/geo. - package.json: Scripts test:unit, test:coverage, test:e2e, test:e2e:gating. - docs/reference/sicherheitshaertung-checkliste.md: jeder Härtungspunkt mit Test/Befehl und Negativ-Probe. Offline verifiziert: tsc --noEmit (0), vitest run (229 passed / 7 db-skipped), drizzle-kit check (ok), next build (exit 0), next lint (0 Fehler), playwright --list (98 Tests, 15 Dateien). DB-/Server-/Browser-abhängige E2E-Läufe sind deferred (kein Postgres/Server in der Sandbox). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
69
tests/support/route-scan.ts
Normal file
69
tests/support/route-scan.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { globSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* Routen-Discovery aus dem Dateisystem für den Driftschutz (Definition of
|
||||
* Done #1). Reine Logik, offline lauffähig (kein Server/DB).
|
||||
*/
|
||||
|
||||
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
/**
|
||||
* Öffentliche Präfixe (Middleware-Allowlist). Routen, die mit einem dieser
|
||||
* Präfixe beginnen, brauchen KEINEN Manifest-Eintrag, weil sie absichtlich
|
||||
* anonym erreichbar sind.
|
||||
*/
|
||||
export const PUBLIC_ALLOWLIST: readonly string[] = [
|
||||
"/login",
|
||||
"/api/auth",
|
||||
"/api/health",
|
||||
"/_next",
|
||||
"/favicon.ico",
|
||||
"/robots.txt",
|
||||
];
|
||||
|
||||
/**
|
||||
* Wandelt einen Datei-Pfad (page.tsx | route.ts) in den zugehörigen URL-Pfad
|
||||
* um. Route-Groups `(name)` werden entfernt, dynamische Segmente `[id]`
|
||||
* bleiben als Platzhalter erhalten.
|
||||
*/
|
||||
export function filePathToRoute(filePath: string): string {
|
||||
const withoutPrefix = filePath
|
||||
.replace(/^.*src\/app\//, "")
|
||||
.replace(/^(page|route)\.(tsx?|jsx?)$/, "")
|
||||
.replace(/\/(page|route)\.(tsx?|jsx?)$/, "");
|
||||
const segments = withoutPrefix
|
||||
.split("/")
|
||||
.filter((seg) => seg.length > 0 && !/^\(.*\)$/.test(seg));
|
||||
return "/" + segments.join("/");
|
||||
}
|
||||
|
||||
function isPublic(route: string): boolean {
|
||||
return PUBLIC_ALLOWLIST.some(
|
||||
(p) => route === p || route.startsWith(p + "/") || route.startsWith(p),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet Routen, die im Dateisystem existieren, aber weder im Manifest
|
||||
* deklariert noch öffentlich sind.
|
||||
*/
|
||||
export function findUndeclaredRoutes(
|
||||
discovered: readonly string[],
|
||||
declared: ReadonlySet<string>,
|
||||
): string[] {
|
||||
return discovered
|
||||
.filter((route) => !isPublic(route))
|
||||
.filter((route) => !declared.has(route));
|
||||
}
|
||||
|
||||
/** Scannt src/app/** nach page.tsx und route.ts und liefert URL-Pfade. */
|
||||
export function discoverAppRoutes(): string[] {
|
||||
const files = globSync("src/app/**/{page,route}.{ts,tsx}", {
|
||||
cwd: REPO_ROOT,
|
||||
});
|
||||
const routes = new Set<string>();
|
||||
for (const f of files) routes.add(filePathToRoute(f));
|
||||
return [...routes].sort();
|
||||
}
|
||||
Reference in New Issue
Block a user