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(); }); });