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:
110
tests/e2e/routes.manifest.ts
Normal file
110
tests/e2e/routes.manifest.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Kanonisches Routen-Manifest für die Auth-Gating-Garantie (Definition of
|
||||
* Done #1, Querschnittsstandard 1–3).
|
||||
*
|
||||
* EINZIGE Quelle der Wahrheit darüber, welche Routen geschützt sind und wie ein
|
||||
* ANONYMER Aufruf sich verhalten muss:
|
||||
* - Seiten -> "redirect" (auf /login, mit callbackUrl)
|
||||
* - APIs -> "401" (ohne Daten-Leak)
|
||||
*
|
||||
* Der Driftschutz (routes.manifest.spec.ts + tests/unit/routes-manifest.test.ts)
|
||||
* stellt sicher, dass jede neue Route unter src/app/** entweder hier steht oder
|
||||
* in PUBLIC_ALLOWLIST. Ungetestete Routen sind damit ausgeschlossen.
|
||||
*
|
||||
* Beispiel-UUIDs für dynamische Segmente: anonyme Aufrufe werden VOR jeder
|
||||
* DB-Abfrage abgewiesen, daher müssen diese IDs nicht existieren.
|
||||
*/
|
||||
export { PUBLIC_ALLOWLIST } from "../support/route-scan";
|
||||
|
||||
export type AnonExpectation = "redirect" | "401";
|
||||
|
||||
export interface RouteEntry {
|
||||
/** Konkreter URL-Pfad (dynamische Segmente bereits aufgelöst). */
|
||||
path: string;
|
||||
/** Erwartetes Verhalten bei anonymem Zugriff. */
|
||||
expectWhenAnon: AnonExpectation;
|
||||
/** true für API-Routen (request statt page-Navigation). */
|
||||
api?: boolean;
|
||||
}
|
||||
|
||||
const EX_VEHICLE = "00000000-0000-0000-0000-0000000000a1";
|
||||
const EX_EQUIP = "00000000-0000-0000-0000-0000000000a2";
|
||||
const EX_BRIGADE = "00000000-0000-0000-0000-0000000000a3";
|
||||
const EX_TEMPLATE = "00000000-0000-0000-0000-0000000000a4";
|
||||
const EX_CATEGORY = "00000000-0000-0000-0000-0000000000a5";
|
||||
|
||||
export const ROUTES: readonly RouteEntry[] = [
|
||||
// (app) – Lese-Oberflächen
|
||||
{ path: "/", expectWhenAnon: "redirect" },
|
||||
{ path: "/start", expectWhenAnon: "redirect" },
|
||||
{ path: "/fahrzeuge", expectWhenAnon: "redirect" },
|
||||
{ path: `/fahrzeuge/${EX_VEHICLE}`, expectWhenAnon: "redirect" },
|
||||
{ path: "/geraete", expectWhenAnon: "redirect" },
|
||||
{ path: `/geraete/${EX_EQUIP}`, expectWhenAnon: "redirect" },
|
||||
{ path: "/wehren", expectWhenAnon: "redirect" },
|
||||
{ path: `/wehren/${EX_BRIGADE}`, expectWhenAnon: "redirect" },
|
||||
// (app)/verwaltung – Wehr-Bereich
|
||||
{ path: "/verwaltung/benutzer", expectWhenAnon: "redirect" },
|
||||
{ path: "/verwaltung/profil", expectWhenAnon: "redirect" },
|
||||
{ path: "/verwaltung/fahrzeuge", expectWhenAnon: "redirect" },
|
||||
{ path: "/verwaltung/fahrzeuge/neu", expectWhenAnon: "redirect" },
|
||||
{ path: `/verwaltung/fahrzeuge/${EX_VEHICLE}`, expectWhenAnon: "redirect" },
|
||||
{ path: "/verwaltung/geraete", expectWhenAnon: "redirect" },
|
||||
{ path: "/verwaltung/geraete/neu", expectWhenAnon: "redirect" },
|
||||
{ path: `/verwaltung/geraete/${EX_EQUIP}`, expectWhenAnon: "redirect" },
|
||||
// (admin) – Plattform-Verwaltung
|
||||
{ path: "/admin", expectWhenAnon: "redirect" },
|
||||
{ path: "/admin/audit", expectWhenAnon: "redirect" },
|
||||
{ path: "/admin/merkmale", expectWhenAnon: "redirect" },
|
||||
{ path: "/admin/merkmale/proposals", expectWhenAnon: "redirect" },
|
||||
{ path: "/admin/vorlagen", expectWhenAnon: "redirect" },
|
||||
{ path: `/admin/vorlagen/${EX_TEMPLATE}`, expectWhenAnon: "redirect" },
|
||||
{ path: "/admin/geraete-kategorien", expectWhenAnon: "redirect" },
|
||||
{
|
||||
path: `/admin/geraete-kategorien/${EX_CATEGORY}`,
|
||||
expectWhenAnon: "redirect",
|
||||
},
|
||||
{ path: "/admin/wehren", expectWhenAnon: "redirect" },
|
||||
{ path: "/admin/wehren/neu", expectWhenAnon: "redirect" },
|
||||
{ path: `/admin/wehren/${EX_BRIGADE}`, expectWhenAnon: "redirect" },
|
||||
// APIs (kein /api/auth, /api/health -> öffentlich/Allowlist)
|
||||
{ path: "/api/geo/geocode", expectWhenAnon: "401", api: true },
|
||||
{ path: "/api/geo/health", expectWhenAnon: "401", api: true },
|
||||
];
|
||||
|
||||
/**
|
||||
* Die Routen-Vorlagen (dynamische Segmente als Platzhalter), wie sie im
|
||||
* Dateisystem erscheinen. Wird vom Driftschutz mit discoverAppRoutes()
|
||||
* abgeglichen.
|
||||
*/
|
||||
export const DECLARED_ROUTE_TEMPLATES: ReadonlySet<string> = new Set([
|
||||
"/",
|
||||
"/start",
|
||||
"/fahrzeuge",
|
||||
"/fahrzeuge/[id]",
|
||||
"/geraete",
|
||||
"/geraete/[id]",
|
||||
"/wehren",
|
||||
"/wehren/[id]",
|
||||
"/verwaltung/benutzer",
|
||||
"/verwaltung/profil",
|
||||
"/verwaltung/fahrzeuge",
|
||||
"/verwaltung/fahrzeuge/neu",
|
||||
"/verwaltung/fahrzeuge/[id]",
|
||||
"/verwaltung/geraete",
|
||||
"/verwaltung/geraete/neu",
|
||||
"/verwaltung/geraete/[id]",
|
||||
"/admin",
|
||||
"/admin/audit",
|
||||
"/admin/merkmale",
|
||||
"/admin/merkmale/proposals",
|
||||
"/admin/vorlagen",
|
||||
"/admin/vorlagen/[id]",
|
||||
"/admin/geraete-kategorien",
|
||||
"/admin/geraete-kategorien/[id]",
|
||||
"/admin/wehren",
|
||||
"/admin/wehren/neu",
|
||||
"/admin/wehren/[id]",
|
||||
"/api/geo/geocode",
|
||||
"/api/geo/health",
|
||||
]);
|
||||
Reference in New Issue
Block a user