From 8320842b27e85537493f87425689b5eadcc1e440 Mon Sep 17 00:00:00 2001 From: AGWAM001 <281425994+AGWAM001@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:25:21 +0100 Subject: [PATCH] test(mcp): give the four legacy paid_call tests the shapes the real libraries throw Three of the four simulated errors were invented: the ledger-number sentence appears in no installed package, and the two Response stand-ins carried only {ok, status, text}, so the ledger's settlement-header decode never ran. Fixtures now match what the payment path actually produces: @x402/fetch rethrows a scheme failure as "Failed to create payment payload: ", a rejected payment comes back as the seller's second 402 Response rather than a thrown Error, and a settled call echoes X-PAYMENT-RESPONSE. The trustline case is left as a plain string on purpose. Horizon answers it as BadResponseError with result_codes at err.response.extras.result_codes, and extractStructuredSignal reads err.result_codes, so the faithful fixture classifies as ERR_BAD_REQUEST instead. That is a source gap, not a test one, and out of scope here. --- tests/mcp.test.ts | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 9187d6df..19a5e2be 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -24,7 +24,12 @@ describe('BazaarMcpServer — server identity', () => { describe('BazaarMcpServer', () => { it('should map signature expiration ledger error to ERR_EXPIRED_AUTHORISATION', async () => { - const mockFetch = vi.fn().mockRejectedValue(new Error('Transaction signature expired. Signature expiration ledger 123 is less than current ledger 125')); + // What the payment client actually surfaces: @x402/fetch rethrows a scheme + // failure as `Failed to create payment payload: `, and the cause for a + // lapsed signature is the SDK's own `The transaction has expired` + // (@stellar/stellar-sdk lib/webauth/utils.js). + const expired = new Error('Failed to create payment payload: The transaction has expired'); + const mockFetch = vi.fn().mockRejectedValue(expired); const server = new BazaarMcpServer({ fetchWithPayment: mockFetch, paidCallAllowlist: ['example.com'] }); // The SDK server doesn't expose the router directly without a client, but we can test the handlePaidCallError method directly, @@ -80,11 +85,15 @@ describe('BazaarMcpServer', () => { }); it('should map 402 rejected error to ERR_PAYMENT_REJECTED', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: false, - status: 402, - text: async () => 'Payment Required' - }); + // fetchWithPayment does not throw on a rejected payment: it pays, retries, + // and hands back the seller's second response, so this is a real Response + // carrying the x402 error body rather than a hand-rolled object. + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ x402Version: 1, error: 'Payment failed: signature invalid' }), + { status: 402, headers: { 'Content-Type': 'application/json' } }, + ), + ); const server = new BazaarMcpServer({ fetchWithPayment: mockFetch, paidCallAllowlist: ['example.com'] }); @@ -111,11 +120,19 @@ describe('BazaarMcpServer', () => { }); it('should return successfully on a successful call', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - text: async () => JSON.stringify({ success: true, data: "hello" }) - }); + // A settled call is a real Response whose body is the resource's payload and + // whose X-PAYMENT-RESPONSE header carries the seller's settlement echo; the + // ledger decodes that header, so a stand-in without Headers would skip the + // only code path that reads it. + const settlement = Buffer.from( + JSON.stringify({ success: true, network: 'stellar:testnet' }), + ).toString('base64url'); + const mockFetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ success: true, data: 'hello' }), { + status: 200, + headers: { 'Content-Type': 'application/json', 'X-PAYMENT-RESPONSE': settlement }, + }), + ); const server = new BazaarMcpServer({ fetchWithPayment: mockFetch, paidCallAllowlist: ['example.com'] });