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) <noreply@anthropic.com>
This commit is contained in:
@@ -108,4 +108,45 @@ describe("buildMerkmalValuesSchema", () => {
|
|||||||
]);
|
]);
|
||||||
expect(r.success).toBe(false);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -138,7 +138,26 @@ export function buildMerkmalValuesSchema(definitionen: MerkmalDefinition[]) {
|
|||||||
return { merkmalId: raw.merkmalId, text: text ?? null };
|
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. */
|
/** `undefined` = ungültig (NaN), `null` = leer, sonst die Zahl. */
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export async function updateBrigadeProfile(
|
|||||||
geocodedAt: new Date(),
|
geocodedAt: new Date(),
|
||||||
...(geo.status === "ok"
|
...(geo.status === "ok"
|
||||||
? { lat: geo.coords.lat, lng: geo.coords.lng, geocodeStatus: "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));
|
.where(eq(brigades.id, s.user.brigadeId));
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ test.describe("Verwaltung: Fuhrpark & Benutzer (Happy-Path)", () => {
|
|||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto("/verwaltung/fahrzeuge/neu");
|
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 …).
|
// Vorlagen-Merkmale werden nachgeladen (Löschwassertank, Feuerlöschpumpe …).
|
||||||
await expect(page.getByText("Löschwassertank")).toBeVisible();
|
await expect(page.getByText("Löschwassertank")).toBeVisible();
|
||||||
await page.getByLabel("Name").fill("HLF 2 Musterdorf");
|
await page.getByLabel("Name").fill("HLF 2 Musterdorf");
|
||||||
|
|||||||
Reference in New Issue
Block a user