import { test } from "node:test"; import assert from "node:assert/strict"; import { cmsErrorLogLine, shopErrorFromCms } from "../../server/utils/cmsError.ts"; const UUID = "11111111-2222-4333-8444-555555555555"; const CART = `http://cms:5555/api/orders/${UUID}/cart`; const CHECKOUT = `http://cms:5555/api/orders/${UUID}/checkout?returnUrl=https%3A%2F%2Fshop.example%2Fcheckout%2F3`; const CAPTURE = `http://cms:5555/api/orders/${UUID}/capture/5O190127TN364715T`; // Built like ofetch's createFetchError (ofetch 1.5): an Error named FetchError, with getters for the response's status and parsed body. const fetchError = (method: string, url: string, response?: { status: number; statusText: string; body: unknown }): Error => { const status = response ? `${response.status} ${response.statusText}` : " fetch failed"; const error = new Error(`[${method}] ${JSON.stringify(url)}: ${status}`); error.name = "FetchError"; const fields: [string, unknown][] = [ ["data", response?.body], ["status", response?.status], ["statusCode", response?.status], ["statusText", response?.statusText], ["statusMessage", response?.statusText] ]; for (const [key, value] of fields) Object.defineProperty(error, key, { get: () => value }); return error; }; // Strapi's error body, as ctx.badRequest, ctx.notFound, ctx.conflict and ctx.badGateway write it. const strapiError = (status: number, name: string, message: string, details: Record = {}) => ({ data: null, error: { status, name, message, details } }); const cmsAnswer = (method: string, url: string, status: number, statusText: string, body: unknown) => shopErrorFromCms(fetchError(method, url, { status, statusText, body })); test("passes a rejected order update on as 400 with the CMS's errors", () => { const errors = ["data.email: must be an email address of at most 254 characters", "data.total: not a field the customer may set"]; assert.deepEqual(cmsAnswer("PUT", CART, 400, "Bad Request", strapiError(400, "BadRequestError", "Invalid order update", { errors })), { statusCode: 400, statusMessage: "Invalid order update", data: { message: "Invalid order update", errors } }); }); test("passes a checkout that is not ready on as 400 with the fields it misses", () => { const body = strapiError(400, "BadRequestError", "Order is not ready for checkout", { missing: ["email", "delivery"] }); assert.deepEqual(cmsAnswer("POST", CHECKOUT, 400, "Bad Request", body), { statusCode: 400, statusMessage: "Order is not ready for checkout", data: { message: "Order is not ready for checkout", missing: ["email", "delivery"] } }); }); test("passes on no detail but errors and missing", () => { const details = { reason: "deliveryAddress has no postal code", order: { email: "erika@example.org" }, errors: "data.email" }; const body = { ...strapiError(400, "BadRequestError", "Order is not ready for checkout", details), meta: { email: "erika@example.org" } }; assert.deepEqual(cmsAnswer("POST", CHECKOUT, 400, "Bad Request", body), { statusCode: 400, statusMessage: "Order is not ready for checkout", data: { message: "Order is not ready for checkout" } }); }); test("passes the conflicts of a paid order, an unavailable product and a payment that does not fit the order on as 409", () => { for (const [url, message] of [ [CART, "Order can no longer be changed"], [CHECKOUT, "A product in the cart is no longer available"], [CAPTURE, "Order is already paid"], [CAPTURE, "Payment does not belong to this order"], [CAPTURE, "Payment does not match this order"], [CAPTURE, "Order changed during the payment"] ] as const) { assert.deepEqual( cmsAnswer("POST", url, 409, "Conflict", strapiError(409, "ConflictError", message)), { statusCode: 409, statusMessage: message, data: { message } }, message ); } }); test("passes a missing order on as 404", () => { assert.deepEqual(cmsAnswer("GET", CART, 404, "Not Found", strapiError(404, "NotFoundError", "Order not found")), { statusCode: 404, statusMessage: "Order not found", data: { message: "Order not found" } }); }); test("passes PayPal's errors on as 502", () => { for (const [url, message] of [ [CHECKOUT, "Could not create the PayPal order"], [CAPTURE, "PayPal could not capture the payment"], [CAPTURE, "Payment could not be confirmed"] ] as const) { assert.deepEqual( cmsAnswer("POST", url, 502, "Bad Gateway", strapiError(502, "BadGatewayError", message)), { statusCode: 502, statusMessage: message, data: { message } }, message ); } }); test("passes a payment the CMS could not record on as 500 with its message", () => { const body = strapiError(500, "InternalServerError", "Payment could not be recorded"); assert.deepEqual(cmsAnswer("POST", CAPTURE, 500, "Internal Server Error", body), { statusCode: 500, statusMessage: "Payment could not be recorded", data: { message: "Payment could not be recorded" } }); }); test("answers 500 without the CMS's message for a status that is the shop's own fault", () => { for (const [status, statusText] of [ [401, "Unauthorized"], [403, "Forbidden"], [405, "Method Not Allowed"], [413, "Payload Too Large"] ] as const) { assert.deepEqual( cmsAnswer("PUT", CART, status, statusText, strapiError(status, "Error", "Missing or invalid credentials")), { statusCode: 500, statusMessage: "Internal Server Error", data: { message: "Internal Server Error" } }, String(status) ); } }); test("answers 503 when the CMS does not answer", () => { assert.deepEqual(shopErrorFromCms(fetchError("GET", CART)), { statusCode: 503, statusMessage: "Service Unavailable", data: { message: "The CMS did not answer" } }); }); test("leaves any other error to be rethrown as it is", () => { assert.equal(shopErrorFromCms(new TypeError("Cannot read properties of undefined (reading 'uuid')")), undefined); assert.equal(shopErrorFromCms({ name: "FetchError", status: 409, data: strapiError(409, "ConflictError", "Order is already paid") }), undefined); assert.equal(shopErrorFromCms("FetchError"), undefined); assert.equal(shopErrorFromCms(undefined), undefined); }); test("keeps only the strings of the CMS's lists, at most 20, and cuts a long one", () => { // Strapi's own validation errors are objects with the path and the message: they are dropped. const objects = strapiError(400, "ValidationError", "Invalid order update", { errors: [{ path: ["email"], message: "email must be a valid email" }] }); assert.deepEqual(cmsAnswer("PUT", CART, 400, "Bad Request", objects)?.data, { message: "Invalid order update" }); const many = Array.from({ length: 25 }, (_, index) => `data.field${index}: not a field the customer may set`); const long = `data.${"x".repeat(300)}: not a field the customer may set`; const body = strapiError(400, "BadRequestError", "Invalid order update", { errors: [long, 7, ...many] }); const data = cmsAnswer("PUT", CART, 400, "Bad Request", body)?.data; assert.equal(data?.errors?.length, 20); assert.equal(data?.errors?.[0], `${long.slice(0, 200)}…`); assert.equal(data?.errors?.[1], many[0]); }); test("answers with the status's reason phrase when the body is no CMS error", () => { assert.deepEqual(cmsAnswer("POST", CHECKOUT, 502, "Bad Gateway", "502 Bad Gateway"), { statusCode: 502, statusMessage: "Bad Gateway", data: { message: "Bad Gateway" } }); assert.deepEqual(cmsAnswer("GET", CART, 404, "Not Found", strapiError(404, "NotFoundError", "")), { statusCode: 404, statusMessage: "Not Found", data: { message: "Not Found" } }); }); test("keeps a message the status line cannot carry in the data only", () => { const long = `Order ${"x".repeat(250)}`; assert.deepEqual(cmsAnswer("PUT", CART, 409, "Conflict", strapiError(409, "ConflictError", "Bestellung gesperrt…")), { statusCode: 409, statusMessage: "Conflict", data: { message: "Bestellung gesperrt…" } }); assert.deepEqual(cmsAnswer("PUT", CART, 409, "Conflict", strapiError(409, "ConflictError", long)), { statusCode: 409, statusMessage: "Conflict", data: { message: `${long.slice(0, 200)}…` } }); }); test("logs a failed request without its query and without the order's uuid", () => { const error = { statusCode: 502, statusMessage: "Bad Gateway", data: { message: "Could not create the PayPal order" } }; assert.equal( cmsErrorLogLine("post", `/orders/${UUID}/checkout?returnUrl=https%3A%2F%2Fshop.example`, error), "[cms] POST /orders/:uuid/checkout: 502 Could not create the PayPal order" ); assert.equal( cmsErrorLogLine("GET", `/orders/${UUID.toUpperCase()}/cart`, error), "[cms] GET /orders/:uuid/cart: 502 Could not create the PayPal order" ); });