@bsv/sdk
Version:
BSV Blockchain Software Development Kit
189 lines • 8.74 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const globals_1 = require("@jest/globals");
const AuthFetch_js_1 = require("../AuthFetch.js");
const index_js_1 = require("../../../primitives/index.js");
globals_1.jest.mock('../../utils/createNonce.js', () => ({
createNonce: globals_1.jest.fn()
}));
const createNonce_js_1 = require("../../utils/createNonce.js");
const createNonceMock = createNonce_js_1.createNonce;
function createWalletStub() {
const identityKey = new index_js_1.PrivateKey(10).toPublicKey().toString();
const derivedKey = new index_js_1.PrivateKey(11).toPublicKey().toString();
return {
getPublicKey: globals_1.jest.fn(async (options) => {
if (options?.identityKey === true) {
return { publicKey: identityKey };
}
return { publicKey: derivedKey };
}),
createAction: globals_1.jest.fn(async () => ({
tx: index_js_1.Utils.toArray('mock-transaction', 'utf8')
})),
createHmac: globals_1.jest.fn(async () => ({
hmac: new Array(32).fill(7)
}))
};
}
function createPaymentRequiredResponse(overrides = {}) {
const headers = {
'x-bsv-payment-version': '1.0',
'x-bsv-payment-satoshis-required': '5',
'x-bsv-auth-identity-key': 'server-key',
'x-bsv-payment-derivation-prefix': 'prefix',
...overrides
};
return new Response('', { status: 402, headers });
}
afterEach(() => {
globals_1.jest.restoreAllMocks();
createNonceMock.mockReset();
});
describe('AuthFetch payment handling', () => {
test('createPaymentContext builds a complete retry context', async () => {
const wallet = createWalletStub();
const authFetch = new AuthFetch_js_1.AuthFetch(wallet);
createNonceMock.mockResolvedValueOnce('suffix-from-test');
const config = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: { hello: 'world' }
};
const context = await authFetch.createPaymentContext('https://api.example.com/resource', config, 42, 'remote-identity-key', 'test-prefix');
expect(context.satoshisRequired).toBe(42);
expect(context.serverIdentityKey).toBe('remote-identity-key');
expect(context.derivationPrefix).toBe('test-prefix');
expect(context.derivationSuffix).toBe('suffix-from-test');
expect(context.transactionBase64).toBe(index_js_1.Utils.toBase64(index_js_1.Utils.toArray('mock-transaction', 'utf8')));
expect(context.clientIdentityKey).toEqual(expect.any(String));
expect(context.attempts).toBe(0);
expect(context.maxAttempts).toBe(3);
expect(context.errors).toEqual([]);
expect(context.requestSummary).toMatchObject({
url: 'https://api.example.com/resource',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
bodyType: 'object'
});
expect(context.requestSummary.bodyByteLength).toBe(index_js_1.Utils.toArray(JSON.stringify(config.body), 'utf8').length);
expect(wallet.createAction).toHaveBeenCalledWith(expect.objectContaining({
description: expect.stringContaining('https://api.example.com'),
outputs: [
expect.objectContaining({
satoshis: 42,
customInstructions: expect.stringContaining('remote-identity-key')
})
]
}), undefined);
});
test('handlePaymentAndRetry reuses compatible contexts and adds payment header', async () => {
const wallet = createWalletStub();
const authFetch = new AuthFetch_js_1.AuthFetch(wallet);
const paymentContext = {
satoshisRequired: 5,
transactionBase64: index_js_1.Utils.toBase64([1, 2, 3]),
derivationPrefix: 'prefix',
derivationSuffix: 'suffix',
serverIdentityKey: 'server-key',
clientIdentityKey: 'client-key',
attempts: 0,
maxAttempts: 3,
errors: [],
requestSummary: {
url: 'https://api.example.com/resource',
method: 'POST',
headers: { 'X-Test': '1' },
bodyType: 'none',
bodyByteLength: 0
}
};
const fetchSpy = globals_1.jest.spyOn(authFetch, 'fetch').mockResolvedValue({ status: 200 });
globals_1.jest.spyOn(authFetch, 'logPaymentAttempt').mockImplementation(() => { });
const createPaymentContextSpy = globals_1.jest.spyOn(authFetch, 'createPaymentContext');
const config = {
headers: { 'x-custom': 'value' },
paymentContext
};
const response = createPaymentRequiredResponse();
const result = await authFetch.handlePaymentAndRetry('https://api.example.com/resource', config, response);
expect(result).toEqual({ status: 200 });
expect(paymentContext.attempts).toBe(1);
expect(fetchSpy).toHaveBeenCalledTimes(1);
const callArgs = fetchSpy.mock.calls[0];
const nextConfig = callArgs?.[1];
expect(nextConfig).toBeDefined();
expect(nextConfig.paymentContext).toBe(paymentContext);
expect(nextConfig.retryCounter).toBe(3);
const paymentHeader = JSON.parse(nextConfig.headers['x-bsv-payment']);
expect(paymentHeader).toEqual({
derivationPrefix: 'prefix',
derivationSuffix: 'suffix',
transaction: index_js_1.Utils.toBase64([1, 2, 3])
});
expect(createPaymentContextSpy).not.toHaveBeenCalled();
});
test('handlePaymentAndRetry exhausts attempts and throws detailed error', async () => {
const wallet = createWalletStub();
const authFetch = new AuthFetch_js_1.AuthFetch(wallet);
globals_1.jest.spyOn(authFetch, 'logPaymentAttempt').mockImplementation(() => { });
globals_1.jest.spyOn(authFetch, 'wait').mockResolvedValue(undefined);
const firstError = new Error('payment attempt 1 failed');
const secondError = new Error('payment attempt 2 failed');
globals_1.jest.spyOn(authFetch, 'fetch')
.mockRejectedValueOnce(firstError)
.mockRejectedValueOnce(secondError);
const paymentContext = {
satoshisRequired: 5,
transactionBase64: index_js_1.Utils.toBase64([9, 9, 9]),
derivationPrefix: 'prefix',
derivationSuffix: 'suffix',
serverIdentityKey: 'server-key',
clientIdentityKey: 'client-key',
attempts: 0,
maxAttempts: 2,
errors: [],
requestSummary: {
url: 'https://api.example.com/resource',
method: 'GET',
headers: {},
bodyType: 'none',
bodyByteLength: 0
}
};
const config = { paymentContext };
const response = createPaymentRequiredResponse();
await expect((async () => {
try {
await authFetch.handlePaymentAndRetry('https://api.example.com/resource', config, response);
}
catch (error) {
const err = error;
expect(err.message).toBe('Paid request to https://api.example.com/resource failed after 2/2 attempts. Sent 5 satoshis to server-key.');
expect(err.details).toMatchObject({
attempts: { used: 2, max: 2 },
payment: expect.objectContaining({
satoshis: 5,
serverIdentityKey: 'server-key',
clientIdentityKey: 'client-key'
})
});
expect(err.details.errors).toHaveLength(2);
expect(err.details.errors[0]).toEqual(expect.objectContaining({
attempt: 1,
message: 'payment attempt 1 failed'
}));
expect(err.details.errors[1]).toEqual(expect.objectContaining({
attempt: 2,
message: 'payment attempt 2 failed'
}));
expect(typeof err.details.errors[0].timestamp).toBe('string');
expect(err.cause).toBe(secondError);
throw error;
}
})()).rejects.toThrow('Paid request to https://api.example.com/resource failed after 2/2 attempts. Sent 5 satoshis to server-key.');
expect(paymentContext.attempts).toBe(2);
expect(paymentContext.errors).toHaveLength(2);
});
});
//# sourceMappingURL=AuthFetch.test.js.map