Files
Florian-netz/tests/e2e/auth-gating.spec.ts
Matthias Hochmeister 44050c7278 Workstream 8: Detailseiten & Kontakt (Phase 5)
Drei serverseitige Lese-Detailseiten (Fahrzeug, Gerät, Wehr), default-deny:
- src/lib/detail/merkmale.ts: formatMerkmal (de-AT Tausenderpunkt + NBSP,
  Ja/Nein, enum-Label, „–"), toEckdaten. ICU-unabhängige Zahl-Formatierung
  (formatToParts -> Punkt/Komma), da de-AT je nach ICU-Build U+202F gruppiert.
- src/lib/detail/queries.ts: read-only, wehrübergreifend; loadMerkmalRows
  (Join merkmal_values↔merkmale↔merkmal_optionen via wert=value_text),
  getFahrzeugDetail (+Beladung, +WehrCard), getGeraetDetail (Fahrzeug-Link
  oder „im Gerätehaus"), getWehrDetail (Fuhrpark + Geräte im Haus),
  getBrigadeCard. UUID-IDs.
- Komponenten: detail/{DetailHeader,EckdatenGrid,BeladungListe,StatusBadge},
  kontakt/{KontaktButton (tel:/mailto:, Telefon ohne Leerzeichen, subject;
  Empty-State),WehrCard}.
- Seiten (app)/{fahrzeuge,geraete,wehren}/[id]/page.tsx mit requireSession()
  als erster Zeile (Default-deny in der Tiefe) + fahrzeuge/[id]/not-found.tsx.
- i18n-Keys (detail/kontakt/wehr) ergänzt; keine hartkodierten Strings.

Tests: merkmale.test.ts (11), queries.test.ts (3, gemockte DB für
„im Gerätehaus" + not-found). Playwright detail-auth.spec.ts geschrieben
(deferred: kein Server/DB in Sandbox); Detailrouten ins Gating-Manifest
aufgenommen.

Offline verifiziert: vitest src/lib/detail grün; tsc --noEmit ok; eslint
ok; next build erfolgreich (alle drei [id]-Routen vorhanden).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:35:34 +02:00

80 lines
2.5 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 { test, expect } from "@playwright/test";
/**
* Auth-Gating-Garantie (Definition of Done, oberstes Prinzip).
*
* NICHT in der Sandbox ausführbar (kein Server/DB) — deferred. Wird über
* `npm run test:e2e:gating` gegen einen laufenden Server ausgeführt.
*
* Kerngarantie (Querschnittsstandard 13, default-deny dreifach):
* - Anonyme Aufrufe von Seiten -> Redirect auf /login (mit callbackUrl).
* - Anonyme Aufrufe von API-Routen -> 401 OHNE Daten-Leak.
* - Öffentliche Routen (Login, Health) bleiben erreichbar.
*
* Negativ-Probe (manuell/CI): Entfernen von `requireSession()` aus
* `(app)/layout.tsx` muss diese Suite rot machen.
*/
// Geschützte Seiten (Redirect-Manifest). Neue Seiten hier ergänzen.
const PROTECTED_PAGES = [
"/",
"/start",
"/fahrzeuge",
"/fahrzeuge/00000000-0000-0000-0000-000000000001",
"/geraete/00000000-0000-0000-0000-000000000002",
"/wehren/00000000-0000-0000-0000-000000000003",
"/geraete",
"/wehren",
"/verwaltung",
"/admin",
];
// Geschützte API-Routen (401-Manifest). Neue API-Routen hier ergänzen.
const PROTECTED_API = ["/api/fahrzeuge", "/api/geraete", "/api/verwaltung"];
// Öffentliche Routen (Middleware-Allowlist).
const PUBLIC_ROUTES = ["/login", "/api/health"];
test.describe("Default-deny: geschützte Seiten", () => {
for (const path of PROTECTED_PAGES) {
test(`anonymer Aufruf von ${path} leitet auf /login um`, async ({
page,
}) => {
await page.goto(path);
await expect(page).toHaveURL(/\/login/);
});
}
});
test.describe("Default-deny: geschützte API-Routen", () => {
for (const path of PROTECTED_API) {
test(`anonymer Aufruf von ${path} liefert 401 ohne Daten-Leak`, async ({
request,
}) => {
const res = await request.get(path);
expect(res.status()).toBe(401);
const body = await res.text();
// Kein Daten-Leak: nur eine generische Fehlermeldung.
expect(body).not.toContain("brigade");
expect(body).not.toContain("passwort");
});
}
});
test.describe("Öffentliche Routen bleiben erreichbar", () => {
test("Login-Seite ist anonym erreichbar", async ({ page }) => {
await page.goto("/login");
await expect(
page.getByRole("heading", { name: "Anmelden" }),
).toBeVisible();
});
test("Health-Check ist anonym 200", async ({ request }) => {
const res = await request.get("/api/health");
expect(res.status()).toBe(200);
expect(await res.json()).toEqual({ status: "ok" });
});
});
void PUBLIC_ROUTES;