taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit fab425b7a3e44fff1b7046e7440c1e4bd5d702fa
parent 8c286a684659fb714b16b3c7eaef282a9a7eab7e
Author: Florian Dold <dold@taler.net>
Date:   Mon, 27 Jul 2026 19:24:32 +0200

harness: check the merchant's response to claiming a claimed order

Issue: https://bugs.taler.net/n/11660

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-claim-loop.ts | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 105 insertions(+), 3 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-claim-loop.ts b/packages/taler-harness/src/integrationtests/test-claim-loop.ts @@ -18,7 +18,11 @@ * Imports. */ import { + encodeCrock, + getRandomBytes, + HttpStatusCode, succeedOrThrow, + TalerErrorCode, TalerMerchantInstanceHttpClient, TransactionMajorState, TransactionMinorState, @@ -32,10 +36,13 @@ import { import { GlobalTestState } from "../harness/harness.js"; /** - * Run test for the merchant's order lifecycle. + * Run test for the merchant's order lifecycle and for the responses of + * POST /orders/$ORDER_ID/claim. * - * FIXME: Is this test still necessary? We initially wrote if to confirm/document - * assumptions about how the merchant should work. + * An order can only ever be claimed by one nonce. Everything the merchant + * answers afterwards follows from that: the nonce that holds the claim gets + * its contract back as often as it asks, every other nonce gets a conflict, + * and an order nobody created is not a conflict but a 404. */ export async function runClaimLoopTest(t: GlobalTestState) { // Set up test environment @@ -109,6 +116,101 @@ export async function runClaimLoopTest(t: GlobalTestState) { ); t.assertTrue(orderStatusAfter.order_status === "claimed"); + // A wallet that scans the pay URI of an order some other wallet has already + // claimed presents the very same claim token, but a nonce of its own. The + // claim token does not help it: the order belongs to whoever claimed first. + await t.runSpanAsync("claim by a second wallet conflicts", async () => { + const claimResp = await merchantClient.claimOrder({ + orderId: orderResp.order_id, + body: { + nonce: encodeCrock(getRandomBytes(32)), + token: orderResp.token, + }, + }); + t.assertTrue(claimResp.type === "fail"); + t.assertTrue(claimResp.case === HttpStatusCode.Conflict); + t.assertDeepEqual( + claimResp.detail?.code, + TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED, + ); + + // The rejected claim leaves the order alone. + const orderStatus = succeedOrThrow( + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), + ); + t.assertTrue(orderStatus.order_status === "claimed"); + }); + + // Claiming again with the nonce that holds the claim is not a conflict but + // a repetition: a wallet that lost the response to its first claim must be + // able to ask again instead of losing the order. + await t.runSpanAsync("re-claim with the same nonce", async () => { + const secondOrderResp = succeedOrThrow( + await merchantClient.createOrder(merchantAdminAccessToken, { + order: { + summary: "Buy me too!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }), + ); + const orderId = secondOrderResp.order_id; + const ownNonce = encodeCrock(getRandomBytes(32)); + + const firstClaim = succeedOrThrow( + await merchantClient.claimOrder({ + orderId, + body: { nonce: ownNonce, token: secondOrderResp.token }, + }), + ); + t.assertDeepEqual(firstClaim.contract_terms.nonce, ownNonce); + + // Somebody else tries in between and is turned away. + const conflictingClaim = await merchantClient.claimOrder({ + orderId, + body: { + nonce: encodeCrock(getRandomBytes(32)), + token: secondOrderResp.token, + }, + }); + t.assertTrue(conflictingClaim.type === "fail"); + t.assertTrue(conflictingClaim.case === HttpStatusCode.Conflict); + t.assertDeepEqual( + conflictingClaim.detail?.code, + TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED, + ); + + // The failed claim did not take the order away from its owner, which + // still gets the identical contract back. + const repeatedClaim = succeedOrThrow( + await merchantClient.claimOrder({ + orderId, + body: { nonce: ownNonce, token: secondOrderResp.token }, + }), + ); + t.assertDeepEqual(repeatedClaim.contract_terms, firstClaim.contract_terms); + }); + + // A conflict must mean "already claimed" and nothing else, so an order that + // was never created has to be distinguishable from one that is taken. + await t.runSpanAsync( + "claim of an unknown order is not a conflict", + async () => { + const claimResp = await merchantClient.claimOrder({ + orderId: "does-not-exist", + body: { nonce: encodeCrock(getRandomBytes(32)) }, + }); + t.assertTrue(claimResp.type === "fail"); + t.assertTrue( + claimResp.case === + TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND, + ); + }, + ); + await t.shutdown(); }