import { test } from "node:test"; import assert from "node:assert/strict"; import { CUSTOMER_UPDATE_FIELDS, invalidOrderUpdate, pickCustomerUpdate } from "../../server/utils/customerUpdate.ts"; // The body as readBody hands it to the route: sent by useShopApi().updateOrder as JSON. const overTheWire = (body: unknown): unknown => JSON.parse(JSON.stringify(body)); const address = { givenName: "Erika", familyName: "Mustermann", streetAddress: "Musterstraße 1", postalCode: "70190", addressLevel2: "Stuttgart", country: "DE" }; const otherAddress = { givenName: "Max", familyName: "Muster", streetAddress: "Hauptstraße 5", postalCode: "10115", addressLevel2: "Berlin", country: "DE" }; // pages/checkout/1.vue: cart.update({ email, acceptedTermsAndConditionsAt }). const step1 = { data: { email: "erika@example.org", acceptedTermsAndConditionsAt: "2026-10-09T08:15:00.000Z" } }; // pages/checkout/2.vue without a separate delivery address: the invoice address is sent as the delivery address too. const step2 = { data: { invoiceAddress: "Erika Mustermann\nMusterstraße 1\n70190 Stuttgart", deliveryAddress: "Erika Mustermann\nMusterstraße 1\n70190 Stuttgart", invoiceAddressStructured: address, deliveryAddressStructured: address, delivery: 1 } }; // The order's attributes in the CMS (src/api/order/content-types/order/schema.json) that only the server writes, and Strapi's own. const SERVER_FIELDS = [ "id", "uuid", "date", "customer", "invoice", "deliveryNote", "hash", "payment", "VAT", "subtotal", "total", "cart", "paymentAuthorised", "paymentStatus", "paypalOrderId", "paypalCaptureId", "paymentCapturedAt", "emailSent", "invoiceSent", "deliveryNoteSent", "invoiceNumber", "deliveryNoteNumber", "deliveryTrackingNumber", "createdAt", "updatedAt", "publishedAt" ]; test("lists the seven fields of the checkout's steps", () => { assert.deepEqual( [...CUSTOMER_UPDATE_FIELDS], [ "email", "acceptedTermsAndConditionsAt", "invoiceAddress", "deliveryAddress", "invoiceAddressStructured", "deliveryAddressStructured", "delivery" ] ); }); test("passes the payload of checkout step 1 unchanged", () => { assert.deepEqual(pickCustomerUpdate(overTheWire(step1)), { ok: true, data: step1.data }); }); test("passes the payload of checkout step 2 unchanged", () => { assert.deepEqual(pickCustomerUpdate(overTheWire(step2)), { ok: true, data: step2.data }); }); test("passes the payload of checkout step 2 with a separate delivery address unchanged", () => { const body = { data: { ...step2.data, deliveryAddress: "Max Muster\nHauptstraße 5\n10115 Berlin", deliveryAddressStructured: otherAddress, delivery: 2 } }; assert.deepEqual(pickCustomerUpdate(overTheWire(body)), { ok: true, data: body.data }); }); test("leaves the values to the CMS, which checks them", () => { const body = { data: { email: "no address", delivery: null, invoiceAddressStructured: { street: "x" } } }; assert.deepEqual(pickCustomerUpdate(body), { ok: true, data: body.data }); }); test("rejects each field only the server writes", () => { for (const field of SERVER_FIELDS) { assert.deepEqual( pickCustomerUpdate({ data: { [field]: 1 } }), { ok: false, unknown: [`data.${field}`], errors: [`data.${field}: not a field the customer may set`] }, field ); } }); test("rejects the whole body instead of dropping the field, and names every rejected field", () => { const body = { data: { ...step1.data, paymentAuthorised: true, total: 0.01 } }; assert.deepEqual(pickCustomerUpdate(body), { ok: false, unknown: ["data.paymentAuthorised", "data.total"], errors: ["data.paymentAuthorised: not a field the customer may set", "data.total: not a field the customer may set"] }); }); test("rejects a field sent beside data", () => { assert.deepEqual(pickCustomerUpdate({ email: "erika@example.org", data: {} }), { ok: false, unknown: ["email"], errors: ["email: not accepted, the fields belong in data"] }); }); test("rejects a body without data, with data that is not an object, and a body that is not an object", () => { assert.deepEqual(pickCustomerUpdate({}), { ok: false, unknown: [], errors: ["data: missing"] }); assert.deepEqual(pickCustomerUpdate({ data: [step1.data] }), { ok: false, unknown: [], errors: ["data: must be an object"] }); assert.deepEqual(pickCustomerUpdate({ data: null }), { ok: false, unknown: [], errors: ["data: must be an object"] }); for (const body of [undefined, null, "data", [step1]]) { assert.deepEqual(pickCustomerUpdate(body), { ok: false, unknown: [], errors: ["body: must be an object of the form { data: { … } }"] }); } }); test("rejects __proto__ and constructor without touching any prototype", () => { const result = pickCustomerUpdate(JSON.parse('{"data":{"__proto__":{"paymentAuthorised":true},"constructor":{"prototype":{}}}}')); assert.equal(result.ok, false); assert.deepEqual(result.ok === false && result.unknown, ["data.__proto__", "data.constructor"]); assert.equal(({} as Record).paymentAuthorised, undefined); }); test("cuts a long field name in the answer", () => { const result = pickCustomerUpdate({ data: { ["x".repeat(100)]: 1 } }); assert.deepEqual(result.ok === false && result.unknown, [`data.${"x".repeat(64)}…`]); }); test("answers a rejected body with a 400 in the shape of the CMS's own 400", () => { assert.deepEqual(invalidOrderUpdate(["data.total: not a field the customer may set"]), { statusCode: 400, statusMessage: "Invalid order update", data: { message: "Invalid order update", errors: ["data.total: not a field the customer may set"] } }); });