@bsv/sdk 2.0.12 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/cjs/src/transaction/MerklePath.js +132 -0
- package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/esm/src/transaction/MerklePath.js +132 -0
- package/dist/esm/src/transaction/MerklePath.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
- package/dist/types/src/transaction/MerklePath.d.ts +27 -0
- package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/storage.md +1 -1
- package/docs/reference/transaction.md +40 -0
- package/package.json +1 -1
- package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
- package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
- package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
- package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
- package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
- package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
- package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
- package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
- package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
- package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
- package/src/primitives/__tests/Curve.additional.test.ts +208 -0
- package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
- package/src/primitives/__tests/Hash.additional.test.ts +59 -0
- package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
- package/src/primitives/__tests/Point.additional.test.ts +503 -0
- package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
- package/src/primitives/__tests/Random.additional.test.ts +262 -0
- package/src/primitives/__tests/Signature.test.ts +333 -0
- package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
- package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
- package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
- package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
- package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
- package/src/script/__tests/Script.additional.test.ts +100 -0
- package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
- package/src/script/__tests/Spend.additional.test.ts +837 -0
- package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
- package/src/transaction/MerklePath.ts +155 -0
- package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
- package/src/transaction/__tests/Broadcaster.test.ts +159 -0
- package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
- package/src/transaction/__tests/MerklePath.test.ts +80 -0
- package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
- package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
- package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
- package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
- package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
- package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
- package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
- package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
- package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
- package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
- package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
- package/src/wallet/__tests/WERR.test.ts +212 -0
- package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
- package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
- package/src/wallet/__tests/WalletError.test.ts +290 -0
- package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
- package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
- package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AuthFetch additional tests.
|
|
4
|
+
*
|
|
5
|
+
* Covers branches not exercised by the primary AuthFetch.test.ts:
|
|
6
|
+
* - fetch(): retryCounter exhaustion, non-auth fallback path, stale-session retry
|
|
7
|
+
* - serializeRequest(): all header/body/URL variants
|
|
8
|
+
* - handleFetchAndValidate(): success, x-bsv header spoofing, non-ok response
|
|
9
|
+
* - handlePaymentAndRetry(): missing/invalid headers, incompatible context regeneration
|
|
10
|
+
* - describeRequestBodyForLogging(): all body types
|
|
11
|
+
* - getMaxPaymentAttempts(): edge cases
|
|
12
|
+
* - getPaymentRetryDelay(): values at different attempt counts
|
|
13
|
+
* - wait(): zero / positive
|
|
14
|
+
* - isPaymentContextCompatible(): match / mismatch branches
|
|
15
|
+
* - consumeReceivedCertificates(): drains the internal buffer
|
|
16
|
+
* - sendCertificateRequest(): creates new peer when none exists
|
|
17
|
+
* - logPaymentAttempt(): all three log levels
|
|
18
|
+
* - createPaymentErrorEntry(): Error vs non-Error values
|
|
19
|
+
* - buildPaymentFailureError(): shapes the error correctly
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
const globals_1 = require("@jest/globals");
|
|
23
|
+
const AuthFetch_js_1 = require("../AuthFetch.js");
|
|
24
|
+
const index_js_1 = require("../../../primitives/index.js");
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Module mock for createNonce (matches what primary test does)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
globals_1.jest.mock('../../utils/createNonce.js', () => ({
|
|
29
|
+
createNonce: globals_1.jest.fn()
|
|
30
|
+
}));
|
|
31
|
+
const createNonce_js_1 = require("../../utils/createNonce.js");
|
|
32
|
+
const createNonceMock = createNonce_js_1.createNonce;
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
function buildWallet() {
|
|
37
|
+
const identityKey = new index_js_1.PrivateKey(10).toPublicKey().toString();
|
|
38
|
+
const derivedKey = new index_js_1.PrivateKey(11).toPublicKey().toString();
|
|
39
|
+
return {
|
|
40
|
+
getPublicKey: globals_1.jest.fn(async (opts) => opts?.identityKey === true ? { publicKey: identityKey } : { publicKey: derivedKey }),
|
|
41
|
+
createAction: globals_1.jest.fn(async () => ({
|
|
42
|
+
tx: index_js_1.Utils.toArray('mock-tx', 'utf8')
|
|
43
|
+
})),
|
|
44
|
+
createHmac: globals_1.jest.fn(async () => ({ hmac: new Array(32).fill(0) }))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function make402Response(overrides = {}) {
|
|
48
|
+
const headers = {
|
|
49
|
+
'x-bsv-payment-version': '1.0',
|
|
50
|
+
'x-bsv-payment-satoshis-required': '10',
|
|
51
|
+
'x-bsv-auth-identity-key': 'srv-key',
|
|
52
|
+
'x-bsv-payment-derivation-prefix': 'pfx',
|
|
53
|
+
...overrides
|
|
54
|
+
};
|
|
55
|
+
return new Response('', { status: 402, headers });
|
|
56
|
+
}
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
globals_1.jest.restoreAllMocks();
|
|
59
|
+
createNonceMock.mockReset();
|
|
60
|
+
});
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// 1. fetch() – retryCounter exhaustion
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
describe('AuthFetch.fetch – retryCounter', () => {
|
|
65
|
+
it('throws when retryCounter reaches 0', async () => {
|
|
66
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
67
|
+
await expect(authFetch.fetch('https://example.com', { retryCounter: 0 })).rejects.toThrow('Request failed after maximum number of retries.');
|
|
68
|
+
});
|
|
69
|
+
it('decrements retryCounter before making the request', async () => {
|
|
70
|
+
// Verify that the stale-session retry path calls fetch() again, which means
|
|
71
|
+
// retryCounter gets decremented. We intercept the recursive fetch() call using
|
|
72
|
+
// a spy so a real Peer is never constructed inside the unit-test environment.
|
|
73
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
74
|
+
let fetchCallCount = 0;
|
|
75
|
+
const originalFetch = authFetch.fetch.bind(authFetch);
|
|
76
|
+
globals_1.jest.spyOn(authFetch, 'fetch').mockImplementation(async (url, config) => {
|
|
77
|
+
fetchCallCount++;
|
|
78
|
+
if (fetchCallCount === 1) {
|
|
79
|
+
// First call: run the real code path so the stale-session branch triggers
|
|
80
|
+
return originalFetch(url, config);
|
|
81
|
+
}
|
|
82
|
+
// Subsequent calls (recursive retry after stale-session): throw to prove
|
|
83
|
+
// the retry occurred with a decremented retryCounter.
|
|
84
|
+
throw new Error('second call');
|
|
85
|
+
});
|
|
86
|
+
// Inject a stub peer that throws a stale-session error on toPeer()
|
|
87
|
+
const peerStub = {
|
|
88
|
+
listenForCertificatesReceived: globals_1.jest.fn(),
|
|
89
|
+
listenForCertificatesRequested: globals_1.jest.fn(),
|
|
90
|
+
listenForGeneralMessages: globals_1.jest.fn(() => 1),
|
|
91
|
+
stopListeningForGeneralMessages: globals_1.jest.fn(),
|
|
92
|
+
toPeer: globals_1.jest.fn(async () => {
|
|
93
|
+
throw new Error('Session not found for nonce xyz');
|
|
94
|
+
})
|
|
95
|
+
};
|
|
96
|
+
authFetch.peers['https://example.com'] = {
|
|
97
|
+
peer: peerStub,
|
|
98
|
+
identityKey: 'some-key',
|
|
99
|
+
supportsMutualAuth: true,
|
|
100
|
+
pendingCertificateRequests: []
|
|
101
|
+
};
|
|
102
|
+
// With retryCounter: 2, the stale-session branch retries once; the spy
|
|
103
|
+
// intercepts the recursive call and throws 'second call'.
|
|
104
|
+
await expect(authFetch.fetch('https://example.com/path', { retryCounter: 2 })).rejects.toThrow('second call');
|
|
105
|
+
expect(fetchCallCount).toBe(2);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// 2. fetch() – supportsMutualAuth === false fallback
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
describe('AuthFetch.fetch – non-auth fallback (supportsMutualAuth=false)', () => {
|
|
112
|
+
it('falls back to handleFetchAndValidate when peer does not support mutual auth', async () => {
|
|
113
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
114
|
+
const handleFetchSpy = globals_1.jest
|
|
115
|
+
.spyOn(authFetch, 'handleFetchAndValidate')
|
|
116
|
+
.mockResolvedValue(new Response('ok', { status: 200 }));
|
|
117
|
+
const peerStub = {
|
|
118
|
+
peer: { toPeer: globals_1.jest.fn() },
|
|
119
|
+
supportsMutualAuth: false,
|
|
120
|
+
pendingCertificateRequests: []
|
|
121
|
+
};
|
|
122
|
+
authFetch.peers['https://example.com'] = peerStub;
|
|
123
|
+
const result = await authFetch.fetch('https://example.com/resource');
|
|
124
|
+
expect(handleFetchSpy).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(result.status).toBe(200);
|
|
126
|
+
});
|
|
127
|
+
it('rejects when handleFetchAndValidate throws in non-auth fallback', async () => {
|
|
128
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
129
|
+
globals_1.jest
|
|
130
|
+
.spyOn(authFetch, 'handleFetchAndValidate')
|
|
131
|
+
.mockRejectedValue(new Error('fetch validation failed'));
|
|
132
|
+
const peerStub = {
|
|
133
|
+
peer: { toPeer: globals_1.jest.fn() },
|
|
134
|
+
supportsMutualAuth: false,
|
|
135
|
+
pendingCertificateRequests: []
|
|
136
|
+
};
|
|
137
|
+
authFetch.peers['https://example.com'] = peerStub;
|
|
138
|
+
await expect(authFetch.fetch('https://example.com/resource')).rejects.toThrow('fetch validation failed');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// 3. handleFetchAndValidate
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
describe('AuthFetch.handleFetchAndValidate (private)', () => {
|
|
145
|
+
it('returns the response when fetch succeeds with no x-bsv headers', async () => {
|
|
146
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
147
|
+
const mockResponse = new Response('body', {
|
|
148
|
+
status: 200,
|
|
149
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
150
|
+
});
|
|
151
|
+
globals_1.jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
|
152
|
+
const peerToUse = { supportsMutualAuth: undefined };
|
|
153
|
+
const response = await authFetch.handleFetchAndValidate('https://example.com', { method: 'GET' }, peerToUse);
|
|
154
|
+
expect(response.status).toBe(200);
|
|
155
|
+
expect(peerToUse.supportsMutualAuth).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
it('throws when response contains an x-bsv header (spoofing detection)', async () => {
|
|
158
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
159
|
+
// The source iterates response.headers.forEach((value, name) => ...)
|
|
160
|
+
// and checks if the VALUE starts with 'x-bsv'. To trigger spoofing
|
|
161
|
+
// detection we need a header whose value starts with 'x-bsv'.
|
|
162
|
+
const mockResponse = new Response('', {
|
|
163
|
+
status: 200,
|
|
164
|
+
headers: { 'x-custom-header': 'x-bsv-auth-identity-key' }
|
|
165
|
+
});
|
|
166
|
+
globals_1.jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
|
167
|
+
await expect(authFetch.handleFetchAndValidate('https://example.com', {}, { supportsMutualAuth: undefined })).rejects.toThrow('The server is trying to claim it has been authenticated');
|
|
168
|
+
});
|
|
169
|
+
it('throws when response is not ok', async () => {
|
|
170
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
171
|
+
const mockResponse = new Response('Not Found', { status: 404 });
|
|
172
|
+
globals_1.jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
|
173
|
+
await expect(authFetch.handleFetchAndValidate('https://example.com', {}, { supportsMutualAuth: undefined })).rejects.toThrow('Request failed with status: 404');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// 4. handlePaymentAndRetry – missing/invalid headers
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
describe('AuthFetch.handlePaymentAndRetry – header validation', () => {
|
|
180
|
+
it('throws when x-bsv-payment-version header is missing', async () => {
|
|
181
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
182
|
+
const response = new Response('', { status: 402 }); // no payment headers
|
|
183
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Unsupported x-bsv-payment-version response header');
|
|
184
|
+
});
|
|
185
|
+
it('throws when x-bsv-payment-version header has wrong value', async () => {
|
|
186
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
187
|
+
const response = make402Response({ 'x-bsv-payment-version': '2.0' });
|
|
188
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Unsupported x-bsv-payment-version response header');
|
|
189
|
+
});
|
|
190
|
+
it('throws when x-bsv-payment-satoshis-required header is missing', async () => {
|
|
191
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
192
|
+
const response = make402Response({ 'x-bsv-payment-satoshis-required': '' });
|
|
193
|
+
// Force re-check: create a response without the satoshis header entirely
|
|
194
|
+
const headersRaw = {
|
|
195
|
+
'x-bsv-payment-version': '1.0',
|
|
196
|
+
'x-bsv-auth-identity-key': 'srv-key',
|
|
197
|
+
'x-bsv-payment-derivation-prefix': 'pfx'
|
|
198
|
+
// satoshis-required intentionally omitted
|
|
199
|
+
};
|
|
200
|
+
const respNoSatoshis = new Response('', { status: 402, headers: headersRaw });
|
|
201
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, respNoSatoshis)).rejects.toThrow('Missing x-bsv-payment-satoshis-required response header');
|
|
202
|
+
});
|
|
203
|
+
it('throws when satoshis value is NaN', async () => {
|
|
204
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
205
|
+
const response = make402Response({ 'x-bsv-payment-satoshis-required': 'not-a-number' });
|
|
206
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Invalid x-bsv-payment-satoshis-required response header value');
|
|
207
|
+
});
|
|
208
|
+
it('throws when satoshis value is zero', async () => {
|
|
209
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
210
|
+
const response = make402Response({ 'x-bsv-payment-satoshis-required': '0' });
|
|
211
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Invalid x-bsv-payment-satoshis-required response header value');
|
|
212
|
+
});
|
|
213
|
+
it('throws when x-bsv-auth-identity-key header is missing', async () => {
|
|
214
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
215
|
+
const headersRaw = {
|
|
216
|
+
'x-bsv-payment-version': '1.0',
|
|
217
|
+
'x-bsv-payment-satoshis-required': '10',
|
|
218
|
+
'x-bsv-payment-derivation-prefix': 'pfx'
|
|
219
|
+
// identity-key omitted
|
|
220
|
+
};
|
|
221
|
+
const response = new Response('', { status: 402, headers: headersRaw });
|
|
222
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Missing x-bsv-auth-identity-key response header');
|
|
223
|
+
});
|
|
224
|
+
it('throws when x-bsv-payment-derivation-prefix header is missing', async () => {
|
|
225
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
226
|
+
const headersRaw = {
|
|
227
|
+
'x-bsv-payment-version': '1.0',
|
|
228
|
+
'x-bsv-payment-satoshis-required': '10',
|
|
229
|
+
'x-bsv-auth-identity-key': 'srv-key'
|
|
230
|
+
// derivation-prefix omitted
|
|
231
|
+
};
|
|
232
|
+
const response = new Response('', { status: 402, headers: headersRaw });
|
|
233
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Missing x-bsv-payment-derivation-prefix response header');
|
|
234
|
+
});
|
|
235
|
+
it('throws when derivation-prefix is an empty string', async () => {
|
|
236
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
237
|
+
const response = make402Response({ 'x-bsv-payment-derivation-prefix': '' });
|
|
238
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', {}, response)).rejects.toThrow('Missing x-bsv-payment-derivation-prefix response header');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// 5. handlePaymentAndRetry – incompatible context triggers new context creation
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
describe('AuthFetch.handlePaymentAndRetry – context compatibility', () => {
|
|
245
|
+
it('regenerates context when server changes payment requirements', async () => {
|
|
246
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
247
|
+
globals_1.jest.spyOn(authFetch, 'logPaymentAttempt').mockImplementation(() => { });
|
|
248
|
+
globals_1.jest.spyOn(authFetch, 'wait').mockResolvedValue(undefined);
|
|
249
|
+
createNonceMock.mockResolvedValue('new-suffix');
|
|
250
|
+
const existingContext = {
|
|
251
|
+
satoshisRequired: 5, // server now asks for 10
|
|
252
|
+
transactionBase64: index_js_1.Utils.toBase64([1, 2, 3]),
|
|
253
|
+
derivationPrefix: 'pfx',
|
|
254
|
+
derivationSuffix: 'old-suffix',
|
|
255
|
+
serverIdentityKey: 'srv-key',
|
|
256
|
+
clientIdentityKey: 'client-key',
|
|
257
|
+
attempts: 0,
|
|
258
|
+
maxAttempts: 3,
|
|
259
|
+
errors: [],
|
|
260
|
+
requestSummary: {
|
|
261
|
+
url: 'https://example.com',
|
|
262
|
+
method: 'GET',
|
|
263
|
+
headers: {},
|
|
264
|
+
bodyType: 'none',
|
|
265
|
+
bodyByteLength: 0
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const fetchSpy = globals_1.jest
|
|
269
|
+
.spyOn(authFetch, 'fetch')
|
|
270
|
+
.mockResolvedValue(new Response('ok', { status: 200 }));
|
|
271
|
+
const response = make402Response({ 'x-bsv-payment-satoshis-required': '10' }); // changed from 5
|
|
272
|
+
await authFetch.handlePaymentAndRetry('https://example.com', { paymentContext: existingContext }, response);
|
|
273
|
+
// createNonce should have been called because the context was regenerated
|
|
274
|
+
expect(createNonceMock).toHaveBeenCalled();
|
|
275
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// 6. handlePaymentAndRetry – maxAttempts exceeded before first try
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
describe('AuthFetch.handlePaymentAndRetry – maxAttempts exceeded pre-check', () => {
|
|
282
|
+
it('throws immediately when attempts >= maxAttempts', async () => {
|
|
283
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
284
|
+
globals_1.jest.spyOn(authFetch, 'logPaymentAttempt').mockImplementation(() => { });
|
|
285
|
+
const exhaustedContext = {
|
|
286
|
+
satoshisRequired: 10,
|
|
287
|
+
transactionBase64: index_js_1.Utils.toBase64([1]),
|
|
288
|
+
derivationPrefix: 'pfx',
|
|
289
|
+
derivationSuffix: 'sfx',
|
|
290
|
+
serverIdentityKey: 'srv-key',
|
|
291
|
+
clientIdentityKey: 'client-key',
|
|
292
|
+
attempts: 3,
|
|
293
|
+
maxAttempts: 3,
|
|
294
|
+
errors: [],
|
|
295
|
+
requestSummary: {
|
|
296
|
+
url: 'https://example.com',
|
|
297
|
+
method: 'GET',
|
|
298
|
+
headers: {},
|
|
299
|
+
bodyType: 'none',
|
|
300
|
+
bodyByteLength: 0
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const response = make402Response();
|
|
304
|
+
await expect(authFetch.handlePaymentAndRetry('https://example.com', { paymentContext: exhaustedContext }, response)).rejects.toThrow('Paid request to https://example.com failed after 3/3 attempts');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// 7. serializeRequest – header and body branches
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
describe('AuthFetch.serializeRequest (private)', () => {
|
|
311
|
+
it('serializes a GET request with no body or headers', async () => {
|
|
312
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
313
|
+
const nonce = new Array(32).fill(0);
|
|
314
|
+
const writer = await authFetch.serializeRequest('GET', {}, undefined, new URL('https://example.com/path'), nonce);
|
|
315
|
+
expect(writer).toBeDefined();
|
|
316
|
+
expect(writer.toArray().length).toBeGreaterThan(32);
|
|
317
|
+
});
|
|
318
|
+
it('serializes a POST request with a JSON body', async () => {
|
|
319
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
320
|
+
const nonce = new Array(32).fill(1);
|
|
321
|
+
const writer = await authFetch.serializeRequest('POST', { 'content-type': 'application/json' }, { hello: 'world' }, new URL('https://example.com/api'), nonce);
|
|
322
|
+
expect(writer.toArray().length).toBeGreaterThan(32);
|
|
323
|
+
});
|
|
324
|
+
it('serializes a request with search params', async () => {
|
|
325
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
326
|
+
const nonce = new Array(32).fill(2);
|
|
327
|
+
const writer = await authFetch.serializeRequest('GET', {}, undefined, new URL('https://example.com/api?q=hello'), nonce);
|
|
328
|
+
expect(writer.toArray().length).toBeGreaterThan(32);
|
|
329
|
+
});
|
|
330
|
+
it('includes x-bsv-* custom headers', async () => {
|
|
331
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
332
|
+
const nonce = new Array(32).fill(3);
|
|
333
|
+
const writer = await authFetch.serializeRequest('GET', { 'x-bsv-custom': 'value123' }, undefined, new URL('https://example.com/'), nonce);
|
|
334
|
+
expect(writer.toArray().length).toBeGreaterThan(32);
|
|
335
|
+
});
|
|
336
|
+
it('includes authorization header', async () => {
|
|
337
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
338
|
+
const nonce = new Array(32).fill(4);
|
|
339
|
+
const writer = await authFetch.serializeRequest('GET', { authorization: 'Bearer token123' }, undefined, new URL('https://example.com/'), nonce);
|
|
340
|
+
expect(writer.toArray().length).toBeGreaterThan(32);
|
|
341
|
+
});
|
|
342
|
+
it('throws for x-bsv-auth-* headers', async () => {
|
|
343
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
344
|
+
const nonce = new Array(32).fill(5);
|
|
345
|
+
await expect(authFetch.serializeRequest('GET', { 'x-bsv-auth-identity-key': 'spoofed' }, undefined, new URL('https://example.com/'), nonce)).rejects.toThrow('No BSV auth headers allowed here!');
|
|
346
|
+
});
|
|
347
|
+
it('throws for unsupported headers', async () => {
|
|
348
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
349
|
+
const nonce = new Array(32).fill(6);
|
|
350
|
+
await expect(authFetch.serializeRequest('GET', { 'accept': 'application/json' }, undefined, new URL('https://example.com/'), nonce)).rejects.toThrow('Unsupported header');
|
|
351
|
+
});
|
|
352
|
+
it('normalizes content-type by stripping parameters', async () => {
|
|
353
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
354
|
+
const nonce = new Array(32).fill(7);
|
|
355
|
+
// Should not throw — content-type is allowed but parameters are stripped
|
|
356
|
+
const writer = await authFetch.serializeRequest('POST', { 'content-type': 'application/json; charset=utf-8' }, '{"x":1}', new URL('https://example.com/api'), nonce);
|
|
357
|
+
expect(writer.toArray().length).toBeGreaterThan(32);
|
|
358
|
+
});
|
|
359
|
+
it('defaults POST body to {} when content-type is application/json and body is undefined', async () => {
|
|
360
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
361
|
+
const nonce = new Array(32).fill(8);
|
|
362
|
+
// Should not throw
|
|
363
|
+
const writer = await authFetch.serializeRequest('POST', { 'content-type': 'application/json' }, undefined, new URL('https://example.com/api'), nonce);
|
|
364
|
+
expect(writer.toArray().length).toBeGreaterThan(32);
|
|
365
|
+
});
|
|
366
|
+
it('defaults DELETE body to empty string when no content-type and body is undefined', async () => {
|
|
367
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
368
|
+
const nonce = new Array(32).fill(9);
|
|
369
|
+
const writer = await authFetch.serializeRequest('DELETE', {}, undefined, new URL('https://example.com/resource'), nonce);
|
|
370
|
+
expect(writer.toArray().length).toBeGreaterThan(32);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// 8. describeRequestBodyForLogging – all body types
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
describe('AuthFetch.describeRequestBodyForLogging (private)', () => {
|
|
377
|
+
let authFetch;
|
|
378
|
+
beforeEach(() => {
|
|
379
|
+
authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
380
|
+
});
|
|
381
|
+
it('returns type=none for null body', () => {
|
|
382
|
+
const result = authFetch.describeRequestBodyForLogging(null);
|
|
383
|
+
expect(result).toEqual({ type: 'none', byteLength: 0 });
|
|
384
|
+
});
|
|
385
|
+
it('returns type=none for undefined body', () => {
|
|
386
|
+
const result = authFetch.describeRequestBodyForLogging(undefined);
|
|
387
|
+
expect(result).toEqual({ type: 'none', byteLength: 0 });
|
|
388
|
+
});
|
|
389
|
+
it('returns type=string with correct byteLength', () => {
|
|
390
|
+
const result = authFetch.describeRequestBodyForLogging('hello');
|
|
391
|
+
expect(result.type).toBe('string');
|
|
392
|
+
expect(result.byteLength).toBe(5);
|
|
393
|
+
});
|
|
394
|
+
it('returns type=number[] for number array', () => {
|
|
395
|
+
const result = authFetch.describeRequestBodyForLogging([1, 2, 3]);
|
|
396
|
+
expect(result).toEqual({ type: 'number[]', byteLength: 3 });
|
|
397
|
+
});
|
|
398
|
+
it('returns type=array for non-number array', () => {
|
|
399
|
+
const result = authFetch.describeRequestBodyForLogging(['a', 'b']);
|
|
400
|
+
expect(result).toEqual({ type: 'array', byteLength: 2 });
|
|
401
|
+
});
|
|
402
|
+
it('returns type=ArrayBuffer for ArrayBuffer', () => {
|
|
403
|
+
const buf = new ArrayBuffer(8);
|
|
404
|
+
const result = authFetch.describeRequestBodyForLogging(buf);
|
|
405
|
+
expect(result).toEqual({ type: 'ArrayBuffer', byteLength: 8 });
|
|
406
|
+
});
|
|
407
|
+
it('returns typed array name for Uint8Array', () => {
|
|
408
|
+
const arr = new Uint8Array([1, 2, 3, 4]);
|
|
409
|
+
const result = authFetch.describeRequestBodyForLogging(arr);
|
|
410
|
+
expect(result.type).toBe('Uint8Array');
|
|
411
|
+
expect(result.byteLength).toBe(4);
|
|
412
|
+
});
|
|
413
|
+
it('returns type=Blob for Blob', () => {
|
|
414
|
+
const blob = new Blob(['hello world']);
|
|
415
|
+
const result = authFetch.describeRequestBodyForLogging(blob);
|
|
416
|
+
expect(result.type).toBe('Blob');
|
|
417
|
+
expect(result.byteLength).toBeGreaterThan(0);
|
|
418
|
+
});
|
|
419
|
+
it('returns type=URLSearchParams for URLSearchParams', () => {
|
|
420
|
+
const params = new URLSearchParams({ key: 'value' });
|
|
421
|
+
const result = authFetch.describeRequestBodyForLogging(params);
|
|
422
|
+
expect(result.type).toBe('URLSearchParams');
|
|
423
|
+
expect(result.byteLength).toBeGreaterThan(0);
|
|
424
|
+
});
|
|
425
|
+
it('returns type=FormData for FormData', () => {
|
|
426
|
+
const fd = new FormData();
|
|
427
|
+
fd.append('field', 'value');
|
|
428
|
+
const result = authFetch.describeRequestBodyForLogging(fd);
|
|
429
|
+
expect(result.type).toBe('FormData');
|
|
430
|
+
expect(result.byteLength).toBe(0);
|
|
431
|
+
});
|
|
432
|
+
it('returns type=object for a plain object', () => {
|
|
433
|
+
const result = authFetch.describeRequestBodyForLogging({ a: 1 });
|
|
434
|
+
expect(result.type).toBe('object');
|
|
435
|
+
expect(result.byteLength).toBeGreaterThan(0);
|
|
436
|
+
});
|
|
437
|
+
it('returns type=ReadableStream for ReadableStream', () => {
|
|
438
|
+
const stream = new ReadableStream();
|
|
439
|
+
const result = authFetch.describeRequestBodyForLogging(stream);
|
|
440
|
+
expect(result).toEqual({ type: 'ReadableStream', byteLength: 0 });
|
|
441
|
+
});
|
|
442
|
+
it('falls back to typeof for an unrecognised type', () => {
|
|
443
|
+
// A Symbol cannot be JSON-stringified, triggering the fallback
|
|
444
|
+
const sym = Symbol('test');
|
|
445
|
+
const result = authFetch.describeRequestBodyForLogging(sym);
|
|
446
|
+
expect(result.type).toBe('symbol');
|
|
447
|
+
expect(result.byteLength).toBe(0);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// 9. normalizeBodyToNumberArray – edge cases
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
describe('AuthFetch.normalizeBodyToNumberArray (private)', () => {
|
|
454
|
+
let authFetch;
|
|
455
|
+
beforeEach(() => {
|
|
456
|
+
authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
457
|
+
});
|
|
458
|
+
it('returns [] for null', async () => {
|
|
459
|
+
const result = await authFetch.normalizeBodyToNumberArray(null);
|
|
460
|
+
expect(result).toEqual([]);
|
|
461
|
+
});
|
|
462
|
+
it('returns [] for undefined', async () => {
|
|
463
|
+
const result = await authFetch.normalizeBodyToNumberArray(undefined);
|
|
464
|
+
expect(result).toEqual([]);
|
|
465
|
+
});
|
|
466
|
+
it('converts a string to a number array', async () => {
|
|
467
|
+
const result = await authFetch.normalizeBodyToNumberArray('abc');
|
|
468
|
+
expect(result.length).toBe(3);
|
|
469
|
+
});
|
|
470
|
+
it('converts a number[] to a JSON-encoded number array', async () => {
|
|
471
|
+
// Arrays are objects, so they hit the typeof === 'object' branch first
|
|
472
|
+
// and are serialized via JSON.stringify before the number[] guard runs.
|
|
473
|
+
const input = [1, 2, 3];
|
|
474
|
+
const result = await authFetch.normalizeBodyToNumberArray(input);
|
|
475
|
+
// '[1,2,3]' encoded as UTF-8 bytes
|
|
476
|
+
const expected = index_js_1.Utils.toArray(JSON.stringify(input), 'utf8');
|
|
477
|
+
expect(result).toEqual(expected);
|
|
478
|
+
});
|
|
479
|
+
it('converts ArrayBuffer to a JSON-encoded number array', async () => {
|
|
480
|
+
// ArrayBuffer is an object, so it hits the typeof === 'object' branch first.
|
|
481
|
+
const buf = new Uint8Array([10, 20, 30]).buffer;
|
|
482
|
+
const result = await authFetch.normalizeBodyToNumberArray(buf);
|
|
483
|
+
// JSON.stringify of an ArrayBuffer produces '{}'
|
|
484
|
+
const expected = index_js_1.Utils.toArray(JSON.stringify(buf), 'utf8');
|
|
485
|
+
expect(result).toEqual(expected);
|
|
486
|
+
});
|
|
487
|
+
it('converts Uint8Array to a JSON-encoded number array', async () => {
|
|
488
|
+
// Uint8Array is an object, so it hits the typeof === 'object' branch first.
|
|
489
|
+
const arr = new Uint8Array([5, 6, 7]);
|
|
490
|
+
const result = await authFetch.normalizeBodyToNumberArray(arr);
|
|
491
|
+
// JSON.stringify of a Uint8Array produces e.g. '{"0":5,"1":6,"2":7}'
|
|
492
|
+
const expected = index_js_1.Utils.toArray(JSON.stringify(arr), 'utf8');
|
|
493
|
+
expect(result).toEqual(expected);
|
|
494
|
+
});
|
|
495
|
+
it('converts Blob via JSON.stringify (object branch)', async () => {
|
|
496
|
+
// Blob is an object — hits the typeof === 'object' branch before the Blob check.
|
|
497
|
+
const blob = new Blob(['hi']);
|
|
498
|
+
const result = await authFetch.normalizeBodyToNumberArray(blob);
|
|
499
|
+
// JSON.stringify(new Blob(...)) → '{}'
|
|
500
|
+
const expected = index_js_1.Utils.toArray(JSON.stringify(blob), 'utf8');
|
|
501
|
+
expect(result).toEqual(expected);
|
|
502
|
+
});
|
|
503
|
+
it('converts FormData via JSON.stringify (object branch)', async () => {
|
|
504
|
+
// FormData is an object — hits the typeof === 'object' branch before the FormData check.
|
|
505
|
+
const fd = new FormData();
|
|
506
|
+
fd.append('name', 'alice');
|
|
507
|
+
const result = await authFetch.normalizeBodyToNumberArray(fd);
|
|
508
|
+
// JSON.stringify(FormData) → '{}'
|
|
509
|
+
const expected = index_js_1.Utils.toArray(JSON.stringify(fd), 'utf8');
|
|
510
|
+
expect(result).toEqual(expected);
|
|
511
|
+
});
|
|
512
|
+
it('converts URLSearchParams via JSON.stringify (object branch)', async () => {
|
|
513
|
+
// URLSearchParams is an object — hits typeof === 'object' branch first.
|
|
514
|
+
const params = new URLSearchParams({ q: 'hello' });
|
|
515
|
+
const result = await authFetch.normalizeBodyToNumberArray(params);
|
|
516
|
+
// JSON.stringify(URLSearchParams) → '{}'
|
|
517
|
+
const expected = index_js_1.Utils.toArray(JSON.stringify(params), 'utf8');
|
|
518
|
+
expect(result).toEqual(expected);
|
|
519
|
+
});
|
|
520
|
+
it('converts ReadableStream via JSON.stringify (object branch)', async () => {
|
|
521
|
+
// ReadableStream is an object, so it hits the typeof === 'object' branch first
|
|
522
|
+
// and is serialized via JSON.stringify (produces '{}') rather than throwing.
|
|
523
|
+
const stream = new ReadableStream();
|
|
524
|
+
const result = await authFetch.normalizeBodyToNumberArray(stream);
|
|
525
|
+
const expected = index_js_1.Utils.toArray(JSON.stringify(stream), 'utf8');
|
|
526
|
+
expect(result).toEqual(expected);
|
|
527
|
+
});
|
|
528
|
+
it('converts a plain object via JSON.stringify', async () => {
|
|
529
|
+
const obj = { key: 'value' };
|
|
530
|
+
const result = await authFetch.normalizeBodyToNumberArray(obj);
|
|
531
|
+
expect(result.length).toBeGreaterThan(0);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
// 10. getMaxPaymentAttempts
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
describe('AuthFetch.getMaxPaymentAttempts (private)', () => {
|
|
538
|
+
let authFetch;
|
|
539
|
+
beforeEach(() => {
|
|
540
|
+
authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
541
|
+
});
|
|
542
|
+
it('returns 3 by default', () => {
|
|
543
|
+
expect(authFetch.getMaxPaymentAttempts({})).toBe(3);
|
|
544
|
+
});
|
|
545
|
+
it('returns the configured positive integer', () => {
|
|
546
|
+
expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: 5 })).toBe(5);
|
|
547
|
+
});
|
|
548
|
+
it('floors the value for a float', () => {
|
|
549
|
+
expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: 4.9 })).toBe(4);
|
|
550
|
+
});
|
|
551
|
+
it('returns 3 when paymentRetryAttempts is 0', () => {
|
|
552
|
+
expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: 0 })).toBe(3);
|
|
553
|
+
});
|
|
554
|
+
it('returns 3 when paymentRetryAttempts is negative', () => {
|
|
555
|
+
expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: -1 })).toBe(3);
|
|
556
|
+
});
|
|
557
|
+
it('returns 3 when paymentRetryAttempts is a string', () => {
|
|
558
|
+
expect(authFetch.getMaxPaymentAttempts({ paymentRetryAttempts: 'five' })).toBe(3);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
// 11. getPaymentRetryDelay
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
describe('AuthFetch.getPaymentRetryDelay (private)', () => {
|
|
565
|
+
let authFetch;
|
|
566
|
+
beforeEach(() => {
|
|
567
|
+
authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
568
|
+
});
|
|
569
|
+
it('returns 250 for attempt 1 (250 * 1)', () => {
|
|
570
|
+
expect(authFetch.getPaymentRetryDelay(1)).toBe(250);
|
|
571
|
+
});
|
|
572
|
+
it('returns 500 for attempt 2', () => {
|
|
573
|
+
expect(authFetch.getPaymentRetryDelay(2)).toBe(500);
|
|
574
|
+
});
|
|
575
|
+
it('caps multiplier at 5 for attempt >= 5', () => {
|
|
576
|
+
expect(authFetch.getPaymentRetryDelay(5)).toBe(1250);
|
|
577
|
+
expect(authFetch.getPaymentRetryDelay(10)).toBe(1250);
|
|
578
|
+
expect(authFetch.getPaymentRetryDelay(100)).toBe(1250);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// 12. wait()
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
describe('AuthFetch.wait (private)', () => {
|
|
585
|
+
it('resolves immediately for ms <= 0', async () => {
|
|
586
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
587
|
+
const start = Date.now();
|
|
588
|
+
await authFetch.wait(0);
|
|
589
|
+
expect(Date.now() - start).toBeLessThan(50);
|
|
590
|
+
});
|
|
591
|
+
it('resolves immediately for negative ms', async () => {
|
|
592
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
593
|
+
const start = Date.now();
|
|
594
|
+
await authFetch.wait(-100);
|
|
595
|
+
expect(Date.now() - start).toBeLessThan(50);
|
|
596
|
+
});
|
|
597
|
+
it('uses a timer for positive ms', async () => {
|
|
598
|
+
globals_1.jest.useFakeTimers();
|
|
599
|
+
try {
|
|
600
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
601
|
+
let resolved = false;
|
|
602
|
+
const promise = authFetch.wait(500).then(() => { resolved = true; });
|
|
603
|
+
expect(resolved).toBe(false);
|
|
604
|
+
await globals_1.jest.advanceTimersByTimeAsync(500);
|
|
605
|
+
await promise;
|
|
606
|
+
expect(resolved).toBe(true);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
globals_1.jest.useRealTimers();
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
// 13. isPaymentContextCompatible
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
describe('AuthFetch.isPaymentContextCompatible (private)', () => {
|
|
617
|
+
let authFetch;
|
|
618
|
+
beforeEach(() => {
|
|
619
|
+
authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
620
|
+
});
|
|
621
|
+
function makeCtx(overrides = {}) {
|
|
622
|
+
return {
|
|
623
|
+
satoshisRequired: 10,
|
|
624
|
+
serverIdentityKey: 'srv',
|
|
625
|
+
derivationPrefix: 'pfx',
|
|
626
|
+
...overrides
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
it('returns true when all fields match', () => {
|
|
630
|
+
const ctx = makeCtx();
|
|
631
|
+
expect(authFetch.isPaymentContextCompatible(ctx, 10, 'srv', 'pfx')).toBe(true);
|
|
632
|
+
});
|
|
633
|
+
it('returns false when satoshis differ', () => {
|
|
634
|
+
const ctx = makeCtx();
|
|
635
|
+
expect(authFetch.isPaymentContextCompatible(ctx, 20, 'srv', 'pfx')).toBe(false);
|
|
636
|
+
});
|
|
637
|
+
it('returns false when serverIdentityKey differs', () => {
|
|
638
|
+
const ctx = makeCtx();
|
|
639
|
+
expect(authFetch.isPaymentContextCompatible(ctx, 10, 'other', 'pfx')).toBe(false);
|
|
640
|
+
});
|
|
641
|
+
it('returns false when derivationPrefix differs', () => {
|
|
642
|
+
const ctx = makeCtx();
|
|
643
|
+
expect(authFetch.isPaymentContextCompatible(ctx, 10, 'srv', 'other')).toBe(false);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
// 14. consumeReceivedCertificates
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
describe('AuthFetch.consumeReceivedCertificates', () => {
|
|
650
|
+
it('returns all received certs and empties the buffer', () => {
|
|
651
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
652
|
+
authFetch.certificatesReceived.push({ serialNumber: 'cert1' }, { serialNumber: 'cert2' });
|
|
653
|
+
const certs = authFetch.consumeReceivedCertificates();
|
|
654
|
+
expect(certs).toHaveLength(2);
|
|
655
|
+
expect(certs[0]).toMatchObject({ serialNumber: 'cert1' });
|
|
656
|
+
// Buffer should now be empty
|
|
657
|
+
const second = authFetch.consumeReceivedCertificates();
|
|
658
|
+
expect(second).toHaveLength(0);
|
|
659
|
+
});
|
|
660
|
+
it('returns an empty array when no certs have been received', () => {
|
|
661
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
662
|
+
expect(authFetch.consumeReceivedCertificates()).toEqual([]);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
// ---------------------------------------------------------------------------
|
|
666
|
+
// 15. sendCertificateRequest – creates a new peer when none exists
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
describe('AuthFetch.sendCertificateRequest – new peer creation', () => {
|
|
669
|
+
it('creates a new Peer transport when no peer exists for the base URL', async () => {
|
|
670
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
671
|
+
// Verify there is no existing peer
|
|
672
|
+
expect(authFetch.peers['https://new-server.com']).toBeUndefined();
|
|
673
|
+
const fakeCerts = [{ serialNumber: 'abc' }];
|
|
674
|
+
let capturedListener;
|
|
675
|
+
const peerProto = {
|
|
676
|
+
listenForCertificatesReceived: globals_1.jest.fn((cb) => {
|
|
677
|
+
capturedListener = cb;
|
|
678
|
+
return 99;
|
|
679
|
+
}),
|
|
680
|
+
stopListeningForCertificatesReceived: globals_1.jest.fn(),
|
|
681
|
+
requestCertificates: globals_1.jest.fn(async () => {
|
|
682
|
+
capturedListener?.('srv-key', fakeCerts);
|
|
683
|
+
})
|
|
684
|
+
};
|
|
685
|
+
// Intercept Peer constructor by injecting peer directly after fetch call starts
|
|
686
|
+
const origFetch = authFetch.sendCertificateRequest.bind(authFetch);
|
|
687
|
+
globals_1.jest.spyOn(authFetch, 'sendCertificateRequest').mockImplementationOnce(async (url, certs) => {
|
|
688
|
+
// Manually inject our stub peer so Peer constructor is bypassed
|
|
689
|
+
;
|
|
690
|
+
authFetch.peers['https://new-server.com'] = {
|
|
691
|
+
peer: peerProto,
|
|
692
|
+
pendingCertificateRequests: []
|
|
693
|
+
};
|
|
694
|
+
return origFetch(url, certs);
|
|
695
|
+
});
|
|
696
|
+
const result = await authFetch.sendCertificateRequest('https://new-server.com/path', { certifiers: [], types: {} });
|
|
697
|
+
expect(result).toHaveLength(1);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
// 16. logPaymentAttempt – all levels
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
703
|
+
describe('AuthFetch.logPaymentAttempt (private)', () => {
|
|
704
|
+
let authFetch;
|
|
705
|
+
beforeEach(() => {
|
|
706
|
+
authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
707
|
+
});
|
|
708
|
+
it('calls console.error for level=error', () => {
|
|
709
|
+
const spy = globals_1.jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
710
|
+
authFetch.logPaymentAttempt('error', 'test error', { a: 1 });
|
|
711
|
+
expect(spy).toHaveBeenCalledWith('[AuthFetch][Payment] test error', { a: 1 });
|
|
712
|
+
});
|
|
713
|
+
it('calls console.warn for level=warn', () => {
|
|
714
|
+
const spy = globals_1.jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
715
|
+
authFetch.logPaymentAttempt('warn', 'test warn', { b: 2 });
|
|
716
|
+
expect(spy).toHaveBeenCalledWith('[AuthFetch][Payment] test warn', { b: 2 });
|
|
717
|
+
});
|
|
718
|
+
it('calls console.info for level=info when available', () => {
|
|
719
|
+
const spy = globals_1.jest.spyOn(console, 'info').mockImplementation(() => { });
|
|
720
|
+
authFetch.logPaymentAttempt('info', 'test info', { c: 3 });
|
|
721
|
+
expect(spy).toHaveBeenCalledWith('[AuthFetch][Payment] test info', { c: 3 });
|
|
722
|
+
});
|
|
723
|
+
it('falls back to console.log for level=info when console.info is not a function', () => {
|
|
724
|
+
const originalInfo = console.info;
|
|
725
|
+
console.info = undefined;
|
|
726
|
+
const spy = globals_1.jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
727
|
+
try {
|
|
728
|
+
;
|
|
729
|
+
authFetch.logPaymentAttempt('info', 'fallback log', {});
|
|
730
|
+
expect(spy).toHaveBeenCalledWith('[AuthFetch][Payment] fallback log', {});
|
|
731
|
+
}
|
|
732
|
+
finally {
|
|
733
|
+
console.info = originalInfo;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
// 17. createPaymentErrorEntry
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
describe('AuthFetch.createPaymentErrorEntry (private)', () => {
|
|
741
|
+
let authFetch;
|
|
742
|
+
beforeEach(() => {
|
|
743
|
+
authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
744
|
+
});
|
|
745
|
+
it('extracts message and stack from an Error instance', () => {
|
|
746
|
+
const err = new Error('something went wrong');
|
|
747
|
+
const entry = authFetch.createPaymentErrorEntry(2, err);
|
|
748
|
+
expect(entry.attempt).toBe(2);
|
|
749
|
+
expect(entry.message).toBe('something went wrong');
|
|
750
|
+
expect(typeof entry.stack).toBe('string');
|
|
751
|
+
expect(typeof entry.timestamp).toBe('string');
|
|
752
|
+
});
|
|
753
|
+
it('converts non-Error to string message', () => {
|
|
754
|
+
const entry = authFetch.createPaymentErrorEntry(1, 'just a string error');
|
|
755
|
+
expect(entry.message).toBe('just a string error');
|
|
756
|
+
expect(entry.stack).toBeUndefined();
|
|
757
|
+
});
|
|
758
|
+
it('converts numeric error to string message', () => {
|
|
759
|
+
const entry = authFetch.createPaymentErrorEntry(1, 42);
|
|
760
|
+
expect(entry.message).toBe('42');
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
// ---------------------------------------------------------------------------
|
|
764
|
+
// 18. buildPaymentFailureError
|
|
765
|
+
// ---------------------------------------------------------------------------
|
|
766
|
+
describe('AuthFetch.buildPaymentFailureError (private)', () => {
|
|
767
|
+
let authFetch;
|
|
768
|
+
beforeEach(() => {
|
|
769
|
+
authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
770
|
+
});
|
|
771
|
+
function makeContext() {
|
|
772
|
+
return {
|
|
773
|
+
satoshisRequired: 10,
|
|
774
|
+
transactionBase64: 'tx-base64',
|
|
775
|
+
derivationPrefix: 'pfx',
|
|
776
|
+
derivationSuffix: 'sfx',
|
|
777
|
+
serverIdentityKey: 'srv',
|
|
778
|
+
clientIdentityKey: 'cli',
|
|
779
|
+
attempts: 3,
|
|
780
|
+
maxAttempts: 3,
|
|
781
|
+
errors: [],
|
|
782
|
+
requestSummary: { url: 'https://ex.com', method: 'GET', headers: {}, bodyType: 'none', bodyByteLength: 0 }
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
it('creates an Error with a descriptive message', () => {
|
|
786
|
+
const err = authFetch.buildPaymentFailureError('https://example.com/pay', makeContext(), new Error('last error'));
|
|
787
|
+
expect(err).toBeInstanceOf(Error);
|
|
788
|
+
expect(err.message).toContain('https://example.com/pay');
|
|
789
|
+
expect(err.message).toContain('3/3');
|
|
790
|
+
expect(err.message).toContain('10 satoshis');
|
|
791
|
+
});
|
|
792
|
+
it('attaches details to the error', () => {
|
|
793
|
+
const err = authFetch.buildPaymentFailureError('https://example.com/pay', makeContext(), new Error('x'));
|
|
794
|
+
expect(err.details).toBeDefined();
|
|
795
|
+
expect(err.details.payment.satoshis).toBe(10);
|
|
796
|
+
expect(err.details.attempts.used).toBe(3);
|
|
797
|
+
});
|
|
798
|
+
it('sets cause when lastError is an Error', () => {
|
|
799
|
+
const cause = new Error('root cause');
|
|
800
|
+
const err = authFetch.buildPaymentFailureError('https://example.com/pay', makeContext(), cause);
|
|
801
|
+
expect(err.cause).toBe(cause);
|
|
802
|
+
});
|
|
803
|
+
it('does not set cause when lastError is a string', () => {
|
|
804
|
+
const err = authFetch.buildPaymentFailureError('https://example.com/pay', makeContext(), 'string error');
|
|
805
|
+
expect(err.cause).toBeUndefined();
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
// ---------------------------------------------------------------------------
|
|
809
|
+
// 19. buildPaymentRequestSummary
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
describe('AuthFetch.buildPaymentRequestSummary (private)', () => {
|
|
812
|
+
it('builds a summary with correct fields', () => {
|
|
813
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
814
|
+
const summary = authFetch.buildPaymentRequestSummary('https://example.com/resource', { method: 'post', headers: { 'X-Custom': 'value' }, body: 'hello' });
|
|
815
|
+
expect(summary.url).toBe('https://example.com/resource');
|
|
816
|
+
expect(summary.method).toBe('POST');
|
|
817
|
+
expect(summary.headers).toMatchObject({ 'X-Custom': 'value' });
|
|
818
|
+
expect(summary.bodyType).toBe('string');
|
|
819
|
+
expect(summary.bodyByteLength).toBe(5);
|
|
820
|
+
});
|
|
821
|
+
it('defaults method to GET when not provided', () => {
|
|
822
|
+
const authFetch = new AuthFetch_js_1.AuthFetch(buildWallet());
|
|
823
|
+
const summary = authFetch.buildPaymentRequestSummary('https://example.com', {});
|
|
824
|
+
expect(summary.method).toBe('GET');
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
//# sourceMappingURL=AuthFetch.additional.test.js.map
|