import { test } from "node:test"; import assert from "node:assert/strict"; import { CHECKOUT_ERROR_MESSAGES as M, checkoutErrorMessage, checkoutErrorMessageFor } from "../../utils/checkoutError.ts"; // The shop's answer for a CMS error (server/utils/cmsError.ts), as Nitro writes it for a handled error. const shopError = (statusCode: number, data: { message: string; errors?: string[]; missing?: string[] }) => ({ error: true, url: "/api/orders/11111111-2222-4333-8444-555555555555", statusCode, statusMessage: data.message, message: data.message, data }); // The CMS's own answer, Strapi's error body. const strapiError = (status: number, message: string, details: Record = {}) => ({ data: null, error: { status, name: "Error", message, details } }); // Built like ofetch's FetchError in the browser: getters for the answer's status and parsed body. const fetchError = (status: number | undefined, body: unknown): Error => { const error = new Error(`[PUT] "/api/orders/11111111-2222-4333-8444-555555555555": ${status ?? ""}`); error.name = "FetchError"; for (const [key, value] of [ ["data", body], ["status", status], ["statusCode", status] ] as const) { Object.defineProperty(error, key, { get: () => value }); } return error; }; const ALL_MESSAGES = new Set(Object.values(M)); test("asks for a valid e-mail address when the CMS rejects data.email", () => { const body = shopError(400, { message: "Invalid order update", errors: ["data.email: must be an email address of at most 254 characters"] }); assert.equal(checkoutErrorMessage(400, body), "Bitte gib eine gültige E-Mail-Adresse ein."); }); test("asks to check the input when the CMS rejects another field", () => { const body = shopError(400, { message: "Invalid order update", errors: ["data.invoiceAddressStructured.postalCode: must be a string of at most 200 characters, or null"] }); assert.equal(checkoutErrorMessage(400, body), M.invalidInput); assert.equal( checkoutErrorMessage(400, shopError(400, { message: "Invalid order update", errors: ["data.delivery: no delivery method has this id"] })), M.invalidInput ); }); test("asks to check the input when the shop rejects a field before the CMS", () => { const body = shopError(400, { message: "Invalid order update", errors: ["data.paymentAuthorised: not a field the customer may set"] }); assert.equal(checkoutErrorMessage(400, body), M.invalidInput); }); test("mentions the e-mail address when it is among several rejected fields", () => { const body = shopError(400, { message: "Invalid order update", errors: [ "data.acceptedTermsAndConditionsAt: must be sent only to accept the terms, as true or a timestamp", "data.email: must be an email address" ] }); assert.equal(checkoutErrorMessage(400, body), M.invalidEmail); }); test("says that a paid order can no longer be changed", () => { assert.equal(checkoutErrorMessage(409, shopError(409, { message: "Order can no longer be changed" })), M.alreadyPaid); assert.equal(checkoutErrorMessage(409, shopError(409, { message: "Order is already paid" })), M.alreadyPaid); assert.equal(M.alreadyPaid, "Diese Bestellung ist bereits bezahlt und kann nicht mehr geändert werden."); }); test("says that a product is no longer available", () => { assert.equal(checkoutErrorMessage(409, shopError(409, { message: "A product in the cart is no longer available" })), M.productUnavailable); assert.equal(M.productUnavailable, "Ein Produkt in deinem Warenkorb ist nicht mehr verfügbar. Bitte prüfe deinen Warenkorb."); }); test("names the fields an order that is not ready for checkout misses", () => { const body = (missing: string[]) => shopError(400, { message: "Order is not ready for checkout", missing }); assert.equal(checkoutErrorMessage(400, body(["email"])), "Deine Bestellung ist noch nicht vollständig. Bitte ergänze: E-Mail-Adresse."); assert.equal( checkoutErrorMessage(400, body(["email", "invoiceAddress", "delivery"])), "Deine Bestellung ist noch nicht vollständig. Bitte ergänze: E-Mail-Adresse, Rechnungsadresse, Versandart." ); }); test("says that an order is not ready without naming a field the shop does not know", () => { assert.equal(checkoutErrorMessage(400, shopError(400, { message: "Order is not ready for checkout" })), M.notReady); assert.equal(checkoutErrorMessage(400, shopError(400, { message: "Order is not ready for checkout", missing: ["phone"] })), M.notReady); assert.equal( checkoutErrorMessage(400, shopError(400, { message: "Order is not ready for checkout", missing: ["phone", "delivery"] })), "Deine Bestellung ist noch nicht vollständig. Bitte ergänze: Versandart." ); }); test("asks to restart the payment when it does not fit the order or the order changed", () => { for (const message of ["Payment does not belong to this order", "Payment does not match this order", "Order changed during the payment"]) { assert.equal(checkoutErrorMessage(409, shopError(409, { message })), M.restartPayment, message); } assert.match(M.restartPayment, /Bitte starte die Zahlung neu\.$/); }); test("asks to try again in a few minutes when PayPal is not reachable", () => { for (const message of ["Could not create the PayPal order", "PayPal could not capture the payment"]) { assert.equal(checkoutErrorMessage(502, shopError(502, { message })), M.paypalUnavailable, message); } assert.equal(checkoutErrorMessage(502, "502 Bad Gateway"), M.paypalUnavailable); assert.match(M.paypalUnavailable, /versuche es in ein paar Minuten noch einmal\.$/); }); test("asks not to pay again when PayPal may have taken the money but the order is not marked paid", () => { assert.equal(checkoutErrorMessage(502, shopError(502, { message: "Payment could not be confirmed" })), M.paymentUnconfirmed); assert.equal(checkoutErrorMessage(500, shopError(500, { message: "Payment could not be recorded" })), M.paymentUnconfirmed); }); test("falls back to a general message for anything else", () => { assert.equal(checkoutErrorMessage(404, shopError(404, { message: "Order not found" })), M.generic); assert.equal(checkoutErrorMessage(400, shopError(400, { message: "Missing order UUID" })), M.generic); assert.equal(checkoutErrorMessage(400, shopError(400, { message: "Product not found in cart" })), M.generic); assert.equal(checkoutErrorMessage(409, shopError(409, { message: "Something else" })), M.generic); assert.equal(checkoutErrorMessage(500, { error: true, statusCode: 500, statusMessage: "Server Error", message: "Server Error" }), M.generic); assert.equal(checkoutErrorMessage(503, shopError(503, { message: "The CMS did not answer" })), M.generic); assert.equal(checkoutErrorMessage(undefined, undefined), M.generic); assert.equal(checkoutErrorMessage(409, null), M.generic); assert.equal(M.generic, "Es ist ein Fehler aufgetreten. Bitte versuche es erneut."); }); test("reads the CMS's own error body as well", () => { assert.equal( checkoutErrorMessage(400, strapiError(400, "Invalid order update", { errors: ["data.email: must be an email address"] })), M.invalidEmail ); assert.equal( checkoutErrorMessage(400, strapiError(400, "Order is not ready for checkout", { missing: ["delivery"] })), "Deine Bestellung ist noch nicht vollständig. Bitte ergänze: Versandart." ); assert.equal(checkoutErrorMessage(409, strapiError(409, "Order can no longer be changed")), M.alreadyPaid); assert.equal(checkoutErrorMessage(409, strapiError(409, "Order changed during the payment")), M.restartPayment); }); test("never shows the server's text or personal data", () => { const personal = "erika@example.org, Musterstraße 1, 70190 Stuttgart"; const bodies = [ shopError(400, { message: personal, errors: [`data.email: ${personal}`], missing: [personal] }), shopError(409, { message: personal }), shopError(502, { message: personal }), strapiError(400, personal, { errors: [personal] }), { message: personal }, personal ]; for (const status of [undefined, 400, 404, 409, 500, 502, 503]) { for (const body of bodies) { const message = checkoutErrorMessage(status, body); assert.ok(ALL_MESSAGES.has(message) || message.startsWith("Deine Bestellung ist noch nicht vollständig."), message); assert.ok(!message.includes("erika") && !message.includes("Muster"), message); } } }); test("reads the status and the body of the error $fetch throws", () => { const body = shopError(409, { message: "Order can no longer be changed" }); assert.equal(checkoutErrorMessageFor(fetchError(409, body)), M.alreadyPaid); assert.equal(checkoutErrorMessageFor(fetchError(502, shopError(502, { message: "Could not create the PayPal order" }))), M.paypalUnavailable); assert.equal( checkoutErrorMessageFor({ statusCode: 400, data: shopError(400, { message: "Invalid order update", errors: ["data.email: x"] }) }), M.invalidEmail ); }); test("falls back to the general message for an error without an answer", () => { assert.equal(checkoutErrorMessageFor(fetchError(undefined, undefined)), M.generic); assert.equal(checkoutErrorMessageFor(new TypeError("Failed to fetch")), M.generic); assert.equal(checkoutErrorMessageFor(undefined), M.generic); assert.equal(checkoutErrorMessageFor("error"), M.generic); });