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");