From 632ba2b081a25a5f0675f929f9150a0b4cf4b47a Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Tue, 9 Jun 2026 11:18:31 +0200 Subject: [PATCH] fix(verwaltung): BLOCKING-Befunde Fuhrpark & Benutzer beheben - tests/e2e/verwaltung-fuhrpark.spec.ts: selectOption({ label: /HLF 2/ }) uebergab eine RegExp an Playwrights string-typisierte label-Option (TS2345 -> tsc-Gate rot). Stattdessen erste echte Vorlage per { index: 1 } waehlen (Index 0 ist "Ohne Vorlage (frei)"). tsc clean. - src/lib/validation/vehicle.ts: Pflichtmerkmale wurden nur pro Element geprueft; ein komplett ausgelassenes Pflichtmerkmal (z.B. []) entging der Validierung. buildMerkmalValuesSchema prueft jetzt auf Array-Ebene per superRefine, dass jede pflicht-Definition einen gesetzten, typgerechten, nicht-leeren Wert hat ("validieren, nicht vertrauen", Querschnittsstandard 4). Tests ergaenzt (TDD). - src/server/actions/brigade.ts: bei fehlgeschlagenem Geocoding (geo.status !== "ok") werden lat/lng nun auf null gesetzt, analog zu createBrigadeWithFirstAdmin, damit keine veralteten Koordinaten zuruechbleiben (WS4-Konsistenz). Verifikation offline: tsc --noEmit exit 0; vitest src/lib/validation 47/47 gruen. E2E (DB/Server) deferred (kein Postgres/Server im Sandbox). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/validation/__tests__/vehicle.test.ts | 41 ++++++++++++++++++++ src/lib/validation/vehicle.ts | 21 +++++++++- src/server/actions/brigade.ts | 2 +- tests/e2e/verwaltung-fuhrpark.spec.ts | 3 +- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/lib/validation/__tests__/vehicle.test.ts b/src/lib/validation/__tests__/vehicle.test.ts index b653b8b..e02939c 100644 --- a/src/lib/validation/__tests__/vehicle.test.ts +++ b/src/lib/validation/__tests__/vehicle.test.ts @@ -108,4 +108,45 @@ describe("buildMerkmalValuesSchema", () => { ]); expect(r.success).toBe(false); }); + + it("erzwingt Pflichtmerkmale auch bei vollständiger Auslassung (leeres Array)", () => { + const schema = buildMerkmalValuesSchema([ + def({ typ: "number", pflicht: true, name: "Löschwassertank" }), + ]); + // Pflichtmerkmal fehlt komplett -> Validierung muss greifen (nicht trauen). + expect(schema.safeParse([]).success).toBe(false); + }); + + it("erzwingt fehlende Pflichtmerkmale bei teilweise befülltem Array", () => { + const idA = "11111111-1111-1111-1111-11111111aaaa"; + const idB = "11111111-1111-1111-1111-11111111bbbb"; + const schema = buildMerkmalValuesSchema([ + def({ merkmalId: idA, typ: "text", pflicht: true, name: "A" }), + def({ merkmalId: idB, typ: "boolean", pflicht: true, name: "B" }), + ]); + // Nur A geliefert, Pflicht-B fehlt komplett. + const r = schema.safeParse([{ merkmalId: idA, text: "x" }]); + expect(r.success).toBe(false); + }); + + it("akzeptiert ein leeres Array, wenn keine Pflichtmerkmale definiert sind", () => { + const schema = buildMerkmalValuesSchema([def({ typ: "text", pflicht: false })]); + expect(schema.safeParse([]).success).toBe(true); + }); + + it("akzeptiert vollständig gesetzte Pflichtmerkmale", () => { + const idA = "11111111-1111-1111-1111-11111111aaaa"; + const schema = buildMerkmalValuesSchema([ + def({ merkmalId: idA, typ: "text", pflicht: true, name: "A" }), + ]); + expect(schema.safeParse([{ merkmalId: idA, text: "x" }]).success).toBe(true); + }); + + it("lehnt ein vorhandenes, aber leeres Pflichtmerkmal weiterhin ab", () => { + const idA = "11111111-1111-1111-1111-11111111aaaa"; + const schema = buildMerkmalValuesSchema([ + def({ merkmalId: idA, typ: "text", pflicht: true, name: "A" }), + ]); + expect(schema.safeParse([{ merkmalId: idA, text: "" }]).success).toBe(false); + }); }); diff --git a/src/lib/validation/vehicle.ts b/src/lib/validation/vehicle.ts index f7c292e..b6acf8d 100644 --- a/src/lib/validation/vehicle.ts +++ b/src/lib/validation/vehicle.ts @@ -138,7 +138,26 @@ export function buildMerkmalValuesSchema(definitionen: MerkmalDefinition[]) { return { merkmalId: raw.merkmalId, text: text ?? null }; }); - return z.array(single); + return z.array(single).superRefine((werte, ctx) => { + // Vollständigkeit auf Array-Ebene: ein Pflichtmerkmal, das komplett fehlt + // (kein Element mit gesetztem Wert), wird sonst von der Pro-Element-Prüfung + // nicht erfasst. "Validieren, nicht vertrauen" (Querschnittsstandard 4). + for (const def of definitionen) { + if (!def.pflicht) continue; + const hatWert = werte.some((w) => { + if (w.merkmalId !== def.merkmalId) return false; + if (def.typ === "number") return w.num != null; + if (def.typ === "boolean") return w.bool != null; + return w.text != null && w.text !== ""; + }); + if (!hatWert) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `„${def.name}“ ist Pflicht.`, + }); + } + } + }); } /** `undefined` = ungültig (NaN), `null` = leer, sonst die Zahl. */ diff --git a/src/server/actions/brigade.ts b/src/server/actions/brigade.ts index d7d2ca7..ebfe392 100644 --- a/src/server/actions/brigade.ts +++ b/src/server/actions/brigade.ts @@ -50,7 +50,7 @@ export async function updateBrigadeProfile( geocodedAt: new Date(), ...(geo.status === "ok" ? { lat: geo.coords.lat, lng: geo.coords.lng, geocodeStatus: "ok" } - : { geocodeStatus: geo.status }), + : { lat: null, lng: null, geocodeStatus: geo.status }), }) .where(eq(brigades.id, s.user.brigadeId)); diff --git a/tests/e2e/verwaltung-fuhrpark.spec.ts b/tests/e2e/verwaltung-fuhrpark.spec.ts index fd62307..4a64d7c 100644 --- a/tests/e2e/verwaltung-fuhrpark.spec.ts +++ b/tests/e2e/verwaltung-fuhrpark.spec.ts @@ -22,7 +22,8 @@ test.describe("Verwaltung: Fuhrpark & Benutzer (Happy-Path)", () => { page, }) => { await page.goto("/verwaltung/fahrzeuge/neu"); - await page.getByLabel("Fahrzeug-Vorlage").selectOption({ label: /HLF 2/ }); + // Erste echte Vorlage wählen (Index 0 ist „Ohne Vorlage (frei)"). + await page.getByLabel("Fahrzeug-Vorlage").selectOption({ index: 1 }); // Vorlagen-Merkmale werden nachgeladen (Löschwassertank, Feuerlöschpumpe …). await expect(page.getByText("Löschwassertank")).toBeVisible(); await page.getByLabel("Name").fill("HLF 2 Musterdorf");