@atproto/lex-client 0.0.16 → 0.0.18

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/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 {
@@ -7,13 +9,23 @@ import {
7
9
  XrpcInternalError,
8
10
  XrpcInvalidResponseError,
9
11
  XrpcResponseError,
10
- XrpcUpstreamError,
12
+ XrpcResponseValidationError,
11
13
  } from './errors.js'
12
14
  import { XrpcResponse } from './response.js'
13
15
  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: FetchHandler = async () =>
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: FetchHandler = async () =>
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: FetchHandler = async () =>
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: FetchHandler = async () =>
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: FetchHandler = async () =>
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,22 +212,34 @@ describe(xrpc, () => {
174
212
  })
175
213
 
176
214
  it('accepts optional params as omitted', async () => {
177
- const fetchHandler: FetchHandler = async () =>
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
 
182
221
  expect(response.success).toBe(true)
183
222
  expect(response.body).toEqual({ value: 'ok' })
184
223
  })
224
+
225
+ it('ignores output for no-output queries', async () => {
226
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
227
+ return Response.json({ unexpected: 'data' })
228
+ })
229
+
230
+ const response = await xrpc(fetchHandler, testNoOutputQuery)
231
+
232
+ expect(response.success).toBe(true)
233
+ expect(response.body).toStrictEqual({ unexpected: 'data' })
234
+ })
185
235
  })
186
236
 
187
237
  describe('error handling', () => {
188
238
  describe('fetch errors', () => {
189
239
  it('throws XrpcFetchError when fetchHandler throws', async () => {
190
- const fetchHandler: FetchHandler = async () => {
240
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
191
241
  throw new TypeError('fetch failed')
192
- }
242
+ })
193
243
 
194
244
  await expect(
195
245
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
@@ -203,9 +253,9 @@ describe(xrpc, () => {
203
253
  })
204
254
 
205
255
  it('throws XrpcFetchError when fetchHandler rejects', async () => {
206
- const fetchHandler: FetchHandler = async () => {
256
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
207
257
  throw new Error('network timeout')
208
- }
258
+ })
209
259
 
210
260
  await expect(
211
261
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
@@ -220,18 +270,19 @@ describe(xrpc, () => {
220
270
 
221
271
  describe('response errors', () => {
222
272
  it('throws XrpcResponseError for 400 with valid error payload', async () => {
223
- const fetchHandler: FetchHandler = async () =>
224
- Response.json(
273
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
274
+ return Response.json(
225
275
  { error: 'TestError', message: 'bad request' },
226
276
  { status: 400 },
227
277
  )
278
+ })
228
279
 
229
280
  await expect(
230
281
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
231
282
  ).rejects.toSatisfy((err) => {
232
283
  assert(err instanceof XrpcResponseError)
233
284
  expect(err.status).toBe(400)
234
- expect(err.body).toEqual({
285
+ expect(err.toJSON()).toEqual({
235
286
  error: 'TestError',
236
287
  message: 'bad request',
237
288
  })
@@ -240,11 +291,12 @@ describe(xrpc, () => {
240
291
  })
241
292
 
242
293
  it('throws XrpcAuthenticationError for 401', async () => {
243
- const fetchHandler: FetchHandler = async () =>
244
- Response.json(
294
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
295
+ return Response.json(
245
296
  { error: 'AuthenticationRequired', message: 'Token expired' },
246
297
  { status: 401 },
247
298
  )
299
+ })
248
300
 
249
301
  await expect(
250
302
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
@@ -256,51 +308,54 @@ describe(xrpc, () => {
256
308
  })
257
309
  })
258
310
 
259
- it('throws XrpcUpstreamError for non-XRPC error response', async () => {
260
- const fetchHandler: FetchHandler = async () =>
261
- new Response('Not Found', {
311
+ it('throws XrpcResponseError for non-XRPC error response', async () => {
312
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
313
+ return new Response('Not Found', {
262
314
  status: 404,
263
315
  headers: { 'content-type': 'text/plain' },
264
316
  })
317
+ })
265
318
 
266
319
  await expect(
267
320
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
268
321
  ).rejects.toSatisfy((err) => {
269
- assert(err instanceof XrpcUpstreamError)
270
- expect(err.message).toBe('Invalid response payload')
322
+ assert(err instanceof XrpcResponseError)
323
+ expect(err.message).toBe('Upstream server responded with a 404 error')
271
324
  return true
272
325
  })
273
326
  })
274
327
 
275
- it('throws XrpcUpstreamError for 500 without valid error payload', async () => {
276
- const fetchHandler: FetchHandler = async () =>
277
- new Response('Internal Server Error', {
328
+ it('throws XrpcResponseError for 500 without valid error payload', async () => {
329
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
330
+ return new Response('Internal Server Error', {
278
331
  status: 500,
279
332
  headers: { 'content-type': 'text/html' },
280
333
  })
334
+ })
281
335
 
282
336
  await expect(
283
337
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
284
338
  ).rejects.toSatisfy((err) => {
285
- assert(err instanceof XrpcUpstreamError)
286
- expect(err.message).toBe('Upstream server encountered an error')
339
+ assert(err instanceof XrpcResponseError)
340
+ expect(err.message).toBe('Upstream server responded with a 500 error')
287
341
  return true
288
342
  })
289
343
  })
290
344
 
291
345
  it('Reflects upstream 5xx errors with valid XRPC payload', async () => {
292
- const fetchHandler: FetchHandler = async () =>
293
- Response.json(
346
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
347
+ return Response.json(
294
348
  { error: 'ServerError', message: 'Something went wrong' },
295
349
  { status: 502 },
296
350
  )
351
+ })
297
352
 
298
353
  await expect(
299
354
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
300
355
  ).rejects.toSatisfy((err) => {
301
356
  assert(err instanceof XrpcResponseError)
302
357
  expect(err.status).toBe(502)
303
- expect(err.body).toEqual({
358
+ expect(err.toJSON()).toEqual({
304
359
  error: 'ServerError',
305
360
  message: 'Something went wrong',
306
361
  })
@@ -310,32 +365,34 @@ describe(xrpc, () => {
310
365
  })
311
366
 
312
367
  describe('invalid response errors', () => {
313
- it('throws XrpcInvalidResponseError when response body fails validation', async () => {
368
+ it('throws XrpcResponseValidationError when response body fails validation', async () => {
314
369
  // Schema expects { value: string } but we return { value: 123 }
315
- const fetchHandler: FetchHandler = async () =>
316
- Response.json({ value: 123 })
370
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
371
+ return Response.json({ value: 123 })
372
+ })
317
373
 
318
374
  await expect(
319
375
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
320
376
  ).rejects.toSatisfy((err) => {
321
- assert(err instanceof XrpcInvalidResponseError)
322
- expect(err).toBeInstanceOf(XrpcUpstreamError)
377
+ assert(err instanceof XrpcResponseValidationError)
378
+ expect(err).toBeInstanceOf(XrpcInvalidResponseError)
323
379
  expect(err.cause).toBeInstanceOf(Error)
324
380
  return true
325
381
  })
326
382
  })
327
383
 
328
- it('throws XrpcUpstreamError when response has wrong content-type', async () => {
329
- const fetchHandler: FetchHandler = async () =>
330
- new Response('binary data', {
384
+ it('throws XrpcInvalidResponseError when response has wrong content-type', async () => {
385
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
386
+ return new Response('binary data', {
331
387
  status: 200,
332
388
  headers: { 'content-type': 'text/plain' },
333
389
  })
390
+ })
334
391
 
335
392
  await expect(
336
393
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
337
394
  ).rejects.toSatisfy((err) => {
338
- assert(err instanceof XrpcUpstreamError)
395
+ assert(err instanceof XrpcInvalidResponseError)
339
396
  expect(err.message).toContain('application/json')
340
397
  return true
341
398
  })
@@ -344,8 +401,9 @@ describe(xrpc, () => {
344
401
 
345
402
  describe('content-type header errors', () => {
346
403
  it('throws XrpcInternalError when content-type header is set', async () => {
347
- const fetchHandler: FetchHandler = async () =>
348
- Response.json({ value: 'ok' })
404
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
405
+ return Response.json({ value: 'ok' })
406
+ })
349
407
 
350
408
  await expect(
351
409
  xrpc(fetchHandler, testQuery, {
@@ -361,64 +419,54 @@ describe(xrpc, () => {
361
419
  })
362
420
 
363
421
  describe('response payload parsing', () => {
364
- it('throws XrpcUpstreamError when error response body cannot be parsed', async () => {
365
- const fetchHandler: FetchHandler = async () =>
366
- new Response('not valid json', {
422
+ it('throws XrpcInvalidResponseError when error response body cannot be parsed', async () => {
423
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
424
+ return new Response('not valid json', {
367
425
  status: 400,
368
426
  headers: { 'content-type': 'application/json' },
369
427
  })
428
+ })
370
429
 
371
430
  await expect(
372
431
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
373
432
  ).rejects.toSatisfy((err) => {
374
- assert(err instanceof XrpcUpstreamError)
375
- expect(err.message).toBe('Unable to parse response payload')
433
+ assert(err instanceof XrpcInvalidResponseError)
434
+ expect(err.message).toMatch('Unable to parse response payload')
376
435
  assert(err.cause instanceof Error)
377
436
  expect(err.cause.message).toContain('Unexpected token')
378
437
  return true
379
438
  })
380
439
  })
381
440
 
382
- it('throws XrpcUpstreamError when success response body cannot be parsed', async () => {
383
- const fetchHandler: FetchHandler = async () =>
384
- new Response('not valid json', {
441
+ it('throws XrpcInvalidResponseError when success response body cannot be parsed', async () => {
442
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
443
+ return new Response('not valid json', {
385
444
  status: 200,
386
445
  headers: { 'content-type': 'application/json' },
387
446
  })
447
+ })
388
448
 
389
449
  await expect(
390
450
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
391
451
  ).rejects.toSatisfy((err) => {
392
- assert(err instanceof XrpcUpstreamError)
393
- expect(err.message).toBe('Unable to parse response payload')
452
+ assert(err instanceof XrpcInvalidResponseError)
453
+ expect(err.message).toMatch('Unable to parse response payload')
394
454
  assert(err.cause instanceof Error)
395
455
  expect(err.cause.message).toContain('Unexpected token')
396
456
  return true
397
457
  })
398
458
  })
399
459
 
400
- it('throws XrpcUpstreamError when schema expects no payload but got one', async () => {
401
- const fetchHandler: FetchHandler = async () =>
402
- Response.json({ unexpected: 'data' })
403
-
404
- await expect(xrpc(fetchHandler, testNoOutputQuery)).rejects.toSatisfy(
405
- (err) => {
406
- assert(err instanceof XrpcUpstreamError)
407
- expect(err.message).toContain('no body')
408
- return true
409
- },
410
- )
411
- })
412
-
413
- it('throws XrpcUpstreamError when schema expects payload but response is empty', async () => {
414
- const fetchHandler: FetchHandler = async () =>
415
- new Response(null, { status: 200 })
460
+ it('throws XrpcInvalidResponseError when schema expects payload but response is empty', async () => {
461
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
462
+ return new Response(null, { status: 200 })
463
+ })
416
464
 
417
465
  await expect(
418
466
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
419
467
  ).rejects.toSatisfy((err) => {
420
- assert(err instanceof XrpcUpstreamError)
421
- expect(err.message).toContain('non-empty response')
468
+ assert(err instanceof XrpcInvalidResponseError)
469
+ expect(err.message).toContain('got no payload')
422
470
  return true
423
471
  })
424
472
  })
@@ -426,11 +474,12 @@ describe(xrpc, () => {
426
474
 
427
475
  describe('content-type handling', () => {
428
476
  it('parses content-type with charset parameter', async () => {
429
- const fetchHandler: FetchHandler = async () =>
430
- new Response(JSON.stringify({ value: 'hello' }), {
477
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
478
+ return new Response(JSON.stringify({ value: 'hello' }), {
431
479
  status: 200,
432
480
  headers: { 'content-type': 'application/json; charset=utf-8' },
433
481
  })
482
+ })
434
483
 
435
484
  const response = await xrpc(fetchHandler, testQuery, {
436
485
  params: { limit: 10 },
@@ -464,15 +513,15 @@ describe(xrpc, () => {
464
513
  })
465
514
 
466
515
  describe('non-2xx non-4xx/5xx responses', () => {
467
- it('throws XrpcUpstreamError for 3xx status codes', async () => {
516
+ it('throws XrpcInvalidResponseError for 3xx status codes', async () => {
468
517
  const fetchHandler: FetchHandler = async () =>
469
518
  Response.json({ value: 'redirect' }, { status: 302 })
470
519
 
471
520
  await expect(
472
521
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
473
522
  ).rejects.toSatisfy((err) => {
474
- assert(err instanceof XrpcUpstreamError)
475
- expect(err.message).toBe('Invalid response status code')
523
+ assert(err instanceof XrpcInvalidResponseError)
524
+ expect(err.message).toBe('Unexpected status code 302')
476
525
  return true
477
526
  })
478
527
  })
@@ -481,8 +530,9 @@ describe(xrpc, () => {
481
530
 
482
531
  describe('validateRequest', () => {
483
532
  it('rejects invalid query params when enabled', async () => {
484
- const fetchHandler: FetchHandler = async () =>
485
- Response.json({ value: 'ok' })
533
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
534
+ return Response.json({ value: 'ok' })
535
+ })
486
536
 
487
537
  await expect(
488
538
  xrpc(fetchHandler, testQuery, {
@@ -498,8 +548,9 @@ describe(xrpc, () => {
498
548
  })
499
549
 
500
550
  it('rejects invalid procedure body when enabled', async () => {
501
- const fetchHandler: FetchHandler = async () =>
502
- Response.json({ id: 'abc' })
551
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
552
+ return Response.json({ id: 'abc' })
553
+ })
503
554
 
504
555
  await expect(
505
556
  xrpc(fetchHandler, testProcedure, {
@@ -528,8 +579,9 @@ describe(xrpc, () => {
528
579
  })
529
580
 
530
581
  it('succeeds with valid body when enabled', async () => {
531
- const fetchHandler: FetchHandler = async () =>
532
- Response.json({ id: 'valid' })
582
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
583
+ return Response.json({ id: 'valid' })
584
+ })
533
585
 
534
586
  const response = await xrpc(fetchHandler, testProcedure, {
535
587
  body: { text: 'hello' },
@@ -544,22 +596,24 @@ describe(xrpc, () => {
544
596
  describe('validateResponse', () => {
545
597
  it('rejects invalid response body by default', async () => {
546
598
  // Schema expects { value: string } but server returns { value: 123 }
547
- const fetchHandler: FetchHandler = async () =>
548
- Response.json({ value: 123 })
599
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
600
+ return Response.json({ value: 123 })
601
+ })
549
602
 
550
603
  await expect(
551
604
  xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),
552
605
  ).rejects.toSatisfy((err) => {
553
- assert(err instanceof XrpcInvalidResponseError)
554
- expect(err).toBeInstanceOf(XrpcUpstreamError)
606
+ assert(err instanceof XrpcResponseValidationError)
607
+ expect(err).toBeInstanceOf(XrpcInvalidResponseError)
555
608
  return true
556
609
  })
557
610
  })
558
611
 
559
612
  it('accepts invalid response body when disabled', async () => {
560
613
  // Schema expects { value: string } but server returns { value: 123 }
561
- const fetchHandler: FetchHandler = async () =>
562
- Response.json({ value: 123 })
614
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
615
+ return Response.json({ value: 123 })
616
+ })
563
617
 
564
618
  const response = await xrpc(fetchHandler, testQuery, {
565
619
  params: { limit: 10 },
@@ -571,8 +625,9 @@ describe(xrpc, () => {
571
625
  })
572
626
 
573
627
  it('succeeds with valid response body when enabled', async () => {
574
- const fetchHandler: FetchHandler = async () =>
575
- Response.json({ value: 'hello' })
628
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
629
+ return Response.json({ value: 'hello' })
630
+ })
576
631
 
577
632
  const response = await xrpc(fetchHandler, testQuery, {
578
633
  params: { limit: 10 },
@@ -583,13 +638,174 @@ describe(xrpc, () => {
583
638
  expect(response.body).toEqual({ value: 'hello' })
584
639
  })
585
640
  })
641
+
642
+ describe('strictResponseProcessing', () => {
643
+ // Helper: returns a JSON response containing a float (invalid lex data)
644
+ const validWithFloatHandler: FetchHandler = async () => {
645
+ return Response.json({ value: 'hello', extra: 1.5 }, { status: 200 })
646
+ }
647
+
648
+ // Helper: returns a JSON error response containing a float
649
+ const errorWithFloatHandler: FetchHandler = async () => {
650
+ return Response.json(
651
+ { error: 'TestError', message: 'test-error-description', extra: 1.5 },
652
+ { status: 400 },
653
+ )
654
+ }
655
+
656
+ it('rejects response with invalid lex data by default (strict parsing)', async () => {
657
+ await expect(
658
+ xrpc(validWithFloatHandler, testQuery, { params: { limit: 10 } }),
659
+ ).rejects.toSatisfy((err) => {
660
+ assert(err instanceof XrpcInvalidResponseError)
661
+ expect(err.message).toMatch('Unable to parse response payload')
662
+ expect(err.cause).toBeInstanceOf(TypeError)
663
+ return true
664
+ })
665
+ })
666
+
667
+ it('accepts response with invalid lex data when strict processing is disabled', async () => {
668
+ const response = await xrpc(validWithFloatHandler, testQuery, {
669
+ params: { limit: 10 },
670
+ strictResponseProcessing: false,
671
+ })
672
+
673
+ expect(response.success).toBe(true)
674
+ expect(response.body).toEqual({ value: 'hello', extra: 1.5 })
675
+ })
676
+
677
+ it('rejects response with invalid lex data when strict processing is explicitly enabled', async () => {
678
+ await expect(
679
+ xrpc(validWithFloatHandler, testQuery, {
680
+ strictResponseProcessing: true,
681
+ }),
682
+ ).rejects.toSatisfy((err) => {
683
+ assert(err instanceof XrpcInvalidResponseError)
684
+ expect(err.message).toMatch('Unable to parse response payload')
685
+ expect(err.cause).toBeInstanceOf(TypeError)
686
+ return true
687
+ })
688
+ })
689
+
690
+ it('rejects error response with invalid lex data by default', async () => {
691
+ await expect(xrpc(errorWithFloatHandler, testQuery)).rejects.toSatisfy(
692
+ (err) => {
693
+ assert(err instanceof XrpcResponseError)
694
+ expect(err.message).toBe('test-error-description')
695
+ return true
696
+ },
697
+ )
698
+ })
699
+
700
+ it('parses error response with invalid lex data when strict processing is disabled', async () => {
701
+ await expect(
702
+ xrpc(errorWithFloatHandler, testQuery, {
703
+ params: { limit: 10 },
704
+ strictResponseProcessing: false,
705
+ }),
706
+ ).rejects.toSatisfy((err) => {
707
+ // Error response is still an error, but it should be parsed successfully
708
+ assert(err instanceof XrpcResponseError)
709
+ expect(err.status).toBe(400)
710
+ expect(err.payload?.body).toEqual({
711
+ error: 'TestError',
712
+ message: 'test-error-description',
713
+ extra: 1.5,
714
+ })
715
+ return true
716
+ })
717
+ })
718
+
719
+ it('with strictResponseProcessing: false and validateResponse: true, schema validation still runs', async () => {
720
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
721
+ return Response.json({ value: 3, unknownValue: 1.5 }, { status: 200 })
722
+ })
723
+
724
+ await expect(
725
+ xrpc(fetchHandler, testQuery, {
726
+ params: { limit: 10 },
727
+ strictResponseProcessing: false,
728
+ validateResponse: true,
729
+ }),
730
+ ).rejects.toSatisfy((err) => {
731
+ assert(err instanceof XrpcResponseValidationError)
732
+ expect(err).toBeInstanceOf(XrpcInvalidResponseError)
733
+ return true
734
+ })
735
+ })
736
+
737
+ it('with strictResponseProcessing: false and validateResponse: false, schema validation is skipped', async () => {
738
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
739
+ return Response.json(
740
+ {
741
+ // Invalid value
742
+ value: 3,
743
+ // Non-strict Lex Data values:
744
+ unknownValue: 1.2,
745
+ foo: { $bytes: 3 },
746
+ },
747
+ { status: 200 },
748
+ )
749
+ })
750
+
751
+ const response = await xrpc(fetchHandler, testQuery, {
752
+ params: { limit: 10 },
753
+ strictResponseProcessing: false,
754
+ validateResponse: false,
755
+ })
756
+
757
+ expect(response.success).toBe(true)
758
+ expect(response.body).toEqual({
759
+ value: 3,
760
+ unknownValue: 1.2,
761
+ foo: { $bytes: 3 },
762
+ })
763
+
764
+ // @NOTE "validateResponse: false" basically acts as type casting
765
+ expectTypeOf(response.body).toMatchObjectType<{
766
+ value: string
767
+ }>()
768
+ })
769
+
770
+ it('with strictResponseProcessing: true and validateResponse: false, strict parsing still applies', async () => {
771
+ await expect(
772
+ xrpc(validWithFloatHandler, testQuery, {
773
+ params: { limit: 10 },
774
+ strictResponseProcessing: true,
775
+ validateResponse: false,
776
+ }),
777
+ ).rejects.toSatisfy((err) => {
778
+ assert(err instanceof XrpcInvalidResponseError)
779
+ expect(err.message).toMatch('Unable to parse response payload')
780
+ return true
781
+ })
782
+ })
783
+
784
+ it('does not affect binary responses', async () => {
785
+ const bytes = new Uint8Array([1, 2, 3])
786
+
787
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
788
+ return new Response(bytes, {
789
+ headers: { 'content-type': 'application/octet-stream' },
790
+ })
791
+ })
792
+
793
+ const response = await xrpc(fetchHandler, testBinaryQuery, {
794
+ strictResponseProcessing: false,
795
+ })
796
+
797
+ expect(response.success).toBe(true)
798
+ expect(response.body).toEqual(bytes)
799
+ })
800
+ })
586
801
  })
587
802
 
588
803
  describe(xrpcSafe, () => {
589
804
  describe('success paths', () => {
590
805
  it('returns successful result for a JSON query', async () => {
591
- const fetchHandler: FetchHandler = async () =>
592
- Response.json({ value: 'hello' })
806
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
807
+ return Response.json({ value: 'hello' })
808
+ })
593
809
 
594
810
  const result = await xrpcSafe(fetchHandler, testQuery, {
595
811
  params: { limit: 5 },
@@ -603,8 +819,9 @@ describe(xrpcSafe, () => {
603
819
  })
604
820
 
605
821
  it('returns successful result for a JSON procedure', async () => {
606
- const fetchHandler: FetchHandler = async () =>
607
- Response.json({ id: 'new-id' })
822
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
823
+ return Response.json({ id: 'new-id' })
824
+ })
608
825
 
609
826
  const result = await xrpcSafe(fetchHandler, testProcedure, {
610
827
  body: { text: 'hello' },
@@ -616,10 +833,11 @@ describe(xrpcSafe, () => {
616
833
 
617
834
  it('returns successful result for a binary query', async () => {
618
835
  const bytes = new Uint8Array([5, 6, 7])
619
- const fetchHandler: FetchHandler = async () =>
620
- new Response(bytes, {
836
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
837
+ return new Response(bytes, {
621
838
  headers: { 'content-type': 'application/octet-stream' },
622
839
  })
840
+ })
623
841
 
624
842
  const result = await xrpcSafe(fetchHandler, testBinaryQuery)
625
843
 
@@ -631,10 +849,11 @@ describe(xrpcSafe, () => {
631
849
 
632
850
  it('returns successful result for a binary procedure', async () => {
633
851
  const bytes = new Uint8Array([42])
634
- const fetchHandler: FetchHandler = async () =>
635
- new Response(bytes, {
852
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
853
+ return new Response(bytes, {
636
854
  headers: { 'content-type': 'application/octet-stream' },
637
855
  })
856
+ })
638
857
 
639
858
  const result = await xrpcSafe(fetchHandler, testBinaryProcedure, {
640
859
  body: new Uint8Array([1, 2]),
@@ -646,8 +865,9 @@ describe(xrpcSafe, () => {
646
865
  })
647
866
 
648
867
  it('returns successful result for a no-output query', async () => {
649
- const fetchHandler: FetchHandler = async () =>
650
- new Response(null, { status: 200 })
868
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
869
+ return new Response(null, { status: 200 })
870
+ })
651
871
 
652
872
  const result = await xrpcSafe(fetchHandler, testNoOutputQuery)
653
873
 
@@ -660,9 +880,9 @@ describe(xrpcSafe, () => {
660
880
  describe('error handling', () => {
661
881
  describe('fetch errors', () => {
662
882
  it('returns XrpcFetchError when fetchHandler throws', async () => {
663
- const fetchHandler: FetchHandler = async () => {
883
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
664
884
  throw new TypeError('fetch failed')
665
- }
885
+ })
666
886
 
667
887
  const result = await xrpcSafe(fetchHandler, testQuery, {
668
888
  params: { limit: 10 },
@@ -674,9 +894,9 @@ describe(xrpcSafe, () => {
674
894
  })
675
895
 
676
896
  it('returns XrpcFetchError when fetchHandler rejects', async () => {
677
- const fetchHandler: FetchHandler = async () => {
897
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
678
898
  throw new Error('network timeout')
679
- }
899
+ })
680
900
 
681
901
  const result = await xrpcSafe(fetchHandler, testQuery, {
682
902
  params: { limit: 10 },
@@ -702,8 +922,8 @@ describe(xrpcSafe, () => {
702
922
 
703
923
  assert(!result.success)
704
924
  assert(result instanceof XrpcResponseError)
705
- expect(result.status).toBe(400)
706
- expect(result.body).toEqual({
925
+ expect(result.response.status).toBe(400)
926
+ expect(result.toJSON()).toEqual({
707
927
  error: 'TestError',
708
928
  message: 'bad request',
709
929
  })
@@ -723,10 +943,10 @@ describe(xrpcSafe, () => {
723
943
  assert(!result.success)
724
944
  assert(result instanceof XrpcResponseError)
725
945
  expect(result).toBeInstanceOf(XrpcAuthenticationError)
726
- expect(result.status).toBe(401)
946
+ expect(result.response.status).toBe(401)
727
947
  })
728
948
 
729
- it('returns XrpcUpstreamError for non-XRPC error response', async () => {
949
+ it('returns XrpcResponseError for non-XRPC error response', async () => {
730
950
  const fetchHandler: FetchHandler = async () =>
731
951
  new Response('Not Found', {
732
952
  status: 404,
@@ -738,10 +958,10 @@ describe(xrpcSafe, () => {
738
958
  })
739
959
 
740
960
  assert(!result.success)
741
- expect(result).toBeInstanceOf(XrpcUpstreamError)
961
+ expect(result).toBeInstanceOf(XrpcResponseError)
742
962
  })
743
963
 
744
- it('returns XrpcUpstreamError for 500 without valid error payload', async () => {
964
+ it('returns XrpcResponseError for 500 without valid error payload', async () => {
745
965
  const fetchHandler: FetchHandler = async () =>
746
966
  new Response('Internal Server Error', {
747
967
  status: 500,
@@ -753,12 +973,16 @@ describe(xrpcSafe, () => {
753
973
  })
754
974
 
755
975
  assert(!result.success)
756
- expect(result).toBeInstanceOf(XrpcUpstreamError)
976
+ expect(result).toBeInstanceOf(XrpcResponseError)
977
+ expect(result.error).toBe('InternalServerError')
978
+ expect(result.message).toMatch(
979
+ 'Upstream server responded with a 500 error',
980
+ )
757
981
  })
758
982
  })
759
983
 
760
984
  describe('invalid response errors', () => {
761
- it('returns XrpcInvalidResponseError when response body fails validation', async () => {
985
+ it('returns XrpcResponseValidationError when response body fails validation', async () => {
762
986
  const fetchHandler: FetchHandler = async () =>
763
987
  Response.json({ value: 123 })
764
988
 
@@ -767,11 +991,11 @@ describe(xrpcSafe, () => {
767
991
  })
768
992
 
769
993
  assert(!result.success)
994
+ expect(result).toBeInstanceOf(XrpcResponseValidationError)
770
995
  expect(result).toBeInstanceOf(XrpcInvalidResponseError)
771
- expect(result).toBeInstanceOf(XrpcUpstreamError)
772
996
  })
773
997
 
774
- it('returns XrpcUpstreamError when response has wrong content-type', async () => {
998
+ it('returns XrpcInvalidResponseError when response has wrong content-type', async () => {
775
999
  const fetchHandler: FetchHandler = async () =>
776
1000
  new Response('binary data', {
777
1001
  status: 200,
@@ -783,7 +1007,7 @@ describe(xrpcSafe, () => {
783
1007
  })
784
1008
 
785
1009
  assert(!result.success)
786
- expect(result).toBeInstanceOf(XrpcUpstreamError)
1010
+ expect(result).toBeInstanceOf(XrpcInvalidResponseError)
787
1011
  })
788
1012
  })
789
1013
 
@@ -806,8 +1030,9 @@ describe(xrpcSafe, () => {
806
1030
 
807
1031
  describe('validateRequest', () => {
808
1032
  it('returns XrpcInternalError for invalid query params when enabled', async () => {
809
- const fetchHandler: FetchHandler = async () =>
810
- Response.json({ value: 'ok' })
1033
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1034
+ return Response.json({ value: 'ok' })
1035
+ })
811
1036
 
812
1037
  const result = await xrpcSafe(fetchHandler, testQuery, {
813
1038
  // @ts-expect-error intentionally passing invalid params
@@ -818,11 +1043,13 @@ describe(xrpcSafe, () => {
818
1043
  assert(!result.success)
819
1044
  expect(result).toBeInstanceOf(XrpcInternalError)
820
1045
  expect(result).not.toBeInstanceOf(XrpcFetchError)
1046
+ expect(fetchHandler).not.toHaveBeenCalled()
821
1047
  })
822
1048
 
823
1049
  it('returns XrpcInternalError for invalid body when enabled', async () => {
824
- const fetchHandler: FetchHandler = async () =>
825
- Response.json({ id: 'abc' })
1050
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1051
+ return Response.json({ id: 'abc' })
1052
+ })
826
1053
 
827
1054
  const result = await xrpcSafe(fetchHandler, testProcedure, {
828
1055
  // @ts-expect-error intentionally passing invalid body
@@ -848,8 +1075,9 @@ describe(xrpcSafe, () => {
848
1075
  })
849
1076
 
850
1077
  it('succeeds with valid body when enabled', async () => {
851
- const fetchHandler: FetchHandler = async () =>
852
- Response.json({ id: 'valid' })
1078
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1079
+ return Response.json({ id: 'valid' })
1080
+ })
853
1081
 
854
1082
  const result = await xrpcSafe(fetchHandler, testProcedure, {
855
1083
  body: { text: 'hello' },
@@ -862,22 +1090,24 @@ describe(xrpcSafe, () => {
862
1090
  })
863
1091
 
864
1092
  describe('validateResponse', () => {
865
- it('returns XrpcInvalidResponseError for invalid body by default', async () => {
866
- const fetchHandler: FetchHandler = async () =>
867
- Response.json({ value: 123 })
1093
+ it('returns XrpcResponseValidationError for invalid body by default', async () => {
1094
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1095
+ return Response.json({ value: 123 })
1096
+ })
868
1097
 
869
1098
  const result = await xrpcSafe(fetchHandler, testQuery, {
870
1099
  params: { limit: 10 },
871
1100
  })
872
1101
 
873
1102
  assert(!result.success)
1103
+ expect(result).toBeInstanceOf(XrpcResponseValidationError)
874
1104
  expect(result).toBeInstanceOf(XrpcInvalidResponseError)
875
- expect(result).toBeInstanceOf(XrpcUpstreamError)
876
1105
  })
877
1106
 
878
1107
  it('accepts invalid response body when disabled', async () => {
879
- const fetchHandler: FetchHandler = async () =>
880
- Response.json({ value: 123 })
1108
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1109
+ return Response.json({ value: 123 })
1110
+ })
881
1111
 
882
1112
  const result = await xrpcSafe(fetchHandler, testQuery, {
883
1113
  params: { limit: 10 },
@@ -889,8 +1119,9 @@ describe(xrpcSafe, () => {
889
1119
  })
890
1120
 
891
1121
  it('succeeds with valid response body when enabled', async () => {
892
- const fetchHandler: FetchHandler = async () =>
893
- Response.json({ value: 'hello' })
1122
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1123
+ return Response.json({ value: 'hello' })
1124
+ })
894
1125
 
895
1126
  const result = await xrpcSafe(fetchHandler, testQuery, {
896
1127
  params: { limit: 10 },
@@ -900,5 +1131,323 @@ describe(xrpcSafe, () => {
900
1131
  assert(result.success)
901
1132
  expect(result.body).toEqual({ value: 'hello' })
902
1133
  })
1134
+
1135
+ it('applies defaults', async () => {
1136
+ const fetchHandler = vi.fn<FetchHandler>(async (path) => {
1137
+ const url = new URL(path, 'http://localhost')
1138
+ const foo = url.searchParams.get('foo')
1139
+
1140
+ // default applied while building the request
1141
+ expect(foo).toBe('foo-default')
1142
+
1143
+ return Response.json({ foo: 'foo-value' })
1144
+ })
1145
+
1146
+ const result = await xrpcSafe(fetchHandler, testQueryWithDefaults, {
1147
+ params: {},
1148
+ validateResponse: true,
1149
+ })
1150
+
1151
+ expect(fetchHandler).toHaveBeenCalled()
1152
+ assert(result.success)
1153
+ expect(result.body).toEqual({
1154
+ bar: 'bar-default', // default applied while parsing the response
1155
+ foo: 'foo-value',
1156
+ })
1157
+ })
1158
+ })
1159
+
1160
+ describe('blob constraints', () => {
1161
+ it('rejects invalid blob refs', async () => {
1162
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1163
+ // missing properties jere
1164
+ return Response.json({ blobRef: { $type: 'blob' } })
1165
+ })
1166
+
1167
+ const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)
1168
+ assert(!result.success)
1169
+ expect(result).toBeInstanceOf(XrpcInvalidResponseError)
1170
+ expect(result.message).toMatch('Unable to parse response payload')
1171
+ assert(result.cause instanceof TypeError)
1172
+ expect(result.cause.message).toBe('Invalid blob object')
1173
+ })
1174
+
1175
+ it('rejects blob-refs with cbor data CIDs', async () => {
1176
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1177
+ return Response.json(
1178
+ lexToJson({
1179
+ blobRef: {
1180
+ $type: 'blob',
1181
+ // ref should be a "raw" CID to be strictly valid
1182
+ ref: cborCid,
1183
+ mimeType: 'image/png',
1184
+ size: 1,
1185
+ },
1186
+ }),
1187
+ )
1188
+ })
1189
+
1190
+ const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)
1191
+ assert(!result.success)
1192
+ expect(result).toBeInstanceOf(XrpcInvalidResponseError)
1193
+ expect(result.message).toMatch('Unable to parse response payload')
1194
+ assert(result.cause instanceof TypeError)
1195
+ expect(result.cause.message).toBe('Invalid blob object')
1196
+ })
1197
+
1198
+ it('enforces blob mime-type constraint by default', async () => {
1199
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1200
+ return Response.json(
1201
+ lexToJson({
1202
+ blobRef: {
1203
+ $type: 'blob',
1204
+ ref: rawCid,
1205
+ mimeType: 'invalid/mime',
1206
+ size: 10,
1207
+ },
1208
+ }),
1209
+ )
1210
+ })
1211
+
1212
+ const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)
1213
+ assert(!result.success)
1214
+ expect(result).toBeInstanceOf(XrpcInvalidResponseError)
1215
+ expect(result.message).toBe(
1216
+ 'Invalid response payload: Expected "image/png" (got "invalid/mime") at $.blobRef.mimeType',
1217
+ )
1218
+ })
1219
+
1220
+ it('enforces blob size constraint by default', async () => {
1221
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1222
+ return Response.json(
1223
+ lexToJson({
1224
+ blobRef: {
1225
+ $type: 'blob',
1226
+ ref: rawCid,
1227
+ mimeType: 'image/png',
1228
+ size: 100,
1229
+ },
1230
+ }),
1231
+ )
1232
+ })
1233
+
1234
+ const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)
1235
+ assert(!result.success)
1236
+ expect(result).toBeInstanceOf(XrpcInvalidResponseError)
1237
+ expect(result.message).toBe(
1238
+ 'Invalid response payload: blob too big (maximum 10, got 100) at $.blobRef',
1239
+ )
1240
+ })
1241
+
1242
+ it('ignores blob constraints in non-strict mode', async () => {
1243
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1244
+ return Response.json(
1245
+ lexToJson({
1246
+ blobRef: {
1247
+ $type: 'blob',
1248
+ ref: rawCid,
1249
+ mimeType: 'invalid/mime',
1250
+ size: 100,
1251
+ },
1252
+ }),
1253
+ )
1254
+ })
1255
+
1256
+ const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {
1257
+ strictResponseProcessing: false,
1258
+ })
1259
+
1260
+ assert(result.success)
1261
+ expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()
1262
+ expect(result.body).toEqual({
1263
+ blobRef: {
1264
+ $type: 'blob',
1265
+ ref: rawCid,
1266
+ mimeType: 'invalid/mime',
1267
+ size: 100,
1268
+ },
1269
+ })
1270
+ })
1271
+
1272
+ it('transforms legacy blobs in non-strict mode', async () => {
1273
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1274
+ return Response.json({
1275
+ blobRef: {
1276
+ cid: rawCid.toString(),
1277
+ mimeType: 'invalid/mime',
1278
+ },
1279
+ })
1280
+ })
1281
+
1282
+ const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {
1283
+ strictResponseProcessing: false,
1284
+ })
1285
+
1286
+ assert(result.success)
1287
+ expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()
1288
+ expect(result.body).toEqual({
1289
+ blobRef: {
1290
+ $type: 'blob',
1291
+ ref: rawCid,
1292
+ mimeType: 'invalid/mime',
1293
+ size: -1,
1294
+ },
1295
+ })
1296
+ })
1297
+
1298
+ it('allows blob-refs with negative size in non-strict mode', async () => {
1299
+ const fetchHandler = vi.fn<FetchHandler>(async () => {
1300
+ return Response.json({
1301
+ blobRef: {
1302
+ $type: 'blob',
1303
+ ref: { $link: rawCid.toString() },
1304
+ mimeType: 'invalid/mime',
1305
+ size: -1,
1306
+ },
1307
+ })
1308
+ })
1309
+
1310
+ const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {
1311
+ strictResponseProcessing: false,
1312
+ })
1313
+
1314
+ assert(result.success)
1315
+ expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()
1316
+ expect(result.body).toEqual({
1317
+ blobRef: {
1318
+ $type: 'blob',
1319
+ ref: rawCid,
1320
+ mimeType: 'invalid/mime',
1321
+ size: -1,
1322
+ },
1323
+ })
1324
+ })
1325
+ })
1326
+
1327
+ describe('strictResponseProcessing', () => {
1328
+ const jsonResponseWithFloat: FetchHandler = async () => {
1329
+ return Response.json({ value: 'hello', extra: 1.5 }, { status: 200 })
1330
+ }
1331
+
1332
+ const jsonErrorResponseWithFloat: FetchHandler = async () => {
1333
+ return Response.json(
1334
+ { error: 'TestError', message: 'test-error-description', extra: 1.5 },
1335
+ { status: 400 },
1336
+ )
1337
+ }
1338
+
1339
+ it('returns error for invalid lex data by default (strict parsing)', async () => {
1340
+ const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)
1341
+
1342
+ const result = await xrpcSafe(fetchHandler, testQuery, {
1343
+ params: { limit: 10 },
1344
+ })
1345
+
1346
+ assert(!result.success)
1347
+ expect(result).toBeInstanceOf(XrpcInvalidResponseError)
1348
+ expect(result.message).toMatch('Unable to parse response payload')
1349
+ assert(result.cause instanceof TypeError)
1350
+ expect(result.cause.message).toBe('Invalid non-integer number: 1.5')
1351
+ })
1352
+
1353
+ it('accepts response with invalid lex data when strict processing is disabled', async () => {
1354
+ const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)
1355
+
1356
+ const result = await xrpcSafe(fetchHandler, testQuery, {
1357
+ params: { limit: 10 },
1358
+ strictResponseProcessing: false,
1359
+ })
1360
+
1361
+ assert(result.success)
1362
+ expect(result.body).toEqual({ value: 'hello', extra: 1.5 })
1363
+ })
1364
+
1365
+ it('returns error for invalid lex data when strict processing is explicitly enabled', async () => {
1366
+ const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)
1367
+
1368
+ const result = await xrpcSafe(fetchHandler, testQuery, {
1369
+ params: { limit: 10 },
1370
+ strictResponseProcessing: true,
1371
+ })
1372
+
1373
+ assert(!result.success)
1374
+ expect(result).toBeInstanceOf(XrpcInvalidResponseError)
1375
+ expect(result.message).toMatch('Unable to parse response payload')
1376
+ assert(result.cause instanceof TypeError)
1377
+ expect(result.cause.message).toBe('Invalid non-integer number: 1.5')
1378
+ })
1379
+
1380
+ it('returns error for error response with invalid lex data by default', async () => {
1381
+ const fetchHandler = vi.fn<FetchHandler>(jsonErrorResponseWithFloat)
1382
+
1383
+ const result = await xrpcSafe(fetchHandler, testQuery, {
1384
+ params: { limit: 10 },
1385
+ })
1386
+
1387
+ assert(!result.success)
1388
+ expect(result).toBeInstanceOf(XrpcResponseError)
1389
+ expect(result.message).toBe('test-error-description')
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.response.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(XrpcResponseValidationError)
1419
+ expect(result).toBeInstanceOf(XrpcInvalidResponseError)
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(XrpcInvalidResponseError)
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
  })