@openaddresses/batch-schema 10.24.1 → 10.26.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.
@@ -1,6 +1,7 @@
1
1
  import { describe, it, before, after } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import express from 'express';
4
+ import { Type } from '@sinclair/typebox';
4
5
  import Schema from '../index.js';
5
6
 
6
7
  const app = express();
@@ -13,6 +14,86 @@ let server: ReturnType<typeof app.listen>;
13
14
  describe('Schema Server', () => {
14
15
  before((_, done) => {
15
16
  server = app.listen(2000, async () => {
17
+ await schema.get('/legacy', {
18
+ deprecated: true,
19
+ description: 'Legacy route',
20
+ res: Type.Object({
21
+ ok: Type.Boolean()
22
+ })
23
+ }, async (_, res) => {
24
+ res.json({ ok: true });
25
+ });
26
+
27
+ await schema.post('/json-only', {
28
+ description: 'Backwards compatible JSON body',
29
+ body: Type.Object({
30
+ name: Type.String()
31
+ }),
32
+ res: Type.Object({ ok: Type.Boolean() })
33
+ }, async (req, res) => {
34
+ res.json({ ok: typeof req.body.name === 'string' });
35
+ });
36
+
37
+ await schema.post('/multi-body', {
38
+ description: 'Multi content-type body',
39
+ body: {
40
+ 'application/json': Type.Object({ name: Type.String() }),
41
+ 'text/xml': true
42
+ },
43
+ res: Type.Object({
44
+ contentType: Type.String(),
45
+ body: Type.Any()
46
+ })
47
+ }, async (req, res) => {
48
+ res.json({
49
+ contentType: (req.headers['content-type'] || '').split(';')[0],
50
+ body: req.body
51
+ });
52
+ });
53
+
54
+ await schema.post('/wildcard-body', {
55
+ description: 'Wildcard content-type body',
56
+ body: {
57
+ 'application/json': Type.Object({ name: Type.String() }),
58
+ 'text/*': true
59
+ },
60
+ res: Type.Object({
61
+ contentType: Type.String(),
62
+ body: Type.Any()
63
+ })
64
+ }, async (req, res) => {
65
+ res.json({
66
+ contentType: (req.headers['content-type'] || '').split(';')[0],
67
+ body: req.body
68
+ });
69
+ });
70
+
71
+ await schema.post('/optional-body', {
72
+ description: 'Optional body',
73
+ body: Type.Object({ name: Type.String() }),
74
+ bodyRequired: false,
75
+ res: Type.Object({ ok: Type.Boolean() })
76
+ }, async (_req, res) => {
77
+ res.json({ ok: true });
78
+ });
79
+
80
+ await schema.post('/with-examples', {
81
+ description: 'Body with examples in OpenAPI',
82
+ body: {
83
+ 'application/json': {
84
+ schema: Type.Object({ name: Type.String() }),
85
+ example: { name: 'sample' },
86
+ examples: {
87
+ primary: { summary: 'Primary', value: { name: 'alice' } },
88
+ secondary: { summary: 'Secondary', value: { name: 'bob' } }
89
+ }
90
+ }
91
+ },
92
+ res: Type.Object({ ok: Type.Boolean() })
93
+ }, async (_req, res) => {
94
+ res.json({ ok: true });
95
+ });
96
+
16
97
  await schema.api();
17
98
  done();
18
99
  });
@@ -27,6 +108,36 @@ describe('Schema Server', () => {
27
108
  const body = await res.json();
28
109
 
29
110
  assert.deepStrictEqual(body, {
111
+ 'GET /legacy': {
112
+ body: false,
113
+ query: false,
114
+ res: true
115
+ },
116
+ 'POST /json-only': {
117
+ body: true,
118
+ query: false,
119
+ res: true
120
+ },
121
+ 'POST /multi-body': {
122
+ body: true,
123
+ query: false,
124
+ res: true
125
+ },
126
+ 'POST /wildcard-body': {
127
+ body: true,
128
+ query: false,
129
+ res: true
130
+ },
131
+ 'POST /optional-body': {
132
+ body: true,
133
+ query: false,
134
+ res: true
135
+ },
136
+ 'POST /with-examples': {
137
+ body: true,
138
+ query: false,
139
+ res: true
140
+ },
30
141
  'GET /schema':{
31
142
  body: false,
32
143
  query: true,
@@ -78,6 +189,182 @@ describe('Schema Server', () => {
78
189
  const res = await fetch('http://localhost:2000/api/schema?method=GET&url=/schema');
79
190
 
80
191
  assert.strictEqual(res.status, 200, 'http: 200');
81
- assert.deepStrictEqual(Object.keys(await res.json()).sort(), ['query', 'res'].sort());
192
+ assert.deepStrictEqual(Object.keys(await res.json()).sort(), ['deprecated', 'query', 'res'].sort());
193
+ });
194
+
195
+ it('GET: api/schema?method=GET&url=/legacy', async () => {
196
+ const res = await fetch('http://localhost:2000/api/schema?method=GET&url=/legacy');
197
+
198
+ assert.strictEqual(res.status, 200, 'http: 200');
199
+ assert.deepStrictEqual(await res.json(), {
200
+ deprecated: true,
201
+ res: {
202
+ type: 'object',
203
+ properties: {
204
+ ok: {
205
+ type: 'boolean'
206
+ }
207
+ },
208
+ required: ['ok']
209
+ }
210
+ });
211
+ });
212
+
213
+ it('GET: api/openapi marks deprecated operations', async () => {
214
+ const res = await fetch('http://localhost:2000/api/openapi');
215
+ const body = await res.json();
216
+
217
+ assert.strictEqual(res.status, 200, 'http: 200');
218
+ assert.strictEqual(body.paths['/legacy'].get.deprecated, true);
219
+ assert.strictEqual(body.paths['/schema'].get.deprecated, false);
220
+ });
221
+
222
+ it('POST: /json-only with valid JSON body (backwards compat)', async () => {
223
+ const res = await fetch('http://localhost:2000/api/json-only', {
224
+ method: 'POST',
225
+ headers: { 'Content-Type': 'application/json' },
226
+ body: JSON.stringify({ name: 'foo' })
227
+ });
228
+ assert.strictEqual(res.status, 200);
229
+ assert.deepStrictEqual(await res.json(), { ok: true });
230
+ });
231
+
232
+ it('POST: /multi-body with application/json', async () => {
233
+ const res = await fetch('http://localhost:2000/api/multi-body', {
234
+ method: 'POST',
235
+ headers: { 'Content-Type': 'application/json' },
236
+ body: JSON.stringify({ name: 'foo' })
237
+ });
238
+ assert.strictEqual(res.status, 200);
239
+ assert.deepStrictEqual(await res.json(), {
240
+ contentType: 'application/json',
241
+ body: { name: 'foo' }
242
+ });
243
+ });
244
+
245
+ it('POST: /multi-body with text/xml (true => no validation)', async () => {
246
+ const res = await fetch('http://localhost:2000/api/multi-body', {
247
+ method: 'POST',
248
+ headers: { 'Content-Type': 'text/xml' },
249
+ body: '<root><a>1</a></root>'
250
+ });
251
+ assert.strictEqual(res.status, 200);
252
+ assert.deepStrictEqual(await res.json(), {
253
+ contentType: 'text/xml',
254
+ body: '<root><a>1</a></root>'
255
+ });
256
+ });
257
+
258
+ it('POST: /multi-body with unsupported content-type returns 400', async () => {
259
+ const res = await fetch('http://localhost:2000/api/multi-body', {
260
+ method: 'POST',
261
+ headers: { 'Content-Type': 'text/plain' },
262
+ body: 'hello'
263
+ });
264
+ assert.strictEqual(res.status, 400);
265
+ const body = await res.json();
266
+ assert.strictEqual(body.status, 400);
267
+ assert.match(body.message, /Content-Type text\/plain not supported/);
268
+ });
269
+
270
+ it('POST: /multi-body with invalid JSON body fails validation', async () => {
271
+ const res = await fetch('http://localhost:2000/api/multi-body', {
272
+ method: 'POST',
273
+ headers: { 'Content-Type': 'application/json' },
274
+ body: JSON.stringify({})
275
+ });
276
+ assert.strictEqual(res.status, 400);
277
+ const body = await res.json();
278
+ assert.strictEqual(body.message, 'Validation Error POST /multi-body');
279
+ });
280
+
281
+ it('GET: api/openapi documents multiple content types', async () => {
282
+ const res = await fetch('http://localhost:2000/api/openapi');
283
+ const body = await res.json();
284
+
285
+ assert.deepStrictEqual(
286
+ Object.keys(body.paths['/multi-body'].post.requestBody.content).sort(),
287
+ ['application/json', 'text/xml']
288
+ );
289
+ assert.deepStrictEqual(
290
+ body.paths['/multi-body'].post.requestBody.content['text/xml'],
291
+ { schema: {} }
292
+ );
293
+ assert.strictEqual(
294
+ body.paths['/json-only'].post.requestBody.content['application/json'].schema.type,
295
+ 'object'
296
+ );
297
+ });
298
+
299
+ it('POST: /wildcard-body matches text/* with text/csv', async () => {
300
+ const res = await fetch('http://localhost:2000/api/wildcard-body', {
301
+ method: 'POST',
302
+ headers: { 'Content-Type': 'text/csv' },
303
+ body: 'a,b,c\n1,2,3'
304
+ });
305
+ assert.strictEqual(res.status, 200);
306
+ assert.deepStrictEqual(await res.json(), {
307
+ contentType: 'text/csv',
308
+ body: 'a,b,c\n1,2,3'
309
+ });
310
+ });
311
+
312
+ it('POST: /wildcard-body still validates application/json strictly', async () => {
313
+ const res = await fetch('http://localhost:2000/api/wildcard-body', {
314
+ method: 'POST',
315
+ headers: { 'Content-Type': 'application/json' },
316
+ body: JSON.stringify({ name: 'foo' })
317
+ });
318
+ assert.strictEqual(res.status, 200);
319
+ });
320
+
321
+ it('POST: /wildcard-body rejects unsupported family', async () => {
322
+ const res = await fetch('http://localhost:2000/api/wildcard-body', {
323
+ method: 'POST',
324
+ headers: { 'Content-Type': 'image/png' },
325
+ body: 'binary'
326
+ });
327
+ assert.strictEqual(res.status, 400);
328
+ const body = await res.json();
329
+ assert.match(body.message, /Content-Type image\/png not supported/);
330
+ });
331
+
332
+ it('POST: /optional-body accepts no body when bodyRequired=false', async () => {
333
+ const res = await fetch('http://localhost:2000/api/optional-body', {
334
+ method: 'POST'
335
+ });
336
+ assert.strictEqual(res.status, 200);
337
+ assert.deepStrictEqual(await res.json(), { ok: true });
338
+ });
339
+
340
+ it('POST: /optional-body still validates when a body is sent', async () => {
341
+ const res = await fetch('http://localhost:2000/api/optional-body', {
342
+ method: 'POST',
343
+ headers: { 'Content-Type': 'application/json' },
344
+ body: JSON.stringify({})
345
+ });
346
+ assert.strictEqual(res.status, 400);
347
+ });
348
+
349
+ it('GET: api/openapi marks optional body required=false', async () => {
350
+ const res = await fetch('http://localhost:2000/api/openapi');
351
+ const body = await res.json();
352
+ assert.strictEqual(body.paths['/optional-body'].post.requestBody.required, false);
353
+ assert.strictEqual(body.paths['/json-only'].post.requestBody.required, true);
354
+ });
355
+
356
+ it('GET: api/openapi includes example and examples on media type', async () => {
357
+ const res = await fetch('http://localhost:2000/api/openapi');
358
+ const body = await res.json();
359
+ const media = body.paths['/with-examples'].post.requestBody.content['application/json'];
360
+ assert.deepStrictEqual(media.example, { name: 'sample' });
361
+ assert.deepStrictEqual(media.examples.primary, {
362
+ summary: 'Primary',
363
+ value: { name: 'alice' }
364
+ });
365
+ assert.deepStrictEqual(media.examples.secondary, {
366
+ summary: 'Secondary',
367
+ value: { name: 'bob' }
368
+ });
82
369
  });
83
370
  });