fix(detail): validate route-param UUID and align e2e storageState
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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { requireSession } from "@/lib/auth/guards";
|
import { requireSession } from "@/lib/auth/guards";
|
||||||
import { getFahrzeugDetail } from "@/lib/detail/queries";
|
import { getFahrzeugDetail } from "@/lib/detail/queries";
|
||||||
|
import { uuidSchema } from "@/lib/validation/common";
|
||||||
import { DetailHeader } from "@/components/detail/DetailHeader";
|
import { DetailHeader } from "@/components/detail/DetailHeader";
|
||||||
import { EckdatenGrid } from "@/components/detail/EckdatenGrid";
|
import { EckdatenGrid } from "@/components/detail/EckdatenGrid";
|
||||||
import { BeladungListe } from "@/components/detail/BeladungListe";
|
import { BeladungListe } from "@/components/detail/BeladungListe";
|
||||||
@@ -15,7 +16,11 @@ export default async function FahrzeugDetailPage({
|
|||||||
// Default-deny in der Tiefe (Querschnittsstandard 1): Guard als erste Zeile.
|
// Default-deny in der Tiefe (Querschnittsstandard 1): Guard als erste Zeile.
|
||||||
await requireSession();
|
await requireSession();
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const v = await getFahrzeugDetail(id);
|
// Route-Param ist Nutzereingabe an der Grenze (Querschnittsstandard 4):
|
||||||
|
// nicht-UUID -> saubere deutsche 404 statt Postgres `invalid input syntax`.
|
||||||
|
const parsed = uuidSchema.safeParse(id);
|
||||||
|
if (!parsed.success) notFound();
|
||||||
|
const v = await getFahrzeugDetail(parsed.data);
|
||||||
if (!v) notFound();
|
if (!v) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { requireSession } from "@/lib/auth/guards";
|
import { requireSession } from "@/lib/auth/guards";
|
||||||
import { getGeraetDetail } from "@/lib/detail/queries";
|
import { getGeraetDetail } from "@/lib/detail/queries";
|
||||||
|
import { uuidSchema } from "@/lib/validation/common";
|
||||||
import { DetailHeader } from "@/components/detail/DetailHeader";
|
import { DetailHeader } from "@/components/detail/DetailHeader";
|
||||||
import { EckdatenGrid } from "@/components/detail/EckdatenGrid";
|
import { EckdatenGrid } from "@/components/detail/EckdatenGrid";
|
||||||
import { WehrCard } from "@/components/kontakt/WehrCard";
|
import { WehrCard } from "@/components/kontakt/WehrCard";
|
||||||
@@ -15,7 +16,11 @@ export default async function GeraetDetailPage({
|
|||||||
// Default-deny in der Tiefe (Querschnittsstandard 1): Guard als erste Zeile.
|
// Default-deny in der Tiefe (Querschnittsstandard 1): Guard als erste Zeile.
|
||||||
await requireSession();
|
await requireSession();
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const g = await getGeraetDetail(id);
|
// Route-Param ist Nutzereingabe an der Grenze (Querschnittsstandard 4):
|
||||||
|
// nicht-UUID -> saubere deutsche 404 statt Postgres `invalid input syntax`.
|
||||||
|
const parsed = uuidSchema.safeParse(id);
|
||||||
|
if (!parsed.success) notFound();
|
||||||
|
const g = await getGeraetDetail(parsed.data);
|
||||||
if (!g) notFound();
|
if (!g) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { requireSession } from "@/lib/auth/guards";
|
import { requireSession } from "@/lib/auth/guards";
|
||||||
import { getWehrDetail } from "@/lib/detail/queries";
|
import { getWehrDetail } from "@/lib/detail/queries";
|
||||||
|
import { uuidSchema } from "@/lib/validation/common";
|
||||||
import { DetailHeader } from "@/components/detail/DetailHeader";
|
import { DetailHeader } from "@/components/detail/DetailHeader";
|
||||||
import { KontaktButton } from "@/components/kontakt/KontaktButton";
|
import { KontaktButton } from "@/components/kontakt/KontaktButton";
|
||||||
import { StatusBadge } from "@/components/ui/badge";
|
import { StatusBadge } from "@/components/ui/badge";
|
||||||
@@ -15,7 +16,11 @@ export default async function WehrDetailPage({
|
|||||||
// Default-deny in der Tiefe (Querschnittsstandard 1): Guard als erste Zeile.
|
// Default-deny in der Tiefe (Querschnittsstandard 1): Guard als erste Zeile.
|
||||||
await requireSession();
|
await requireSession();
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const w = await getWehrDetail(id);
|
// Route-Param ist Nutzereingabe an der Grenze (Querschnittsstandard 4):
|
||||||
|
// nicht-UUID -> saubere deutsche 404 statt Postgres `invalid input syntax`.
|
||||||
|
const parsed = uuidSchema.safeParse(id);
|
||||||
|
if (!parsed.success) notFound();
|
||||||
|
const w = await getWehrDetail(parsed.data);
|
||||||
if (!w) notFound();
|
if (!w) notFound();
|
||||||
|
|
||||||
const adresse = [w.strasse, [w.plz, w.ort].filter(Boolean).join(" ")]
|
const adresse = [w.strasse, [w.plz, w.ort].filter(Boolean).join(" ")]
|
||||||
|
|||||||
@@ -42,7 +42,13 @@ test.describe("Default-deny: Detailseiten (anonym)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Eingeloggt: Detail-Inhalte", () => {
|
test.describe("Eingeloggt: Detail-Inhalte", () => {
|
||||||
test.use({ storageState: "tests/e2e/.auth/wehr-read.json" });
|
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 ({
|
test("Fahrzeug-Detail zeigt Eckdaten, Beladung-Links und Wehr-Kontakt", async ({
|
||||||
page,
|
page,
|
||||||
@@ -80,4 +86,14 @@ test.describe("Eingeloggt: Detail-Inhalte", () => {
|
|||||||
await page.goto(`/fahrzeuge/${UNGUELTIGE_ID}`);
|
await page.goto(`/fahrzeuge/${UNGUELTIGE_ID}`);
|
||||||
await expect(page.getByText("Nicht gefunden.")).toBeVisible();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user