From 2f9bf4ac7d11df127cff1eb956a631bf2b2a88a2 Mon Sep 17 00:00:00 2001 From: XiaolongZhang Date: Fri, 31 Jul 2026 11:55:07 +0800 Subject: [PATCH] fix(rest/nodejs): block completion when fulfillment methods array is empty The completion gate used `checkout.fulfillment?.methods?.every(...)`, which is vacuously true when `methods` is an empty array. A checkout created with `fulfillment: { methods: [] }` therefore passed the "fulfillment selected" check and could be completed (order placed) with no fulfillment at all, while a checkout with no fulfillment field was correctly rejected. Require the methods array to be non-empty before applying `.every(...)`, matching the Python sample, which blocks completion when methods is empty. Adds a regression test that creates a checkout with `fulfillment: { methods: [] }` and asserts the complete call is rejected with 400. --- rest/nodejs/src/api/checkout.ts | 17 +++++++++----- rest/nodejs/test/fulfillment.test.ts | 34 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/rest/nodejs/src/api/checkout.ts b/rest/nodejs/src/api/checkout.ts index 2eca398..6643192 100644 --- a/rest/nodejs/src/api/checkout.ts +++ b/rest/nodejs/src/api/checkout.ts @@ -744,12 +744,17 @@ export class CheckoutService { return c.json({ detail: "Checkout session not found" }, 404); } - // Validate Fulfillment is complete - const hasFulfillment = checkout.fulfillment?.methods?.every( - (method) => - method.selected_destination_id && - method.groups?.every((group) => group.selected_option_id) - ); + // Validate Fulfillment is complete. Require at least one method: an empty + // methods array must not satisfy the gate via [].every(...) === true. + const methods = checkout.fulfillment?.methods; + const hasFulfillment = + Array.isArray(methods) && + methods.length > 0 && + methods.every( + (method) => + method.selected_destination_id && + method.groups?.every((group) => group.selected_option_id) + ); if (!hasFulfillment) { return c.json( diff --git a/rest/nodejs/test/fulfillment.test.ts b/rest/nodejs/test/fulfillment.test.ts index 7b05c7a..2065ebe 100644 --- a/rest/nodejs/test/fulfillment.test.ts +++ b/rest/nodejs/test/fulfillment.test.ts @@ -265,3 +265,37 @@ test("a checkout with fulfillment fully selected can be completed", async () => assert.equal(body.status, "completed"); assert.ok(body.order?.id, "completion must assign an order id"); }); + +test("empty fulfillment methods array blocks completion", async () => { + const app = buildApp(); + // Distinct from "no fulfillment at all": the request carries an explicit + // fulfillment object whose methods array is empty. The completion gate must + // not treat [].every(...) === true as "all methods satisfied". + const created = await app.request("/checkout-sessions", { + method: "POST", + headers: JSON_HEADERS, + body: JSON.stringify({ + currency: "USD", + line_items: LINE_ITEMS, + payment: {}, + buyer: KNOWN_BUYER, + fulfillment: { methods: [] }, + }), + }); + assert.equal(created.status, 201); + const checkout = (await created.json()) as Checkout; + assert.ok( + Array.isArray(checkout.fulfillment?.methods) && + checkout.fulfillment!.methods!.length === 0, + "the empty methods array is what makes the gate a vacuous truth" + ); + + const res = await app.request(`/checkout-sessions/${checkout.id}/complete`, { + method: "POST", + headers: JSON_HEADERS, + body: JSON.stringify(SUCCESS_PAYMENT), + }); + assert.equal(res.status, 400); + const body = (await res.json()) as { detail: string }; + assert.match(body.detail, /fulfillment/i); +});