import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; const ENV_KEYS = ["DHL_API_KEY", "DHL_API_SECRET", "DHL_GKP_USERNAME", "DHL_GKP_PASSWORD", "DHL_ACCOUNT_NUMBER"] as const; const originalEnv: Record = {}; function configureDhl(): void { process.env.DHL_API_KEY = "api-key"; process.env.DHL_API_SECRET = "api-secret"; process.env.DHL_GKP_USERNAME = "gkp-user"; process.env.DHL_GKP_PASSWORD = "gkp-pass"; process.env.DHL_ACCOUNT_NUMBER = "33333333330101"; } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); } const validAddress = { firstName: "Erika", lastName: "Musterfrau", street: "Musterstraße 12a", zip: "12345", city: "Musterstadt", country: "DE", }; const validShipper = { ...validAddress, companyName: "Wabenhain" }; // dhl.ts haelt das OAuth2-Token in einem modulweiten In-Memory-Cache (siehe // Kommentar dort) — vi.resetModules() + dynamischer Import pro Test stellt // sicher, dass kein Token zwischen Tests "durchsickert" und einen erwarteten // Token-Request-Mock stillschweigend ueberspringt. beforeEach(() => { vi.resetModules(); for (const key of ENV_KEYS) { originalEnv[key] = process.env[key]; delete process.env[key]; } delete process.env.DHL_ENVIRONMENT; }); afterEach(() => { for (const key of ENV_KEYS) { if (originalEnv[key] === undefined) delete process.env[key]; else process.env[key] = originalEnv[key]; } vi.unstubAllGlobals(); }); describe("isDhlConfigured", () => { test("is false when env vars are missing", async () => { const { isDhlConfigured } = await import("./dhl"); expect(isDhlConfigured()).toBe(false); }); test("is true once all required env vars are set", async () => { configureDhl(); const { isDhlConfigured } = await import("./dhl"); expect(isDhlConfigured()).toBe(true); }); }); describe("createDhlShipmentLabel", () => { test("throws DhlNotConfiguredError instead of calling the network when unconfigured", async () => { const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); const { createDhlShipmentLabel, DhlNotConfiguredError } = await import("./dhl"); await expect( createDhlShipmentLabel({ reference: "WH-2026-0001", weightGrams: 500, shipper: validShipper, receiver: validAddress, }) ).rejects.toThrow(DhlNotConfiguredError); expect(fetchMock).not.toHaveBeenCalled(); }); test("splits the street/house-number, maps the ISO2 country to ISO3, and returns the tracking number + label on success", async () => { configureDhl(); const fetchMock = vi .fn() // 1. Token-Aufruf .mockResolvedValueOnce(jsonResponse({ access_token: "token-abc", expires_in: 3600 })) // 2. Sendungserstellung .mockResolvedValueOnce( jsonResponse({ items: [{ shipmentNo: "00340434161094123456", label: { b64: "cGRmLWJ5dGVz" } }], }) ); vi.stubGlobal("fetch", fetchMock); const { createDhlShipmentLabel } = await import("./dhl"); const result = await createDhlShipmentLabel({ reference: "WH-2026-0001", weightGrams: 500, shipper: validShipper, receiver: validAddress, }); expect(result).toEqual({ trackingNumber: "00340434161094123456", labelPdfBase64: "cGRmLWJ5dGVz" }); // Zweiter fetch-Aufruf ist die eigentliche Sendungserstellung — Body // pruefen, dass Adress-Mapping korrekt durchgereicht wurde. const shipmentCall = fetchMock.mock.calls[1]; const requestBody = JSON.parse(shipmentCall[1].body as string); const consignee = requestBody.shipments[0].consignee; expect(consignee.address.addressStreet).toBe("Musterstraße"); expect(consignee.address.addressHouse).toBe("12a"); expect(consignee.address.country).toBe("DEU"); expect(shipmentCall[1].headers["dhl-api-key"]).toBe("api-key"); expect(shipmentCall[1].headers["Authorization"]).toBe("Bearer token-abc"); }); test("rejects an unsupported destination country instead of guessing a code", async () => { configureDhl(); const fetchMock = vi.fn().mockResolvedValueOnce(jsonResponse({ access_token: "token-abc", expires_in: 3600 })); vi.stubGlobal("fetch", fetchMock); const { createDhlShipmentLabel, DhlApiError } = await import("./dhl"); await expect( createDhlShipmentLabel({ reference: "WH-2026-0002", weightGrams: 500, shipper: validShipper, receiver: { ...validAddress, country: "US" }, }) ).rejects.toThrow(DhlApiError); }); test("surfaces a DHL-side validation error instead of returning a fake success", async () => { configureDhl(); const fetchMock = vi .fn() .mockResolvedValueOnce(jsonResponse({ access_token: "token-abc", expires_in: 3600 })) .mockResolvedValueOnce( jsonResponse({ items: [ { validationMessages: [{ validationState: "Error", validationMessage: "postalCode ungültig" }], }, ], }) ); vi.stubGlobal("fetch", fetchMock); const { createDhlShipmentLabel, DhlApiError } = await import("./dhl"); await expect( createDhlShipmentLabel({ reference: "WH-2026-0003", weightGrams: 500, shipper: validShipper, receiver: validAddress, }) ).rejects.toThrow(DhlApiError); }); test("surfaces an HTTP error from the DHL API with its detail message", async () => { configureDhl(); const fetchMock = vi .fn() .mockResolvedValueOnce(jsonResponse({ access_token: "token-abc", expires_in: 3600 })) .mockResolvedValueOnce(jsonResponse({ detail: "Ungültige Anfrage" }, 400)); vi.stubGlobal("fetch", fetchMock); const { createDhlShipmentLabel, DhlApiError } = await import("./dhl"); const attempt = createDhlShipmentLabel({ reference: "WH-2026-0004", weightGrams: 500, shipper: validShipper, receiver: validAddress, }); // Neben der Meldung auch den Fehlertyp pruefen — wie in den beiden Tests // darueber. Ohne diese Zusage blieb das bereits importierte DhlApiError // hier ungenutzt, und der Test haette jeden beliebigen anderen Fehler mit // passendem Text durchgehen lassen. await expect(attempt).rejects.toThrow(DhlApiError); await expect(attempt).rejects.toThrow(/Ungültige Anfrage/); }); });