Files
Wabenhain/src/lib/dhl-tracking.test.ts
T
Nick SzaunigandClaude Opus 5.5 01eaca39dc feat: DHL-Sendungsverlauf direkt auf der Tracking-Seite anzeigen
/bestellung-verfolgen holt den Sendungsverlauf serverseitig ueber die
DHL Shipment Tracking - Unified API und zeigt ihn im eigenen Layout an,
statt Kundschaft auf dhl.de weiterzuleiten.

- lib/dhl-tracking.ts: Abfrage mit API-Key, Zod-Validierung, Timeout,
  Cache pro Sendung (unterwegs 10 min, zugestellt 12 h, Fehler 2 min)
- Abfrage erst nach erfolgreicher Pruefung von Bestellnummer + E-Mail,
  damit die Seite nicht als offener Proxy zu DHL dient
- Ohne freigeschalteten Key bleibt die Seite unveraendert (Nummer + Link)
- Optional DHL_TRACKING_API_KEY, falls das Tracking in eigener DHL-App liegt
- Datenschutzerklaerung um die Statusabfrage bei DHL ergaenzt

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 03:27:58 +02:00

212 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
const ENV_KEYS = ["DHL_API_KEY", "DHL_TRACKING_API_KEY"] as const;
const originalEnv: Record<string, string | undefined> = {};
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<string, unknown> = {}) {
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<string, unknown> = {}) {
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("<html>")).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<string, string>)["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<string, string>)["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("<html>", { 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);
});
});