mockaton 8.12.4 → 8.12.6

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "8.12.4",
5
+ "version": "8.12.6",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -51,7 +51,7 @@ export const apiPatchRequests = new Map([
51
51
  ])
52
52
 
53
53
 
54
- /* === GET === */
54
+ /** # GET */
55
55
 
56
56
  function serveDashboard(_, response) {
57
57
  sendFile(response, join(import.meta.dirname, 'Dashboard.html'))
@@ -97,7 +97,7 @@ function longPollAR_Events(req, response) {
97
97
 
98
98
 
99
99
 
100
- /* === PATCH === */
100
+ /** # PATCH */
101
101
 
102
102
  function reinitialize(_, response) {
103
103
  mockBrokersCollection.init()
package/src/Commander.js CHANGED
@@ -92,9 +92,9 @@ export class Commander {
92
92
  return this.#patch(API.reset)
93
93
  }
94
94
 
95
- getAR_EventsCount(nAR_EventReceived) {
95
+ getAR_EventsCount(nAR_EventReceived, abortSignal) {
96
96
  return fetch(API.arEvents, {
97
- signal: AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000),
97
+ signal: AbortSignal.any([abortSignal, AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)]),
98
98
  headers: {
99
99
  [DF.lastReceived_nAR]: nAR_EventReceived
100
100
  }
package/src/Dashboard.js CHANGED
@@ -68,11 +68,7 @@ let globalDelay = 1200
68
68
 
69
69
 
70
70
  init()
71
- pollAR_Events() // Add or Remove Mocks from File System
72
- document.addEventListener('visibilitychange', () => {
73
- if (!document.hidden)
74
- pollAR_Events()
75
- })
71
+ initLongPoll()
76
72
 
77
73
  function init() {
78
74
  return Promise.all([
@@ -97,7 +93,8 @@ function App([brokersByMethod, cookies, comments, delay, collectProxied, fallbac
97
93
  r(StaticFilesList, { staticFiles })))
98
94
  }
99
95
 
100
- // Header ===============
96
+
97
+ /** # Header */
101
98
 
102
99
  function Header({ cookies, comments, delay, fallbackAddress, collectProxied }) {
103
100
  return (
@@ -240,8 +237,7 @@ function ResetButton() {
240
237
  }
241
238
 
242
239
 
243
-
244
- // MockList ===============
240
+ /** # MockList */
245
241
 
246
242
  function MockList({ brokersByMethod, canProxy }) {
247
243
  const hasMocks = Object.keys(brokersByMethod).length
@@ -256,7 +252,6 @@ function MockList({ brokersByMethod, canProxy }) {
256
252
  r(PayloadViewer)))
257
253
  }
258
254
 
259
-
260
255
  function SectionByMethod({ method, brokers, canProxy }) {
261
256
  return (
262
257
  r('tbody', null,
@@ -273,7 +268,6 @@ function SectionByMethod({ method, brokers, canProxy }) {
273
268
  r('td', null, r(ProxyToggler, { broker, disabled: !canProxy }))))))
274
269
  }
275
270
 
276
-
277
271
  function PreviewLink({ method, urlMask }) {
278
272
  async function onClick(event) {
279
273
  event.preventDefault()
@@ -294,7 +288,6 @@ function PreviewLink({ method, urlMask }) {
294
288
  }, urlMask))
295
289
  }
296
290
 
297
-
298
291
  function MockSelector({ broker }) {
299
292
  function onChange() {
300
293
  const { urlMask, method } = parseFilename(this.value)
@@ -349,7 +342,6 @@ function DelayRouteToggler({ broker }) {
349
342
  TimerIcon()))
350
343
  }
351
344
 
352
-
353
345
  function InternalServerErrorToggler({ broker }) {
354
346
  function onChange() {
355
347
  const { urlMask, method } = parseFilename(broker.mocks[0])
@@ -375,7 +367,6 @@ function InternalServerErrorToggler({ broker }) {
375
367
  r('span', null, '500')))
376
368
  }
377
369
 
378
-
379
370
  function ProxyToggler({ broker, disabled }) {
380
371
  function onChange() {
381
372
  const { urlMask, method } = parseFilename(broker.mocks[0])
@@ -399,8 +390,7 @@ function ProxyToggler({ broker, disabled }) {
399
390
  }
400
391
 
401
392
 
402
-
403
- // Payload Preview ===============
393
+ /** # Payload Preview */
404
394
 
405
395
  const payloadViewerTitleRef = useRef()
406
396
  const payloadViewerRef = useRef()
@@ -497,8 +487,7 @@ function mockSelectorFor(method, urlMask) {
497
487
  }
498
488
 
499
489
 
500
-
501
- // StaticFilesList ===============
490
+ /** # StaticFilesList */
502
491
 
503
492
  function StaticFilesList({ staticFiles }) {
504
493
  if (!staticFiles.length)
@@ -515,7 +504,7 @@ function StaticFilesList({ staticFiles }) {
515
504
  }
516
505
 
517
506
 
518
- // Misc ===============
507
+ /** # Misc */
519
508
 
520
509
  function onError(error) {
521
510
  if (error?.message === 'Failed to fetch')
@@ -537,16 +526,29 @@ function CloudIcon() {
537
526
  }
538
527
 
539
528
 
540
- // AR Events (Add or Remove mock) ============
529
+ /** # Poll AR Events - Add or Remove mock */
530
+
531
+ function initLongPoll() {
532
+ pollAR_Events.oldAR_EventsCount = 0
533
+ pollAR_Events.isPolling = false
534
+ pollAR_Events.controller = new AbortController()
535
+ pollAR_Events()
536
+ document.addEventListener('visibilitychange', () => {
537
+ if (document.hidden) {
538
+ pollAR_Events.controller.abort('_was_hidden_')
539
+ pollAR_Events.controller = new AbortController()
540
+ }
541
+ else
542
+ pollAR_Events()
543
+ })
544
+ }
541
545
 
542
- pollAR_Events.isPolling = false
543
- pollAR_Events.oldAR_EventsCount = 0
544
546
  async function pollAR_Events() {
545
- if (pollAR_Events.isPolling || document.hidden)
547
+ if (pollAR_Events.isPolling)
546
548
  return
547
549
  try {
548
550
  pollAR_Events.isPolling = true
549
- const response = await mockaton.getAR_EventsCount(pollAR_Events.oldAR_EventsCount)
551
+ const response = await mockaton.getAR_EventsCount(pollAR_Events.oldAR_EventsCount, pollAR_Events.controller.signal)
550
552
  if (response.ok) {
551
553
  const nAR_Events = await response.json()
552
554
  if (pollAR_Events.oldAR_EventsCount !== nAR_Events) { // because it could be < or >
@@ -559,21 +561,22 @@ async function pollAR_Events() {
559
561
  else
560
562
  throw response.status
561
563
  }
562
- catch (_) {
564
+ catch (error) {
563
565
  pollAR_Events.isPolling = false
564
- setTimeout(pollAR_Events, 5000)
566
+ if (error !== '_was_hidden_')
567
+ setTimeout(pollAR_Events, 3000)
565
568
  }
566
569
  }
567
570
 
568
571
 
569
- // Utils ============
572
+ /** # Utils */
570
573
 
571
574
  function cssClass(...args) {
572
575
  return args.filter(Boolean).join(' ')
573
576
  }
574
577
 
575
578
 
576
- // These are simplified React-compatible implementations
579
+ /** ## React-compatible simplified implementations */
577
580
 
578
581
  function createElement(elem, props = null, ...children) {
579
582
  if (typeof elem === 'function')
package/src/MockBroker.js CHANGED
@@ -2,8 +2,8 @@ import { includesComment, extractComments, parseFilename } from './Filename.js'
2
2
  import { DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
3
3
 
4
4
 
5
- // MockBroker is a state for a particular route. It knows the available mock files
6
- // that can be served for the route, the currently selected file, and if it’s delayed.
5
+ /** MockBroker is a state for a particular route. It knows the available mock files
6
+ * that can be served for the route, the currently selected file, and if it’s delayed. */
7
7
  export class MockBroker {
8
8
  constructor(file) {
9
9
  this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
package/src/Watcher.js CHANGED
@@ -7,7 +7,7 @@ import { isFile } from './utils/fs.js'
7
7
  import * as mockBrokerCollection from './mockBrokersCollection.js'
8
8
 
9
9
 
10
- // AR = Add or Remove Mock
10
+ /** # AR = Add or Remove Mock */
11
11
  export const arEvents = new class extends EventEmitter {
12
12
  count = 0
13
13
 
@@ -16,7 +16,7 @@ export const arEvents = new class extends EventEmitter {
16
16
  super.emit('AR')
17
17
  }
18
18
  subscribe(listener) {
19
- this.on('AR', listener)
19
+ this.once('AR', listener)
20
20
  }
21
21
  unsubscribe(listener) {
22
22
  this.removeListener('AR', listener)
@@ -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
- })