galbe 0.1.0

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.
@@ -0,0 +1,909 @@
1
+ import { expect, test, describe, beforeAll } from 'bun:test'
2
+ import { Galbe, T } from '../src'
3
+ import {
4
+ formdata,
5
+ type Case,
6
+ fileHash,
7
+ schema_objectBase,
8
+ schema_object,
9
+ handleBody,
10
+ isAsyncIterator,
11
+ handleUrlFormStream
12
+ } from './test.utils'
13
+
14
+ const port = 7358
15
+ const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options']
16
+
17
+ describe('requests', () => {
18
+ beforeAll(async () => {
19
+ const galbe = new Galbe()
20
+
21
+ galbe.get('/test', () => {})
22
+ galbe.post('/test', () => {})
23
+ galbe.put('/test', () => {})
24
+ galbe.patch('/test', () => {})
25
+ galbe.delete('/test', () => {})
26
+ galbe.options('/test', () => {})
27
+
28
+ galbe.get('/headers', ctx => {
29
+ return ctx.headers
30
+ })
31
+ galbe.get(
32
+ '/headers/schema',
33
+ {
34
+ headers: {
35
+ string: T.String(),
36
+ zero: T.Number(),
37
+ number: T.Number(),
38
+ 'neg-number': T.Number(),
39
+ float: T.Number(),
40
+ integer: T.Integer(),
41
+ 'boolean-true': T.Boolean(),
42
+ 'boolean-false': T.Boolean()
43
+ }
44
+ },
45
+ ctx => {
46
+ return ctx.headers
47
+ }
48
+ )
49
+
50
+ galbe.get('/params/:param1/separate/:param2/:param3', ctx => {
51
+ return ctx.params
52
+ })
53
+ galbe.get(
54
+ '/params/schema/:p1/:p2/:p3/:p4',
55
+ {
56
+ params: {
57
+ p1: T.String(),
58
+ p2: T.Number(),
59
+ p3: T.Integer(),
60
+ p4: T.Boolean()
61
+ }
62
+ },
63
+ ctx => {
64
+ return ctx.params
65
+ }
66
+ )
67
+
68
+ galbe.get('/query/params', ctx => {
69
+ return ctx.query
70
+ })
71
+ galbe.get(
72
+ '/query/params/schema',
73
+ {
74
+ query: {
75
+ p1: T.String(),
76
+ p2: T.Number(),
77
+ p3: T.Boolean(),
78
+ p4: T.Union([T.Number(), T.Boolean()]),
79
+ p5: T.Optional(T.String())
80
+ }
81
+ },
82
+ ctx => {
83
+ return ctx.query
84
+ }
85
+ )
86
+
87
+ galbe.post('/none', handleBody)
88
+ galbe.post('/ba', { body: T.ByteArray() }, handleBody)
89
+ galbe.post('/bool', { body: T.Boolean() }, handleBody)
90
+ galbe.post('/num', { body: T.Number() }, handleBody)
91
+ galbe.post('/str', { body: T.String() }, handleBody)
92
+ galbe.post('/arr', { body: T.Array(T.Any()) }, handleBody)
93
+ galbe.post('/obj', { body: T.Object(T.Any()) }, handleBody)
94
+
95
+ galbe.post('/form', { body: T.UrlForm(T.Any()) }, handleBody)
96
+ galbe.post('/mp', { body: T.MultipartForm(T.Any()) }, handleBody)
97
+
98
+ galbe.post('/stream/ba', { body: T.Stream(T.ByteArray()) }, handleBody)
99
+ galbe.post('/stream/str', { body: T.Stream(T.String()) }, handleBody)
100
+ galbe.post('/stream/form', { body: T.Stream(T.UrlForm(T.Any())) }, async ctx => {
101
+ let resp: Record<string, any> = {}
102
+ for await (const [k, v] of ctx.body) {
103
+ if (k in resp) {
104
+ resp[k] = Array.isArray(resp[k]) ? [...resp[k], v] : [resp[k], v]
105
+ } else resp[k] = v
106
+ }
107
+ return { type: 'object', content: resp }
108
+ })
109
+ galbe.post('/stream/mp', { body: T.Stream(T.MultipartForm(T.Any())) }, async ctx => {
110
+ let resp: Record<string, any> = {}
111
+ for await (const field of ctx.body) {
112
+ let k = field.headers.name
113
+ if (k in resp) {
114
+ resp[k].content = Array.isArray(resp[k]?.content)
115
+ ? [...resp[k].content, field.content]
116
+ : [resp[k]?.content, field.content]
117
+ } else resp[k] = field
118
+ }
119
+ return { type: 'object', content: resp }
120
+ })
121
+
122
+ galbe.post('/obj/schema/base', { body: T.Object(schema_object) }, handleBody)
123
+ galbe.post(
124
+ '/form/schema/base',
125
+ {
126
+ body: T.UrlForm({
127
+ ...schema_objectBase,
128
+ union: T.Optional(T.Union([T.Number(), T.Boolean()])),
129
+ literal: T.Optional(T.Literal('x')),
130
+ array: T.Array(T.Any())
131
+ })
132
+ },
133
+ handleBody
134
+ )
135
+ galbe.post(
136
+ '/form/stream/schema/base',
137
+ {
138
+ body: T.Stream(
139
+ T.UrlForm({
140
+ ...schema_objectBase,
141
+ union: T.Optional(T.Union([T.Number(), T.Boolean()])),
142
+ literal: T.Optional(T.Literal('x')),
143
+ array: T.Array(T.Any()),
144
+ numArray: T.Optional(T.Array(T.Number()))
145
+ })
146
+ )
147
+ },
148
+ handleUrlFormStream
149
+ )
150
+ galbe.post(
151
+ '/mp/schema/base',
152
+ {
153
+ body: T.MultipartForm({
154
+ ...schema_objectBase,
155
+ union: T.Optional(T.Union([T.Number(), T.Boolean()])),
156
+ literal: T.Optional(T.Literal('x')),
157
+ array: T.Array(T.Any())
158
+ })
159
+ },
160
+ handleBody
161
+ )
162
+ galbe.post(
163
+ '/mp/stream/schema/base',
164
+ {
165
+ body: T.Stream(
166
+ T.MultipartForm({
167
+ ...schema_objectBase,
168
+ union: T.Optional(T.Union([T.Number(), T.Boolean()])),
169
+ literal: T.Optional(T.Literal('x')),
170
+ array: T.Array(T.Any())
171
+ })
172
+ )
173
+ },
174
+ handleBody
175
+ )
176
+
177
+ let schema_jsonFile = {
178
+ string: T.String(),
179
+ number: T.Number(),
180
+ bool: T.Boolean(),
181
+ arrayStr: T.Array(T.String()),
182
+ arrayNumber: T.Array(T.Number()),
183
+ arrayBool: T.Array(T.Boolean())
184
+ }
185
+
186
+ galbe.post(
187
+ '/mp/file',
188
+ { body: T.MultipartForm({ imgFile: T.ByteArray(), jsonFile: T.Object(schema_jsonFile) }) },
189
+ handleBody
190
+ )
191
+ galbe.post(
192
+ '/mp/stream/file',
193
+ { body: T.Stream(T.MultipartForm({ imgFile: T.ByteArray(), jsonFile: T.Object(schema_jsonFile) })) },
194
+ async ctx => {
195
+ if (isAsyncIterator(ctx.body)) {
196
+ const chunks: any[] = []
197
+ for await (const chunk of ctx.body) {
198
+ if (chunk.content instanceof Uint8Array) chunk.content = await fileHash(chunk.content)
199
+ chunks.push(chunk)
200
+ }
201
+ return { type: 'AsyncIterator', content: chunks }
202
+ } else {
203
+ return { type: null, content: 'error' }
204
+ }
205
+ }
206
+ )
207
+ galbe.post('/ba/file', { body: T.ByteArray() }, async ctx => {
208
+ if (ctx?.body instanceof Uint8Array) {
209
+ return { type: 'ByteArray', content: await fileHash(ctx.body) }
210
+ } else {
211
+ return { type: null, content: 'error' }
212
+ }
213
+ })
214
+ galbe.post('/ba/stream/file', { body: T.Stream(T.ByteArray()) }, async ctx => {
215
+ if (isAsyncIterator(ctx.body)) {
216
+ let bytes = new Uint8Array()
217
+ for await (const b of ctx.body) {
218
+ bytes = new Uint8Array([...bytes, ...b])
219
+ }
220
+ return { type: 'AsyncIterator', content: await fileHash(bytes) }
221
+ } else {
222
+ return { type: null, content: 'error' }
223
+ }
224
+ })
225
+
226
+ await galbe.listen(port)
227
+ })
228
+
229
+ test('methods, empty calls', async () => {
230
+ for (let method of METHODS) {
231
+ let resp = await fetch(`http://localhost:${port}/test`, { method: method.toUpperCase() })
232
+ expect(resp.ok).toBeTrue()
233
+ }
234
+ })
235
+
236
+ test('methods, path not found', async () => {
237
+ for (let method of METHODS) {
238
+ let resp = await fetch(`http://localhost:${port}/does/not/exist`, { method: method.toUpperCase() })
239
+ expect(resp.status).toBe(404)
240
+ }
241
+ })
242
+
243
+ test('headers', async () => {
244
+ const headers = {
245
+ String: 'hello mom',
246
+ Number: '42',
247
+ Boolean: 'true'
248
+ }
249
+ const expected = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]))
250
+
251
+ let resp = await fetch(`http://localhost:${port}/headers`, { headers })
252
+ let body = await resp.json()
253
+
254
+ expect(body).toMatchObject(expected)
255
+ })
256
+
257
+ test('path params', async () => {
258
+ const expected = {
259
+ param1: 'one',
260
+ param2: '2',
261
+ param3: 'true'
262
+ }
263
+ let resp = await fetch(`http://localhost:${port}/params/one/separate/2/true`)
264
+ let body = await resp.json()
265
+
266
+ expect(body).toEqual(expected)
267
+ })
268
+
269
+ test('query params', async () => {
270
+ const expected = {
271
+ param1: 'one',
272
+ param2: '2',
273
+ param3: 'true'
274
+ }
275
+ let resp = await fetch(`http://localhost:${port}/query/params?param1=one&param2=2&param3=true`)
276
+ let body = await resp.json()
277
+
278
+ expect(body).toEqual(expected)
279
+ })
280
+
281
+ test('no body, no header', async () => {
282
+ const cases: Case[] = [
283
+ { body: null, type: undefined, schema: 'none', expected: { status: 200, resp: null } },
284
+ { body: null, type: undefined, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: '' } },
285
+ { body: null, type: undefined, schema: 'bool', expected: { status: 400 } },
286
+ { body: null, type: undefined, schema: 'num', expected: { status: 400 } },
287
+ { body: null, type: undefined, schema: 'str', expected: { status: 400 } },
288
+ { body: null, type: undefined, schema: 'arr', expected: { status: 400 } },
289
+ { body: null, type: undefined, schema: 'obj', expected: { status: 400 } },
290
+ { body: null, type: undefined, schema: 'form', expected: { status: 400 } },
291
+ { body: null, type: undefined, schema: 'mp', expected: { status: 400 } },
292
+ { body: null, type: undefined, schema: 'stream/ba', expected: { status: 200, type: 'ReadableStream', resp: '' } },
293
+ { body: null, type: undefined, schema: 'stream/str', expected: { status: 400 } },
294
+ { body: null, type: undefined, schema: 'stream/form', expected: { status: 400 } },
295
+ { body: null, type: undefined, schema: 'stream/mp', expected: { status: 400 } }
296
+ ]
297
+ for (let { body, type, schema, expected } of cases) {
298
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
299
+ method: 'POST',
300
+ body,
301
+ headers: {
302
+ ...(type ? { 'content-type': type } : {})
303
+ }
304
+ })
305
+
306
+ let respBody = (await resp.json()) as { type: string; content: any }
307
+
308
+ expect(resp.status).toBe(expected.status)
309
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
310
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
311
+ else expect(respBody.content).toEqual(expected.resp)
312
+ }
313
+ })
314
+
315
+ test('no body, application/octet-stream', async () => {
316
+ const contentType = 'application/octet-stream'
317
+ const cases: Case[] = [
318
+ { body: null, type: contentType, schema: 'none', expected: { status: 200, resp: '' } },
319
+ { body: null, type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: '' } },
320
+ { body: null, type: contentType, schema: 'bool', expected: { status: 400 } },
321
+ { body: null, type: contentType, schema: 'num', expected: { status: 400 } },
322
+ { body: null, type: contentType, schema: 'str', expected: { status: 400 } },
323
+ { body: null, type: contentType, schema: 'arr', expected: { status: 400 } },
324
+ { body: null, type: contentType, schema: 'obj', expected: { status: 400 } },
325
+ { body: null, type: contentType, schema: 'form', expected: { status: 400 } },
326
+ { body: null, type: contentType, schema: 'mp', expected: { status: 400 } },
327
+ {
328
+ body: null,
329
+ type: contentType,
330
+ schema: 'stream/ba',
331
+ expected: { status: 200, type: 'ReadableStream', resp: '' }
332
+ },
333
+ { body: null, type: contentType, schema: 'stream/str', expected: { status: 400 } },
334
+ { body: null, type: contentType, schema: 'stream/form', expected: { status: 400 } },
335
+ { body: null, type: contentType, schema: 'stream/mp', expected: { status: 400 } }
336
+ ]
337
+
338
+ for (let { body, type, schema, expected } of cases) {
339
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
340
+ method: 'POST',
341
+ body,
342
+ headers: {
343
+ ...(type ? { 'content-type': type } : {})
344
+ }
345
+ })
346
+
347
+ let respBody = (await resp.json()) as { type: string; content: any }
348
+
349
+ expect(resp.status).toBe(expected.status)
350
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
351
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
352
+ else expect(respBody.content).toEqual(expected.resp)
353
+ }
354
+ })
355
+
356
+ test('no body, application/json', async () => {
357
+ const contentType = 'application/json'
358
+ const cases: Case[] = [
359
+ { body: null, type: contentType, schema: 'none', expected: { status: 400 } },
360
+ { body: null, type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: '' } },
361
+ { body: null, type: contentType, schema: 'bool', expected: { status: 400 } },
362
+ { body: null, type: contentType, schema: 'num', expected: { status: 400 } },
363
+ { body: null, type: contentType, schema: 'str', expected: { status: 400 } },
364
+ { body: null, type: contentType, schema: 'arr', expected: { status: 400 } },
365
+ { body: null, type: contentType, schema: 'obj', expected: { status: 400 } },
366
+ { body: null, type: contentType, schema: 'form', expected: { status: 400 } },
367
+ { body: null, type: contentType, schema: 'mp', expected: { status: 400 } },
368
+ {
369
+ body: null,
370
+ type: contentType,
371
+ schema: 'stream/ba',
372
+ expected: { status: 200, type: 'ReadableStream', resp: '' }
373
+ },
374
+ { body: null, type: contentType, schema: 'stream/str', expected: { status: 400 } },
375
+ { body: null, type: contentType, schema: 'stream/form', expected: { status: 400 } },
376
+ { body: null, type: contentType, schema: 'stream/mp', expected: { status: 400 } }
377
+ ]
378
+
379
+ for (let { body, type, schema, expected } of cases) {
380
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
381
+ method: 'POST',
382
+ body,
383
+ headers: {
384
+ ...(type ? { 'content-type': type } : {})
385
+ }
386
+ })
387
+
388
+ let respBody = (await resp.json()) as { type: string; content: any }
389
+
390
+ expect(resp.status).toBe(expected.status)
391
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
392
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
393
+ else expect(respBody.content).toEqual(expected.resp)
394
+ }
395
+ })
396
+
397
+ test('no body, text/plain', async () => {
398
+ const contentType = 'text/plain'
399
+ const cases: Case[] = [
400
+ { body: null, type: contentType, schema: 'none', expected: { status: 400 } },
401
+ { body: null, type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: '' } },
402
+ { body: null, type: contentType, schema: 'bool', expected: { status: 400 } },
403
+ { body: null, type: contentType, schema: 'num', expected: { status: 400 } },
404
+ { body: null, type: contentType, schema: 'str', expected: { status: 400 } },
405
+ { body: null, type: contentType, schema: 'arr', expected: { status: 400 } },
406
+ { body: null, type: contentType, schema: 'obj', expected: { status: 400 } },
407
+ { body: null, type: contentType, schema: 'form', expected: { status: 400 } },
408
+ { body: null, type: contentType, schema: 'mp', expected: { status: 400 } },
409
+ {
410
+ body: null,
411
+ type: contentType,
412
+ schema: 'stream/ba',
413
+ expected: { status: 200, type: 'ReadableStream', resp: '' }
414
+ },
415
+ { body: null, type: contentType, schema: 'stream/str', expected: { status: 400 } },
416
+ { body: null, type: contentType, schema: 'stream/form', expected: { status: 400 } },
417
+ { body: null, type: contentType, schema: 'stream/mp', expected: { status: 400 } }
418
+ ]
419
+
420
+ for (let { body, type, schema, expected } of cases) {
421
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
422
+ method: 'POST',
423
+ body,
424
+ headers: {
425
+ ...(type ? { 'content-type': type } : {})
426
+ }
427
+ })
428
+
429
+ let respBody = (await resp.json()) as { type: string; content: any }
430
+
431
+ expect(resp.status).toBe(expected.status)
432
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
433
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
434
+ else expect(respBody.content).toEqual(expected.resp)
435
+ }
436
+ })
437
+
438
+ test('no body, application/x-www-form-urlencoded', async () => {
439
+ const contentType = 'application/x-www-form-urlencoded'
440
+ const cases: Case[] = [
441
+ { body: null, type: contentType, schema: 'none', expected: { status: 400 } },
442
+ { body: null, type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: '' } },
443
+ { body: null, type: contentType, schema: 'bool', expected: { status: 400 } },
444
+ { body: null, type: contentType, schema: 'num', expected: { status: 400 } },
445
+ { body: null, type: contentType, schema: 'str', expected: { status: 400 } },
446
+ { body: null, type: contentType, schema: 'arr', expected: { status: 400 } },
447
+ { body: null, type: contentType, schema: 'obj', expected: { status: 400 } },
448
+ { body: null, type: contentType, schema: 'form', expected: { status: 400 } },
449
+ { body: null, type: contentType, schema: 'mp', expected: { status: 400 } },
450
+ {
451
+ body: null,
452
+ type: contentType,
453
+ schema: 'stream/ba',
454
+ expected: { status: 200, type: 'ReadableStream', resp: '' }
455
+ },
456
+ { body: null, type: contentType, schema: 'stream/str', expected: { status: 400 } },
457
+ { body: null, type: contentType, schema: 'stream/form', expected: { status: 400 } },
458
+ { body: null, type: contentType, schema: 'stream/mp', expected: { status: 400 } }
459
+ ]
460
+
461
+ for (let { body, type, schema, expected } of cases) {
462
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
463
+ method: 'POST',
464
+ body,
465
+ headers: {
466
+ ...(type ? { 'content-type': type } : {})
467
+ }
468
+ })
469
+
470
+ let respBody = (await resp.json()) as { type: string; content: any }
471
+
472
+ expect(resp.status).toBe(expected.status)
473
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
474
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
475
+ else expect(respBody.content).toEqual(expected.resp)
476
+ }
477
+ })
478
+
479
+ test('no body, multipart/form-data', async () => {
480
+ const contentType = 'multipart/form-data'
481
+ const cases: Case[] = [
482
+ { body: null, type: contentType, schema: 'none', expected: { status: 400 } },
483
+ { body: null, type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: '' } },
484
+ { body: null, type: contentType, schema: 'bool', expected: { status: 400 } },
485
+ { body: null, type: contentType, schema: 'num', expected: { status: 400 } },
486
+ { body: null, type: contentType, schema: 'str', expected: { status: 400 } },
487
+ { body: null, type: contentType, schema: 'arr', expected: { status: 400 } },
488
+ { body: null, type: contentType, schema: 'obj', expected: { status: 400 } },
489
+ { body: null, type: contentType, schema: 'form', expected: { status: 400 } },
490
+ { body: null, type: contentType, schema: 'mp', expected: { status: 400 } },
491
+ {
492
+ body: null,
493
+ type: contentType,
494
+ schema: 'stream/ba',
495
+ expected: { status: 200, type: 'ReadableStream', resp: '' }
496
+ },
497
+ { body: null, type: contentType, schema: 'stream/str', expected: { status: 400 } },
498
+ { body: null, type: contentType, schema: 'stream/form', expected: { status: 400 } },
499
+ { body: null, type: contentType, schema: 'stream/mp', expected: { status: 400 } }
500
+ ]
501
+
502
+ for (let { body, type, schema, expected } of cases) {
503
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
504
+ method: 'POST',
505
+ body,
506
+ headers: {
507
+ ...(type ? { 'content-type': type } : {})
508
+ }
509
+ })
510
+
511
+ let respBody = (await resp.json()) as { type: string; content: any }
512
+
513
+ expect(resp.status).toBe(expected.status)
514
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
515
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
516
+ else expect(respBody.content).toEqual(expected.resp)
517
+ }
518
+ })
519
+
520
+ test('body, no header', async () => {
521
+ const cases: Case[] = [
522
+ { body: 'test', schema: 'none', expected: { status: 200, type: 'ByteArray', resp: 'test' } },
523
+ { body: 'test', schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: 'test' } },
524
+ { body: 'true', schema: 'bool', expected: { status: 200, type: 'boolean', resp: true } },
525
+ { body: 'false', schema: 'bool', expected: { status: 200, type: 'boolean', resp: false } },
526
+ { body: 'test', schema: 'bool', expected: { status: 400 } },
527
+ { body: '0', schema: 'num', expected: { status: 200, type: 'number', resp: 0 } },
528
+ { body: '42', schema: 'num', expected: { status: 200, type: 'number', resp: 42 } },
529
+ { body: '-8000', schema: 'num', expected: { status: 200, type: 'number', resp: -8000 } },
530
+ { body: '3.14', schema: 'num', expected: { status: 200, type: 'number', resp: 3.14 } },
531
+ { body: 'test', schema: 'num', expected: { status: 400 } },
532
+ { body: 'test', schema: 'str', expected: { status: 200, type: 'string', resp: 'test' } },
533
+ { body: '42', schema: 'str', expected: { status: 200, type: 'string', resp: '42' } },
534
+ { body: 'false', schema: 'str', expected: { status: 200, type: 'string', resp: 'false' } },
535
+ { body: '[]', schema: 'arr', expected: { status: 200, type: 'array', resp: [] } },
536
+ {
537
+ body: '["a", 42, true, null, {"foo": "bar"}]',
538
+ schema: 'arr',
539
+ expected: { status: 200, type: 'array', resp: ['a', 42, true, null, { foo: 'bar' }] }
540
+ },
541
+ { body: 'test', schema: 'arr', expected: { status: 400 } },
542
+ { body: '{}', schema: 'obj', expected: { status: 200, type: 'object', resp: {} } },
543
+ { body: '{"foo": "bar"}', schema: 'obj', expected: { status: 200, type: 'object', resp: { foo: 'bar' } } },
544
+ { body: 'test', schema: 'obj', expected: { status: 400 } },
545
+ { body: 'test', schema: 'form', expected: { status: 400 } },
546
+ { body: 'test', schema: 'mp', expected: { status: 400 } },
547
+ {
548
+ body: 'test',
549
+ schema: 'stream/ba',
550
+ expected: { status: 200, type: 'AsyncIterator', resp: new Uint8Array([116, 101, 115, 116]) }
551
+ },
552
+ { body: 'test', schema: 'stream/str', expected: { status: 200, type: 'AsyncIterator', resp: 'test' } },
553
+ { body: 'test', schema: 'stream/form', expected: { status: 400 } },
554
+ { body: 'test', schema: 'stream/mp', expected: { status: 400 } }
555
+ ]
556
+
557
+ for (let { body, type, schema, expected } of cases) {
558
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
559
+ method: 'POST',
560
+ body,
561
+ headers: {
562
+ ...(type ? { 'content-type': type } : {})
563
+ }
564
+ })
565
+
566
+ let respBody = (await resp.json()) as { type: string; content: any }
567
+
568
+ expect(resp.status).toBe(expected.status)
569
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
570
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
571
+ else expect(respBody.content).toEqual(expected.resp)
572
+ }
573
+ })
574
+
575
+ test('body, application/octet-stream', async () => {
576
+ const contentType = 'application/octet-stream'
577
+ const cases: Case[] = [
578
+ { body: 'test', type: contentType, schema: 'none', expected: { status: 200, type: 'ByteArray', resp: 'test' } },
579
+ { body: 'test', type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: 'test' } },
580
+ { body: 'true', type: contentType, schema: 'bool', expected: { status: 200, type: 'boolean', resp: true } },
581
+ { body: 'false', type: contentType, schema: 'bool', expected: { status: 200, type: 'boolean', resp: false } },
582
+ { body: 'test', type: contentType, schema: 'bool', expected: { status: 400 } },
583
+ { body: '0', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 0 } },
584
+ { body: '42', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 42 } },
585
+ { body: '-8000', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: -8000 } },
586
+ { body: '3.14', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 3.14 } },
587
+ { body: 'test', type: contentType, schema: 'num', expected: { status: 400 } },
588
+ { body: 'test', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: 'test' } },
589
+ { body: '42', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: '42' } },
590
+ { body: 'false', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: 'false' } },
591
+ { body: '[]', type: contentType, schema: 'arr', expected: { status: 200, type: 'array', resp: [] } },
592
+ {
593
+ body: '["a", 42, true, null, {"foo": "bar"}]',
594
+ type: contentType,
595
+ schema: 'arr',
596
+ expected: { status: 200, type: 'array', resp: ['a', 42, true, null, { foo: 'bar' }] }
597
+ },
598
+ { body: 'test', type: contentType, schema: 'arr', expected: { status: 400 } },
599
+ { body: '{}', type: contentType, schema: 'obj', expected: { status: 200, type: 'object', resp: {} } },
600
+ {
601
+ body: '{"foo": "bar"}',
602
+ type: contentType,
603
+ schema: 'obj',
604
+ expected: { status: 200, type: 'object', resp: { foo: 'bar' } }
605
+ },
606
+ { body: 'test', type: contentType, schema: 'obj', expected: { status: 400 } },
607
+ { body: 'test', type: contentType, schema: 'form', expected: { status: 400 } },
608
+ { body: 'test', type: contentType, schema: 'mp', expected: { status: 400 } },
609
+ {
610
+ body: 'test',
611
+ type: contentType,
612
+ schema: 'stream/ba',
613
+ expected: { status: 200, type: 'AsyncIterator', resp: new Uint8Array([116, 101, 115, 116]) }
614
+ },
615
+ {
616
+ body: 'test',
617
+ type: contentType,
618
+ schema: 'stream/str',
619
+ expected: { status: 200, type: 'AsyncIterator', resp: 'test' }
620
+ },
621
+ { body: 'test', type: contentType, schema: 'stream/form', expected: { status: 400 } },
622
+ { body: 'test', type: contentType, schema: 'stream/mp', expected: { status: 400 } }
623
+ ]
624
+
625
+ for (let { body, type, schema, expected } of cases) {
626
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
627
+ method: 'POST',
628
+ body,
629
+ headers: {
630
+ ...(type ? { 'content-type': type } : {})
631
+ }
632
+ })
633
+
634
+ let respBody = (await resp.json()) as { type: string; content: any }
635
+
636
+ expect(resp.status).toBe(expected.status)
637
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
638
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
639
+ else expect(respBody.content).toEqual(expected.resp)
640
+ }
641
+ })
642
+
643
+ test('body, application/json', async () => {
644
+ const contentType = 'application/json'
645
+ const cases: Case[] = [
646
+ {
647
+ body: '{"foo":"bar"}',
648
+ type: contentType,
649
+ schema: 'none',
650
+ expected: { status: 200, type: 'object', resp: { foo: 'bar' } }
651
+ },
652
+ { body: 'test', type: contentType, schema: 'none', expected: { status: 400 } },
653
+ { body: 'test', type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: 'test' } },
654
+ { body: 'true', type: contentType, schema: 'bool', expected: { status: 200, type: 'boolean', resp: true } },
655
+ { body: 'false', type: contentType, schema: 'bool', expected: { status: 200, type: 'boolean', resp: false } },
656
+ { body: 'test', type: contentType, schema: 'bool', expected: { status: 400 } },
657
+ { body: '0', type: contentType, schema: 'bool', expected: { status: 400 } },
658
+ { body: '0', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 0 } },
659
+ { body: '42', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 42 } },
660
+ { body: '-8000', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: -8000 } },
661
+ { body: '3.14', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 3.14 } },
662
+ { body: 'test', type: contentType, schema: 'num', expected: { status: 400 } },
663
+ { body: '"test"', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: 'test' } },
664
+ { body: '"42"', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: '42' } },
665
+ { body: '"false"', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: 'false' } },
666
+ { body: 'test', type: contentType, schema: 'str', expected: { status: 400 } },
667
+ { body: '[]', type: contentType, schema: 'arr', expected: { status: 200, type: 'array', resp: [] } },
668
+ {
669
+ body: '["a", 42, true, null, {"foo": "bar"}]',
670
+ type: contentType,
671
+ schema: 'arr',
672
+ expected: { status: 200, type: 'array', resp: ['a', 42, true, null, { foo: 'bar' }] }
673
+ },
674
+ { body: 'test', type: contentType, schema: 'arr', expected: { status: 400 } },
675
+ { body: '{}', type: contentType, schema: 'obj', expected: { status: 200, type: 'object', resp: {} } },
676
+ {
677
+ body: '{"foo": "bar"}',
678
+ type: contentType,
679
+ schema: 'obj',
680
+ expected: { status: 200, type: 'object', resp: { foo: 'bar' } }
681
+ },
682
+ { body: 'test', type: contentType, schema: 'obj', expected: { status: 400 } },
683
+ { body: 'test', type: contentType, schema: 'form', expected: { status: 400 } },
684
+ { body: 'test', type: contentType, schema: 'mp', expected: { status: 400 } },
685
+ {
686
+ body: 'test',
687
+ type: contentType,
688
+ schema: 'stream/ba',
689
+ expected: { status: 200, type: 'AsyncIterator', resp: new Uint8Array([116, 101, 115, 116]) }
690
+ },
691
+ {
692
+ body: 'test',
693
+ type: contentType,
694
+ schema: 'stream/str',
695
+ expected: { status: 200, type: 'AsyncIterator', resp: 'test' }
696
+ },
697
+ { body: 'test', type: contentType, schema: 'stream/form', expected: { status: 400 } },
698
+ { body: 'test', type: contentType, schema: 'stream/mp', expected: { status: 400 } }
699
+ ]
700
+
701
+ for (let { body, type, schema, expected } of cases) {
702
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
703
+ method: 'POST',
704
+ body,
705
+ headers: {
706
+ ...(type ? { 'content-type': type } : {})
707
+ }
708
+ })
709
+
710
+ let respBody = (await resp.json()) as { type: string; content: any }
711
+
712
+ expect(resp.status).toBe(expected.status)
713
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
714
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
715
+ else expect(respBody.content).toEqual(expected.resp)
716
+ }
717
+ })
718
+
719
+ test('body, text/plain', async () => {
720
+ const contentType = 'text/plain'
721
+ const cases: Case[] = [
722
+ { body: 'test', type: contentType, schema: 'none', expected: { status: 200, type: 'string', resp: 'test' } },
723
+ { body: 'test', type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: 'test' } },
724
+ { body: 'true', type: contentType, schema: 'bool', expected: { status: 200, type: 'boolean', resp: true } },
725
+ { body: 'false', type: contentType, schema: 'bool', expected: { status: 200, type: 'boolean', resp: false } },
726
+ { body: 'test', type: contentType, schema: 'bool', expected: { status: 400 } },
727
+ { body: '0', type: contentType, schema: 'bool', expected: { status: 400 } },
728
+ { body: '0', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 0 } },
729
+ { body: '42', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 42 } },
730
+ { body: '-8000', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: -8000 } },
731
+ { body: '3.14', type: contentType, schema: 'num', expected: { status: 200, type: 'number', resp: 3.14 } },
732
+ { body: 'test', type: contentType, schema: 'num', expected: { status: 400 } },
733
+ { body: 'test', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: 'test' } },
734
+ { body: '"test"', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: '"test"' } },
735
+ { body: '42', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: '42' } },
736
+ { body: 'false', type: contentType, schema: 'str', expected: { status: 200, type: 'string', resp: 'false' } },
737
+ { body: '[]', type: contentType, schema: 'arr', expected: { status: 200, type: 'array', resp: [] } },
738
+ {
739
+ body: '["a", 42, true, null, {"foo": "bar"}]',
740
+ type: contentType,
741
+ schema: 'arr',
742
+ expected: { status: 200, type: 'array', resp: ['a', 42, true, null, { foo: 'bar' }] }
743
+ },
744
+ { body: 'test', type: contentType, schema: 'arr', expected: { status: 400 } },
745
+ { body: '{}', type: contentType, schema: 'obj', expected: { status: 200, type: 'object', resp: {} } },
746
+ {
747
+ body: '{"foo": "bar"}',
748
+ type: contentType,
749
+ schema: 'obj',
750
+ expected: { status: 200, type: 'object', resp: { foo: 'bar' } }
751
+ },
752
+ { body: 'test', type: contentType, schema: 'obj', expected: { status: 400 } },
753
+ { body: 'test', type: contentType, schema: 'form', expected: { status: 400 } },
754
+ { body: 'test', type: contentType, schema: 'mp', expected: { status: 400 } },
755
+ {
756
+ body: 'test',
757
+ type: contentType,
758
+ schema: 'stream/ba',
759
+ expected: { status: 200, type: 'AsyncIterator', resp: new Uint8Array([116, 101, 115, 116]) }
760
+ },
761
+ {
762
+ body: 'test',
763
+ type: contentType,
764
+ schema: 'stream/str',
765
+ expected: { status: 200, type: 'AsyncIterator', resp: 'test' }
766
+ },
767
+ { body: 'test', type: contentType, schema: 'stream/form', expected: { status: 400 } },
768
+ { body: 'test', type: contentType, schema: 'stream/mp', expected: { status: 400 } }
769
+ ]
770
+
771
+ for (let { body, type, schema, expected } of cases) {
772
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
773
+ method: 'POST',
774
+ body,
775
+ headers: {
776
+ ...(type ? { 'content-type': type } : {})
777
+ }
778
+ })
779
+
780
+ let respBody = (await resp.json()) as { type: string; content: any }
781
+
782
+ expect(resp.status).toBe(expected.status)
783
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
784
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
785
+ else expect(respBody.content).toEqual(expected.resp)
786
+ }
787
+ })
788
+
789
+ test('body, application/x-www-form-urlencoded', async () => {
790
+ const contentType = 'application/x-www-form-urlencoded'
791
+ const strBody = 'string=text&number=42&boolean=true&encoded%3D=%3D'
792
+ const objBody = { string: 'text', number: '42', boolean: 'true', 'encoded=': '=' }
793
+ const cases: Case[] = [
794
+ { body: strBody, type: contentType, schema: 'none', expected: { status: 200, type: 'object', resp: objBody } },
795
+ { body: strBody, type: contentType, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: strBody } },
796
+ { body: strBody, type: contentType, schema: 'bool', expected: { status: 400 } },
797
+ { body: strBody, type: contentType, schema: 'num', expected: { status: 400 } },
798
+ { body: strBody, type: contentType, schema: 'str', expected: { status: 400 } },
799
+ { body: strBody, type: contentType, schema: 'arr', expected: { status: 400 } },
800
+ { body: strBody, type: contentType, schema: 'obj', expected: { status: 400 } },
801
+ { body: strBody, type: contentType, schema: 'form', expected: { status: 200, type: 'object', resp: objBody } },
802
+ { body: strBody, type: contentType, schema: 'mp', expected: { status: 400 } },
803
+ {
804
+ body: strBody,
805
+ type: contentType,
806
+ schema: 'stream/ba',
807
+ expected: { status: 200, type: 'AsyncIterator', resp: Uint8Array.from(strBody, c => c.charCodeAt(0)) }
808
+ },
809
+ {
810
+ body: strBody,
811
+ type: contentType,
812
+ schema: 'stream/str',
813
+ expected: { status: 200, type: 'AsyncIterator', resp: strBody }
814
+ },
815
+ {
816
+ body: strBody,
817
+ type: contentType,
818
+ schema: 'stream/form',
819
+ expected: { status: 200, type: 'object', resp: objBody }
820
+ },
821
+ { body: strBody, type: contentType, schema: 'stream/mp', expected: { status: 400 } }
822
+ ]
823
+
824
+ for (let { body, type, schema, expected } of cases) {
825
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
826
+ method: 'POST',
827
+ body,
828
+ headers: {
829
+ ...(type ? { 'content-type': type } : {})
830
+ }
831
+ })
832
+
833
+ let respBody = (await resp.json()) as { type: string; content: any }
834
+
835
+ expect(resp.status).toBe(expected.status)
836
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
837
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
838
+ else expect(respBody.content).toEqual(expected.resp)
839
+ }
840
+ })
841
+
842
+ test('body, multipart/form-data', async () => {
843
+ const objBody = { string: 'text', number: '42', boolean: 'true' }
844
+ const objResp = {
845
+ string: {
846
+ content: 'text',
847
+ headers: {
848
+ name: 'string'
849
+ }
850
+ },
851
+ number: {
852
+ content: '42',
853
+ headers: {
854
+ name: 'number'
855
+ }
856
+ },
857
+ boolean: {
858
+ content: 'true',
859
+ headers: {
860
+ name: 'boolean'
861
+ }
862
+ }
863
+ }
864
+ const objRespStrRgx = new RegExp(
865
+ '^---WebkitFormBoundary[0-9a-f]{32}\r\nContent-Disposition: form-data; name="string"\r\n\r\ntext\r\n' +
866
+ '---WebkitFormBoundary[0-9a-f]{32}\r\nContent-Disposition: form-data; name="number"\r\n\r\n42\r\n' +
867
+ '---WebkitFormBoundary[0-9a-f]{32}\r\nContent-Disposition: form-data; name="boolean"\r\n\r\ntrue\r\n' +
868
+ '---WebkitFormBoundary[0-9a-f]{32}--\r\n$'
869
+ )
870
+ const form = formdata(objBody)
871
+
872
+ const cases: Case[] = [
873
+ { body: form, schema: 'none', expected: { status: 200, type: 'object', resp: objResp } },
874
+ { body: form, schema: 'ba', expected: { status: 200, type: 'ByteArray', resp: objRespStrRgx } },
875
+ { body: form, schema: 'bool', expected: { status: 400 } },
876
+ { body: form, schema: 'num', expected: { status: 400 } },
877
+ { body: form, schema: 'str', expected: { status: 400 } },
878
+ { body: form, schema: 'arr', expected: { status: 400 } },
879
+ { body: form, schema: 'obj', expected: { status: 400 } },
880
+ { body: form, schema: 'form', expected: { status: 400 } },
881
+ { body: form, schema: 'mp', expected: { status: 200, type: 'object', resp: objResp } },
882
+ {
883
+ body: form,
884
+ schema: 'stream/str',
885
+ expected: { status: 200, type: 'AsyncIterator', resp: objRespStrRgx }
886
+ },
887
+ { body: form, schema: 'stream/form', expected: { status: 400 } },
888
+ { body: form, schema: 'stream/mp', expected: { status: 200, type: 'object', resp: objResp } }
889
+ ]
890
+
891
+ for (let { body, type, schema, expected } of cases) {
892
+ let resp = await fetch(`http://localhost:${port}/${schema}`, {
893
+ method: 'POST',
894
+ body,
895
+ headers: {
896
+ ...(type ? { 'content-type': type } : {})
897
+ }
898
+ })
899
+
900
+ let respBody = (await resp.json()) as { type: string; content: any }
901
+
902
+ expect(resp.status).toBe(expected.status)
903
+ if (expected.type) expect(respBody.type).toEqual(expected.type)
904
+ if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
905
+ else if (expected.resp instanceof RegExp) expect(respBody.content).toMatch(expected.resp)
906
+ else expect(respBody.content).toEqual(expected.resp)
907
+ }
908
+ })
909
+ })