mockaton 8.12.3 → 8.12.5

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/README.md CHANGED
@@ -361,7 +361,7 @@ Defaults to `0`, which means auto-assigned
361
361
 
362
362
  ### `delay?: number`
363
363
  Defaults to `1200` milliseconds. Although routes can individually be delayed
364
- with the 🕓 checkbox, the delay amount is globally configurable with option.
364
+ with the 🕓 Checkbox, the amount is globally configurable with this option.
365
365
 
366
366
  <br/>
367
367
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "mockaton",
3
- "description": "A deterministic server-side for developing and testing APIs",
3
+ "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "8.12.3",
5
+ "version": "8.12.5",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/TODO.md DELETED
@@ -1,11 +0,0 @@
1
- # TODO
2
-
3
- - Refactor tests
4
- - Group By Route/Method localstorage
5
- - openapi
6
- - parsing it for examples?
7
- - displaying documentation (.openapi)
8
- - perhaps instead using .js functions `export const doc`
9
- - Preserve focus when refreshing dashboard `init()`
10
- - More real-time updates. Currently, it's only for add/remove mock but not for
11
- static files and changes from another client (Browser, or Commander).
package/dev-mockaton.js DELETED
@@ -1,17 +0,0 @@
1
- import { join } from 'node:path'
2
- import { Mockaton, jwtCookie } from './index.js'
3
-
4
-
5
- Mockaton({
6
- port: 2345,
7
- mocksDir: join(import.meta.dirname, 'fixtures-mocks'),
8
- staticDir: join(import.meta.dirname, 'fixtures-static-mocks'),
9
- cookies: {
10
- 'My Admin User': 'my-cookie=1;Path=/;SameSite=strict',
11
- 'My Normal User': 'my-cookie=0;Path=/;SameSite=strict',
12
- 'My JWT': jwtCookie('my-cookie', {
13
- email: 'john.doe@example.com',
14
- picture: 'https://cdn.auth0.com/avatars/jd.png'
15
- })
16
- }
17
- })
@@ -1,659 +0,0 @@
1
- import { tmpdir } from 'node:os'
2
- import { promisify } from 'node:util'
3
- import { describe, it } from 'node:test'
4
- import { createServer } from 'node:http'
5
- import { dirname, join } from 'node:path'
6
- import { randomUUID } from 'node:crypto'
7
- import { equal, deepEqual, match } from 'node:assert/strict'
8
- import { writeFileSync, mkdtempSync, mkdirSync, unlinkSync, readFileSync } from 'node:fs'
9
-
10
- import { config } from './config.js'
11
- import { mimeFor } from './utils/mime.js'
12
- import { Mockaton } from './Mockaton.js'
13
- import { readBody } from './utils/http-request.js'
14
- import { Commander } from './Commander.js'
15
- import { CorsHeader } from './utils/http-cors.js'
16
- import { parseFilename } from './Filename.js'
17
- import { listFilesRecursively } from './utils/fs.js'
18
- import { API, DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
19
-
20
-
21
- const tmpDir = mkdtempSync(tmpdir() + '/mocks') + '/'
22
- const staticTmpDir = mkdtempSync(tmpdir() + '/static') + '/'
23
-
24
- const fixtureCustomMime = [
25
- '/api/custom-mime',
26
- 'api/custom-mime.GET.200.my_custom_extension',
27
- 'Custom Extension and MIME'
28
- ]
29
- const fixtureNonDefaultInName = [
30
- '/api/the-route',
31
- 'api/the-route.GET.200.json',
32
- 'default my route body content'
33
- ]
34
- const fixtureDefaultInName = [
35
- '/api/the-route',
36
- 'api/the-route(default).GET.200.json',
37
- 'default my route body content'
38
- ]
39
- const fixtureDelayed = [
40
- '/api/delayed',
41
- 'api/delayed.GET.200.json',
42
- 'Route_To_Be_Delayed'
43
- ]
44
-
45
- /* Only fixtures with PUT */
46
- const fixtureForRegisteringPutA = [
47
- '/api/register',
48
- 'api/register(a).PUT.200.json',
49
- 'fixture_for_registering_a'
50
- ]
51
- const fixtureForRegisteringPutB = [
52
- '/api/register',
53
- 'api/register(b).PUT.200.json',
54
- 'fixture_for_registering_b'
55
- ]
56
- const fixtureForRegisteringPutA500 = [
57
- '/api/register',
58
- 'api/register.PUT.500.json',
59
- 'fixture_for_registering_500'
60
- ]
61
- const fixtureForUnregisteringPutC = [
62
- '/api/unregister',
63
- 'api/unregister.PUT.200.json',
64
- 'fixture_for_unregistering'
65
- ]
66
-
67
-
68
- const fixtures = [
69
- [
70
- '/api',
71
- 'api/.GET.200.json',
72
- 'index-like route for /api, which could just be the extension convention'
73
- ],
74
-
75
- // Exact route paths
76
- fixtureDefaultInName,
77
- fixtureDelayed,
78
- [
79
- '/api/the-route',
80
- 'api/the-route(default).GET.200.json',
81
- 'default my route body content'
82
- ],
83
- [
84
- '/api/the-mime',
85
- 'api/the-mime.GET.200.txt',
86
- 'determines the content type'
87
- ], [
88
- '/api/the-method-and-status',
89
- 'api/the-method-and-status.POST.201.json',
90
- 'obeys the HTTP method and response status'
91
- ], [
92
- '/api/the-comment',
93
- 'api/the-comment(this is the actual comment).GET.200(another comment).txt',
94
- ''
95
- ], [
96
- '/api/alternative',
97
- 'api/alternative(comment-1).GET.200.json',
98
- 'With_Comment_1'
99
- ], [
100
- '/api/dot.in.path',
101
- 'api/dot.in.path.GET.200.json',
102
- 'Dot_in_Path'
103
- ], [
104
- '/api/space & colon:',
105
- 'api/space & colon:.GET.200.json',
106
- 'Decodes URI'
107
- ],
108
-
109
- [
110
- '/api/uncommon-method',
111
- '/api/uncommon-method.ACL.200.json',
112
- 'node.js doesn’t support arbitrary HTTP methods, but it does support a few non-standard ones'
113
- ],
114
-
115
-
116
- // Dynamic Params
117
- [
118
- '/api/user/1234',
119
- 'api/user/[id]/.GET.200.json',
120
- 'variable at end'
121
- ], [
122
- '/api/user/1234/suffix',
123
- 'api/user/[id]/suffix.GET.200.json',
124
- 'sandwich a variable that another route has at the end'
125
- ], [
126
- '/api/user/exact-route',
127
- 'api/user/exact-route.GET.200.json',
128
- 'ensure dynamic params do not take precedence over exact routes'
129
- ],
130
-
131
- // Query String
132
- // TODO ignore on Windows (because of ?)
133
- [
134
- '/api/my-query-string?foo=[foo]&bar=[bar]',
135
- 'api/my-query-string?foo=[foo]&bar=[bar].GET.200.json',
136
- 'two query string params'
137
- ], [
138
- '/api/company-a',
139
- 'api/company-a/[id]?limit=[limit].GET.200.json',
140
- 'without pretty-param nor query-params'
141
- ], [
142
- '/api/company-b/',
143
- 'api/company-b/[id]?limit=[limit].GET.200.json',
144
- 'without pretty-param nor query-params with trailing slash'
145
- ], [
146
- '/api/company-c/1234',
147
- 'api/company-c/[id]?limit=[limit].GET.200.json',
148
- 'with pretty-param and without query-params'
149
- ], [
150
- '/api/company-d/1234/?',
151
- 'api/company-d/[id]?limit=[limit].GET.200.json',
152
- 'with pretty-param and without query-params, but with trailing slash and "?"'
153
- ], [
154
- '/api/company-e/1234/?limit=4',
155
- 'api/company-e/[id]?limit=[limit].GET.200.json',
156
- 'with pretty-param and query-params'
157
- ],
158
- fixtureCustomMime
159
- ]
160
- for (const [, file, body] of [fixtureNonDefaultInName, ...fixtures])
161
- write(file, file.endsWith('.json') ? JSON.stringify(body) : body)
162
-
163
- write('api/.GET.500.txt', 'keeps non-autogenerated 500')
164
- write('api/alternative(comment-2).GET.200.json', JSON.stringify({ comment: 2 }))
165
- write('api/my-route(comment-2).GET.200.json', JSON.stringify({ comment: 2 }))
166
- write('api/ignored.GET.200.json~', '')
167
-
168
- // JavaScript to JSON
169
- write('/api/object.GET.200.js', 'export default { JSON_FROM_JS: true }')
170
-
171
- const staticFiles = [
172
- ['index.html', '<h1>Static</h1>'],
173
- ['assets/app.js', 'const app = 1'],
174
- ['another-entry/index.html', '<h1>Another</h1>']
175
- ]
176
- writeStatic('ignored.js~', 'ignored_file_body')
177
- for (const [file, body] of staticFiles)
178
- writeStatic(file, body)
179
-
180
- const server = Mockaton({
181
- mocksDir: tmpDir,
182
- staticDir: staticTmpDir,
183
- delay: 80,
184
- onReady: () => {},
185
- cookies: {
186
- userA: 'CookieA',
187
- userB: 'CookieB'
188
- },
189
- extraHeaders: ['Server', 'MockatonTester'],
190
- extraMimes: {
191
- my_custom_extension: 'my_custom_mime'
192
- },
193
- corsOrigins: ['http://example.com'],
194
- corsExposedHeaders: ['Content-Encoding']
195
- })
196
- server.on('listening', runTests)
197
-
198
- function mockatonAddr() {
199
- const { address, port } = server.address()
200
- return `http://${address}:${port}`
201
- }
202
-
203
- function request(path, options = {}) {
204
- return fetch(`${mockatonAddr()}${path}`, options)
205
- }
206
-
207
- let commander
208
- async function runTests() {
209
- commander = new Commander(mockatonAddr())
210
-
211
- await testItRendersDashboard()
212
- await test404()
213
-
214
- for (const [url, file, body] of fixtures)
215
- await testMockDispatching(url, file, body)
216
-
217
- await testDefaultMock()
218
-
219
- await testItUpdatesRouteDelay(...fixtureDelayed)
220
- await testBadRequestWhenUpdatingNonExistingMockAlternative()
221
-
222
- await testAutogenerates500(
223
- '/api/alternative',
224
- `api/alternative${DEFAULT_500_COMMENT}.GET.500.empty`)
225
-
226
- await testPreservesExiting500(
227
- '/api',
228
- 'api/.GET.500.txt',
229
- 'keeps non-autogenerated 500')
230
-
231
- await commander.reset()
232
- await testItUpdatesTheCurrentSelectedMock(
233
- '/api/alternative',
234
- 'api/alternative(comment-2).GET.200.json',
235
- 200,
236
- JSON.stringify({ comment: 2 }))
237
-
238
- await commander.reset()
239
- await testExtractsAllComments([
240
- '(comment-1)',
241
- '(comment-2)',
242
- DEFAULT_500_COMMENT,
243
- '(this is the actual comment)',
244
- '(another comment)',
245
- DEFAULT_MOCK_COMMENT
246
- ])
247
- await testItBulkSelectsByComment('(comment-2)', [
248
- ['/api/alternative', 'api/alternative(comment-2).GET.200.json', { comment: 2 }],
249
- ['/api/my-route', 'api/my-route(comment-2).GET.200.json', { comment: 2 }]
250
- ])
251
- await testItBulkSelectsByComment('mment-1', [ // partial match within parentheses
252
- ['/api/alternative', 'api/alternative(comment-1).GET.200.json', 'With_Comment_1']
253
- ])
254
- await commander.reset()
255
-
256
- for (const [url, file, body] of fixtures)
257
- await testMockDispatching(url, file, body)
258
-
259
- await testMockDispatching('/api/object', 'api/object.GET.200.js', { JSON_FROM_JS: true }, mimeFor('.json'))
260
- await testMockDispatching(...fixtureCustomMime, 'my_custom_mime')
261
- await testJsFunctionMocks()
262
-
263
- await testItUpdatesCookie()
264
- await testStaticFileServing()
265
- await testStaticFileList()
266
- await testInvalidFilenamesAreIgnored()
267
- await testEnableFallbackSoRoutesWithoutMocksGetRelayed()
268
- await testValidatesProxyFallbackURL()
269
- await testCorsAllowed()
270
- testWindowsPaths()
271
-
272
- await testRegistering()
273
-
274
- server.close()
275
- }
276
-
277
-
278
- async function testItRendersDashboard() {
279
- const res = await request(API.dashboard)
280
- const body = await res.text()
281
- await describe('Dashboard', () =>
282
- it('Renders HTML', () => match(body, new RegExp('<!DOCTYPE html>'))))
283
- }
284
-
285
- async function test404() {
286
- await it('Sends 404 when there is no mock', async () => {
287
- const res = await request('/api/non-existing')
288
- equal(res.status, 404)
289
- })
290
- await it('Sends 404 when there’s no mock at all for a method', async () => {
291
- const res = await request('/api/non-existing-too', { method: 'DELETE' })
292
- equal(res.status, 404)
293
- })
294
- await it('Ignores files ending in ~ by default, e.g. JetBrains temp files', async () => {
295
- const res = await request('/api/ignored')
296
- equal(res.status, 404)
297
- })
298
- await it('Ignores static files ending in ~ by default, e.g. JetBrains temp files', async () => {
299
- const res = await request('/ignored.js~')
300
- equal(res.status, 404)
301
- })
302
- }
303
-
304
- async function testMockDispatching(url, file, expectedBody, forcedMime = undefined) {
305
- const { urlMask, method, status } = parseFilename(file)
306
- const mime = forcedMime || mimeFor(file)
307
- const res = await request(url, { method })
308
- const body = mime === 'application/json'
309
- ? await res.json()
310
- : await res.text()
311
- await describe('URL Mask: ' + urlMask, () => {
312
- it('file: ' + file, () => deepEqual(body, expectedBody))
313
- it('mime: ' + mime, () => equal(res.headers.get('content-type'), mime))
314
- it('status: ' + status, () => equal(res.status, status))
315
- it('cookie: ' + mime, () => equal(res.headers.get('set-cookie'), 'CookieA'))
316
- it('extra header', () => equal(res.headers.get('server'), 'MockatonTester'))
317
- })
318
- }
319
-
320
- async function testDefaultMock() {
321
- await testMockDispatching(...fixtureDefaultInName)
322
- await it('sorts mocks list with the user specified default first for dashboard display', async () => {
323
- const body = await (await commander.listMocks()).json()
324
- const { mocks } = body['GET'][fixtureDefaultInName[0]]
325
- equal(mocks[0], fixtureDefaultInName[1])
326
- equal(mocks[1], fixtureNonDefaultInName[1])
327
- })
328
- }
329
-
330
- async function testRegistering() {
331
- await describe('Registering', async () => {
332
- const temp500 = `api/register${DEFAULT_500_COMMENT}.PUT.500.empty`
333
-
334
- await it('registering new route creates temp 500 as well and re-registering is a noop', async () => {
335
- write(fixtureForRegisteringPutA[1], '')
336
- await sleep()
337
- write(fixtureForRegisteringPutB[1], '')
338
- await sleep()
339
- write(fixtureForRegisteringPutA[1], '')
340
- await sleep()
341
- const collection = await (await commander.listMocks()).json()
342
- deepEqual(collection['PUT'][fixtureForRegisteringPutA[0]].mocks, [
343
- fixtureForRegisteringPutA[1],
344
- fixtureForRegisteringPutB[1],
345
- temp500
346
- ])
347
- })
348
- await it('registering a 500 removes the temp 500 (and selects the new 500)', async () => {
349
- await commander.select(temp500)
350
- write(fixtureForRegisteringPutA500[1], '')
351
- await sleep()
352
- const collection = await (await commander.listMocks()).json()
353
- const { mocks, currentMock } = collection['PUT'][fixtureForRegisteringPutA[0]]
354
- deepEqual(mocks, [
355
- fixtureForRegisteringPutA[1],
356
- fixtureForRegisteringPutB[1],
357
- fixtureForRegisteringPutA500[1]
358
- ])
359
- deepEqual(currentMock, {
360
- file: fixtureForRegisteringPutA[1],
361
- delayed: false
362
- })
363
- })
364
- await it('unregisters selected', async () => {
365
- await commander.select(fixtureForRegisteringPutA[1])
366
- remove(fixtureForRegisteringPutA[1])
367
- await sleep()
368
- const collection = await (await commander.listMocks()).json()
369
- const { mocks, currentMock } = collection['PUT'][fixtureForRegisteringPutA[0]]
370
- deepEqual(mocks, [
371
- fixtureForRegisteringPutB[1],
372
- fixtureForRegisteringPutA500[1]
373
- ])
374
- deepEqual(currentMock, {
375
- file: fixtureForRegisteringPutB[1],
376
- delayed: false
377
- })
378
- })
379
- await it('unregistering the last mock removes broker', async () => {
380
- write(fixtureForUnregisteringPutC[1], '') // Register another PUT so it doesn't delete PUT from collection
381
- await sleep()
382
- remove(fixtureForUnregisteringPutC[1])
383
- await sleep()
384
- const collection = await (await commander.listMocks()).json()
385
- equal(collection['PUT'][fixtureForUnregisteringPutC[0]], undefined)
386
- })
387
-
388
- await it('unregistering the last PUT mock removes PUT from collection', async () => {
389
- remove(fixtureForRegisteringPutB[1])
390
- remove(fixtureForRegisteringPutA500[1])
391
- await sleep()
392
- const collection = await (await commander.listMocks()).json()
393
- equal(collection['PUT'], undefined)
394
- })
395
- })
396
- }
397
-
398
-
399
- async function testItUpdatesTheCurrentSelectedMock(url, file, expectedStatus, expectedBody) {
400
- await commander.select(file)
401
- const res = await request(url)
402
- const body = await res.text()
403
- await describe('url: ' + url, () => {
404
- it('body is: ' + expectedBody, () => equal(body, expectedBody))
405
- it('status is: ' + expectedStatus, () => equal(res.status, expectedStatus))
406
- })
407
- }
408
-
409
- async function testItUpdatesRouteDelay(url, file, expectedBody) {
410
- const { method } = parseFilename(file)
411
- await commander.setRouteIsDelayed(method, url, true)
412
- const now = new Date()
413
- const res = await request(url)
414
- const body = await res.text()
415
- await describe('url: ' + url, () => {
416
- it('body is: ' + expectedBody, () => equal(body, JSON.stringify(expectedBody)))
417
- it('delay', () => equal((new Date()).getTime() - now.getTime() > config.delay, true))
418
- // TODO flaky test ^
419
- })
420
- }
421
-
422
- async function testBadRequestWhenUpdatingNonExistingMockAlternative() {
423
- await it('There are mocks for /api/the-route but not this one', async () => {
424
- const missingFile = 'api/the-route(non-existing-variant).GET.200.json'
425
- const res = await commander.select(missingFile)
426
- equal(res.status, 422)
427
- equal(await res.text(), `Missing Mock: ${missingFile}`)
428
- })
429
- }
430
-
431
- async function testAutogenerates500(url, file) {
432
- await commander.select(file)
433
- const res = await request(url)
434
- const body = await res.text()
435
- await describe('autogenerated in-memory 500', () => {
436
- it('body is empty', () => equal(body, ''))
437
- it('status is: 500', () => equal(res.status, 500))
438
- it('mime is empty', () => equal(res.headers.get('content-type'), ''))
439
- })
440
- }
441
-
442
- async function testPreservesExiting500(url, file, expectedBody) {
443
- await commander.select(file)
444
- const res = await request(url)
445
- const body = await res.text()
446
- await describe('preserves existing 500', () => {
447
- it('body is empty', () => equal(body, expectedBody))
448
- it('status is: 500', () => equal(res.status, 500))
449
- })
450
- }
451
-
452
- async function testExtractsAllComments(expected) {
453
- const res = await commander.listComments()
454
- const body = await res.json()
455
- await it('Extracts all comments without duplicates', () =>
456
- deepEqual(body, expected))
457
- }
458
-
459
- async function testItBulkSelectsByComment(comment, tests) {
460
- await commander.bulkSelectByComment(comment)
461
- for (const [url, file, body] of tests)
462
- await testMockDispatching(url, file, body)
463
- }
464
-
465
-
466
- async function testItUpdatesCookie() {
467
- await describe('Cookie', () => {
468
- it('Defaults to the first key:value', async () => {
469
- const res = await commander.listCookies()
470
- deepEqual(await res.json(), [
471
- ['userA', true],
472
- ['userB', false]
473
- ])
474
- })
475
-
476
- it('Updates selected cookie', async () => {
477
- await commander.selectCookie('userB')
478
- const res = await commander.listCookies()
479
- deepEqual(await res.json(), [
480
- ['userA', false],
481
- ['userB', true]
482
- ])
483
- })
484
-
485
- it('422 when trying to select non-existing cookie', async () => {
486
- const res = await commander.selectCookie('non-existing-cookie-key')
487
- equal(res.status, 422)
488
- })
489
- })
490
- }
491
-
492
- async function testJsFunctionMocks() {
493
- await describe('JS Function Mocks', async () => {
494
- write('api/js-func.POST.200.js', `
495
- export default function (req, response) {
496
- response.setHeader('content-type', 'custom-mime')
497
- return 'SOME_STRING'
498
- }`)
499
- await commander.reset() // for registering the file
500
- await testMockDispatching('/api/js-func',
501
- 'api/js-func.POST.200.js',
502
- 'SOME_STRING',
503
- 'custom-mime')
504
- })
505
- }
506
-
507
-
508
- async function testStaticFileServing() {
509
- await describe('Static File Serving', () => {
510
- it('404 path traversal', async () => {
511
- const res = await request('/../../../../../../../../../../../%2E%2E/etc/passwd')
512
- equal(res.status, 404)
513
- })
514
-
515
- it('Defaults to index.html', async () => {
516
- const res = await request('/')
517
- const body = await res.text()
518
- equal(body, '<h1>Static</h1>')
519
- equal(res.status, 200)
520
- })
521
-
522
- it('Defaults to in subdirs index.html', async () => {
523
- const res = await request('/another-entry')
524
- const body = await res.text()
525
- equal(body, '<h1>Another</h1>')
526
- equal(res.status, 200)
527
- })
528
-
529
- it('Serves exacts paths', async () => {
530
- const res = await request('/assets/app.js')
531
- const body = await res.text()
532
- equal(body, 'const app = 1')
533
- equal(res.status, 200)
534
- })
535
- })
536
- }
537
-
538
- async function testStaticFileList() {
539
- await it('Static File List', async () => {
540
- const res = await commander.listStaticFiles()
541
- deepEqual((await res.json()).sort(), staticFiles.map(([file]) => file).sort())
542
- })
543
- }
544
-
545
- async function testInvalidFilenamesAreIgnored() {
546
- await it('Invalid filenames get skipped, so they don’t crash the server', async (t) => {
547
- const consoleErrorSpy = t.mock.method(console, 'error')
548
- consoleErrorSpy.mock.mockImplementation(() => {}) // so they don’t render in the test report
549
-
550
- write('api/_INVALID_FILENAME_CONVENTION_.json', '')
551
- write('api/bad-filename-method._INVALID_METHOD_.200.json', '')
552
- write('api/bad-filename-status.GET._INVALID_STATUS_.json', '')
553
- await commander.reset()
554
- equal(consoleErrorSpy.mock.calls[0].arguments[0], 'Invalid Filename Convention')
555
- equal(consoleErrorSpy.mock.calls[1].arguments[0], 'Unrecognized HTTP Method: "_INVALID_METHOD_"')
556
- equal(consoleErrorSpy.mock.calls[2].arguments[0], 'Invalid HTTP Response Status: "NaN"')
557
- })
558
- }
559
-
560
- async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
561
- await describe('Fallback', async () => {
562
- const fallbackServer = createServer(async (req, response) => {
563
- response.writeHead(423, {
564
- 'custom_header': 'my_custom_header',
565
- 'content-type': mimeFor('.txt'),
566
- 'set-cookie': [
567
- 'cookieA=A',
568
- 'cookieB=B'
569
- ]
570
- })
571
- response.end(await readBody(req)) // echoes they req body payload
572
- })
573
- await promisify(fallbackServer.listen).bind(fallbackServer, 0, '127.0.0.1')()
574
-
575
- await commander.setProxyFallback(`http://localhost:${fallbackServer.address().port}`)
576
- await commander.setCollectProxied(true)
577
- await it('Relays to fallback server and saves the mock', async () => {
578
- const reqBodyPayload = 'text_req_body'
579
-
580
- const res = await request(`/api/non-existing-mock/${randomUUID()}`, {
581
- method: 'POST',
582
- body: reqBodyPayload
583
- })
584
- equal(res.status, 423)
585
- equal(res.headers.get('custom_header'), 'my_custom_header')
586
- equal(res.headers.get('set-cookie'), ['cookieA=A', 'cookieB=B'].join(', '))
587
- equal(await res.text(), reqBodyPayload)
588
-
589
- const savedBody = readFileSync(join(tmpDir, 'api/non-existing-mock/[id].POST.423.txt'), 'utf8')
590
- equal(savedBody, reqBodyPayload)
591
-
592
- fallbackServer.close()
593
- })
594
- })
595
- }
596
-
597
- async function testValidatesProxyFallbackURL() {
598
- await it('422 when value is not a valid URL', async () => {
599
- const res = await commander.setProxyFallback('bad url')
600
- equal(res.status, 422)
601
- })
602
- }
603
-
604
- async function testCorsAllowed() {
605
- await it('cors preflight', async () => {
606
- await commander.setCorsAllowed(true)
607
- const res = await request('/does-not-matter', {
608
- method: 'OPTIONS',
609
- headers: {
610
- [CorsHeader.Origin]: 'http://example.com',
611
- [CorsHeader.AcRequestMethod]: 'GET'
612
- }
613
- })
614
- equal(res.status, 204)
615
- equal(res.headers.get(CorsHeader.AcAllowOrigin), 'http://example.com')
616
- equal(res.headers.get(CorsHeader.AcAllowMethods), 'GET')
617
- })
618
- await it('cors actual response', async () => {
619
- const res = await request(fixtureDefaultInName[0], {
620
- headers: {
621
- [CorsHeader.Origin]: 'http://example.com'
622
- }
623
- })
624
- equal(res.status, 200)
625
- equal(res.headers.get(CorsHeader.AcAllowOrigin), 'http://example.com')
626
- equal(res.headers.get(CorsHeader.AcExposeHeaders), 'Content-Encoding')
627
- })
628
- }
629
-
630
- function testWindowsPaths() {
631
- it('normalizes backslashes with forward ones', () => {
632
- const files = listFilesRecursively(config.mocksDir)
633
- equal(files[0], 'api/.GET.200.json')
634
- })
635
- }
636
-
637
-
638
- // Utils
639
-
640
- function write(filename, data) {
641
- _write(tmpDir + filename, data)
642
- }
643
-
644
- function remove(filename) {
645
- unlinkSync(tmpDir + filename)
646
- }
647
-
648
- function writeStatic(filename, data) {
649
- _write(staticTmpDir + filename, data)
650
- }
651
-
652
- function _write(absPath, data) {
653
- mkdirSync(dirname(absPath), { recursive: true })
654
- writeFileSync(absPath, data, 'utf8')
655
- }
656
-
657
- async function sleep(ms = 50) {
658
- return new Promise(resolve => setTimeout(resolve, ms))
659
- }
@@ -1,225 +0,0 @@
1
- import { equal } from 'node:assert/strict'
2
- import { promisify } from 'node:util'
3
- import { createServer } from 'node:http'
4
- import { describe, it, after } from 'node:test'
5
- import { isPreflight, setCorsHeaders, CorsHeader as CH } from './http-cors.js'
6
-
7
-
8
- function headerIs(response, header, value) {
9
- equal(response.headers.get(header), value)
10
- }
11
-
12
- const FooDotCom = 'http://foo.com'
13
- const AllowedDotCom = 'http://allowed.com'
14
- const NotAllowedDotCom = 'http://not-allowed.com'
15
-
16
- await describe('CORS', async () => {
17
- let corsConfig = {}
18
-
19
- const server = createServer((req, response) => {
20
- setCorsHeaders(req, response, corsConfig)
21
- if (isPreflight(req)) {
22
- response.statusCode = 204
23
- response.end()
24
- }
25
- else
26
- response.end('NON_PREFLIGHT')
27
- })
28
- await promisify(server.listen).bind(server, 0, '127.0.0.1')()
29
- after(() => {
30
- server.close()
31
- })
32
- function preflight(headers, method = 'OPTIONS') {
33
- const { address, port } = server.address()
34
- return fetch(`http://${address}:${port}/`, { method, headers })
35
- }
36
- function request(headers, method) {
37
- const { address, port } = server.address()
38
- return fetch(`http://${address}:${port}/`, { method, headers })
39
- }
40
-
41
- await describe('Identifies Preflight Requests', async () => {
42
- const requiredRequestHeaders = {
43
- [CH.Origin]: 'http://localhost:9999',
44
- [CH.AcRequestMethod]: 'POST'
45
- }
46
-
47
- await it('Ignores non-OPTIONS requests', async () => {
48
- const res = await request(requiredRequestHeaders, 'POST')
49
- equal(await res.text(), 'NON_PREFLIGHT')
50
- })
51
-
52
- await it(`Ignores non-parseable req ${CH.Origin} header`, async () => {
53
- const headers = {
54
- ...requiredRequestHeaders,
55
- [CH.Origin]: 'non-url'
56
- }
57
- const res = await preflight(headers)
58
- equal(await res.text(), 'NON_PREFLIGHT')
59
- })
60
-
61
- await it(`Ignores missing method in ${CH.AcRequestMethod} header`, async () => {
62
- const headers = { ...requiredRequestHeaders }
63
- delete headers[CH.AcRequestMethod]
64
- const res = await preflight(headers)
65
- equal(await res.text(), 'NON_PREFLIGHT')
66
- })
67
-
68
- await it(`Ignores non-standard method in ${CH.AcRequestMethod} header`, async () => {
69
- const headers = {
70
- ...requiredRequestHeaders,
71
- [CH.AcRequestMethod]: 'NON_STANDARD'
72
- }
73
- const res = await preflight(headers)
74
- equal(await res.text(), 'NON_PREFLIGHT')
75
- })
76
-
77
- await it('204 valid preflights', async () => {
78
- const res = await preflight(requiredRequestHeaders)
79
- equal(res.status, 204)
80
- })
81
- })
82
-
83
- await describe('Preflight Response Headers', async () => {
84
- await it('no origins allowed', async () => {
85
- corsConfig = {
86
- corsOrigins: [],
87
- corsMethods: ['GET']
88
- }
89
- const p = await preflight({
90
- [CH.Origin]: FooDotCom,
91
- [CH.AcRequestMethod]: 'GET'
92
- })
93
- headerIs(p, CH.AcAllowOrigin, null)
94
- headerIs(p, CH.AcAllowMethods, null)
95
- headerIs(p, CH.AcAllowCredentials, null)
96
- headerIs(p, CH.AcAllowHeaders, null)
97
- headerIs(p, CH.AcMaxAge, null)
98
- })
99
-
100
- await it('not in allowed origins', async () => {
101
- corsConfig = {
102
- corsOrigins: [AllowedDotCom],
103
- corsMethods: ['GET']
104
- }
105
- const p = await preflight({
106
- [CH.Origin]: NotAllowedDotCom,
107
- [CH.AcRequestMethod]: 'GET'
108
- })
109
- headerIs(p, CH.AcAllowOrigin, null)
110
- headerIs(p, CH.AcAllowMethods, null)
111
- headerIs(p, CH.AcAllowCredentials, null)
112
- headerIs(p, CH.AcAllowHeaders, null)
113
- })
114
-
115
- await it('origin and method match', async () => {
116
- corsConfig = {
117
- corsOrigins: [AllowedDotCom],
118
- corsMethods: ['GET']
119
- }
120
- const p = await preflight({
121
- [CH.Origin]: AllowedDotCom,
122
- [CH.AcRequestMethod]: 'GET'
123
- })
124
- headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
125
- headerIs(p, CH.AcAllowMethods, 'GET')
126
- headerIs(p, CH.AcAllowCredentials, null)
127
- headerIs(p, CH.AcAllowHeaders, null)
128
- })
129
-
130
- await it('origin matches from multiple', async () => {
131
- corsConfig = {
132
- corsOrigins: [AllowedDotCom, FooDotCom],
133
- corsMethods: ['GET']
134
- }
135
- const p = await preflight({
136
- [CH.Origin]: AllowedDotCom,
137
- [CH.AcRequestMethod]: 'GET'
138
- })
139
- headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
140
- headerIs(p, CH.AcAllowMethods, 'GET')
141
- headerIs(p, CH.AcAllowCredentials, null)
142
- headerIs(p, CH.AcAllowHeaders, null)
143
- })
144
-
145
- await it('wildcard origin', async () => {
146
- corsConfig = {
147
- corsOrigins: ['*'],
148
- corsMethods: ['GET']
149
- }
150
- const p = await preflight({
151
- [CH.Origin]: FooDotCom,
152
- [CH.AcRequestMethod]: 'GET'
153
- })
154
- headerIs(p, CH.AcAllowOrigin, FooDotCom)
155
- headerIs(p, CH.AcAllowMethods, 'GET')
156
- headerIs(p, CH.AcAllowCredentials, null)
157
- headerIs(p, CH.AcAllowHeaders, null)
158
- })
159
-
160
- await it(`wildcard and credentials`, async () => {
161
- corsConfig = {
162
- corsOrigins: ['*'],
163
- corsMethods: ['GET'],
164
- corsCredentials: true
165
- }
166
- const p = await preflight({
167
- [CH.Origin]: FooDotCom,
168
- [CH.AcRequestMethod]: 'GET'
169
- })
170
- headerIs(p, CH.AcAllowOrigin, FooDotCom)
171
- headerIs(p, CH.AcAllowMethods, 'GET')
172
- headerIs(p, CH.AcAllowCredentials, 'true')
173
- headerIs(p, CH.AcAllowHeaders, null)
174
- })
175
-
176
- await it(`wildcard, credentials, and headers`, async () => {
177
- corsConfig = {
178
- corsOrigins: ['*'],
179
- corsMethods: ['GET'],
180
- corsCredentials: true,
181
- corsHeaders: ['content-type', 'my-header']
182
- }
183
- const p = await preflight({
184
- [CH.Origin]: FooDotCom,
185
- [CH.AcRequestMethod]: 'GET'
186
- })
187
- headerIs(p, CH.AcAllowOrigin, FooDotCom)
188
- headerIs(p, CH.AcAllowMethods, 'GET')
189
- headerIs(p, CH.AcAllowCredentials, 'true')
190
- headerIs(p, CH.AcAllowHeaders, 'content-type,my-header')
191
- })
192
- })
193
-
194
- await describe('Non-Preflight (Actual Response) Headers', async () => {
195
- await it('no origins allowed', async () => {
196
- corsConfig = {
197
- corsOrigins: [],
198
- corsMethods: ['GET']
199
- }
200
- const p = await request({
201
- [CH.Origin]: NotAllowedDotCom
202
- })
203
- equal(p.status, 200)
204
- headerIs(p, CH.AcAllowOrigin, null)
205
- headerIs(p, CH.AcAllowCredentials, null)
206
- headerIs(p, CH.AcExposeHeaders, null)
207
- })
208
-
209
- await it('origin allowed', async () => {
210
- corsConfig = {
211
- corsOrigins: [AllowedDotCom],
212
- corsMethods: ['GET'],
213
- corsCredentials: true,
214
- corsExposedHeaders: ['x-h1', 'x-h2']
215
- }
216
- const p = await request({
217
- [CH.Origin]: AllowedDotCom
218
- })
219
- equal(p.status, 200)
220
- headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
221
- headerIs(p, CH.AcAllowCredentials, 'true')
222
- headerIs(p, CH.AcExposeHeaders, 'x-h1,x-h2')
223
- })
224
- })
225
- })
@@ -1,47 +0,0 @@
1
- import { describe, it } from 'node:test'
2
- import { doesNotThrow, throws } from 'node:assert/strict'
3
- import { validate, is, optional } from './validate.js'
4
-
5
-
6
- describe('validate', () => {
7
- describe('optional', () => {
8
- it('accepts undefined', () =>
9
- doesNotThrow(() =>
10
- validate({}, { field: optional(Number.isInteger) })))
11
-
12
- it('accepts falsy value regardless of type', () =>
13
- doesNotThrow(() =>
14
- validate({ field: 0 }, { field: optional(Array.isArray) })))
15
-
16
- it('accepts when tester func returns truthy', () =>
17
- doesNotThrow(() =>
18
- validate({ field: [] }, { field: optional(Array.isArray) })))
19
-
20
- it('rejects when tester func returns falsy', () =>
21
- throws(() =>
22
- validate({ field: 1 }, { field: optional(Array.isArray) }),
23
- /field=1 is invalid/))
24
- })
25
-
26
- describe('is', () => {
27
- it('rejects mismatched type', () =>
28
- throws(() =>
29
- validate({ field: 1 }, { field: is(String) }),
30
- /field=1 is invalid/))
31
-
32
- it('accepts matched type', () =>
33
- doesNotThrow(() =>
34
- validate({ field: '' }, { field: is(String) })))
35
- })
36
-
37
- describe('custom tester func', () => {
38
- it('rejects mismatched type', () =>
39
- throws(() =>
40
- validate({ field: 0.1 }, { field: Number.isInteger }),
41
- /field=0.1 is invalid/))
42
-
43
- it('accepts matched type', () =>
44
- doesNotThrow(() =>
45
- validate({ field: 1 }, { field: Number.isInteger })))
46
- })
47
- })