Vier offene Befunde standen einem sauberen Produktionslauf im Weg:
- recently-viewed-section und site-chrome riefen setState direkt im
Effektrumpf auf, um einen Leerzustand herzustellen ("keine angesehenen
Produkte" bzw. "Eingabe kuerzer als zwei Zeichen"). Beides ergibt sich
unmittelbar aus vorhandenen Werten und wird jetzt beim Rendern
abgeleitet — das spart je einen zusaetzlichen Renderdurchlauf. Bei der
Suche verschwindet die Vorschlagsliste dadurch sofort beim Leeren des
Feldes statt erst nach dem naechsten Effektlauf.
- STAT_KEYS im Admin-Dashboard wurde ausschliesslich als Typ verwendet,
landete als `as const`-Array aber trotzdem im Bundle. Ersetzt durch
einen Typ-Alias.
- Ein DHL-Test importierte DhlApiError, ohne es zu benutzen, und pruefte
nur den Meldungstext — er haette damit jeden beliebigen anderen Fehler
mit passendem Text durchgehen lassen. Er prueft jetzt zusaetzlich den
Fehlertyp, wie seine beiden Geschwistertests.
Stand danach: eslint ohne Beanstandung (vorher 2 Fehler, 2 Warnungen),
tsc ohne Fehler, 201 Tests gruen, Produktions-Build erfolgreich.
183 lines
6.4 KiB
TypeScript
183 lines
6.4 KiB
TypeScript
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<string, string | undefined> = {};
|
|
|
|
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/);
|
|
});
|
|
});
|