Detailseiten & Kontakt (WS8) – zwei BLOCKING-Befunde behoben:
- UUID-Validierung an der Grenze (Querschnittsstandard 4): Die drei
Detailseiten (fahrzeuge/geraete/wehren [id]) gaben den Route-Param `id`
ungeprüft an die DB (Postgres `uuid`). Ein malformter Pfad wie
/fahrzeuge/abc erzeugte `invalid input syntax for type uuid` -> 500.
Jetzt: uuidSchema.safeParse(id) -> notFound() (deutsche 404) vor dem
Query-Aufruf.
- e2e-Harness: detail-auth.spec.ts nutzte storageState
"tests/e2e/.auth/wehr-read.json" (existiert nicht, ENOENT -> ganzer
'Eingeloggt'-Block errort). Auf Projektkonvention umgestellt:
storageState: process.env.E2E_WEHR_READ_STATE ?? { cookies, origins }
+ test.skip ohne Fixture (analog verwaltung-scoping.spec.ts).
Zusätzlich nicht-UUID-Fall (/fahrzeuge/abc -> deutsche 404) abgesichert.
Verifiziert (offline): tsc --noEmit OK, vitest detail-Unit-Tests OK,
next build "Compiled successfully" + Typecheck OK. Build-Page-Data-Phase
und e2e deferred (kein Postgres/Server in der Sandbox).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
3.8 KiB
TypeScript
100 lines
3.8 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
|
|
/**
|
|
* Detailseiten-Auth & -Inhalt (Workstream 8, Phase 5).
|
|
*
|
|
* NICHT in der Sandbox ausführbar (kein Server/DB) — deferred. Wird über
|
|
* `npm run test:e2e` gegen einen laufenden Server mit Seed-Daten ausgeführt.
|
|
*
|
|
* Garantien:
|
|
* - Default-deny (Querschnittsstandard 1): anonyme Aufrufe der Detailseiten
|
|
* leiten auf /login um (das `(app)`-Layout-Gate + `requireSession()` je Seite).
|
|
* - Eingeloggt: Eckdaten, Beladung-Links (`/geraete/<id>`), Wehr-Kontakt
|
|
* (`tel:`/`mailto:`) sind sichtbar.
|
|
* - Ungültige IDs -> deutsche 404-Seite (not-found).
|
|
*
|
|
* Negativ-Probe (manuell/CI): Entfernen von `requireSession()` aus einer der
|
|
* Detailseiten ODER aus `(app)/layout.tsx` muss die Default-deny-Tests rot
|
|
* machen.
|
|
*
|
|
* Platzhalter-IDs: zur Laufzeit gegen echte Seed-UUIDs ersetzen
|
|
* (Env `E2E_FAHRZEUG_ID` etc.) oder per Suchseite ermitteln.
|
|
*/
|
|
|
|
const FAHRZEUG_ID = process.env.E2E_FAHRZEUG_ID ?? "00000000-0000-0000-0000-000000000001";
|
|
const GERAET_ID = process.env.E2E_GERAET_ID ?? "00000000-0000-0000-0000-000000000002";
|
|
const WEHR_ID = process.env.E2E_WEHR_ID ?? "00000000-0000-0000-0000-000000000003";
|
|
const UNGUELTIGE_ID = "ffffffff-ffff-ffff-ffff-ffffffffffff";
|
|
|
|
const DETAIL_PAGES = [
|
|
`/fahrzeuge/${FAHRZEUG_ID}`,
|
|
`/geraete/${GERAET_ID}`,
|
|
`/wehren/${WEHR_ID}`,
|
|
];
|
|
|
|
test.describe("Default-deny: Detailseiten (anonym)", () => {
|
|
for (const path of DETAIL_PAGES) {
|
|
test(`anonymer Aufruf von ${path} leitet auf /login um`, async ({ page }) => {
|
|
await page.goto(path);
|
|
await expect(page).toHaveURL(/\/login/);
|
|
});
|
|
}
|
|
});
|
|
|
|
test.describe("Eingeloggt: Detail-Inhalte", () => {
|
|
test.skip(
|
|
!process.env.E2E_WEHR_READ_STATE,
|
|
"benötigt wehr_read-Fixture (Test-Workstream)",
|
|
);
|
|
test.use({
|
|
storageState: process.env.E2E_WEHR_READ_STATE ?? { cookies: [], origins: [] },
|
|
});
|
|
|
|
test("Fahrzeug-Detail zeigt Eckdaten, Beladung-Links und Wehr-Kontakt", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(`/fahrzeuge/${FAHRZEUG_ID}`);
|
|
await expect(page.getByRole("heading", { name: "Eckdaten" })).toBeVisible();
|
|
// Beladung verlinkt auf Gerät-Detailseiten.
|
|
const beladungLink = page.locator('a[href^="/geraete/"]').first();
|
|
await expect(beladungLink).toBeVisible();
|
|
// Out-of-band Kontakt: tel:/mailto:-Link vorhanden.
|
|
const kontaktLink = page.locator('a[href^="tel:"], a[href^="mailto:"]').first();
|
|
await expect(kontaktLink).toBeVisible();
|
|
});
|
|
|
|
test("Gerät-Detail verlinkt Fahrzeug oder zeigt „im Gerätehaus“", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(`/geraete/${GERAET_ID}`);
|
|
const hatFahrzeug = await page.locator('a[href^="/fahrzeuge/"]').count();
|
|
if (hatFahrzeug === 0) {
|
|
await expect(page.getByText("im Gerätehaus")).toBeVisible();
|
|
} else {
|
|
await expect(page.locator('a[href^="/fahrzeuge/"]').first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test("Wehr-Detail listet Fuhrpark und Kontakt", async ({ page }) => {
|
|
await page.goto(`/wehren/${WEHR_ID}`);
|
|
await expect(page.getByRole("heading", { name: "Fahrzeuge" })).toBeVisible();
|
|
const kontaktLink = page.locator('a[href^="tel:"], a[href^="mailto:"]').first();
|
|
await expect(kontaktLink).toBeVisible();
|
|
});
|
|
|
|
test("ungültige Fahrzeug-ID -> deutsche 404-Seite", async ({ page }) => {
|
|
await page.goto(`/fahrzeuge/${UNGUELTIGE_ID}`);
|
|
await expect(page.getByText("Nicht gefunden.")).toBeVisible();
|
|
});
|
|
|
|
test("malformter (nicht-UUID) Fahrzeug-Pfad -> deutsche 404-Seite (kein 500)", async ({
|
|
page,
|
|
}) => {
|
|
// Route-Param ist Nutzereingabe an der Grenze: eine nicht-UUID darf nicht
|
|
// als `invalid input syntax for type uuid` bis zur error.tsx (500) laufen,
|
|
// sondern muss sauber `notFound()` (deutsche 404) liefern.
|
|
await page.goto(`/fahrzeuge/abc`);
|
|
await expect(page.getByText("Nicht gefunden.")).toBeVisible();
|
|
});
|
|
});
|