@atproto/lex-client 0.0.16 → 0.0.17
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/CHANGELOG.md +19 -0
- package/dist/client.d.ts +24 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -7
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +6 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +61 -6
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +59 -40
- package/dist/response.js.map +1 -1
- package/dist/types.d.ts +8 -37
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +14 -27
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +15 -6
- package/dist/util.js.map +1 -1
- package/dist/xrpc.d.ts +40 -15
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +3 -1
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/client.ts +81 -31
- package/src/errors.ts +6 -4
- package/src/response.ts +186 -63
- package/src/types.ts +17 -40
- package/src/util.test.ts +11 -11
- package/src/util.ts +33 -36
- package/src/xrpc.test.ts +641 -92
- package/src/xrpc.ts +72 -26
package/src/xrpc.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { assert, describe, expect, it, vi } from 'vitest'
|
|
1
|
+
import { assert, describe, expect, expectTypeOf, it, vi } from 'vitest'
|
|
2
|
+
import { parseCid } from '@atproto/lex-data'
|
|
3
|
+
import { lexToJson } from '@atproto/lex-json'
|
|
2
4
|
import { l } from '@atproto/lex-schema'
|
|
3
5
|
import { FetchHandler } from './agent.js'
|
|
4
6
|
import {
|
|
@@ -14,6 +16,16 @@ import { xrpc, xrpcSafe } from './xrpc.js'
|
|
|
14
16
|
|
|
15
17
|
// Fixtures
|
|
16
18
|
|
|
19
|
+
const rawCid = parseCid(
|
|
20
|
+
'bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4',
|
|
21
|
+
{ flavor: 'raw' },
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const cborCid = parseCid(
|
|
25
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
26
|
+
{ flavor: 'cbor' },
|
|
27
|
+
)
|
|
28
|
+
|
|
17
29
|
const testQuery = l.query(
|
|
18
30
|
'io.example.testQuery',
|
|
19
31
|
l.params({ limit: l.optional(l.integer()) }),
|
|
@@ -48,11 +60,33 @@ const testNoOutputQuery = l.query(
|
|
|
48
60
|
l.payload(),
|
|
49
61
|
)
|
|
50
62
|
|
|
63
|
+
const testQueryWithDefaults = l.query(
|
|
64
|
+
'io.example.testQueryWithDefaults',
|
|
65
|
+
l.params({ foo: l.optional(l.withDefault(l.string(), 'foo-default')) }),
|
|
66
|
+
l.jsonPayload({
|
|
67
|
+
foo: l.string(),
|
|
68
|
+
bar: l.optional(l.withDefault(l.string(), 'bar-default')),
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const testQueryGetBlobRef = l.query(
|
|
73
|
+
'io.example.testQueryGetBlobRef',
|
|
74
|
+
l.params(),
|
|
75
|
+
l.jsonPayload({
|
|
76
|
+
blobRef: l.blob({
|
|
77
|
+
allowLegacy: false,
|
|
78
|
+
accept: ['image/png'],
|
|
79
|
+
maxSize: 10,
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
|
|
51
84
|
describe(xrpc, () => {
|
|
52
85
|
describe('success paths', () => {
|
|
53
86
|
it('returns parsed JSON body for a query', async () => {
|
|
54
|
-
const fetchHandler
|
|
55
|
-
Response.json({ value: 'hello' })
|
|
87
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
88
|
+
return Response.json({ value: 'hello' })
|
|
89
|
+
})
|
|
56
90
|
|
|
57
91
|
const response = await xrpc(fetchHandler, testQuery, {
|
|
58
92
|
params: { limit: 10 },
|
|
@@ -68,8 +102,9 @@ describe(xrpc, () => {
|
|
|
68
102
|
})
|
|
69
103
|
|
|
70
104
|
it('returns parsed JSON body for a procedure', async () => {
|
|
71
|
-
const fetchHandler
|
|
72
|
-
Response.json({ id: 'abc123' })
|
|
105
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
106
|
+
return Response.json({ id: 'abc123' })
|
|
107
|
+
})
|
|
73
108
|
|
|
74
109
|
const response = await xrpc(fetchHandler, testProcedure, {
|
|
75
110
|
body: { text: 'hello world' },
|
|
@@ -83,10 +118,11 @@ describe(xrpc, () => {
|
|
|
83
118
|
|
|
84
119
|
it('returns binary body for a binary query', async () => {
|
|
85
120
|
const bytes = new Uint8Array([1, 2, 3, 4])
|
|
86
|
-
const fetchHandler
|
|
87
|
-
new Response(bytes, {
|
|
121
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
122
|
+
return new Response(bytes, {
|
|
88
123
|
headers: { 'content-type': 'application/octet-stream' },
|
|
89
124
|
})
|
|
125
|
+
})
|
|
90
126
|
|
|
91
127
|
const response = await xrpc(fetchHandler, testBinaryQuery)
|
|
92
128
|
|
|
@@ -99,10 +135,11 @@ describe(xrpc, () => {
|
|
|
99
135
|
|
|
100
136
|
it('returns binary body for a binary procedure', async () => {
|
|
101
137
|
const bytes = new Uint8Array([10, 20, 30])
|
|
102
|
-
const fetchHandler
|
|
103
|
-
new Response(bytes, {
|
|
138
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
139
|
+
return new Response(bytes, {
|
|
104
140
|
headers: { 'content-type': 'application/octet-stream' },
|
|
105
141
|
})
|
|
142
|
+
})
|
|
106
143
|
|
|
107
144
|
const response = await xrpc(fetchHandler, testBinaryProcedure, {
|
|
108
145
|
body: new Uint8Array([99]),
|
|
@@ -115,8 +152,9 @@ describe(xrpc, () => {
|
|
|
115
152
|
})
|
|
116
153
|
|
|
117
154
|
it('returns no body for a no-output query', async () => {
|
|
118
|
-
const fetchHandler
|
|
119
|
-
new Response(null, { status: 200 })
|
|
155
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
156
|
+
return new Response(null, { status: 200 })
|
|
157
|
+
})
|
|
120
158
|
|
|
121
159
|
const response = await xrpc(fetchHandler, testNoOutputQuery)
|
|
122
160
|
|
|
@@ -127,9 +165,9 @@ describe(xrpc, () => {
|
|
|
127
165
|
})
|
|
128
166
|
|
|
129
167
|
it('passes query params as URL search params', async () => {
|
|
130
|
-
const fetchHandler = vi.fn<FetchHandler>(async () =>
|
|
131
|
-
Response.json({ value: 'ok' })
|
|
132
|
-
)
|
|
168
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
169
|
+
return Response.json({ value: 'ok' })
|
|
170
|
+
})
|
|
133
171
|
|
|
134
172
|
await xrpc(fetchHandler, testQuery, { params: { limit: 25 } })
|
|
135
173
|
|
|
@@ -140,9 +178,9 @@ describe(xrpc, () => {
|
|
|
140
178
|
})
|
|
141
179
|
|
|
142
180
|
it('sends POST with JSON body for procedures', async () => {
|
|
143
|
-
const fetchHandler = vi.fn<FetchHandler>(async () =>
|
|
144
|
-
Response.json({ id: 'new-id' })
|
|
145
|
-
)
|
|
181
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
182
|
+
return Response.json({ id: 'new-id' })
|
|
183
|
+
})
|
|
146
184
|
|
|
147
185
|
await xrpc(fetchHandler, testProcedure, {
|
|
148
186
|
body: { text: 'test content' },
|
|
@@ -157,9 +195,9 @@ describe(xrpc, () => {
|
|
|
157
195
|
})
|
|
158
196
|
|
|
159
197
|
it('forwards custom headers', async () => {
|
|
160
|
-
const fetchHandler = vi.fn<FetchHandler>(async () =>
|
|
161
|
-
Response.json({ value: 'ok' })
|
|
162
|
-
)
|
|
198
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
199
|
+
return Response.json({ value: 'ok' })
|
|
200
|
+
})
|
|
163
201
|
|
|
164
202
|
await xrpc(fetchHandler, testQuery, {
|
|
165
203
|
params: { limit: 1 },
|
|
@@ -174,8 +212,9 @@ describe(xrpc, () => {
|
|
|
174
212
|
})
|
|
175
213
|
|
|
176
214
|
it('accepts optional params as omitted', async () => {
|
|
177
|
-
const fetchHandler
|
|
178
|
-
Response.json({ value: 'ok' })
|
|
215
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
216
|
+
return Response.json({ value: 'ok' })
|
|
217
|
+
})
|
|
179
218
|
|
|
180
219
|
const response = await xrpc(fetchHandler, testQuery)
|
|
181
220
|
|
|
@@ -187,9 +226,9 @@ describe(xrpc, () => {
|
|
|
187
226
|
describe('error handling', () => {
|
|
188
227
|
describe('fetch errors', () => {
|
|
189
228
|
it('throws XrpcFetchError when fetchHandler throws', async () => {
|
|
190
|
-
const fetchHandler
|
|
229
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
191
230
|
throw new TypeError('fetch failed')
|
|
192
|
-
}
|
|
231
|
+
})
|
|
193
232
|
|
|
194
233
|
await expect(
|
|
195
234
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -203,9 +242,9 @@ describe(xrpc, () => {
|
|
|
203
242
|
})
|
|
204
243
|
|
|
205
244
|
it('throws XrpcFetchError when fetchHandler rejects', async () => {
|
|
206
|
-
const fetchHandler
|
|
245
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
207
246
|
throw new Error('network timeout')
|
|
208
|
-
}
|
|
247
|
+
})
|
|
209
248
|
|
|
210
249
|
await expect(
|
|
211
250
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -220,11 +259,12 @@ describe(xrpc, () => {
|
|
|
220
259
|
|
|
221
260
|
describe('response errors', () => {
|
|
222
261
|
it('throws XrpcResponseError for 400 with valid error payload', async () => {
|
|
223
|
-
const fetchHandler
|
|
224
|
-
Response.json(
|
|
262
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
263
|
+
return Response.json(
|
|
225
264
|
{ error: 'TestError', message: 'bad request' },
|
|
226
265
|
{ status: 400 },
|
|
227
266
|
)
|
|
267
|
+
})
|
|
228
268
|
|
|
229
269
|
await expect(
|
|
230
270
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -240,11 +280,12 @@ describe(xrpc, () => {
|
|
|
240
280
|
})
|
|
241
281
|
|
|
242
282
|
it('throws XrpcAuthenticationError for 401', async () => {
|
|
243
|
-
const fetchHandler
|
|
244
|
-
Response.json(
|
|
283
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
284
|
+
return Response.json(
|
|
245
285
|
{ error: 'AuthenticationRequired', message: 'Token expired' },
|
|
246
286
|
{ status: 401 },
|
|
247
287
|
)
|
|
288
|
+
})
|
|
248
289
|
|
|
249
290
|
await expect(
|
|
250
291
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -257,11 +298,12 @@ describe(xrpc, () => {
|
|
|
257
298
|
})
|
|
258
299
|
|
|
259
300
|
it('throws XrpcUpstreamError for non-XRPC error response', async () => {
|
|
260
|
-
const fetchHandler
|
|
261
|
-
new Response('Not Found', {
|
|
301
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
302
|
+
return new Response('Not Found', {
|
|
262
303
|
status: 404,
|
|
263
304
|
headers: { 'content-type': 'text/plain' },
|
|
264
305
|
})
|
|
306
|
+
})
|
|
265
307
|
|
|
266
308
|
await expect(
|
|
267
309
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -273,11 +315,12 @@ describe(xrpc, () => {
|
|
|
273
315
|
})
|
|
274
316
|
|
|
275
317
|
it('throws XrpcUpstreamError for 500 without valid error payload', async () => {
|
|
276
|
-
const fetchHandler
|
|
277
|
-
new Response('Internal Server Error', {
|
|
318
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
319
|
+
return new Response('Internal Server Error', {
|
|
278
320
|
status: 500,
|
|
279
321
|
headers: { 'content-type': 'text/html' },
|
|
280
322
|
})
|
|
323
|
+
})
|
|
281
324
|
|
|
282
325
|
await expect(
|
|
283
326
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -289,11 +332,12 @@ describe(xrpc, () => {
|
|
|
289
332
|
})
|
|
290
333
|
|
|
291
334
|
it('Reflects upstream 5xx errors with valid XRPC payload', async () => {
|
|
292
|
-
const fetchHandler
|
|
293
|
-
Response.json(
|
|
335
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
336
|
+
return Response.json(
|
|
294
337
|
{ error: 'ServerError', message: 'Something went wrong' },
|
|
295
338
|
{ status: 502 },
|
|
296
339
|
)
|
|
340
|
+
})
|
|
297
341
|
|
|
298
342
|
await expect(
|
|
299
343
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -312,8 +356,9 @@ describe(xrpc, () => {
|
|
|
312
356
|
describe('invalid response errors', () => {
|
|
313
357
|
it('throws XrpcInvalidResponseError when response body fails validation', async () => {
|
|
314
358
|
// Schema expects { value: string } but we return { value: 123 }
|
|
315
|
-
const fetchHandler
|
|
316
|
-
Response.json({ value: 123 })
|
|
359
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
360
|
+
return Response.json({ value: 123 })
|
|
361
|
+
})
|
|
317
362
|
|
|
318
363
|
await expect(
|
|
319
364
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -326,11 +371,12 @@ describe(xrpc, () => {
|
|
|
326
371
|
})
|
|
327
372
|
|
|
328
373
|
it('throws XrpcUpstreamError when response has wrong content-type', async () => {
|
|
329
|
-
const fetchHandler
|
|
330
|
-
new Response('binary data', {
|
|
374
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
375
|
+
return new Response('binary data', {
|
|
331
376
|
status: 200,
|
|
332
377
|
headers: { 'content-type': 'text/plain' },
|
|
333
378
|
})
|
|
379
|
+
})
|
|
334
380
|
|
|
335
381
|
await expect(
|
|
336
382
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -344,8 +390,9 @@ describe(xrpc, () => {
|
|
|
344
390
|
|
|
345
391
|
describe('content-type header errors', () => {
|
|
346
392
|
it('throws XrpcInternalError when content-type header is set', async () => {
|
|
347
|
-
const fetchHandler
|
|
348
|
-
Response.json({ value: 'ok' })
|
|
393
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
394
|
+
return Response.json({ value: 'ok' })
|
|
395
|
+
})
|
|
349
396
|
|
|
350
397
|
await expect(
|
|
351
398
|
xrpc(fetchHandler, testQuery, {
|
|
@@ -362,17 +409,18 @@ describe(xrpc, () => {
|
|
|
362
409
|
|
|
363
410
|
describe('response payload parsing', () => {
|
|
364
411
|
it('throws XrpcUpstreamError when error response body cannot be parsed', async () => {
|
|
365
|
-
const fetchHandler
|
|
366
|
-
new Response('not valid json', {
|
|
412
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
413
|
+
return new Response('not valid json', {
|
|
367
414
|
status: 400,
|
|
368
415
|
headers: { 'content-type': 'application/json' },
|
|
369
416
|
})
|
|
417
|
+
})
|
|
370
418
|
|
|
371
419
|
await expect(
|
|
372
420
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
373
421
|
).rejects.toSatisfy((err) => {
|
|
374
422
|
assert(err instanceof XrpcUpstreamError)
|
|
375
|
-
expect(err.message).
|
|
423
|
+
expect(err.message).toMatch('Unable to parse response payload')
|
|
376
424
|
assert(err.cause instanceof Error)
|
|
377
425
|
expect(err.cause.message).toContain('Unexpected token')
|
|
378
426
|
return true
|
|
@@ -380,17 +428,18 @@ describe(xrpc, () => {
|
|
|
380
428
|
})
|
|
381
429
|
|
|
382
430
|
it('throws XrpcUpstreamError when success response body cannot be parsed', async () => {
|
|
383
|
-
const fetchHandler
|
|
384
|
-
new Response('not valid json', {
|
|
431
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
432
|
+
return new Response('not valid json', {
|
|
385
433
|
status: 200,
|
|
386
434
|
headers: { 'content-type': 'application/json' },
|
|
387
435
|
})
|
|
436
|
+
})
|
|
388
437
|
|
|
389
438
|
await expect(
|
|
390
439
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
391
440
|
).rejects.toSatisfy((err) => {
|
|
392
441
|
assert(err instanceof XrpcUpstreamError)
|
|
393
|
-
expect(err.message).
|
|
442
|
+
expect(err.message).toMatch('Unable to parse response payload')
|
|
394
443
|
assert(err.cause instanceof Error)
|
|
395
444
|
expect(err.cause.message).toContain('Unexpected token')
|
|
396
445
|
return true
|
|
@@ -398,8 +447,9 @@ describe(xrpc, () => {
|
|
|
398
447
|
})
|
|
399
448
|
|
|
400
449
|
it('throws XrpcUpstreamError when schema expects no payload but got one', async () => {
|
|
401
|
-
const fetchHandler
|
|
402
|
-
Response.json({ unexpected: 'data' })
|
|
450
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
451
|
+
return Response.json({ unexpected: 'data' })
|
|
452
|
+
})
|
|
403
453
|
|
|
404
454
|
await expect(xrpc(fetchHandler, testNoOutputQuery)).rejects.toSatisfy(
|
|
405
455
|
(err) => {
|
|
@@ -411,8 +461,9 @@ describe(xrpc, () => {
|
|
|
411
461
|
})
|
|
412
462
|
|
|
413
463
|
it('throws XrpcUpstreamError when schema expects payload but response is empty', async () => {
|
|
414
|
-
const fetchHandler
|
|
415
|
-
new Response(null, { status: 200 })
|
|
464
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
465
|
+
return new Response(null, { status: 200 })
|
|
466
|
+
})
|
|
416
467
|
|
|
417
468
|
await expect(
|
|
418
469
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -426,11 +477,12 @@ describe(xrpc, () => {
|
|
|
426
477
|
|
|
427
478
|
describe('content-type handling', () => {
|
|
428
479
|
it('parses content-type with charset parameter', async () => {
|
|
429
|
-
const fetchHandler
|
|
430
|
-
new Response(JSON.stringify({ value: 'hello' }), {
|
|
480
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
481
|
+
return new Response(JSON.stringify({ value: 'hello' }), {
|
|
431
482
|
status: 200,
|
|
432
483
|
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
433
484
|
})
|
|
485
|
+
})
|
|
434
486
|
|
|
435
487
|
const response = await xrpc(fetchHandler, testQuery, {
|
|
436
488
|
params: { limit: 10 },
|
|
@@ -481,8 +533,9 @@ describe(xrpc, () => {
|
|
|
481
533
|
|
|
482
534
|
describe('validateRequest', () => {
|
|
483
535
|
it('rejects invalid query params when enabled', async () => {
|
|
484
|
-
const fetchHandler
|
|
485
|
-
Response.json({ value: 'ok' })
|
|
536
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
537
|
+
return Response.json({ value: 'ok' })
|
|
538
|
+
})
|
|
486
539
|
|
|
487
540
|
await expect(
|
|
488
541
|
xrpc(fetchHandler, testQuery, {
|
|
@@ -498,8 +551,9 @@ describe(xrpc, () => {
|
|
|
498
551
|
})
|
|
499
552
|
|
|
500
553
|
it('rejects invalid procedure body when enabled', async () => {
|
|
501
|
-
const fetchHandler
|
|
502
|
-
Response.json({ id: 'abc' })
|
|
554
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
555
|
+
return Response.json({ id: 'abc' })
|
|
556
|
+
})
|
|
503
557
|
|
|
504
558
|
await expect(
|
|
505
559
|
xrpc(fetchHandler, testProcedure, {
|
|
@@ -528,8 +582,9 @@ describe(xrpc, () => {
|
|
|
528
582
|
})
|
|
529
583
|
|
|
530
584
|
it('succeeds with valid body when enabled', async () => {
|
|
531
|
-
const fetchHandler
|
|
532
|
-
Response.json({ id: 'valid' })
|
|
585
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
586
|
+
return Response.json({ id: 'valid' })
|
|
587
|
+
})
|
|
533
588
|
|
|
534
589
|
const response = await xrpc(fetchHandler, testProcedure, {
|
|
535
590
|
body: { text: 'hello' },
|
|
@@ -544,8 +599,9 @@ describe(xrpc, () => {
|
|
|
544
599
|
describe('validateResponse', () => {
|
|
545
600
|
it('rejects invalid response body by default', async () => {
|
|
546
601
|
// Schema expects { value: string } but server returns { value: 123 }
|
|
547
|
-
const fetchHandler
|
|
548
|
-
Response.json({ value: 123 })
|
|
602
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
603
|
+
return Response.json({ value: 123 })
|
|
604
|
+
})
|
|
549
605
|
|
|
550
606
|
await expect(
|
|
551
607
|
xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
|
|
@@ -558,8 +614,9 @@ describe(xrpc, () => {
|
|
|
558
614
|
|
|
559
615
|
it('accepts invalid response body when disabled', async () => {
|
|
560
616
|
// Schema expects { value: string } but server returns { value: 123 }
|
|
561
|
-
const fetchHandler
|
|
562
|
-
Response.json({ value: 123 })
|
|
617
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
618
|
+
return Response.json({ value: 123 })
|
|
619
|
+
})
|
|
563
620
|
|
|
564
621
|
const response = await xrpc(fetchHandler, testQuery, {
|
|
565
622
|
params: { limit: 10 },
|
|
@@ -571,8 +628,9 @@ describe(xrpc, () => {
|
|
|
571
628
|
})
|
|
572
629
|
|
|
573
630
|
it('succeeds with valid response body when enabled', async () => {
|
|
574
|
-
const fetchHandler
|
|
575
|
-
Response.json({ value: 'hello' })
|
|
631
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
632
|
+
return Response.json({ value: 'hello' })
|
|
633
|
+
})
|
|
576
634
|
|
|
577
635
|
const response = await xrpc(fetchHandler, testQuery, {
|
|
578
636
|
params: { limit: 10 },
|
|
@@ -583,13 +641,174 @@ describe(xrpc, () => {
|
|
|
583
641
|
expect(response.body).toEqual({ value: 'hello' })
|
|
584
642
|
})
|
|
585
643
|
})
|
|
644
|
+
|
|
645
|
+
describe('strictResponseProcessing', () => {
|
|
646
|
+
// Helper: returns a JSON response containing a float (invalid lex data)
|
|
647
|
+
const validWithFloatHandler: FetchHandler = async () => {
|
|
648
|
+
return Response.json({ value: 'hello', extra: 1.5 }, { status: 200 })
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Helper: returns a JSON error response containing a float
|
|
652
|
+
const errorWithFloatHandler: FetchHandler = async () => {
|
|
653
|
+
return Response.json(
|
|
654
|
+
{ error: 'TestError', message: 'test-error-description', extra: 1.5 },
|
|
655
|
+
{ status: 400 },
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
it('rejects response with invalid lex data by default (strict parsing)', async () => {
|
|
660
|
+
await expect(
|
|
661
|
+
xrpc(validWithFloatHandler, testQuery, { params: { limit: 10 } }),
|
|
662
|
+
).rejects.toSatisfy((err) => {
|
|
663
|
+
assert(err instanceof XrpcUpstreamError)
|
|
664
|
+
expect(err.message).toMatch('Unable to parse response payload')
|
|
665
|
+
expect(err.cause).toBeInstanceOf(TypeError)
|
|
666
|
+
return true
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('accepts response with invalid lex data when strict processing is disabled', async () => {
|
|
671
|
+
const response = await xrpc(validWithFloatHandler, testQuery, {
|
|
672
|
+
params: { limit: 10 },
|
|
673
|
+
strictResponseProcessing: false,
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
expect(response.success).toBe(true)
|
|
677
|
+
expect(response.body).toEqual({ value: 'hello', extra: 1.5 })
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
it('rejects response with invalid lex data when strict processing is explicitly enabled', async () => {
|
|
681
|
+
await expect(
|
|
682
|
+
xrpc(validWithFloatHandler, testQuery, {
|
|
683
|
+
strictResponseProcessing: true,
|
|
684
|
+
}),
|
|
685
|
+
).rejects.toSatisfy((err) => {
|
|
686
|
+
assert(err instanceof XrpcUpstreamError)
|
|
687
|
+
expect(err.message).toMatch('Unable to parse response payload')
|
|
688
|
+
expect(err.cause).toBeInstanceOf(TypeError)
|
|
689
|
+
return true
|
|
690
|
+
})
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
it('rejects error response with invalid lex data by default', async () => {
|
|
694
|
+
await expect(xrpc(errorWithFloatHandler, testQuery)).rejects.toSatisfy(
|
|
695
|
+
(err) => {
|
|
696
|
+
assert(err instanceof XrpcUpstreamError)
|
|
697
|
+
expect(err.message).toMatch('Unable to parse response payload')
|
|
698
|
+
return true
|
|
699
|
+
},
|
|
700
|
+
)
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
it('parses error response with invalid lex data when strict processing is disabled', async () => {
|
|
704
|
+
await expect(
|
|
705
|
+
xrpc(errorWithFloatHandler, testQuery, {
|
|
706
|
+
params: { limit: 10 },
|
|
707
|
+
strictResponseProcessing: false,
|
|
708
|
+
}),
|
|
709
|
+
).rejects.toSatisfy((err) => {
|
|
710
|
+
// Error response is still an error, but it should be parsed successfully
|
|
711
|
+
assert(err instanceof XrpcResponseError)
|
|
712
|
+
expect(err.status).toBe(400)
|
|
713
|
+
expect(err.payload.body).toEqual({
|
|
714
|
+
error: 'TestError',
|
|
715
|
+
message: 'test-error-description',
|
|
716
|
+
extra: 1.5,
|
|
717
|
+
})
|
|
718
|
+
return true
|
|
719
|
+
})
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
it('with strictResponseProcessing: false and validateResponse: true, schema validation still runs', async () => {
|
|
723
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
724
|
+
return Response.json({ value: 3, unknownValue: 1.5 }, { status: 200 })
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
await expect(
|
|
728
|
+
xrpc(fetchHandler, testQuery, {
|
|
729
|
+
params: { limit: 10 },
|
|
730
|
+
strictResponseProcessing: false,
|
|
731
|
+
validateResponse: true,
|
|
732
|
+
}),
|
|
733
|
+
).rejects.toSatisfy((err) => {
|
|
734
|
+
assert(err instanceof XrpcInvalidResponseError)
|
|
735
|
+
expect(err).toBeInstanceOf(XrpcUpstreamError)
|
|
736
|
+
return true
|
|
737
|
+
})
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it('with strictResponseProcessing: false and validateResponse: false, schema validation is skipped', async () => {
|
|
741
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
742
|
+
return Response.json(
|
|
743
|
+
{
|
|
744
|
+
// Invalid value
|
|
745
|
+
value: 3,
|
|
746
|
+
// Non-strict Lex Data values:
|
|
747
|
+
unknownValue: 1.2,
|
|
748
|
+
foo: { $bytes: 3 },
|
|
749
|
+
},
|
|
750
|
+
{ status: 200 },
|
|
751
|
+
)
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
const response = await xrpc(fetchHandler, testQuery, {
|
|
755
|
+
params: { limit: 10 },
|
|
756
|
+
strictResponseProcessing: false,
|
|
757
|
+
validateResponse: false,
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
expect(response.success).toBe(true)
|
|
761
|
+
expect(response.body).toEqual({
|
|
762
|
+
value: 3,
|
|
763
|
+
unknownValue: 1.2,
|
|
764
|
+
foo: { $bytes: 3 },
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
// @NOTE "validateResponse: false" basically acts as type casting
|
|
768
|
+
expectTypeOf(response.body).toMatchObjectType<{
|
|
769
|
+
value: string
|
|
770
|
+
}>()
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
it('with strictResponseProcessing: true and validateResponse: false, strict parsing still applies', async () => {
|
|
774
|
+
await expect(
|
|
775
|
+
xrpc(validWithFloatHandler, testQuery, {
|
|
776
|
+
params: { limit: 10 },
|
|
777
|
+
strictResponseProcessing: true,
|
|
778
|
+
validateResponse: false,
|
|
779
|
+
}),
|
|
780
|
+
).rejects.toSatisfy((err) => {
|
|
781
|
+
assert(err instanceof XrpcUpstreamError)
|
|
782
|
+
expect(err.message).toMatch('Unable to parse response payload')
|
|
783
|
+
return true
|
|
784
|
+
})
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
it('does not affect binary responses', async () => {
|
|
788
|
+
const bytes = new Uint8Array([1, 2, 3])
|
|
789
|
+
|
|
790
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
791
|
+
return new Response(bytes, {
|
|
792
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
793
|
+
})
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
const response = await xrpc(fetchHandler, testBinaryQuery, {
|
|
797
|
+
strictResponseProcessing: false,
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
expect(response.success).toBe(true)
|
|
801
|
+
expect(response.body).toEqual(bytes)
|
|
802
|
+
})
|
|
803
|
+
})
|
|
586
804
|
})
|
|
587
805
|
|
|
588
806
|
describe(xrpcSafe, () => {
|
|
589
807
|
describe('success paths', () => {
|
|
590
808
|
it('returns successful result for a JSON query', async () => {
|
|
591
|
-
const fetchHandler
|
|
592
|
-
Response.json({ value: 'hello' })
|
|
809
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
810
|
+
return Response.json({ value: 'hello' })
|
|
811
|
+
})
|
|
593
812
|
|
|
594
813
|
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
595
814
|
params: { limit: 5 },
|
|
@@ -603,8 +822,9 @@ describe(xrpcSafe, () => {
|
|
|
603
822
|
})
|
|
604
823
|
|
|
605
824
|
it('returns successful result for a JSON procedure', async () => {
|
|
606
|
-
const fetchHandler
|
|
607
|
-
Response.json({ id: 'new-id' })
|
|
825
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
826
|
+
return Response.json({ id: 'new-id' })
|
|
827
|
+
})
|
|
608
828
|
|
|
609
829
|
const result = await xrpcSafe(fetchHandler, testProcedure, {
|
|
610
830
|
body: { text: 'hello' },
|
|
@@ -616,10 +836,11 @@ describe(xrpcSafe, () => {
|
|
|
616
836
|
|
|
617
837
|
it('returns successful result for a binary query', async () => {
|
|
618
838
|
const bytes = new Uint8Array([5, 6, 7])
|
|
619
|
-
const fetchHandler
|
|
620
|
-
new Response(bytes, {
|
|
839
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
840
|
+
return new Response(bytes, {
|
|
621
841
|
headers: { 'content-type': 'application/octet-stream' },
|
|
622
842
|
})
|
|
843
|
+
})
|
|
623
844
|
|
|
624
845
|
const result = await xrpcSafe(fetchHandler, testBinaryQuery)
|
|
625
846
|
|
|
@@ -631,10 +852,11 @@ describe(xrpcSafe, () => {
|
|
|
631
852
|
|
|
632
853
|
it('returns successful result for a binary procedure', async () => {
|
|
633
854
|
const bytes = new Uint8Array([42])
|
|
634
|
-
const fetchHandler
|
|
635
|
-
new Response(bytes, {
|
|
855
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
856
|
+
return new Response(bytes, {
|
|
636
857
|
headers: { 'content-type': 'application/octet-stream' },
|
|
637
858
|
})
|
|
859
|
+
})
|
|
638
860
|
|
|
639
861
|
const result = await xrpcSafe(fetchHandler, testBinaryProcedure, {
|
|
640
862
|
body: new Uint8Array([1, 2]),
|
|
@@ -646,8 +868,9 @@ describe(xrpcSafe, () => {
|
|
|
646
868
|
})
|
|
647
869
|
|
|
648
870
|
it('returns successful result for a no-output query', async () => {
|
|
649
|
-
const fetchHandler
|
|
650
|
-
new Response(null, { status: 200 })
|
|
871
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
872
|
+
return new Response(null, { status: 200 })
|
|
873
|
+
})
|
|
651
874
|
|
|
652
875
|
const result = await xrpcSafe(fetchHandler, testNoOutputQuery)
|
|
653
876
|
|
|
@@ -660,9 +883,9 @@ describe(xrpcSafe, () => {
|
|
|
660
883
|
describe('error handling', () => {
|
|
661
884
|
describe('fetch errors', () => {
|
|
662
885
|
it('returns XrpcFetchError when fetchHandler throws', async () => {
|
|
663
|
-
const fetchHandler
|
|
886
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
664
887
|
throw new TypeError('fetch failed')
|
|
665
|
-
}
|
|
888
|
+
})
|
|
666
889
|
|
|
667
890
|
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
668
891
|
params: { limit: 10 },
|
|
@@ -674,9 +897,9 @@ describe(xrpcSafe, () => {
|
|
|
674
897
|
})
|
|
675
898
|
|
|
676
899
|
it('returns XrpcFetchError when fetchHandler rejects', async () => {
|
|
677
|
-
const fetchHandler
|
|
900
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
678
901
|
throw new Error('network timeout')
|
|
679
|
-
}
|
|
902
|
+
})
|
|
680
903
|
|
|
681
904
|
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
682
905
|
params: { limit: 10 },
|
|
@@ -806,8 +1029,9 @@ describe(xrpcSafe, () => {
|
|
|
806
1029
|
|
|
807
1030
|
describe('validateRequest', () => {
|
|
808
1031
|
it('returns XrpcInternalError for invalid query params when enabled', async () => {
|
|
809
|
-
const fetchHandler
|
|
810
|
-
Response.json({ value: 'ok' })
|
|
1032
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1033
|
+
return Response.json({ value: 'ok' })
|
|
1034
|
+
})
|
|
811
1035
|
|
|
812
1036
|
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
813
1037
|
// @ts-expect-error intentionally passing invalid params
|
|
@@ -821,8 +1045,9 @@ describe(xrpcSafe, () => {
|
|
|
821
1045
|
})
|
|
822
1046
|
|
|
823
1047
|
it('returns XrpcInternalError for invalid body when enabled', async () => {
|
|
824
|
-
const fetchHandler
|
|
825
|
-
Response.json({ id: 'abc' })
|
|
1048
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1049
|
+
return Response.json({ id: 'abc' })
|
|
1050
|
+
})
|
|
826
1051
|
|
|
827
1052
|
const result = await xrpcSafe(fetchHandler, testProcedure, {
|
|
828
1053
|
// @ts-expect-error intentionally passing invalid body
|
|
@@ -848,8 +1073,9 @@ describe(xrpcSafe, () => {
|
|
|
848
1073
|
})
|
|
849
1074
|
|
|
850
1075
|
it('succeeds with valid body when enabled', async () => {
|
|
851
|
-
const fetchHandler
|
|
852
|
-
Response.json({ id: 'valid' })
|
|
1076
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1077
|
+
return Response.json({ id: 'valid' })
|
|
1078
|
+
})
|
|
853
1079
|
|
|
854
1080
|
const result = await xrpcSafe(fetchHandler, testProcedure, {
|
|
855
1081
|
body: { text: 'hello' },
|
|
@@ -863,8 +1089,9 @@ describe(xrpcSafe, () => {
|
|
|
863
1089
|
|
|
864
1090
|
describe('validateResponse', () => {
|
|
865
1091
|
it('returns XrpcInvalidResponseError for invalid body by default', async () => {
|
|
866
|
-
const fetchHandler
|
|
867
|
-
Response.json({ value: 123 })
|
|
1092
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1093
|
+
return Response.json({ value: 123 })
|
|
1094
|
+
})
|
|
868
1095
|
|
|
869
1096
|
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
870
1097
|
params: { limit: 10 },
|
|
@@ -876,8 +1103,9 @@ describe(xrpcSafe, () => {
|
|
|
876
1103
|
})
|
|
877
1104
|
|
|
878
1105
|
it('accepts invalid response body when disabled', async () => {
|
|
879
|
-
const fetchHandler
|
|
880
|
-
Response.json({ value: 123 })
|
|
1106
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1107
|
+
return Response.json({ value: 123 })
|
|
1108
|
+
})
|
|
881
1109
|
|
|
882
1110
|
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
883
1111
|
params: { limit: 10 },
|
|
@@ -889,8 +1117,9 @@ describe(xrpcSafe, () => {
|
|
|
889
1117
|
})
|
|
890
1118
|
|
|
891
1119
|
it('succeeds with valid response body when enabled', async () => {
|
|
892
|
-
const fetchHandler
|
|
893
|
-
Response.json({ value: 'hello' })
|
|
1120
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1121
|
+
return Response.json({ value: 'hello' })
|
|
1122
|
+
})
|
|
894
1123
|
|
|
895
1124
|
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
896
1125
|
params: { limit: 10 },
|
|
@@ -900,5 +1129,325 @@ describe(xrpcSafe, () => {
|
|
|
900
1129
|
assert(result.success)
|
|
901
1130
|
expect(result.body).toEqual({ value: 'hello' })
|
|
902
1131
|
})
|
|
1132
|
+
|
|
1133
|
+
it('applies defaults', async () => {
|
|
1134
|
+
const fetchHandler = vi.fn<FetchHandler>(async (path) => {
|
|
1135
|
+
const url = new URL(path, 'http://localhost')
|
|
1136
|
+
const foo = url.searchParams.get('foo')
|
|
1137
|
+
|
|
1138
|
+
// default applied while building the request
|
|
1139
|
+
expect(foo).toBe('foo-default')
|
|
1140
|
+
|
|
1141
|
+
return Response.json({ foo: 'foo-value' })
|
|
1142
|
+
})
|
|
1143
|
+
|
|
1144
|
+
const result = await xrpcSafe(fetchHandler, testQueryWithDefaults, {
|
|
1145
|
+
params: {},
|
|
1146
|
+
validateResponse: true,
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
expect(fetchHandler).toHaveBeenCalled()
|
|
1150
|
+
assert(result.success)
|
|
1151
|
+
expect(result.body).toEqual({
|
|
1152
|
+
bar: 'bar-default', // default applied while parsing the response
|
|
1153
|
+
foo: 'foo-value',
|
|
1154
|
+
})
|
|
1155
|
+
})
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
describe('blob constraints', () => {
|
|
1159
|
+
it('rejects invalid blob refs', async () => {
|
|
1160
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1161
|
+
// missing properties jere
|
|
1162
|
+
return Response.json({ blobRef: { $type: 'blob' } })
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)
|
|
1166
|
+
assert(!result.success)
|
|
1167
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1168
|
+
expect(result.message).toMatch('Unable to parse response payload')
|
|
1169
|
+
assert(result.cause instanceof TypeError)
|
|
1170
|
+
expect(result.cause.message).toBe('Invalid blob object')
|
|
1171
|
+
})
|
|
1172
|
+
|
|
1173
|
+
it('rejects blob-refs with cbor data CIDs', async () => {
|
|
1174
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1175
|
+
return Response.json(
|
|
1176
|
+
lexToJson({
|
|
1177
|
+
blobRef: {
|
|
1178
|
+
$type: 'blob',
|
|
1179
|
+
// ref should be a "raw" CID to be strictly valid
|
|
1180
|
+
ref: cborCid,
|
|
1181
|
+
mimeType: 'image/png',
|
|
1182
|
+
size: 1,
|
|
1183
|
+
},
|
|
1184
|
+
}),
|
|
1185
|
+
)
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)
|
|
1189
|
+
assert(!result.success)
|
|
1190
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1191
|
+
expect(result.message).toMatch('Unable to parse response payload')
|
|
1192
|
+
assert(result.cause instanceof TypeError)
|
|
1193
|
+
expect(result.cause.message).toBe('Invalid blob object')
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
it('enforces blob mime-type constraint by default', async () => {
|
|
1197
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1198
|
+
return Response.json(
|
|
1199
|
+
lexToJson({
|
|
1200
|
+
blobRef: {
|
|
1201
|
+
$type: 'blob',
|
|
1202
|
+
ref: rawCid,
|
|
1203
|
+
mimeType: 'invalid/mime',
|
|
1204
|
+
size: 10,
|
|
1205
|
+
},
|
|
1206
|
+
}),
|
|
1207
|
+
)
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)
|
|
1211
|
+
assert(!result.success)
|
|
1212
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1213
|
+
expect(result.message).toBe(
|
|
1214
|
+
'Invalid response: Expected "image/png" (got "invalid/mime") at $.blobRef.mimeType',
|
|
1215
|
+
)
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
it('enforces blob size constraint by default', async () => {
|
|
1219
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1220
|
+
return Response.json(
|
|
1221
|
+
lexToJson({
|
|
1222
|
+
blobRef: {
|
|
1223
|
+
$type: 'blob',
|
|
1224
|
+
ref: rawCid,
|
|
1225
|
+
mimeType: 'image/png',
|
|
1226
|
+
size: 100,
|
|
1227
|
+
},
|
|
1228
|
+
}),
|
|
1229
|
+
)
|
|
1230
|
+
})
|
|
1231
|
+
|
|
1232
|
+
const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)
|
|
1233
|
+
assert(!result.success)
|
|
1234
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1235
|
+
expect(result.message).toBe(
|
|
1236
|
+
'Invalid response: blob too big (maximum 10, got 100) at $.blobRef',
|
|
1237
|
+
)
|
|
1238
|
+
})
|
|
1239
|
+
|
|
1240
|
+
it('ignores blob constraints in non-strict mode', async () => {
|
|
1241
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1242
|
+
return Response.json(
|
|
1243
|
+
lexToJson({
|
|
1244
|
+
blobRef: {
|
|
1245
|
+
$type: 'blob',
|
|
1246
|
+
ref: rawCid,
|
|
1247
|
+
mimeType: 'invalid/mime',
|
|
1248
|
+
size: 100,
|
|
1249
|
+
},
|
|
1250
|
+
}),
|
|
1251
|
+
)
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {
|
|
1255
|
+
strictResponseProcessing: false,
|
|
1256
|
+
})
|
|
1257
|
+
|
|
1258
|
+
assert(result.success)
|
|
1259
|
+
expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()
|
|
1260
|
+
expect(result.body).toEqual({
|
|
1261
|
+
blobRef: {
|
|
1262
|
+
$type: 'blob',
|
|
1263
|
+
ref: rawCid,
|
|
1264
|
+
mimeType: 'invalid/mime',
|
|
1265
|
+
size: 100,
|
|
1266
|
+
},
|
|
1267
|
+
})
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
it('transforms legacy blobs in non-strict mode', async () => {
|
|
1271
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1272
|
+
return Response.json({
|
|
1273
|
+
blobRef: {
|
|
1274
|
+
cid: rawCid.toString(),
|
|
1275
|
+
mimeType: 'invalid/mime',
|
|
1276
|
+
},
|
|
1277
|
+
})
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {
|
|
1281
|
+
strictResponseProcessing: false,
|
|
1282
|
+
})
|
|
1283
|
+
|
|
1284
|
+
assert(result.success)
|
|
1285
|
+
expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()
|
|
1286
|
+
expect(result.body).toEqual({
|
|
1287
|
+
blobRef: {
|
|
1288
|
+
$type: 'blob',
|
|
1289
|
+
ref: rawCid,
|
|
1290
|
+
mimeType: 'invalid/mime',
|
|
1291
|
+
size: -1,
|
|
1292
|
+
},
|
|
1293
|
+
})
|
|
1294
|
+
})
|
|
1295
|
+
|
|
1296
|
+
it('allows blob-refs with negative size in non-strict mode', async () => {
|
|
1297
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1298
|
+
return Response.json({
|
|
1299
|
+
blobRef: {
|
|
1300
|
+
$type: 'blob',
|
|
1301
|
+
ref: { $link: rawCid.toString() },
|
|
1302
|
+
mimeType: 'invalid/mime',
|
|
1303
|
+
size: -1,
|
|
1304
|
+
},
|
|
1305
|
+
})
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {
|
|
1309
|
+
strictResponseProcessing: false,
|
|
1310
|
+
})
|
|
1311
|
+
|
|
1312
|
+
assert(result.success)
|
|
1313
|
+
expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()
|
|
1314
|
+
expect(result.body).toEqual({
|
|
1315
|
+
blobRef: {
|
|
1316
|
+
$type: 'blob',
|
|
1317
|
+
ref: rawCid,
|
|
1318
|
+
mimeType: 'invalid/mime',
|
|
1319
|
+
size: -1,
|
|
1320
|
+
},
|
|
1321
|
+
})
|
|
1322
|
+
})
|
|
1323
|
+
})
|
|
1324
|
+
|
|
1325
|
+
describe('strictResponseProcessing', () => {
|
|
1326
|
+
const jsonResponseWithFloat: FetchHandler = async () => {
|
|
1327
|
+
return Response.json({ value: 'hello', extra: 1.5 }, { status: 200 })
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const jsonErrorResponseWithFloat: FetchHandler = async () => {
|
|
1331
|
+
return Response.json(
|
|
1332
|
+
{ error: 'TestError', message: 'test-error-description', extra: 1.5 },
|
|
1333
|
+
{ status: 400 },
|
|
1334
|
+
)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
it('returns error for invalid lex data by default (strict parsing)', async () => {
|
|
1338
|
+
const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)
|
|
1339
|
+
|
|
1340
|
+
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
1341
|
+
params: { limit: 10 },
|
|
1342
|
+
})
|
|
1343
|
+
|
|
1344
|
+
assert(!result.success)
|
|
1345
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1346
|
+
expect(result.message).toMatch('Unable to parse response payload')
|
|
1347
|
+
assert(result.cause instanceof TypeError)
|
|
1348
|
+
expect(result.cause.message).toBe('Invalid non-integer number: 1.5')
|
|
1349
|
+
})
|
|
1350
|
+
|
|
1351
|
+
it('accepts response with invalid lex data when strict processing is disabled', async () => {
|
|
1352
|
+
const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)
|
|
1353
|
+
|
|
1354
|
+
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
1355
|
+
params: { limit: 10 },
|
|
1356
|
+
strictResponseProcessing: false,
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
assert(result.success)
|
|
1360
|
+
expect(result.body).toEqual({ value: 'hello', extra: 1.5 })
|
|
1361
|
+
})
|
|
1362
|
+
|
|
1363
|
+
it('returns error for invalid lex data when strict processing is explicitly enabled', async () => {
|
|
1364
|
+
const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)
|
|
1365
|
+
|
|
1366
|
+
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
1367
|
+
params: { limit: 10 },
|
|
1368
|
+
strictResponseProcessing: true,
|
|
1369
|
+
})
|
|
1370
|
+
|
|
1371
|
+
assert(!result.success)
|
|
1372
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1373
|
+
expect(result.message).toMatch('Unable to parse response payload')
|
|
1374
|
+
assert(result.cause instanceof TypeError)
|
|
1375
|
+
expect(result.cause.message).toBe('Invalid non-integer number: 1.5')
|
|
1376
|
+
})
|
|
1377
|
+
|
|
1378
|
+
it('returns error for error response with invalid lex data by default', async () => {
|
|
1379
|
+
const fetchHandler = vi.fn<FetchHandler>(jsonErrorResponseWithFloat)
|
|
1380
|
+
|
|
1381
|
+
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
1382
|
+
params: { limit: 10 },
|
|
1383
|
+
})
|
|
1384
|
+
|
|
1385
|
+
assert(!result.success)
|
|
1386
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1387
|
+
expect(result.message).toMatch('Unable to parse response payload')
|
|
1388
|
+
assert(result.cause instanceof TypeError)
|
|
1389
|
+
expect(result.cause.message).toBe('Invalid non-integer number: 1.5')
|
|
1390
|
+
})
|
|
1391
|
+
|
|
1392
|
+
it('parses error response with invalid lex data when strict processing is disabled', async () => {
|
|
1393
|
+
const fetchHandler = vi.fn<FetchHandler>(jsonErrorResponseWithFloat)
|
|
1394
|
+
|
|
1395
|
+
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
1396
|
+
params: { limit: 10 },
|
|
1397
|
+
strictResponseProcessing: false,
|
|
1398
|
+
})
|
|
1399
|
+
|
|
1400
|
+
assert(!result.success)
|
|
1401
|
+
assert(result instanceof XrpcResponseError)
|
|
1402
|
+
expect(result.status).toBe(400)
|
|
1403
|
+
expect(result.message).toBe('test-error-description')
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
it('with strictResponseProcessing: false and validateResponse: true, schema validation still runs', async () => {
|
|
1407
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1408
|
+
return Response.json({ value: 1.5 }, { status: 200 })
|
|
1409
|
+
})
|
|
1410
|
+
|
|
1411
|
+
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
1412
|
+
params: { limit: 10 },
|
|
1413
|
+
strictResponseProcessing: false,
|
|
1414
|
+
validateResponse: true,
|
|
1415
|
+
})
|
|
1416
|
+
|
|
1417
|
+
assert(!result.success)
|
|
1418
|
+
expect(result).toBeInstanceOf(XrpcInvalidResponseError)
|
|
1419
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1420
|
+
})
|
|
1421
|
+
|
|
1422
|
+
it('with strictResponseProcessing: false and validateResponse: false, schema validation is skipped', async () => {
|
|
1423
|
+
const fetchHandler = vi.fn<FetchHandler>(async () => {
|
|
1424
|
+
return Response.json({ value: 1.5 }, { status: 200 })
|
|
1425
|
+
})
|
|
1426
|
+
|
|
1427
|
+
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
1428
|
+
params: { limit: 10 },
|
|
1429
|
+
strictResponseProcessing: false,
|
|
1430
|
+
validateResponse: false,
|
|
1431
|
+
})
|
|
1432
|
+
|
|
1433
|
+
assert(result.success)
|
|
1434
|
+
expect(result.body).toEqual({ value: 1.5 })
|
|
1435
|
+
})
|
|
1436
|
+
|
|
1437
|
+
it('with strictResponseProcessing: true and validateResponse: false, strict parsing still applies', async () => {
|
|
1438
|
+
const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)
|
|
1439
|
+
|
|
1440
|
+
const result = await xrpcSafe(fetchHandler, testQuery, {
|
|
1441
|
+
params: { limit: 10 },
|
|
1442
|
+
strictResponseProcessing: true,
|
|
1443
|
+
validateResponse: false,
|
|
1444
|
+
})
|
|
1445
|
+
|
|
1446
|
+
assert(!result.success)
|
|
1447
|
+
expect(result).toBeInstanceOf(XrpcUpstreamError)
|
|
1448
|
+
expect(result.message).toMatch('Unable to parse response payload')
|
|
1449
|
+
assert(result.cause instanceof TypeError)
|
|
1450
|
+
expect(result.cause.message).toBe('Invalid non-integer number: 1.5')
|
|
1451
|
+
})
|
|
903
1452
|
})
|
|
904
1453
|
})
|