Files
BuTzZ 593dbd5f85
Test and publish container images / test (push) Successful in 2m26s
Test and publish container images / publish (push) Failing after 3s
optimized events
2026-07-28 14:36:47 +02:00

383 lines
11 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
deduplicateMarineContactFeatures,
type FeatureDatabase,
FeatureService,
FeatureSourceUnavailableError,
normalizeDepthFeatureProperties,
normalizeMarineFeatureProperties
} from "../src/services/features.js";
describe("marine feature data modes", () => {
it("fails explicitly when production has no configured feature source", async () => {
const service = new FeatureService({
databaseUrl: undefined,
demoData: false
});
await expect(
service.getFeatures({
bbox: [5, 50, 15, 56],
layers: ["seamarks", "locks", "harbours"]
})
).rejects.toBeInstanceOf(FeatureSourceUnavailableError);
expect(service.sourceMode).toBe("unavailable");
expect(await service.checkReadiness()).toEqual({
ready: false,
source: "unavailable",
detail: "not_configured"
});
await service.close();
});
it("checks that the configured PostGIS schema is ready", async () => {
const pool = featurePool(
{
rows: [
{
marine_features: "marine_features",
marine_fairway_edges: "marine_fairway_edges"
}
]
},
{
rows: [{ harbours: true, locks: true, bridges: true }]
}
);
const service = new FeatureService(
{ databaseUrl: "postgres://features.test/watermaps", demoData: false },
{ pool }
);
expect(service.sourceMode).toBe("postgis");
expect(await service.checkReadiness()).toEqual({
ready: true,
source: "postgis"
});
expect(pool.query).toHaveBeenCalledWith(expect.stringContaining("to_regclass"));
expect(pool.query).toHaveBeenCalledWith(expect.stringContaining("EXISTS"));
await service.close();
expect(pool.end).toHaveBeenCalledOnce();
});
it("reports a configured database with missing feature tables as not ready", async () => {
const pool = featurePool({
rows: [{ marine_features: "marine_features", marine_fairway_edges: null }]
});
const service = new FeatureService(
{ databaseUrl: "postgres://features.test/watermaps", demoData: false },
{ pool }
);
expect(await service.checkReadiness()).toEqual({
ready: false,
source: "postgis",
detail: "schema_missing"
});
await service.close();
});
it("reports initialized PostGIS tables without event data as not ready", async () => {
const pool = featurePool(
{
rows: [
{
marine_features: "marine_features",
marine_fairway_edges: "marine_fairway_edges"
}
]
},
{
rows: [{ harbours: false, locks: true, bridges: false }]
}
);
const service = new FeatureService(
{ databaseUrl: "postgres://features.test/watermaps", demoData: false },
{ pool }
);
expect(await service.checkReadiness()).toEqual({
ready: false,
source: "postgis",
detail: "data_missing",
missingLayers: ["harbours", "bridges"]
});
await service.close();
});
it("returns ordinary bridges from the requested bbox for route-side filtering", async () => {
const pool = featurePool({
rows: [
{
id: "bridge-ordinary",
layer: "bridges",
name: "Normale Kanalbrücke",
source: "osm",
source_id: "w100",
properties: { bridge: "yes" },
updated_at: "2026-07-20T10:00:00.000Z",
geometry: {
type: "LineString",
coordinates: [
[7.2, 53.4],
[7.21, 53.4]
]
}
}
]
});
const service = new FeatureService(
{ databaseUrl: "postgres://features.test/watermaps", demoData: false },
{ pool }
);
const result = await service.getFeatures({
bbox: [7.1, 53.3, 7.5, 53.5],
layers: ["bridges"]
});
const sql = String(vi.mocked(pool.query).mock.calls[0]?.[0]);
expect(sql).not.toContain("properties ? 'maxheight'");
expect(sql).not.toContain("properties->>'bridge'");
expect(result.features).toEqual([
expect.objectContaining({
id: "bridge-ordinary",
properties: expect.objectContaining({
layer: "bridges",
name: "Normale Kanalbrücke",
clearance_m: null
})
})
]);
await service.close();
});
});
describe("marine feature normalization", () => {
it("formats bridge clearance labels from known OSM height tags", () => {
const properties = normalizeMarineFeatureProperties({
layer: "bridges",
name: "Kaiser-Wilhelm-Brücke",
source: "osm",
sourceId: "w123",
properties: {
bridge: "movable",
maxheight: "3"
}
});
expect(properties.clearance_m).toBe(3);
expect(properties.clearance_label).toBe("H 3 m");
expect(properties.label).toBe("Kaiser-Wilhelm-Brücke H 3 m");
});
it("ignores non-numeric default bridge heights", () => {
const properties = normalizeMarineFeatureProperties({
layer: "bridges",
name: null,
source: "osm",
sourceId: "w124",
properties: {
bridge: "yes",
maxheight: "default"
}
});
expect(properties.clearance_m).toBeNull();
expect(properties.label).toBeNull();
});
it("does not mistake a bridge structure height for navigable clearance", () => {
const properties = normalizeMarineFeatureProperties({
layer: "bridges",
name: "Klappbrücke",
source: "osm",
sourceId: "w125",
properties: {
bridge: "movable",
height: "18"
}
});
expect(properties.clearance_m).toBeNull();
expect(properties.clearance_label).toBeNull();
expect(properties.label).toBe("Klappbrücke");
});
it("normalizes contact aliases, address and database timestamps", () => {
const properties = normalizeMarineFeatureProperties({
layer: "locks",
name: "Schleuse Hamm",
source: "osm",
sourceId: "w166568834",
updatedAt: new Date("2026-07-19T08:30:00.000Z"),
properties: {
"contact:phone": "+49 2381 9019280",
"contact:website": "https://example.test/schleuse-hamm",
"contact:email": "schleuse@example.test",
"seamark:lock_basin:communication_channel": "18",
opening_hours: "24/7",
"operator:name": "WSV",
"addr:street": "Fährstraße",
"addr:housenumber": "1",
"addr:postcode": "59071",
"addr:city": "Hamm",
"addr:country": "DE"
}
});
expect(properties.phone).toBe("+49 2381 9019280");
expect(properties.website).toBe("https://example.test/schleuse-hamm");
expect(properties.email).toBe("schleuse@example.test");
expect(properties.vhf).toBe("18");
expect(properties.openingHours).toBe("24/7");
expect(properties.operator).toBe("WSV");
expect(properties.address).toBe("Fährstraße 1, 59071 Hamm, DE");
expect(properties.source).toBe("osm");
expect(properties.sourceId).toBe("w166568834");
expect(properties.updatedAt).toBe("2026-07-19T08:30:00.000Z");
});
it("normalizes lock-specific opening hours for event details", () => {
const properties = normalizeMarineFeatureProperties({
layer: "locks",
name: "Schleuse Rahe",
source: "osm",
sourceId: "w126",
properties: {
"lock:opening_hours": "Mo-Su 07:00-20:00"
}
});
expect(properties.openingHours).toBe("Mo-Su 07:00-20:00");
});
it("prefers direct contact fields and returns stable null values when details are absent", () => {
const properties = normalizeMarineFeatureProperties({
layer: "harbours",
name: "Marina Emden",
source: "osm",
sourceId: "n1",
properties: {
phone: "+49 4921 123",
"contact:phone": "+49 4921 999"
}
});
expect(properties.phone).toBe("+49 4921 123");
expect(properties.website).toBeNull();
expect(properties.email).toBeNull();
expect(properties.vhf).toBeNull();
expect(properties.openingHours).toBeNull();
expect(properties.operator).toBeNull();
expect(properties.address).toBeNull();
expect(properties.updatedAt).toBeNull();
});
it("keeps navigation details but removes unrelated bulk OSM tags from viewport features", () => {
const properties = normalizeMarineFeatureProperties({
layer: "harbours",
name: "Testhafen",
source: "osm",
sourceId: "w42",
properties: {
leisure: "marina",
electricity: "yes",
"contact:phone": "+49 40 123",
"source:geometry": "survey",
note: "A very large unrelated note that is not consumed by the client"
}
});
expect(properties.leisure).toBe("marina");
expect(properties.electricity).toBe("yes");
expect(properties.phone).toBe("+49 40 123");
expect(properties).not.toHaveProperty("source:geometry");
expect(properties).not.toHaveProperty("note");
});
it("formats fairway depth labels", () => {
const properties = normalizeDepthFeatureProperties({
name: "Nord-Ostsee-Kanal",
source: "osm",
sourceId: "w456",
minDepthM: "14",
properties: {
depth: "14"
}
});
expect(properties.depth_m).toBe(14);
expect(properties.depth_label).toBe("14 m");
expect(properties.label).toBe("Nord-Ostsee-Kanal 14 m");
});
});
function featurePool(...results: Array<{ rows: unknown[] }>): FeatureDatabase & {
query: ReturnType<typeof vi.fn>;
end: ReturnType<typeof vi.fn>;
} {
const query = vi.fn();
for (const result of results) {
query.mockResolvedValueOnce(result);
}
return {
query,
end: vi.fn().mockResolvedValue(undefined)
} as unknown as FeatureDatabase & {
query: ReturnType<typeof vi.fn>;
end: ReturnType<typeof vi.fn>;
};
}
describe("marine contact feature deduplication", () => {
it("returns one stable facility feature and reports how many raw objects were merged", () => {
const result = deduplicateMarineContactFeatures([
{
type: "Feature",
id: "12",
geometry: { type: "Point", coordinates: [7.867, 51.695] },
properties: {
layer: "locks",
source: "osm",
sourceId: "w12",
name: "Schleuse Werries",
website: "https://example.test/werries"
}
},
{
type: "Feature",
id: "99",
geometry: { type: "Point", coordinates: [7.86708, 51.69508] },
properties: {
layer: "locks",
source: "euris",
sourceId: "DEHMM00301LOCKS00404",
name: "Werries",
phone: "+49 2381 9019-290",
vhf: "22",
"ref:EU:RIS": "DEHMM00301LOCKS00404"
}
},
{
type: "Feature",
id: "bridge-1",
geometry: { type: "LineString", coordinates: [[7.8, 51.6], [7.9, 51.7]] },
properties: { layer: "bridges", source: "osm", name: "Testbrücke" }
}
]);
expect(result.metadata).toEqual({ inputPoiCount: 2, outputPoiCount: 1, mergedObjectCount: 1 });
expect(result.features).toHaveLength(2);
expect(result.features.find((feature) => feature.properties.layer === "locks")).toMatchObject({
id: "marine-poi:locks:euris:DEHMM00301LOCKS00404",
properties: {
name: "Werries",
phone: "+49 2381 9019-290",
website: "https://example.test/werries",
dedupeMemberCount: 2
}
});
});
});