Files
watermaps/apps/api/tests/euris-lock-sync.test.mjs
2026-07-24 11:29:24 +02:00

218 lines
6.1 KiB
JavaScript

import { describe, expect, it, vi } from "vitest";
import {
MAX_DETAIL_LIMIT,
buildLockRecord,
collectEurisLocks,
compactLocksFilter,
normalizePhones,
parseCountries,
parseDetailLimit,
requestJson,
risIndexFilter,
runEurisSync,
} from "../../../scripts/sync-euris-locks.mjs";
function jsonResponse(payload, init = {}) {
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "content-type": "application/json" },
...init,
});
}
function paginatedFixtureFetch({ recordRequests = [] } = {}) {
const compact = [
{
locode: "DELOCK001",
objectName: "Testschleuse Nord",
waterwayName: "Testkanal",
contactPhone: "0049 201 12345",
comcha: " 18 ",
},
{ locode: "DELOCK002", objectName: "Testschleuse Süd" },
];
const ris = [
{
isrs: "DELOCK001",
objectName: "Testschleuse Nord",
countryCode: "DE",
lon: 7.1,
lat: 51.5,
source: "WSV, Wadaba",
},
{
isrs: "DELOCK002",
objectName: "Testschleuse Süd",
countryCode: "DE",
lon: 7.2,
lat: 51.6,
source: "WSV, Wadaba",
},
{
isrs: "DELOCK003",
objectName: "Nur im RIS-Index",
countryCode: "DE",
lon: 7.3,
lat: 51.7,
source: "WSV",
},
];
return async (input, init) => {
const url = new URL(String(input));
recordRequests.push({ url, init });
const skip = Number(url.searchParams.get("$skip"));
const top = Number(url.searchParams.get("$top"));
if (url.pathname.endsWith("/GetCompactLocks")) {
return jsonResponse({ count: compact.length, items: compact.slice(skip, skip + top) });
}
if (url.pathname.endsWith("/GetRISIndexObjects")) {
return jsonResponse({ count: ris.length, items: ris.slice(skip, skip + top) });
}
throw new Error(`Unerwartete Test-URL: ${url}`);
};
}
describe("EuRIS lock synchronization", () => {
it("validates country filters and caps optional detail requests", () => {
expect(parseCountries(" de,NL de ")).toEqual(["DE", "NL"]);
expect(() => parseCountries("DEU")).toThrow(/ISO-Ländercode/u);
expect(parseDetailLimit("999")).toBe(MAX_DETAIL_LIMIT);
expect(compactLocksFilter(["DE", "NL"])).toContain("startswith(locode,'DE')");
expect(risIndexFilter(["DE", "NL"])).toBe(
"(countryCode eq 'DE' or countryCode eq 'NL') and function eq 'lokare'",
);
});
it("normalizes EuRIS contact data and always uses the RIS coordinate", () => {
expect(normalizePhones("0049 201 12345; +49 201 67890")).toEqual([
"+49 201 12345",
"+49 201 67890",
]);
const record = buildLockRecord({
compact: {
locode: "DELOCK001",
objectName: "Lock",
waterwayName: "Canal",
contactPhone: "0049 201 12345",
comcha: " 18 ",
},
ris: {
isrs: "DELOCK001",
lon: "7.123",
lat: "51.456",
source: "WSV, Wadaba",
countryCode: "DE",
},
detail: {
facility: {
street: "Uferstraße 1",
postCode: "12345",
city: "Teststadt",
country: "DE",
contacts: [
{
company: "Wasserstraßenverwaltung",
emails: ["lock@example.test"],
phones: ["+49 201 999"],
},
],
},
},
fetchedAt: "2026-07-20T10:00:00.000Z",
});
expect(record).toMatchObject({
sourceId: "DELOCK001",
longitude: 7.123,
latitude: 51.456,
properties: {
phone: "+49 201 12345",
vhf: "18",
waterway_name: "Canal",
operator: "Wasserstraßenverwaltung",
email: "lock@example.test",
address: "Uferstraße 1, 12345, Teststadt, DE",
upstream_source: "WSV, Wadaba",
fetched_at: "2026-07-20T10:00:00.000Z",
},
});
expect(record.properties.source_url).toContain("isrs=DELOCK001");
});
it("honors Retry-After for throttled requests and sends an optional bearer token", async () => {
const waits = [];
const headers = [];
let calls = 0;
const fetchImpl = async (_url, init) => {
calls += 1;
headers.push(new Headers(init.headers));
if (calls === 1) {
return new Response("rate limited", {
status: 429,
headers: { "retry-after": "2" },
});
}
return jsonResponse({ ok: true });
};
await expect(
requestJson("https://example.test/euris", {
fetchImpl,
token: "secret-token",
sleepImpl: async (milliseconds) => waits.push(milliseconds),
}),
).resolves.toEqual({ ok: true });
expect(waits).toEqual([2_000]);
expect(headers.every((entry) => entry.get("authorization") === "Bearer secret-token")).toBe(true);
});
it("paginates compact and RIS data stably, joins by ISRS and keeps RIS-only locks", async () => {
const requests = [];
const result = await collectEurisLocks({
countries: ["DE"],
detailLimit: 0,
pageSize: 1,
fetchImpl: paginatedFixtureFetch({ recordRequests: requests }),
token: "token",
fetchedAt: "2026-07-20T10:00:00.000Z",
});
expect(result.records.map((record) => record.sourceId)).toEqual([
"DELOCK001",
"DELOCK002",
"DELOCK003",
]);
expect(result.stats).toMatchObject({
compactLocks: 2,
risLocks: 3,
joinedLocks: 2,
risOnlyLocks: 1,
storedLocks: 3,
});
expect(requests.every(({ url }) => Number(url.searchParams.get("$top")) <= 100)).toBe(true);
expect(requests.every(({ url }) => url.searchParams.has("$orderby"))).toBe(true);
expect(
requests.every(({ init }) => new Headers(init.headers).get("authorization") === "Bearer token"),
).toBe(true);
});
it("does not invoke the database writer in dry-run mode", async () => {
const writer = vi.fn();
const result = await runEurisSync({
dryRun: true,
writer,
collectorOptions: {
countries: ["DE"],
pageSize: 100,
fetchImpl: paginatedFixtureFetch(),
},
});
expect(result.records).toHaveLength(3);
expect(writer).not.toHaveBeenCalled();
});
});