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:
Matthias Hochmeister
2026-06-09 14:17:10 +02:00
parent 9927711192
commit c099b3acd9
19 changed files with 957 additions and 52 deletions

View File

@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest";
import {
GUARD_REGEX,
PUBLIC_ACTION_ALLOWLIST,
findUnguardedActions,
findUnguardedActionsInRepo,
} from "../support/guard-scan";
/**
* Statischer Default-Deny-Beweis für Server Actions (Querschnittsstandard 3,
* Definition of Done #2): JEDE "use server"-Funktion ruft als erste Anweisung
* einen Guard. Dieser Test ist OHNE Server/DB lauffähig und prüft das echte
* Repository.
*/
describe("findUnguardedActions (pure)", () => {
it("flaggt eine exportierte Action ohne Guard", () => {
const src = `"use server";
export async function tut_was(x: unknown) {
return doThing(x);
}`;
expect(findUnguardedActions("x.ts", src)).toEqual([
"x.ts: tut_was",
]);
});
it("akzeptiert eine Action, die mit requireWehrAdmin beginnt", () => {
const src = `"use server";
export async function tut_was(x: unknown) {
const s = await requireWehrAdmin();
return doThing(x, s);
}`;
expect(findUnguardedActions("x.ts", src)).toEqual([]);
});
it("akzeptiert requirePlatformAdmin / requireSession / requireRole / requireOwnBrigade", () => {
for (const guard of [
"requirePlatformAdmin",
"requireSession",
"requireRole",
"requireOwnBrigade",
]) {
const src = `"use server";
export async function f(x: unknown) {
await ${guard}();
return x;
}`;
expect(findUnguardedActions("x.ts", src)).toEqual([]);
}
});
it("ignoriert Dateien ohne \"use server\"-Direktive", () => {
const src = `export async function f(x: unknown) { return x; }`;
expect(findUnguardedActions("x.ts", src)).toEqual([]);
});
it("respektiert die Allowlist genuin öffentlicher Actions", () => {
const src = `"use server";
export async function loginAction(x: unknown) { return x; }`;
expect(
findUnguardedActions(
"src/app/(auth)/login/actions.ts",
src,
new Set(["src/app/(auth)/login/actions.ts:loginAction"]),
),
).toEqual([]);
});
it("hat die Login-Actions in der Default-Allowlist", () => {
expect(PUBLIC_ACTION_ALLOWLIST.has("src/app/(auth)/login/actions.ts:loginAction")).toBe(true);
expect(
PUBLIC_ACTION_ALLOWLIST.has("src/app/(auth)/login/actions.ts:authentikLoginAction"),
).toBe(true);
});
it("GUARD_REGEX matcht alle fünf Guard-Namen", () => {
expect(GUARD_REGEX.test("requireSession(")).toBe(true);
expect(GUARD_REGEX.test("requirePlatformAdmin(")).toBe(true);
expect(GUARD_REGEX.test("doSomething(")).toBe(false);
});
});
describe("findUnguardedActionsInRepo (echtes Repo)", () => {
it("findet KEINE ungeschützten Server Actions im echten src/", () => {
const offenders = findUnguardedActionsInRepo();
expect(
offenders,
`Server Actions ohne Guard:\n${offenders.join("\n")}`,
).toEqual([]);
});
});