Files
FireAdministration/tests/unit/hydrant-import.test.ts
T
Florian WildeandClaude Opus 5 1c1e141666 Kartenmodule ergaenzen
Phase 6: alles, was einen Ort hat — Loeschwasser, Objekte mit Feuerwehrplan,
die Lagekarte eines Einsatzes und die geplante Foerderstrecke.

Keine Kacheln aus dem Netz. Wo eine Wehr ausrueckt und wo ihre Hydranten
liegen, ist niemandes Geschaeft; der Kartenserver laeuft selbst
(tileserver-gl im Compose-Profil "karte"). Ohne ihn zeigt die Karte einen
leeren Grund und sagt das auch — Punkte, Entfernungen und Umkreissuche
funktionieren trotzdem. Dasselbe gilt fuer einen eingetragenen, aber nicht
erreichbaren Server.

Die Umkreissuche laeuft ueber PostGIS: ST_DWithin auf der Geografie mit
GiST-Index rechnet in Metern und nutzt den Index. Gefragt wird waehrend der
Fahrt, und bei tausend Hydranten waere alles andere ein vollstaendiger
Tabellendurchlauf.

Der Import aus GeoJSON und Stadtwerke-CSV ist nachsichtig, aber ehrlich: er
erkennt "DN 100", deutsche Kommazahlen, Datumsangaben und vertauschte Achsen
— raet aber nichts. Fehlt die Position, wird die Zeile uebersprungen; ohne
Statusangabe steht "ungeprueft", nicht "einsatzbereit".

22 taktische Zeichen nach FwDV 102 selbst gezeichnet, die Lagekarte gleicht
ueber denselben Ereignisstrom ab wie die Atemschutzueberwachung.

Die Wasserfoerderung rechnet Reibung, Hoehe und Pumpenzahl; das Ergebnis wird
nie gespeichert, sondern bei jeder Anzeige neu gerechnet. Geht eine Strecke
nicht auf, sagt die Ausgabe das.

Behoben: die Reibungsbeiwerte lagen im ersten Wurf um den Faktor zehn daneben.
Aufgefallen ist es nur, weil die Tests gegen die Tabellenwerte der Ausbildung
pruefen und nicht gegen die eigene Implementierung.

Dazu ein Abruf fuer abgelegte Dateien (/api/dateien/[id]) — bis hierher gab es
nur den Upload.

59 neue Tests (48 Unit, 11 E2E je Browser).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 01:41:24 +02:00

247 lines
7.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
achsenVertauscht,
ausGeoJson,
ausZeilen,
datum,
statusErkennen,
typErkennen,
zahl,
} from "@/lib/karte/hydrant-import";
/**
* Hydranten einlesen.
*
* Was hier geprüft wird, ist keine Formatfrage, sondern eine
* Erfahrungsfrage: Wehren bekommen Hydrantenlisten in jedem denkbaren Zustand
* — mit „DN 100" in der Zahlenspalte, mit vertauschten Achsen, mit deutschen
* Datumsangaben und mit Spaltennamen, die sich niemand ausgedacht hat.
*
* Der Import darf daran nicht scheitern. Er darf aber auch nichts erfinden.
*/
describe("Typ erkennen", () => {
it("erkennt die üblichen Schreibweisen für Unterflur", () => {
expect(typErkennen("UF")).toBe("UNTERFLUR");
expect(typErkennen("Unterflurhydrant")).toBe("UNTERFLUR");
expect(typErkennen("hydrant uf")).toBe("UNTERFLUR");
});
it("erkennt Überflur mit und ohne Umlaut", () => {
expect(typErkennen("OF")).toBe("UEBERFLUR");
expect(typErkennen("Überflurhydrant")).toBe("UEBERFLUR");
expect(typErkennen("ueberflur")).toBe("UEBERFLUR");
});
it("erkennt offene Gewässer an dem, was die Leute schreiben", () => {
expect(typErkennen("Weiher")).toBe("OFFENES_GEWAESSER");
expect(typErkennen("Löschteich")).toBe("OFFENES_GEWAESSER");
expect(typErkennen("Bach hinter dem Hof")).toBe("OFFENES_GEWAESSER");
});
it("erkennt Zisternen und Saugstellen", () => {
expect(typErkennen("Zisterne 50 m³")).toBe("ZISTERNE");
expect(typErkennen("Saugstelle A")).toBe("SAUGSTELLE");
});
it("nimmt ohne Angabe den Regelfall an", () => {
expect(typErkennen("")).toBe("UNTERFLUR");
expect(typErkennen(null)).toBe("UNTERFLUR");
});
});
describe("Status erkennen", () => {
it("erkennt Defekte", () => {
expect(statusErkennen("defekt")).toBe("DEFEKT");
expect(statusErkennen("außer Betrieb")).toBe("DEFEKT");
expect(statusErkennen("stillgelegt")).toBe("DEFEKT");
});
it("erkennt Einschränkungen", () => {
expect(statusErkennen("eingeschränkt nutzbar")).toBe("EINGESCHRAENKT");
expect(statusErkennen("bedingt")).toBe("EINGESCHRAENKT");
});
it("sagt ohne Angabe „unbekannt“ — nicht „einsatzbereit“", () => {
// Der wichtigste Fall: eine Liste ohne Statusspalte darf nicht dazu
// führen, dass tausend Hydranten als geprüft gelten.
expect(statusErkennen("")).toBe("UNBEKANNT");
expect(statusErkennen(null)).toBe("UNBEKANNT");
expect(statusErkennen("k. A.")).toBe("UNBEKANNT");
});
});
describe("Zahlen lesen", () => {
it("liest die Nennweite aus „DN 100“", () => {
expect(zahl("DN 100")).toBe(100);
});
it("versteht das deutsche Dezimalkomma", () => {
expect(zahl("48,512345")).toBeCloseTo(48.512345, 6);
});
it("versteht den Tausenderpunkt", () => {
expect(zahl("1.234,5")).toBeCloseTo(1234.5, 1);
expect(zahl("1.600")).toBe(1600);
});
it("lässt echte Zahlen unangetastet", () => {
expect(zahl(12.5)).toBe(12.5);
});
it("gibt bei Unsinn nichts zurück", () => {
expect(zahl("")).toBeNull();
expect(zahl("k. A.")).toBeNull();
expect(zahl(null)).toBeNull();
});
});
describe("Datum lesen", () => {
it("liest die deutsche Schreibweise", () => {
expect(datum("24.05.2026")).toEqual(new Date("2026-05-24T00:00:00.000Z"));
expect(datum("4.5.26")).toEqual(new Date("2026-05-04T00:00:00.000Z"));
});
it("liest ISO", () => {
expect(datum("2026-05-24")).toEqual(new Date("2026-05-24T00:00:00.000Z"));
});
it("gibt bei allem anderen nichts zurück", () => {
expect(datum("letztes Jahr")).toBeNull();
expect(datum("")).toBeNull();
});
});
describe("Vertauschte Achsen", () => {
it("erkennt vertauschte Werte in Deutschland", () => {
// 12,15 als Breite und 48,5 als Länge kann in Bayern nicht stimmen.
expect(achsenVertauscht(12.15, 48.5)).toBe(true);
});
it("lässt richtige Werte in Ruhe", () => {
expect(achsenVertauscht(48.5, 12.15)).toBe(false);
});
});
describe("GeoJSON", () => {
const geojson = JSON.stringify({
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [12.151, 48.502] },
properties: {
Nummer: "H-0042",
Typ: "Unterflur",
DN: "150",
Strasse: "Hauptstraße",
Status: "in Betrieb",
},
},
{
type: "Feature",
geometry: { type: "LineString", coordinates: [[12, 48]] },
properties: {},
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [999, 999] },
properties: {},
},
],
});
it("liest Punkte samt Eigenschaften", () => {
const ergebnis = ausGeoJson(geojson);
expect(ergebnis.hydranten).toHaveLength(1);
const [hydrant] = ergebnis.hydranten;
expect(hydrant?.externeNummer).toBe("H-0042");
expect(hydrant?.typ).toBe("UNTERFLUR");
expect(hydrant?.nennweite).toBe(150);
expect(hydrant?.status).toBe("EINSATZBEREIT");
expect(hydrant?.strasse).toBe("Hauptstraße");
});
it("nimmt die Reihenfolge von GeoJSON ernst: erst Länge, dann Breite", () => {
const [hydrant] = ausGeoJson(geojson).hydranten;
expect(hydrant?.breitengrad).toBeCloseTo(48.502, 3);
expect(hydrant?.laengengrad).toBeCloseTo(12.151, 3);
});
it("überspringt, was keine Punktgeometrie ist, und sagt warum", () => {
const ergebnis = ausGeoJson(geojson);
expect(ergebnis.uebersprungen).toHaveLength(2);
expect(ergebnis.uebersprungen[0]?.grund).toContain("Punktgeometrie");
expect(ergebnis.uebersprungen[1]?.grund).toContain("Erde");
});
it("scheitert nicht an einer kaputten Datei", () => {
const ergebnis = ausGeoJson("{ das ist kein json");
expect(ergebnis.hydranten).toHaveLength(0);
expect(ergebnis.hinweise.join(" ")).toContain("kein gültiges JSON");
});
});
describe("CSV-Zeilen", () => {
it("findet die Spalten unter verschiedenen Namen", () => {
const ergebnis = ausZeilen([
{
"Hydrant-Nr": "4711",
Bauart: "Überflur",
"DN": "100",
Lage: "Bahnhofstraße 3",
Latitude: "48,5031",
Longitude: "12,1522",
Pruefdatum: "12.03.2026",
Versorger: "Stadtwerke Musterhausen",
},
]);
expect(ergebnis.hydranten).toHaveLength(1);
const [hydrant] = ergebnis.hydranten;
expect(hydrant?.externeNummer).toBe("4711");
expect(hydrant?.typ).toBe("UEBERFLUR");
expect(hydrant?.nennweite).toBe(100);
expect(hydrant?.strasse).toBe("Bahnhofstraße 3");
expect(hydrant?.betreiber).toBe("Stadtwerke Musterhausen");
expect(hydrant?.letztePruefung).toEqual(new Date("2026-03-12T00:00:00.000Z"));
expect(hydrant?.breitengrad).toBeCloseTo(48.5031, 4);
});
it("dreht vertauschte Achsen zurück und sagt es", () => {
const ergebnis = ausZeilen([{ lat: "12,15", lon: "48,50" }]);
expect(ergebnis.hydranten[0]?.breitengrad).toBeCloseTo(48.5, 2);
expect(ergebnis.hydranten[0]?.laengengrad).toBeCloseTo(12.15, 2);
expect(ergebnis.hinweise.join(" ")).toContain("vertauscht");
});
it("überspringt Zeilen ohne Koordinaten und nennt die Zeilennummer", () => {
const ergebnis = ausZeilen([
{ lat: "48,5", lon: "12,15" },
{ Bezeichnung: "Hydrant ohne Ort" },
]);
expect(ergebnis.hydranten).toHaveLength(1);
// Zeile 1 ist die Kopfzeile — die zweite Datenzeile ist Zeile 3.
expect(ergebnis.uebersprungen).toEqual([{ zeile: 3, grund: "Keine Koordinaten in der Zeile." }]);
});
it("warnt, wenn Nummern fehlen — ein zweiter Import legt sonst alles doppelt an", () => {
const ergebnis = ausZeilen([
{ lat: "48,5", lon: "12,15" },
{ lat: "48,51", lon: "12,16" },
]);
expect(ergebnis.hinweise.join(" ")).toContain("keine Nummer");
});
it("kommt mit einer leeren Liste zurecht", () => {
const ergebnis = ausZeilen([]);
expect(ergebnis.hydranten).toHaveLength(0);
expect(ergebnis.uebersprungen).toHaveLength(0);
});
});