@bsv/sdk 2.0.11 → 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/overlay-tools/HostReputationTracker.js +21 -13
- package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
- package/dist/cjs/src/primitives/PrivateKey.js +3 -3
- package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
- package/dist/cjs/src/script/Spend.js +17 -9
- package/dist/cjs/src/script/Spend.js.map +1 -1
- package/dist/cjs/src/storage/StorageDownloader.js +6 -6
- package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
- package/dist/cjs/src/storage/StorageUtils.js +1 -1
- package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
- package/dist/cjs/src/transaction/MerklePath.js +168 -27
- 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/overlay-tools/HostReputationTracker.js +21 -13
- package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
- package/dist/esm/src/primitives/PrivateKey.js +3 -3
- package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
- package/dist/esm/src/script/Spend.js +17 -9
- package/dist/esm/src/script/Spend.js.map +1 -1
- package/dist/esm/src/storage/StorageDownloader.js +6 -6
- package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
- package/dist/esm/src/storage/StorageUtils.js +1 -1
- package/dist/esm/src/storage/StorageUtils.js.map +1 -1
- package/dist/esm/src/transaction/MerklePath.js +168 -27
- 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/overlay-tools/HostReputationTracker.d.ts.map +1 -1
- package/dist/types/src/script/Spend.d.ts.map +1 -1
- 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 +3 -3
- 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/auth/utils/__tests/validateCertificates.test.ts +12 -9
- 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/LocalKVStore.test.ts +4 -6
- package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
- package/src/overlay-tools/HostReputationTracker.ts +17 -14
- 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/PrivateKey.ts +3 -3
- 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/Spend.ts +19 -11
- 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/storage/StorageDownloader.ts +6 -6
- package/src/storage/StorageUtils.ts +1 -1
- package/src/transaction/MerklePath.ts +196 -36
- 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 +232 -21
- 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,654 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const globals_1 = require("@jest/globals");
|
|
37
|
+
const SimplifiedFetchTransport_js_1 = require("../SimplifiedFetchTransport.js");
|
|
38
|
+
const Utils = __importStar(require("../../../primitives/utils.js"));
|
|
39
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
40
|
+
/** Build a serialized general message payload. */
|
|
41
|
+
function buildGeneralPayload({ path = '/api/resource', method = 'GET', search = '', headers = {}, body = null } = {}) {
|
|
42
|
+
const writer = new Utils.Writer();
|
|
43
|
+
// requestId: 32 bytes
|
|
44
|
+
writer.write(new Array(32).fill(0xab));
|
|
45
|
+
// method
|
|
46
|
+
if (method.length > 0) {
|
|
47
|
+
const methodBytes = Utils.toArray(method, 'utf8');
|
|
48
|
+
writer.writeVarIntNum(methodBytes.length);
|
|
49
|
+
writer.write(methodBytes);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
writer.writeVarIntNum(0);
|
|
53
|
+
}
|
|
54
|
+
// path
|
|
55
|
+
if (path.length > 0) {
|
|
56
|
+
const pathBytes = Utils.toArray(path, 'utf8');
|
|
57
|
+
writer.writeVarIntNum(pathBytes.length);
|
|
58
|
+
writer.write(pathBytes);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
writer.writeVarIntNum(0);
|
|
62
|
+
}
|
|
63
|
+
// search
|
|
64
|
+
if (search.length > 0) {
|
|
65
|
+
const searchBytes = Utils.toArray(search, 'utf8');
|
|
66
|
+
writer.writeVarIntNum(searchBytes.length);
|
|
67
|
+
writer.write(searchBytes);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
writer.writeVarIntNum(0);
|
|
71
|
+
}
|
|
72
|
+
// headers
|
|
73
|
+
const headerEntries = Object.entries(headers);
|
|
74
|
+
writer.writeVarIntNum(headerEntries.length);
|
|
75
|
+
for (const [key, value] of headerEntries) {
|
|
76
|
+
const keyBytes = Utils.toArray(key, 'utf8');
|
|
77
|
+
writer.writeVarIntNum(keyBytes.length);
|
|
78
|
+
writer.write(keyBytes);
|
|
79
|
+
const valueBytes = Utils.toArray(value, 'utf8');
|
|
80
|
+
writer.writeVarIntNum(valueBytes.length);
|
|
81
|
+
writer.write(valueBytes);
|
|
82
|
+
}
|
|
83
|
+
// body
|
|
84
|
+
if (body != null && body.length > 0) {
|
|
85
|
+
writer.writeVarIntNum(body.length);
|
|
86
|
+
writer.write(body);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
writer.writeVarIntNum(0);
|
|
90
|
+
}
|
|
91
|
+
return writer.toArray();
|
|
92
|
+
}
|
|
93
|
+
function makeGeneralMessage(overrides = {}) {
|
|
94
|
+
return {
|
|
95
|
+
version: '0.1',
|
|
96
|
+
messageType: 'general',
|
|
97
|
+
identityKey: 'client-key',
|
|
98
|
+
nonce: 'cnonce',
|
|
99
|
+
yourNonce: 'snonce',
|
|
100
|
+
payload: buildGeneralPayload(),
|
|
101
|
+
signature: new Array(64).fill(0),
|
|
102
|
+
...overrides
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function makeAuthMessage(messageType, overrides = {}) {
|
|
106
|
+
return {
|
|
107
|
+
version: '0.1',
|
|
108
|
+
messageType,
|
|
109
|
+
identityKey: 'client-key',
|
|
110
|
+
nonce: 'cnonce',
|
|
111
|
+
yourNonce: 'snonce',
|
|
112
|
+
payload: [],
|
|
113
|
+
signature: new Array(64).fill(0),
|
|
114
|
+
...overrides
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Build a minimal valid general response (all required BSV auth headers). */
|
|
118
|
+
function makeValidGeneralResponse(body = '', extraHeaders = {}) {
|
|
119
|
+
return new Response(body, {
|
|
120
|
+
status: 200,
|
|
121
|
+
headers: {
|
|
122
|
+
'x-bsv-auth-version': '0.1',
|
|
123
|
+
'x-bsv-auth-identity-key': 'server-key',
|
|
124
|
+
'x-bsv-auth-signature': 'aabbcc',
|
|
125
|
+
'x-bsv-auth-message-type': 'general',
|
|
126
|
+
...extraHeaders
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
globals_1.jest.restoreAllMocks();
|
|
132
|
+
});
|
|
133
|
+
// ─── Constructor ─────────────────────────────────────────────────────────────
|
|
134
|
+
describe('SimplifiedFetchTransport constructor', () => {
|
|
135
|
+
test('throws when fetchClient is not a function', () => {
|
|
136
|
+
expect(() => new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://example.com', 'not-a-function'))
|
|
137
|
+
.toThrow('SimplifiedFetchTransport requires a fetch implementation.');
|
|
138
|
+
});
|
|
139
|
+
test('throws when fetchClient is null', () => {
|
|
140
|
+
expect(() => new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://example.com', null))
|
|
141
|
+
.toThrow('SimplifiedFetchTransport requires a fetch implementation.');
|
|
142
|
+
});
|
|
143
|
+
test('stores baseUrl and fetchClient', () => {
|
|
144
|
+
const mockFetch = globals_1.jest.fn();
|
|
145
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://my.server.com', mockFetch);
|
|
146
|
+
expect(transport.baseUrl).toBe('https://my.server.com');
|
|
147
|
+
expect(transport.fetchClient).toBe(mockFetch);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
// ─── send without onData registered ──────────────────────────────────────────
|
|
151
|
+
describe('SimplifiedFetchTransport send without listener', () => {
|
|
152
|
+
test('throws "Listen before you start speaking" when no onData registered', async () => {
|
|
153
|
+
const mockFetch = globals_1.jest.fn();
|
|
154
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://example.com', mockFetch);
|
|
155
|
+
// Never call onData
|
|
156
|
+
await expect(transport.send(makeGeneralMessage())).rejects.toThrow('Listen before you start speaking');
|
|
157
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
// ─── send: non-initialRequest auth message paths ─────────────────────────────
|
|
161
|
+
describe('SimplifiedFetchTransport send — non-general auth message', () => {
|
|
162
|
+
test('non-initialRequest: resolves before response arrives and still calls onDataCallback', async () => {
|
|
163
|
+
let resolveResponse;
|
|
164
|
+
const responsePromise = new Promise((res) => { resolveResponse = res; });
|
|
165
|
+
const mockFetch = globals_1.jest.fn().mockReturnValue(responsePromise);
|
|
166
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
167
|
+
const received = [];
|
|
168
|
+
await transport.onData(async (msg) => { received.push(msg); });
|
|
169
|
+
const sendPromise = transport.send(makeAuthMessage('initialResponse'));
|
|
170
|
+
// resolve before the fetch response arrives to confirm promise doesn't hang
|
|
171
|
+
resolveResponse(new Response(JSON.stringify({ version: '0.1', messageType: 'initialResponse', identityKey: 'k', payload: [], signature: [] }), {
|
|
172
|
+
status: 200,
|
|
173
|
+
headers: { 'Content-Type': 'application/json' }
|
|
174
|
+
}));
|
|
175
|
+
await sendPromise;
|
|
176
|
+
// Flush microtask queue so the background response processing completes
|
|
177
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
178
|
+
// onDataCallback should have been invoked with the response message
|
|
179
|
+
expect(received).toHaveLength(1);
|
|
180
|
+
const firstReceived = received[0];
|
|
181
|
+
expect(firstReceived).toBeDefined();
|
|
182
|
+
expect(firstReceived?.messageType).toBe('initialResponse');
|
|
183
|
+
});
|
|
184
|
+
test('initialRequest: resolves after the response is processed', async () => {
|
|
185
|
+
const responseBody = JSON.stringify({ version: '0.1', messageType: 'initialRequest', identityKey: 'server-key', payload: [], signature: [] });
|
|
186
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(new Response(responseBody, { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
187
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
188
|
+
const received = [];
|
|
189
|
+
await transport.onData(async (msg) => { received.push(msg); });
|
|
190
|
+
await transport.send(makeAuthMessage('initialRequest'));
|
|
191
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/.well-known/auth', expect.objectContaining({ method: 'POST' }));
|
|
192
|
+
expect(received).toHaveLength(1);
|
|
193
|
+
});
|
|
194
|
+
test('non-ok response on auth endpoint throws unauthenticated error', async () => {
|
|
195
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(new Response('Unauthorized', { status: 401, statusText: 'Unauthorized' }));
|
|
196
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
197
|
+
await transport.onData(async () => { });
|
|
198
|
+
await expect(transport.send(makeAuthMessage('initialRequest'))).rejects.toThrow('Received HTTP 401 Unauthorized from https://api.example.com/.well-known/auth without valid BSV authentication');
|
|
199
|
+
});
|
|
200
|
+
test('network failure on auth endpoint wraps error with context', async () => {
|
|
201
|
+
const mockFetch = globals_1.jest.fn().mockRejectedValue(new Error('DNS lookup failed'));
|
|
202
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
203
|
+
await transport.onData(async () => { });
|
|
204
|
+
await expect(transport.send(makeAuthMessage('initialRequest'))).rejects.toThrow('Network error while sending authenticated request to https://api.example.com/.well-known/auth: DNS lookup failed');
|
|
205
|
+
});
|
|
206
|
+
test('non-Error network failure still wraps as Error string', async () => {
|
|
207
|
+
const mockFetch = globals_1.jest.fn().mockRejectedValue('plain string error');
|
|
208
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
209
|
+
await transport.onData(async () => { });
|
|
210
|
+
await expect(transport.send(makeAuthMessage('initialRequest'))).rejects.toThrow('Network error while sending authenticated request to https://api.example.com/.well-known/auth: plain string error');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
// ─── send: general message — body Content-Type handling ──────────────────────
|
|
214
|
+
describe('SimplifiedFetchTransport send — general message body handling', () => {
|
|
215
|
+
async function sendWithBody(body, contentType) {
|
|
216
|
+
const payload = buildGeneralPayload({
|
|
217
|
+
method: 'POST',
|
|
218
|
+
path: '/data',
|
|
219
|
+
headers: { 'content-type': contentType },
|
|
220
|
+
body
|
|
221
|
+
});
|
|
222
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
223
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
224
|
+
await transport.onData(async () => { });
|
|
225
|
+
await transport.send(makeGeneralMessage({ payload }));
|
|
226
|
+
return mockFetch.mock.calls[0][1]?.body;
|
|
227
|
+
}
|
|
228
|
+
test('application/json body is converted to UTF-8 string', async () => {
|
|
229
|
+
const body = Utils.toArray('{"hello":"world"}', 'utf8');
|
|
230
|
+
const payload = buildGeneralPayload({ method: 'POST', path: '/json', headers: { 'content-type': 'application/json' }, body });
|
|
231
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
232
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
233
|
+
await transport.onData(async () => { });
|
|
234
|
+
await transport.send(makeGeneralMessage({ payload }));
|
|
235
|
+
const sentBody = mockFetch.mock.calls[0][1]?.body;
|
|
236
|
+
expect(typeof sentBody).toBe('string');
|
|
237
|
+
expect(sentBody).toContain('hello');
|
|
238
|
+
});
|
|
239
|
+
test('application/x-www-form-urlencoded body is converted to UTF-8 string', async () => {
|
|
240
|
+
const body = Utils.toArray('name=Alice&age=30', 'utf8');
|
|
241
|
+
const payload = buildGeneralPayload({
|
|
242
|
+
method: 'POST',
|
|
243
|
+
path: '/form',
|
|
244
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
245
|
+
body
|
|
246
|
+
});
|
|
247
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
248
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
249
|
+
await transport.onData(async () => { });
|
|
250
|
+
await transport.send(makeGeneralMessage({ payload }));
|
|
251
|
+
const sentBody = mockFetch.mock.calls[0][1]?.body;
|
|
252
|
+
expect(typeof sentBody).toBe('string');
|
|
253
|
+
});
|
|
254
|
+
test('text/plain body is converted to UTF-8 string', async () => {
|
|
255
|
+
const body = Utils.toArray('hello world', 'utf8');
|
|
256
|
+
const payload = buildGeneralPayload({ method: 'POST', path: '/text', headers: { 'content-type': 'text/plain' }, body });
|
|
257
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
258
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
259
|
+
await transport.onData(async () => { });
|
|
260
|
+
await transport.send(makeGeneralMessage({ payload }));
|
|
261
|
+
const sentBody = mockFetch.mock.calls[0][1]?.body;
|
|
262
|
+
expect(typeof sentBody).toBe('string');
|
|
263
|
+
expect(sentBody).toBe('hello world');
|
|
264
|
+
});
|
|
265
|
+
test('binary content-type body is converted to Uint8Array', async () => {
|
|
266
|
+
const body = [0x89, 0x50, 0x4e, 0x47]; // PNG magic bytes
|
|
267
|
+
const payload = buildGeneralPayload({ method: 'POST', path: '/upload', headers: { 'content-type': 'image/png' }, body });
|
|
268
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
269
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
270
|
+
await transport.onData(async () => { });
|
|
271
|
+
await transport.send(makeGeneralMessage({ payload }));
|
|
272
|
+
const sentBody = mockFetch.mock.calls[0][1]?.body;
|
|
273
|
+
expect(sentBody).toBeInstanceOf(Uint8Array);
|
|
274
|
+
});
|
|
275
|
+
test('throws when body is present but content-type header is missing', async () => {
|
|
276
|
+
// No content-type header, but body present
|
|
277
|
+
const body = [1, 2, 3, 4];
|
|
278
|
+
const payload = buildGeneralPayload({ method: 'POST', path: '/no-ct', headers: {}, body });
|
|
279
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
280
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
281
|
+
await transport.onData(async () => { });
|
|
282
|
+
await expect(transport.send(makeGeneralMessage({ payload }))).rejects.toThrow('Content-Type header is required for requests with a body.');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
// ─── send: general message — response header parsing ─────────────────────────
|
|
286
|
+
describe('SimplifiedFetchTransport send — general message response parsing', () => {
|
|
287
|
+
test('invokes onDataCallback with parsed AuthMessage', async () => {
|
|
288
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
289
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
290
|
+
const received = [];
|
|
291
|
+
await transport.onData(async (msg) => { received.push(msg); });
|
|
292
|
+
await transport.send(makeGeneralMessage());
|
|
293
|
+
expect(received).toHaveLength(1);
|
|
294
|
+
expect(received[0].version).toBe('0.1');
|
|
295
|
+
expect(received[0].identityKey).toBe('server-key');
|
|
296
|
+
expect(received[0].messageType).toBe('general');
|
|
297
|
+
});
|
|
298
|
+
test('sets messageType to certificateRequest when x-bsv-auth-message-type is certificateRequest', async () => {
|
|
299
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse('', { 'x-bsv-auth-message-type': 'certificateRequest' }));
|
|
300
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
301
|
+
const received = [];
|
|
302
|
+
await transport.onData(async (msg) => { received.push(msg); });
|
|
303
|
+
await transport.send(makeGeneralMessage());
|
|
304
|
+
expect(received[0].messageType).toBe('certificateRequest');
|
|
305
|
+
});
|
|
306
|
+
test('parses requestedCertificates header into structured object', async () => {
|
|
307
|
+
const certSet = { certifiers: ['certKey1'], types: {} };
|
|
308
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse('', { 'x-bsv-auth-requested-certificates': JSON.stringify(certSet) }));
|
|
309
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
310
|
+
const received = [];
|
|
311
|
+
await transport.onData(async (msg) => { received.push(msg); });
|
|
312
|
+
await transport.send(makeGeneralMessage());
|
|
313
|
+
expect(received[0].requestedCertificates).toEqual(certSet);
|
|
314
|
+
});
|
|
315
|
+
test('throws on malformed requestedCertificates header', async () => {
|
|
316
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse('', { 'x-bsv-auth-requested-certificates': '{invalid json' }));
|
|
317
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
318
|
+
await transport.onData(async () => { });
|
|
319
|
+
await expect(transport.send(makeGeneralMessage())).rejects.toThrow('Failed to parse x-bsv-auth-requested-certificates');
|
|
320
|
+
});
|
|
321
|
+
test('includes x-bsv-auth-request-id in payload when present in response', async () => {
|
|
322
|
+
const requestIdBase64 = Utils.toBase64(new Array(32).fill(0xcc));
|
|
323
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse('', { 'x-bsv-auth-request-id': requestIdBase64 }));
|
|
324
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
325
|
+
const received = [];
|
|
326
|
+
await transport.onData(async (msg) => { received.push(msg); });
|
|
327
|
+
await transport.send(makeGeneralMessage());
|
|
328
|
+
expect(received[0].payload).toBeDefined();
|
|
329
|
+
expect(Array.isArray(received[0].payload)).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
test('includes x-bsv (non-auth) and authorization headers in signed payload', async () => {
|
|
332
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(new Response('', {
|
|
333
|
+
status: 200,
|
|
334
|
+
headers: {
|
|
335
|
+
'x-bsv-auth-version': '0.1',
|
|
336
|
+
'x-bsv-auth-identity-key': 'server-key',
|
|
337
|
+
'x-bsv-auth-signature': 'deadbeef',
|
|
338
|
+
'x-bsv-custom-header': 'custom-value', // should be included
|
|
339
|
+
'authorization': 'Bearer token', // should be included
|
|
340
|
+
'x-bsv-auth-extra': 'excluded' // should NOT be included (x-bsv-auth prefix)
|
|
341
|
+
}
|
|
342
|
+
}));
|
|
343
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
344
|
+
const received = [];
|
|
345
|
+
await transport.onData(async (msg) => { received.push(msg); });
|
|
346
|
+
await transport.send(makeGeneralMessage());
|
|
347
|
+
// The payload should be non-empty (headers were serialized into it)
|
|
348
|
+
const firstReceived = received[0];
|
|
349
|
+
expect(firstReceived).toBeDefined();
|
|
350
|
+
expect(firstReceived?.payload?.length).toBeGreaterThan(0);
|
|
351
|
+
});
|
|
352
|
+
test('network failure on general message wraps error with URL context', async () => {
|
|
353
|
+
const mockFetch = globals_1.jest.fn().mockRejectedValue(new Error('timeout'));
|
|
354
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
355
|
+
await transport.onData(async () => { });
|
|
356
|
+
await expect(transport.send(makeGeneralMessage())).rejects.toThrow('Network error while sending authenticated request to https://api.example.com/api/resource: timeout');
|
|
357
|
+
});
|
|
358
|
+
test('non-Error network failure on general message uses String()', async () => {
|
|
359
|
+
const mockFetch = globals_1.jest.fn().mockRejectedValue(42);
|
|
360
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
361
|
+
await transport.onData(async () => { });
|
|
362
|
+
await expect(transport.send(makeGeneralMessage())).rejects.toThrow('Network error while sending authenticated request to https://api.example.com/api/resource: 42');
|
|
363
|
+
});
|
|
364
|
+
test('appends headers from httpRequest when headers field is not an object', async () => {
|
|
365
|
+
// Build a payload where the deserialized httpRequest has no headers field by
|
|
366
|
+
// providing 0 headers in the encoded payload (the transport then sets headers to {})
|
|
367
|
+
const payload = buildGeneralPayload({ method: 'GET', path: '/resource', headers: {} });
|
|
368
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
369
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
370
|
+
await transport.onData(async () => { });
|
|
371
|
+
await transport.send(makeGeneralMessage({ payload }));
|
|
372
|
+
const requestInit = mockFetch.mock.calls[0][1];
|
|
373
|
+
expect(requestInit.headers).toMatchObject({
|
|
374
|
+
'x-bsv-auth-version': '0.1',
|
|
375
|
+
'x-bsv-auth-identity-key': 'client-key'
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
// ─── deserializeRequestPayload ────────────────────────────────────────────────
|
|
380
|
+
describe('SimplifiedFetchTransport deserializeRequestPayload', () => {
|
|
381
|
+
let transport;
|
|
382
|
+
beforeEach(() => {
|
|
383
|
+
transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://example.com', globals_1.jest.fn());
|
|
384
|
+
});
|
|
385
|
+
test('returns GET and empty urlPostfix when method and path lengths are 0', () => {
|
|
386
|
+
const writer = new Utils.Writer();
|
|
387
|
+
writer.write(new Array(32).fill(0)); // requestId
|
|
388
|
+
writer.writeVarIntNum(0); // method length 0
|
|
389
|
+
writer.writeVarIntNum(0); // path length 0
|
|
390
|
+
writer.writeVarIntNum(0); // search length 0
|
|
391
|
+
writer.writeVarIntNum(0); // 0 headers
|
|
392
|
+
writer.writeVarIntNum(0); // body length 0
|
|
393
|
+
const result = transport.deserializeRequestPayload(writer.toArray());
|
|
394
|
+
expect(result.method).toBe('GET');
|
|
395
|
+
expect(result.urlPostfix).toBe('');
|
|
396
|
+
expect(result.body).toBeUndefined();
|
|
397
|
+
});
|
|
398
|
+
test('combines path and search into urlPostfix', () => {
|
|
399
|
+
const payload = buildGeneralPayload({ path: '/items', search: '?page=2', method: 'GET' });
|
|
400
|
+
const result = transport.deserializeRequestPayload(payload);
|
|
401
|
+
expect(result.urlPostfix).toBe('/items?page=2');
|
|
402
|
+
});
|
|
403
|
+
test('deserializes headers correctly', () => {
|
|
404
|
+
const payload = buildGeneralPayload({
|
|
405
|
+
headers: { 'x-custom': 'value1', 'accept': 'application/json' }
|
|
406
|
+
});
|
|
407
|
+
const result = transport.deserializeRequestPayload(payload);
|
|
408
|
+
expect(result.headers['x-custom']).toBe('value1');
|
|
409
|
+
expect(result.headers['accept']).toBe('application/json');
|
|
410
|
+
});
|
|
411
|
+
test('deserializes body when present', () => {
|
|
412
|
+
const bodyBytes = [10, 20, 30, 40];
|
|
413
|
+
const payload = buildGeneralPayload({
|
|
414
|
+
method: 'POST',
|
|
415
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
416
|
+
body: bodyBytes
|
|
417
|
+
});
|
|
418
|
+
const result = transport.deserializeRequestPayload(payload);
|
|
419
|
+
expect(result.body).toEqual(bodyBytes);
|
|
420
|
+
});
|
|
421
|
+
test('returns undefined body when body length is 0', () => {
|
|
422
|
+
const payload = buildGeneralPayload({ body: null });
|
|
423
|
+
const result = transport.deserializeRequestPayload(payload);
|
|
424
|
+
expect(result.body).toBeUndefined();
|
|
425
|
+
});
|
|
426
|
+
test('returns correct requestId as base64', () => {
|
|
427
|
+
const payload = buildGeneralPayload();
|
|
428
|
+
const result = transport.deserializeRequestPayload(payload);
|
|
429
|
+
// The requestId is the base64 of 32 bytes of 0xab
|
|
430
|
+
expect(typeof result.requestId).toBe('string');
|
|
431
|
+
expect(result.requestId.length).toBeGreaterThan(0);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
// ─── onData callback registration ────────────────────────────────────────────
|
|
435
|
+
describe('SimplifiedFetchTransport onData', () => {
|
|
436
|
+
test('registers callback and errors from callback are swallowed', async () => {
|
|
437
|
+
const mockFetch = globals_1.jest.fn().mockResolvedValue(makeValidGeneralResponse());
|
|
438
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
439
|
+
await transport.onData(async (_msg) => {
|
|
440
|
+
throw new Error('intentional callback error');
|
|
441
|
+
});
|
|
442
|
+
// Should not throw even though callback throws
|
|
443
|
+
await expect(transport.send(makeGeneralMessage())).resolves.toBeUndefined();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
// ─── getBodyPreview (via error path in unauthenticated response) ─────────────
|
|
447
|
+
describe('SimplifiedFetchTransport body preview in error messages', () => {
|
|
448
|
+
test('includes text body preview in unauthenticated error', async () => {
|
|
449
|
+
const mockFetch = globals_1.jest.fn();
|
|
450
|
+
mockFetch.mockResolvedValue(new Response('{"error":"forbidden"}', {
|
|
451
|
+
status: 403,
|
|
452
|
+
statusText: 'Forbidden',
|
|
453
|
+
headers: { 'Content-Type': 'application/json' }
|
|
454
|
+
}));
|
|
455
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
456
|
+
await transport.onData(async () => { });
|
|
457
|
+
let caught;
|
|
458
|
+
try {
|
|
459
|
+
await transport.send(makeGeneralMessage());
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
caught = e;
|
|
463
|
+
}
|
|
464
|
+
expect(caught).toBeDefined();
|
|
465
|
+
expect(caught.message).toContain('forbidden');
|
|
466
|
+
});
|
|
467
|
+
test('body preview is omitted when body is empty', async () => {
|
|
468
|
+
const mockFetch = globals_1.jest.fn();
|
|
469
|
+
mockFetch.mockResolvedValue(new Response('', { status: 503, headers: {} }));
|
|
470
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
471
|
+
await transport.onData(async () => { });
|
|
472
|
+
let caught;
|
|
473
|
+
try {
|
|
474
|
+
await transport.send(makeGeneralMessage());
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
caught = e;
|
|
478
|
+
}
|
|
479
|
+
expect(caught.message).not.toContain('body preview');
|
|
480
|
+
});
|
|
481
|
+
test('binary body produces hex preview', async () => {
|
|
482
|
+
// Binary bytes (low printability ratio)
|
|
483
|
+
const binaryBytes = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0xff, 0xfe]);
|
|
484
|
+
const mockFetch = globals_1.jest.fn();
|
|
485
|
+
mockFetch.mockResolvedValue(new Response(binaryBytes.buffer, {
|
|
486
|
+
status: 401,
|
|
487
|
+
headers: { 'Content-Type': 'application/octet-stream' }
|
|
488
|
+
}));
|
|
489
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
490
|
+
await transport.onData(async () => { });
|
|
491
|
+
let caught;
|
|
492
|
+
try {
|
|
493
|
+
await transport.send(makeGeneralMessage());
|
|
494
|
+
}
|
|
495
|
+
catch (e) {
|
|
496
|
+
caught = e;
|
|
497
|
+
}
|
|
498
|
+
// Should contain 0x prefix from binary hex formatting
|
|
499
|
+
expect(caught.message).toContain('0x');
|
|
500
|
+
});
|
|
501
|
+
test('large body (>1024 bytes) is truncated in preview', async () => {
|
|
502
|
+
const largeBody = 'x'.repeat(2000);
|
|
503
|
+
const mockFetch = globals_1.jest.fn();
|
|
504
|
+
mockFetch.mockResolvedValue(new Response(largeBody, {
|
|
505
|
+
status: 401,
|
|
506
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
507
|
+
}));
|
|
508
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
509
|
+
await transport.onData(async () => { });
|
|
510
|
+
let caught;
|
|
511
|
+
try {
|
|
512
|
+
await transport.send(makeGeneralMessage());
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
caught = e;
|
|
516
|
+
}
|
|
517
|
+
expect(caught.message).toContain('truncated');
|
|
518
|
+
});
|
|
519
|
+
test('preview longer than 512 chars is truncated with ellipsis', async () => {
|
|
520
|
+
// Body that is textual and between 512 and 1024 chars (not truncated due to length, but truncated for preview)
|
|
521
|
+
const mediumBody = 'A'.repeat(600);
|
|
522
|
+
const mockFetch = globals_1.jest.fn();
|
|
523
|
+
mockFetch.mockResolvedValue(new Response(mediumBody, {
|
|
524
|
+
status: 401,
|
|
525
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
526
|
+
}));
|
|
527
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
528
|
+
await transport.onData(async () => { });
|
|
529
|
+
let caught;
|
|
530
|
+
try {
|
|
531
|
+
await transport.send(makeGeneralMessage());
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
caught = e;
|
|
535
|
+
}
|
|
536
|
+
expect(caught.message).toContain('…');
|
|
537
|
+
});
|
|
538
|
+
test('status description includes statusText when non-empty', async () => {
|
|
539
|
+
const mockFetch = globals_1.jest.fn();
|
|
540
|
+
mockFetch.mockResolvedValue(new Response('error text', {
|
|
541
|
+
status: 422,
|
|
542
|
+
statusText: 'Unprocessable Entity',
|
|
543
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
544
|
+
}));
|
|
545
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
546
|
+
await transport.onData(async () => { });
|
|
547
|
+
let caught;
|
|
548
|
+
try {
|
|
549
|
+
await transport.send(makeGeneralMessage());
|
|
550
|
+
}
|
|
551
|
+
catch (e) {
|
|
552
|
+
caught = e;
|
|
553
|
+
}
|
|
554
|
+
expect(caught.message).toContain('422 Unprocessable Entity');
|
|
555
|
+
});
|
|
556
|
+
test('error details contains missingHeaders as empty array when all missing', async () => {
|
|
557
|
+
const mockFetch = globals_1.jest.fn();
|
|
558
|
+
mockFetch.mockResolvedValue(new Response('body', { status: 200 }));
|
|
559
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
560
|
+
await transport.onData(async () => { });
|
|
561
|
+
let caught;
|
|
562
|
+
try {
|
|
563
|
+
await transport.send(makeGeneralMessage());
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
caught = e;
|
|
567
|
+
}
|
|
568
|
+
expect(caught.details.missingHeaders).toEqual([
|
|
569
|
+
'x-bsv-auth-version',
|
|
570
|
+
'x-bsv-auth-identity-key',
|
|
571
|
+
'x-bsv-auth-signature'
|
|
572
|
+
]);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
// ─── createMalformedHeaderError — non-Error cause ────────────────────────────
|
|
576
|
+
describe('SimplifiedFetchTransport malformed header error — non-Error cause', () => {
|
|
577
|
+
test('formats error when JSON.parse throws a non-Error value (string cause)', async () => {
|
|
578
|
+
// Spy on JSON.parse to throw a string
|
|
579
|
+
const originalParse = JSON.parse;
|
|
580
|
+
globals_1.jest.spyOn(JSON, 'parse').mockImplementation((text) => {
|
|
581
|
+
if (text === '{bad}')
|
|
582
|
+
throw 'string error cause';
|
|
583
|
+
return originalParse(text);
|
|
584
|
+
});
|
|
585
|
+
const mockFetch = globals_1.jest.fn();
|
|
586
|
+
mockFetch.mockResolvedValue(new Response('', {
|
|
587
|
+
status: 200,
|
|
588
|
+
headers: {
|
|
589
|
+
'x-bsv-auth-version': '0.1',
|
|
590
|
+
'x-bsv-auth-identity-key': 'server-key',
|
|
591
|
+
'x-bsv-auth-signature': 'aabbcc',
|
|
592
|
+
'x-bsv-auth-requested-certificates': '{bad}'
|
|
593
|
+
}
|
|
594
|
+
}));
|
|
595
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
596
|
+
await transport.onData(async () => { });
|
|
597
|
+
let caught;
|
|
598
|
+
try {
|
|
599
|
+
await transport.send(makeGeneralMessage());
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
caught = e;
|
|
603
|
+
}
|
|
604
|
+
expect(caught).toBeDefined();
|
|
605
|
+
expect(caught.message).toContain('Failed to parse x-bsv-auth-requested-certificates');
|
|
606
|
+
expect(caught.message).toContain('string error cause');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
// ─── isTextualContent heuristics ─────────────────────────────────────────────
|
|
610
|
+
describe('SimplifiedFetchTransport — isTextualContent heuristics (via send response)', () => {
|
|
611
|
+
async function sendAndCatchError(body, contentType) {
|
|
612
|
+
const headers = {};
|
|
613
|
+
if (contentType != null) {
|
|
614
|
+
headers['Content-Type'] = contentType;
|
|
615
|
+
}
|
|
616
|
+
const mockFetch = globals_1.jest.fn();
|
|
617
|
+
mockFetch.mockResolvedValue(new Response(body, { status: 401, headers }));
|
|
618
|
+
const transport = new SimplifiedFetchTransport_js_1.SimplifiedFetchTransport('https://api.example.com', mockFetch);
|
|
619
|
+
await transport.onData(async () => { });
|
|
620
|
+
try {
|
|
621
|
+
await transport.send(makeGeneralMessage());
|
|
622
|
+
throw new Error('expected rejection');
|
|
623
|
+
}
|
|
624
|
+
catch (e) {
|
|
625
|
+
return e;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
test('application/problem+json is treated as text', async () => {
|
|
629
|
+
const err = await sendAndCatchError('{"detail":"bad"}', 'application/problem+json');
|
|
630
|
+
expect(err.message).toContain('bad');
|
|
631
|
+
});
|
|
632
|
+
test('application/xml is treated as text', async () => {
|
|
633
|
+
const err = await sendAndCatchError('<root>value</root>', 'application/xml');
|
|
634
|
+
expect(err.message).toContain('root');
|
|
635
|
+
});
|
|
636
|
+
test('charset= in content type is treated as text', async () => {
|
|
637
|
+
const err = await sendAndCatchError('hello chars', 'application/octet-stream; charset=utf-8');
|
|
638
|
+
expect(err.message).toContain('hello chars');
|
|
639
|
+
});
|
|
640
|
+
test('null content type with mostly printable bytes is treated as text', async () => {
|
|
641
|
+
// >80% printable ASCII
|
|
642
|
+
const printableBody = 'Hello World from the server! This is all ASCII text.';
|
|
643
|
+
const err = await sendAndCatchError(printableBody, null);
|
|
644
|
+
// Should be treated as text, not binary hex
|
|
645
|
+
expect(err.message).not.toMatch(/^.*0x[0-9a-f]+/);
|
|
646
|
+
});
|
|
647
|
+
test('null content type with mostly binary bytes treated as binary', async () => {
|
|
648
|
+
// Lots of control/non-printable bytes
|
|
649
|
+
const binaryBytes = new Uint8Array(50).fill(0x01);
|
|
650
|
+
const err = await sendAndCatchError(binaryBytes.buffer, null);
|
|
651
|
+
expect(err.message).toContain('0x');
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
//# sourceMappingURL=SimplifiedFetchTransport.additional.test.js.map
|