@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/CHANGELOG.md +32 -0
- package/dist/client.d.ts +24 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -7
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +22 -22
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +62 -37
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +66 -7
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +90 -69
- 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 +4 -2
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/client.ts +83 -31
- package/src/errors.test.ts +243 -32
- package/src/errors.ts +91 -52
- package/src/response.ts +229 -102
- package/src/types.ts +17 -40
- package/src/util.test.ts +11 -11
- package/src/util.ts +33 -36
- package/src/xrpc.test.ts +691 -142
- package/src/xrpc.ts +73 -29
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
|
-
|
|
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
|
|
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,22 +212,34 @@ 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
|
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
260
|
-
const fetchHandler
|
|
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
|
|
270
|
-
expect(err.message).toBe('
|
|
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
|
|
276
|
-
const fetchHandler
|
|
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
|
|
286
|
-
expect(err.message).toBe('Upstream server
|
|
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
|
|
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.
|
|
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
|
|
368
|
+
it('throws XrpcResponseValidationError when response body fails validation', async () => {
|
|
314
369
|
// Schema expects { value: string } but we return { value: 123 }
|
|
315
|
-
const fetchHandler
|
|
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
|
|
322
|
-
expect(err).toBeInstanceOf(
|
|
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
|
|
329
|
-
const fetchHandler
|
|
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
|
|
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
|
|
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
|
|
365
|
-
const fetchHandler
|
|
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
|
|
375
|
-
expect(err.message).
|
|
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
|
|
383
|
-
const fetchHandler
|
|
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
|
|
393
|
-
expect(err.message).
|
|
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
|
|
401
|
-
const fetchHandler
|
|
402
|
-
Response
|
|
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
|
|
421
|
-
expect(err.message).toContain('
|
|
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
|
|
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
|
|
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
|
|
475
|
-
expect(err.message).toBe('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
554
|
-
expect(err).toBeInstanceOf(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
961
|
+
expect(result).toBeInstanceOf(XrpcResponseError)
|
|
742
962
|
})
|
|
743
963
|
|
|
744
|
-
it('returns
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
866
|
-
const fetchHandler
|
|
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
|
|
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
|
|
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
|
})
|