import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; const ENV_KEYS = ["DHL_API_KEY", "DHL_TRACKING_API_KEY"] as const; const originalEnv: Record = {}; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); } /** Ein Ereignis im Format der DHL Shipment Tracking – Unified API. */ function dhlEvent(overrides: Record = {}) { return { timestamp: "2026-09-24T08:15:00Z", location: { address: { addressLocality: "Augsburg", postalCode: "86150", countryCode: "DE" } }, statusCode: "transit", status: "In Zustellung", description: "Die Sendung wurde in das Zustellfahrzeug geladen.", ...overrides, }; } function dhlPayload(shipmentOverrides: Record = {}) { return { shipments: [ { id: "00340434161234567890", service: "parcel-de", status: dhlEvent(), estimatedTimeOfDelivery: "2026-09-25T18:00:00Z", events: [ dhlEvent({ timestamp: "2026-09-23T17:40:00Z", statusCode: "pre-transit", status: "Elektronisch angekündigt", description: "Die Sendung wurde elektronisch angekündigt.", location: undefined, }), dhlEvent(), ], ...shipmentOverrides, }, ], }; } const QUERY = { trackingNumber: "00340434161234567890", postalCode: "86450", locale: "de" as const }; // dhl-tracking.ts haelt Ergebnisse in einem modulweiten Cache — pro Test ein // frisches Modul, damit kein Treffer aus einem vorherigen Test den erwarteten // fetch-Aufruf stillschweigend ueberspringt (gleiches Muster wie dhl.test.ts). beforeEach(() => { vi.resetModules(); for (const key of ENV_KEYS) { originalEnv[key] = process.env[key]; delete process.env[key]; } vi.spyOn(console, "warn").mockImplementation(() => {}); }); afterEach(() => { for (const key of ENV_KEYS) { if (originalEnv[key] === undefined) delete process.env[key]; else process.env[key] = originalEnv[key]; } vi.unstubAllGlobals(); vi.restoreAllMocks(); }); describe("parseTrackingResponse", () => { it("liefert Status, voraussichtliche Zustellung und Ereignisse, neuestes zuerst", async () => { const { parseTrackingResponse } = await import("./dhl-tracking"); const info = parseTrackingResponse(dhlPayload()); expect(info?.status).toBe("transit"); expect(info?.statusText).toBe("In Zustellung"); expect(info?.estimatedDelivery?.toISOString()).toBe("2026-09-25T18:00:00.000Z"); expect(info?.events.map((e) => e.status)).toEqual(["transit", "pre-transit"]); expect(info?.events[0].location).toBe("Augsburg"); expect(info?.events[1].location).toBeNull(); }); it("bildet unbekannte Statuscodes auf 'unknown' ab, statt die Antwort zu verwerfen", async () => { const { parseTrackingResponse } = await import("./dhl-tracking"); const info = parseTrackingResponse(dhlPayload({ status: dhlEvent({ statusCode: "neuer-code" }) })); expect(info?.status).toBe("unknown"); }); it("ueberspringt Ereignisse mit ungueltigem Zeitstempel", async () => { const { parseTrackingResponse } = await import("./dhl-tracking"); const info = parseTrackingResponse(dhlPayload({ events: [dhlEvent({ timestamp: "kein-datum" }), dhlEvent()] })); expect(info?.events).toHaveLength(1); }); it("kuerzt uebermaessig lange Texte, damit eine fremde Antwort das Layout nicht sprengt", async () => { const { parseTrackingResponse } = await import("./dhl-tracking"); const info = parseTrackingResponse(dhlPayload({ events: [dhlEvent({ description: "x".repeat(5000) })] })); expect(info?.events[0].description.length).toBeLessThanOrEqual(300); }); it("liefert null bei einer Antwort ohne Sendung", async () => { const { parseTrackingResponse } = await import("./dhl-tracking"); expect(parseTrackingResponse({ shipments: [] })).toBeNull(); expect(parseTrackingResponse("")).toBeNull(); }); }); describe("getDhlTracking", () => { it("ruft ohne API-Key das Netzwerk gar nicht erst auf", async () => { const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); const { getDhlTracking } = await import("./dhl-tracking"); await expect(getDhlTracking(QUERY)).resolves.toEqual({ kind: "disabled" }); expect(fetchMock).not.toHaveBeenCalled(); }); it("schickt eine offensichtlich ungueltige Sendungsnummer nicht an DHL", async () => { process.env.DHL_API_KEY = "api-key"; const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); const { getDhlTracking } = await import("./dhl-tracking"); await expect(getDhlTracking({ ...QUERY, trackingNumber: "../x?y=1" })).resolves.toEqual({ kind: "not-found" }); expect(fetchMock).not.toHaveBeenCalled(); }); it("fragt mit API-Key-Header, Service, PLZ und Sprache an und liefert die Ereignisse", async () => { process.env.DHL_API_KEY = "api-key"; const fetchMock = vi.fn().mockResolvedValue(jsonResponse(dhlPayload())); vi.stubGlobal("fetch", fetchMock); const { getDhlTracking } = await import("./dhl-tracking"); const result = await getDhlTracking(QUERY); expect(result.kind).toBe("ok"); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; const parsedUrl = new URL(url); expect(parsedUrl.origin + parsedUrl.pathname).toBe("https://api-eu.dhl.com/track/shipments"); expect(parsedUrl.searchParams.get("trackingNumber")).toBe(QUERY.trackingNumber); expect(parsedUrl.searchParams.get("service")).toBe("parcel-de"); expect(parsedUrl.searchParams.get("recipientPostalCode")).toBe("86450"); expect(parsedUrl.searchParams.get("language")).toBe("de"); expect((init.headers as Record)["DHL-API-Key"]).toBe("api-key"); }); it("bevorzugt DHL_TRACKING_API_KEY, falls das Tracking in einer eigenen DHL-App freigeschaltet ist", async () => { process.env.DHL_API_KEY = "shipping-key"; process.env.DHL_TRACKING_API_KEY = "tracking-key"; const fetchMock = vi.fn().mockResolvedValue(jsonResponse(dhlPayload())); vi.stubGlobal("fetch", fetchMock); const { getDhlTracking } = await import("./dhl-tracking"); await getDhlTracking(QUERY); const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect((init.headers as Record)["DHL-API-Key"]).toBe("tracking-key"); }); it("beantwortet eine wiederholte Abfrage aus dem Cache, um das DHL-Kontingent zu schonen", async () => { process.env.DHL_API_KEY = "api-key"; const fetchMock = vi.fn().mockResolvedValue(jsonResponse(dhlPayload())); vi.stubGlobal("fetch", fetchMock); const { getDhlTracking } = await import("./dhl-tracking"); await getDhlTracking(QUERY); await getDhlTracking(QUERY); expect(fetchMock).toHaveBeenCalledTimes(1); }); it("meldet 404 als 'not-found' — die Sendung ist DHL (noch) nicht bekannt", async () => { process.env.DHL_API_KEY = "api-key"; vi.stubGlobal("fetch", vi.fn().mockResolvedValue(jsonResponse({ status: 404 }, 404))); const { getDhlTracking } = await import("./dhl-tracking"); await expect(getDhlTracking(QUERY)).resolves.toEqual({ kind: "not-found" }); }); it.each([ ["Kontingent erschoepft", () => Promise.resolve(jsonResponse({}, 429))], ["falscher Key", () => Promise.resolve(jsonResponse({}, 401))], ["Netzwerkfehler", () => Promise.reject(new TypeError("fetch failed"))], ["kaputtes JSON", () => Promise.resolve(new Response("", { status: 200 }))], ])("liefert bei %s 'unavailable' statt die Seite scheitern zu lassen", async (_label, impl) => { process.env.DHL_API_KEY = "api-key"; vi.stubGlobal("fetch", vi.fn().mockImplementation(impl)); const { getDhlTracking } = await import("./dhl-tracking"); await expect(getDhlTracking(QUERY)).resolves.toEqual({ kind: "unavailable" }); }); it("wiederholt eine fehlgeschlagene Abfrage nicht sofort, sondern haelt kurz Abstand", async () => { process.env.DHL_API_KEY = "api-key"; const fetchMock = vi.fn().mockResolvedValue(jsonResponse({}, 429)); vi.stubGlobal("fetch", fetchMock); const { getDhlTracking } = await import("./dhl-tracking"); await getDhlTracking(QUERY); await getDhlTracking(QUERY); expect(fetchMock).toHaveBeenCalledTimes(1); }); });