mockaton 10.1.0 → 10.2.1

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
@@ -754,6 +754,13 @@ const mockaton = new Commander(myMockatonAddr)
754
754
  await mockaton.select('api/foo.200.GET.json')
755
755
  ```
756
756
 
757
+ ### Toggle 500
758
+ Either selects the first found 500, which could be
759
+ the autogenerated one, or selects the default file.
760
+ ```js
761
+ await mockaton.toggle500('GET', '/api/foo')
762
+ ```
763
+
757
764
  ### Select all mocks that have a particular comment
758
765
  ```js
759
766
  await mockaton.bulkSelectByComment('(demo-a)')
package/index.d.ts CHANGED
@@ -97,5 +97,5 @@ export interface State {
97
97
  collectProxied: boolean
98
98
  proxyFallback: string
99
99
 
100
- corsAllowed: boolean
100
+ corsAllowed?: boolean
101
101
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "10.1.0",
5
+ "version": "10.2.1",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -38,6 +38,7 @@ export const apiPatchRequests = new Map([
38
38
  [API.proxied, setRouteIsProxied],
39
39
  [API.cookies, selectCookie],
40
40
  [API.fallback, updateProxyFallback],
41
+ [API.toggle500, toggle500],
41
42
  [API.bulkSelect, bulkUpdateBrokersByCommentTag],
42
43
  [API.globalDelay, setGlobalDelay],
43
44
  [API.collectProxied, setCollectProxied],
@@ -104,7 +105,7 @@ function reinitialize(_, response) {
104
105
  async function selectCookie(req, response) {
105
106
  const error = cookie.setCurrent(await parseJSON(req))
106
107
  if (error)
107
- sendUnprocessableContent(response, error)
108
+ sendUnprocessableContent(response, error?.message || error)
108
109
  else
109
110
  sendOK(response)
110
111
  }
@@ -120,6 +121,19 @@ async function selectMock(req, response) {
120
121
  }
121
122
  }
122
123
 
124
+ async function toggle500(req, response) {
125
+ const body = await parseJSON(req)
126
+ const broker = mockBrokersCollection.brokerByRoute(
127
+ body[DF.routeMethod],
128
+ body[DF.routeUrlMask])
129
+ if (!broker)
130
+ sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
131
+ else {
132
+ broker.toggle500()
133
+ sendOK(response)
134
+ }
135
+ }
136
+
123
137
  async function setRouteIsDelayed(req, response) {
124
138
  const body = await parseJSON(req)
125
139
  const delayed = body[DF.delayed]
@@ -19,7 +19,7 @@ export class Commander {
19
19
  getState() {
20
20
  return fetch(this.#addr + API.state)
21
21
  }
22
-
22
+
23
23
  /** @returns {JsonPromise<number>} */
24
24
  getSyncVersion(currentSyncVersion, abortSignal) {
25
25
  return fetch(this.#addr + API.syncVersion, {
@@ -39,6 +39,13 @@ export class Commander {
39
39
  return this.#patch(API.select, file)
40
40
  }
41
41
 
42
+ toggle500(routeMethod, routeUrlMask) {
43
+ return this.#patch(API.toggle500, {
44
+ [DF.routeMethod]: routeMethod,
45
+ [DF.routeUrlMask]: routeUrlMask,
46
+ })
47
+ }
48
+
42
49
  bulkSelectByComment(comment) {
43
50
  return this.#patch(API.bulkSelect, comment)
44
51
  }
@@ -16,6 +16,7 @@ export const API = {
16
16
  staticStatus: MOUNT + '/static-status',
17
17
  syncVersion: MOUNT + '/sync-version',
18
18
  throws: MOUNT + '/throws',
19
+ toggle500: MOUNT + '/toggle500'
19
20
  }
20
21
 
21
22
  export const DF = { // Dashboard Fields (XHR)
@@ -27,10 +28,9 @@ export const DF = { // Dashboard Fields (XHR)
27
28
  syncVersion: 'last_received_sync_version'
28
29
  }
29
30
 
30
- export const DEFAULT_500_COMMENT = '(Mockaton 500)'
31
+ export const AUTOGENERATED_500_COMMENT = '(Mockaton 500)'
31
32
  export const DEFAULT_MOCK_COMMENT = '(default)'
32
33
  export const EXT_FOR_UNKNOWN_MIME = 'unknown'
33
34
  export const LONG_POLL_SERVER_TIMEOUT = 8_000
34
35
 
35
-
36
- export const HEADER_FOR_502 = 'mockaton502'
36
+ export const HEADER_FOR_502 = 'Mockaton502'
package/src/Dashboard.css CHANGED
@@ -485,7 +485,6 @@ table {
485
485
  }
486
486
 
487
487
  .ProxyToggler {
488
- padding: 1px 3px;
489
488
  border: 1px solid var(--colorSecondaryActionBorder);
490
489
  margin-right: 8px;
491
490
  border-radius: var(--radius);
@@ -504,7 +503,7 @@ table {
504
503
  path:last-of-type { /* inner cloud curve */
505
504
  stroke: var(--colorBackground);
506
505
  }
507
- transform: scale(1.2);
506
+ transform: scale(1.1);
508
507
  }
509
508
 
510
509
  &:enabled:hover:not(:checked) ~ svg {
@@ -526,8 +525,9 @@ table {
526
525
  }
527
526
 
528
527
  > svg {
529
- width: 18px;
530
- height: 18px;
528
+ width: 22px;
529
+ height: 22px;
530
+ padding: 1px 3px;
531
531
  stroke-width: 2px;
532
532
  border-radius: var(--radius);
533
533
  }
@@ -543,7 +543,7 @@ table {
543
543
  > input {
544
544
  appearance: none;
545
545
 
546
- &:focus-visible {
546
+ &:focus {
547
547
  outline: 0;
548
548
  & ~ span {
549
549
  outline: 2px solid var(--colorAccent)
@@ -5,7 +5,7 @@
5
5
  <link rel="stylesheet" href="./mockaton/Dashboard.css">
6
6
  <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
- <meta name="description" content="Mock Server for developing UIs">
8
+ <meta name="description" content="HTTP Mock Server">
9
9
  <title>Mockaton</title>
10
10
  </head>
11
11
  <body>
package/src/Dashboard.js CHANGED
@@ -1,9 +1,10 @@
1
- import { DEFAULT_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
1
+ import { AUTOGENERATED_500_COMMENT, HEADER_FOR_502, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
2
2
  import { parseFilename, extractComments } from './Filename.js'
3
3
  import { Commander } from './ApiCommander.js'
4
4
 
5
5
 
6
6
  const Strings = {
7
+ auto500: 'Auto500',
7
8
  bulk_select: 'Bulk Select',
8
9
  bulk_select_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
9
10
  click_link_to_preview: 'Click a link to preview it',
@@ -78,8 +79,10 @@ for (const k of Object.keys(CSS))
78
79
 
79
80
 
80
81
  /** @type {State & {
81
- * groupByMethod: boolean,
82
82
  * canProxy: boolean
83
+ * groupByMethod: boolean
84
+ * toggleGroupByMethod: () => void
85
+ * leftSideWidth?: number
83
86
  * }} */
84
87
  const state = {
85
88
  brokersByMethod: {},
@@ -87,6 +90,7 @@ const state = {
87
90
  cookies: [],
88
91
  comments: [],
89
92
  delay: 0,
93
+
90
94
  collectProxied: false,
91
95
  proxyFallback: '',
92
96
  get canProxy() {
@@ -127,19 +131,19 @@ const leftSideRef = useRef()
127
131
  function App() {
128
132
  const { leftSideWidth } = state
129
133
  return [
130
- r(Header),
131
- r(Menu),
134
+ Header(),
135
+ Menu(),
132
136
  r('main', null,
133
137
  r('div', {
134
138
  ref: leftSideRef,
135
139
  style: { width: leftSideWidth + 'px' },
136
140
  className: CSS.leftSide
137
141
  },
138
- r(MockList),
139
- r(StaticFilesList)),
142
+ MockList(),
143
+ StaticFilesList()),
140
144
  r('div', { className: CSS.rightSide },
141
- r(Resizer),
142
- r(PayloadViewer)))
145
+ Resizer(),
146
+ PayloadViewer()))
143
147
  ]
144
148
  }
145
149
 
@@ -153,15 +157,15 @@ function Header() {
153
157
  width: 160
154
158
  }),
155
159
  r('div', null,
156
- r(GlobalDelayField),
157
- r(CookieSelector),
158
- r(BulkSelector),
159
- r(ProxyFallbackField),
160
- r(ResetButton)),
160
+ GlobalDelayField(),
161
+ CookieSelector(),
162
+ BulkSelector(),
163
+ ProxyFallbackField(),
164
+ ResetButton()),
161
165
  r('button', {
162
166
  className: CSS.MenuTrigger,
163
167
  popovertarget: 'Menu'
164
- }, r(SettingsIcon))
168
+ }, SettingsIcon())
165
169
  ))
166
170
  }
167
171
 
@@ -226,7 +230,10 @@ function BulkSelector() {
226
230
  const disabled = !comments.length
227
231
  const list = disabled
228
232
  ? []
229
- : [firstOption].concat(comments)
233
+ : [firstOption, ...comments].map(c => [
234
+ c,
235
+ c === AUTOGENERATED_500_COMMENT ? Strings.auto500 : c
236
+ ])
230
237
  return (
231
238
  r('label', className(CSS.Field),
232
239
  r('span', null, Strings.bulk_select),
@@ -237,8 +244,8 @@ function BulkSelector() {
237
244
  disabled,
238
245
  title: disabled ? Strings.bulk_select_disabled_title : '',
239
246
  onChange
240
- }, list.map(value =>
241
- r('option', { value }, value)))))
247
+ }, list.map(([value, name]) =>
248
+ r('option', { value }, name)))))
242
249
  }
243
250
 
244
251
  function GlobalDelayField() {
@@ -251,7 +258,7 @@ function GlobalDelayField() {
251
258
  }
252
259
  return (
253
260
  r('label', className(CSS.Field, CSS.GlobalDelayField),
254
- r('span', null, r(TimerIcon), Strings.delay_ms),
261
+ r('span', null, TimerIcon(), Strings.delay_ms),
255
262
  r('input', {
256
263
  type: 'number',
257
264
  min: 0,
@@ -263,7 +270,7 @@ function GlobalDelayField() {
263
270
  }
264
271
 
265
272
  function ProxyFallbackField() {
266
- const { proxyFallback, collectProxied } = state
273
+ const { proxyFallback } = state
267
274
  function onChange() {
268
275
  const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
269
276
  saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
@@ -279,7 +286,7 @@ function ProxyFallbackField() {
279
286
  return (
280
287
  r('div', className(CSS.Field, CSS.FallbackBackend),
281
288
  r('label', null,
282
- r('span', null, r(CloudIcon), Strings.fallback_server),
289
+ r('span', null, CloudIcon(), Strings.fallback_server),
283
290
  r('input', {
284
291
  type: 'url',
285
292
  autocomplete: 'none',
@@ -287,14 +294,11 @@ function ProxyFallbackField() {
287
294
  value: proxyFallback,
288
295
  onChange
289
296
  })),
290
- r(SaveProxiedCheckbox, {
291
- collectProxied,
292
- disabled: !proxyFallback
293
- })))
297
+ SaveProxiedCheckbox()))
294
298
  }
295
299
 
296
- function SaveProxiedCheckbox({ disabled }) {
297
- const { collectProxied } = state
300
+ function SaveProxiedCheckbox() {
301
+ const { collectProxied, canProxy } = state
298
302
  function onChange() {
299
303
  mockaton.setCollectProxied(this.checked)
300
304
  .then(parseError)
@@ -304,7 +308,7 @@ function SaveProxiedCheckbox({ disabled }) {
304
308
  r('label', className(CSS.SaveProxiedCheckbox),
305
309
  r('input', {
306
310
  type: 'checkbox',
307
- disabled,
311
+ disabled: !canProxy,
308
312
  checked: collectProxied,
309
313
  onChange
310
314
  }),
@@ -330,8 +334,7 @@ function ResetButton() {
330
334
  /** # MockList */
331
335
 
332
336
  function MockList() {
333
- const { brokersByMethod, groupByMethod } = state
334
- const canProxy = state.canProxy
337
+ const { brokersByMethod, groupByMethod, canProxy } = state
335
338
 
336
339
  if (!Object.keys(brokersByMethod).length)
337
340
  return (
@@ -355,14 +358,14 @@ function MockList() {
355
358
  }
356
359
 
357
360
  function Row({ method, urlMask, urlMaskDittoed, broker }) {
358
- const canProxy = state.canProxy
361
+ const { canProxy } = state
359
362
  return (
360
363
  r('tr', { 'data-method': method, 'data-urlMask': urlMask },
361
- canProxy && r('td', null, r(ProxyToggler, { broker })),
362
- r('td', null, r(DelayRouteToggler, { broker })),
363
- r('td', null, r(InternalServerErrorToggler, { broker })),
364
- r('td', null, r(PreviewLink, { method, urlMask, urlMaskDittoed })),
365
- r('td', null, r(MockSelector, { broker }))))
364
+ canProxy && r('td', null, ProxyToggler(broker)),
365
+ r('td', null, DelayRouteToggler(broker)),
366
+ r('td', null, InternalServerErrorToggler(broker)),
367
+ r('td', null, PreviewLink(method, urlMask, urlMaskDittoed)),
368
+ r('td', null, MockSelector(broker))))
366
369
  }
367
370
 
368
371
  function rowsFor(targetMethod) {
@@ -385,7 +388,7 @@ function rowsFor(targetMethod) {
385
388
  }))
386
389
  }
387
390
 
388
- function PreviewLink({ method, urlMask, urlMaskDittoed }) {
391
+ function PreviewLink(method, urlMask, urlMaskDittoed) {
389
392
  async function onClick(event) {
390
393
  event.preventDefault()
391
394
  try {
@@ -408,8 +411,8 @@ function PreviewLink({ method, urlMask, urlMaskDittoed }) {
408
411
  : tail))
409
412
  }
410
413
 
411
- /** @param {{ broker: ClientMockBroker }} props */
412
- function MockSelector({ broker }) {
414
+ /** @param {ClientMockBroker} broker */
415
+ function MockSelector(broker) {
413
416
  const { groupByMethod } = state
414
417
 
415
418
  function onChange() {
@@ -425,7 +428,7 @@ function MockSelector({ broker }) {
425
428
  const { status, urlMask } = parseFilename(selected)
426
429
  const files = broker.mocks.filter(item =>
427
430
  status === 500 ||
428
- !item.includes(DEFAULT_500_COMMENT))
431
+ !item.includes(AUTOGENERATED_500_COMMENT))
429
432
  if (!selected) {
430
433
  selected = Strings.proxied
431
434
  files.push(selected)
@@ -435,9 +438,14 @@ function MockSelector({ broker }) {
435
438
  if (file === Strings.proxied)
436
439
  return Strings.proxied
437
440
  const { status, ext, method } = parseFilename(file)
438
- return groupByMethod
439
- ? `${status} ${ext} ${extractComments(file).join(' ')}`
440
- : `${method} ${status} ${ext} ${extractComments(file).join(' ')}`
441
+ const comments = extractComments(file)
442
+ const isAutogen500 = comments.includes(AUTOGENERATED_500_COMMENT)
443
+ return [
444
+ groupByMethod ? '' : method,
445
+ isAutogen500 ? '' : status,
446
+ ext === 'empty' || ext === 'unknown' ? '' : ext,
447
+ isAutogen500 ? Strings.auto500 : comments.join(' ')
448
+ ].filter(Boolean).join(' ')
441
449
  }
442
450
 
443
451
  return (
@@ -457,8 +465,8 @@ function MockSelector({ broker }) {
457
465
  }, nameFor(file))))))
458
466
  }
459
467
 
460
- /** @param {{ broker: ClientMockBroker }} props */
461
- function DelayRouteToggler({ broker }) {
468
+ /** @param {ClientMockBroker} broker */
469
+ function DelayRouteToggler(broker) {
462
470
  function commit(checked) {
463
471
  const { method, urlMask } = parseFilename(broker.mocks[0])
464
472
  mockaton.setRouteIsDelayed(method, urlMask, checked)
@@ -472,14 +480,11 @@ function DelayRouteToggler({ broker }) {
472
480
  }
473
481
 
474
482
 
475
- /** @param {{ broker: ClientMockBroker }} props */
476
- function InternalServerErrorToggler({ broker }) {
483
+ /** @param {ClientMockBroker} broker */
484
+ function InternalServerErrorToggler(broker) {
477
485
  function onChange() {
478
- const { urlMask, method } = parseFilename(broker.mocks[0])
479
- mockaton.select(
480
- this.checked
481
- ? broker.mocks.find(f => parseFilename(f).status === 500)
482
- : broker.mocks[0])
486
+ const { method, urlMask } = parseFilename(broker.mocks[0])
487
+ mockaton.toggle500(method, urlMask)
483
488
  .then(parseError)
484
489
  .then(updateState)
485
490
  .then(() => linkFor(method, urlMask)?.click())
@@ -499,8 +504,8 @@ function InternalServerErrorToggler({ broker }) {
499
504
  r('span', null, '500')))
500
505
  }
501
506
 
502
- /** @param {{ broker: ClientMockBroker }} props */
503
- function ProxyToggler({ broker }) {
507
+ /** @param {ClientMockBroker} broker */
508
+ function ProxyToggler(broker) {
504
509
  function onChange() {
505
510
  const { urlMask, method } = parseFilename(broker.mocks[0])
506
511
  mockaton.setRouteIsProxied(method, urlMask, this.checked)
@@ -519,7 +524,7 @@ function ProxyToggler({ broker }) {
519
524
  checked: !broker.currentMock.file,
520
525
  onChange
521
526
  }),
522
- r(CloudIcon)))
527
+ CloudIcon()))
523
528
  }
524
529
 
525
530
 
@@ -527,8 +532,7 @@ function ProxyToggler({ broker }) {
527
532
  /** # StaticFilesList */
528
533
 
529
534
  function StaticFilesList() {
530
- const { staticBrokers } = state
531
- const canProxy = state.canProxy
535
+ const { staticBrokers, canProxy } = state
532
536
  if (!Object.keys(staticBrokers).length)
533
537
  return null
534
538
  const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
@@ -543,15 +547,15 @@ function StaticFilesList() {
543
547
  r('tbody', null,
544
548
  Object.values(staticBrokers).map((broker, i) =>
545
549
  r('tr', null,
546
- canProxy && r('td', null, r(ProxyStaticToggler, {})),
547
- r('td', null, r(DelayStaticRouteToggler, { broker })),
548
- r('td', null, r(NotFoundToggler, { broker })),
550
+ canProxy && r('td', null, ProxyStaticToggler()),
551
+ r('td', null, DelayStaticRouteToggler(broker)),
552
+ r('td', null, NotFoundToggler(broker)),
549
553
  r('td', null, r('a', { href: broker.route, target: '_blank' }, dp[i]))
550
554
  )))))
551
555
  }
552
556
 
553
- /** @param {{ broker: ClientStaticBroker }} props */
554
- function DelayStaticRouteToggler({ broker }) {
557
+ /** @param {ClientStaticBroker} broker */
558
+ function DelayStaticRouteToggler(broker) {
555
559
  function commit(checked) {
556
560
  mockaton.setStaticRouteIsDelayed(broker.route, checked)
557
561
  .then(parseError)
@@ -563,8 +567,8 @@ function DelayStaticRouteToggler({ broker }) {
563
567
  })
564
568
  }
565
569
 
566
- /** @param {{ broker: ClientStaticBroker }} props */
567
- function NotFoundToggler({ broker }) {
570
+ /** @param {ClientStaticBroker} broker */
571
+ function NotFoundToggler(broker) {
568
572
  function onChange() {
569
573
  mockaton.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
570
574
  .then(parseError)
@@ -583,7 +587,7 @@ function NotFoundToggler({ broker }) {
583
587
  r('span', null, '404')))
584
588
  }
585
589
 
586
- function ProxyStaticToggler({}) { // TODO
590
+ function ProxyStaticToggler() { // TODO
587
591
  function onChange() {
588
592
  }
589
593
  return (
@@ -597,13 +601,13 @@ function ProxyStaticToggler({}) { // TODO
597
601
  disabled: true,
598
602
  onChange
599
603
  }),
600
- r(CloudIcon)))
604
+ CloudIcon()))
601
605
  }
602
606
 
603
607
 
604
608
  function ClickDragToggler({ checked, commit }) {
605
609
  function onPointerEnter(event) {
606
- if (event.buttons === 1)
610
+ if (event.buttons === 1)
607
611
  onPointerDown.call(this)
608
612
  }
609
613
  function onPointerDown() {
@@ -881,9 +885,6 @@ function className(...args) {
881
885
 
882
886
 
883
887
  function createElement(tag, props, ...children) {
884
- if (typeof tag === 'function')
885
- return tag(props)
886
-
887
888
  const node = document.createElement(tag)
888
889
  for (const [k, v] of Object.entries(props || {}))
889
890
  if (k === 'ref') v.current = node
package/src/MockBroker.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { includesComment, extractComments, parseFilename } from './Filename.js'
2
- import { DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
2
+ import { AUTOGENERATED_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
3
3
 
4
4
 
5
5
  /** MockBroker is a state for a particular route. It knows the available mock files
@@ -42,7 +42,7 @@ export class MockBroker {
42
42
  }
43
43
 
44
44
  #isTemp500(file) {
45
- return includesComment(file, DEFAULT_500_COMMENT)
45
+ return includesComment(file, AUTOGENERATED_500_COMMENT)
46
46
  }
47
47
 
48
48
  #sortMocks() {
@@ -63,7 +63,7 @@ export class MockBroker {
63
63
  #registerTemp500() {
64
64
  const { urlMask, method } = parseFilename(this.mocks[0])
65
65
  const file = urlMask.replace(/^\//, '') // Removes leading slash
66
- this.mocks.push(`${file}${DEFAULT_500_COMMENT}.${method}.500.empty`)
66
+ this.mocks.push(`${file}${AUTOGENERATED_500_COMMENT}.${method}.500.empty`)
67
67
  }
68
68
 
69
69
  unregister(file) {
@@ -82,6 +82,12 @@ export class MockBroker {
82
82
  selectFile(filename) {
83
83
  this.currentMock.file = filename
84
84
  }
85
+
86
+ toggle500() {
87
+ this.#is500(this.currentMock.file)
88
+ ? this.selectDefaultFile()
89
+ : this.selectFile(this.mocks.find(this.#is500))
90
+ }
85
91
 
86
92
  setDelayed(delayed) {
87
93
  this.currentMock.delayed = delayed
@@ -19,7 +19,7 @@ export async function dispatchMock(req, response) {
19
19
  broker = mockBrokerCollection.brokerByRoute('GET', req.url)
20
20
  if (!broker || broker.proxied) {
21
21
  if (config.proxyFallback)
22
- await proxy(req, response, Number(broker?.delayed && calcDelay()))
22
+ await proxy(req, response, broker?.delayed ? calcDelay() : 0)
23
23
  else
24
24
  sendNotFound(response)
25
25
  return
package/src/Mockaton.js CHANGED
@@ -6,12 +6,12 @@ import { config, setup } from './config.js'
6
6
  import { dispatchMock } from './MockDispatcher.js'
7
7
  import { dispatchStatic } from './StaticDispatcher.js'
8
8
  import * as staticCollection from './staticCollection.js'
9
- import { BodyReaderError } from './utils/http-request.js'
10
9
  import * as mockBrokerCollection from './mockBrokersCollection.js'
11
10
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
12
11
  import { watchMocksDir, watchStaticDir } from './Watcher.js'
13
12
  import { apiPatchRequests, apiGetRequests } from './Api.js'
14
- import { sendNoContent, sendInternalServerError, sendUnprocessableContent } from './utils/http-response.js'
13
+ import { BodyReaderError, isControlCharFree } from './utils/http-request.js'
14
+ import { sendNoContent, sendInternalServerError, sendUnprocessableContent, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
15
15
 
16
16
 
17
17
  export function Mockaton(options) {
@@ -39,27 +39,34 @@ export function Mockaton(options) {
39
39
 
40
40
  async function onRequest(req, response) {
41
41
  response.on('error', logger.warn)
42
+ response.setHeader('Server', 'Mockaton')
43
+
44
+ const url = req.url || ''
45
+
46
+ if (url.length > 2048) {
47
+ sendTooLongURI(response)
48
+ return
49
+ }
50
+
51
+ if (!isControlCharFree(url)) {
52
+ sendBadRequest(response)
53
+ return
54
+ }
42
55
 
43
56
  try {
44
- response.setHeader('Server', 'Mockaton')
57
+ const { method } = req
45
58
 
46
59
  if (config.corsAllowed)
47
60
  setCorsHeaders(req, response, config)
48
61
 
49
- const { url, method } = req
50
-
51
62
  if (isPreflight(req))
52
63
  sendNoContent(response)
53
-
54
64
  else if (method === 'PATCH' && apiPatchRequests.has(url))
55
65
  await apiPatchRequests.get(url)(req, response)
56
-
57
66
  else if (method === 'GET' && apiGetRequests.has(url))
58
67
  apiGetRequests.get(url)(req, response)
59
-
60
68
  else if (method === 'GET' && staticCollection.brokerByRoute(url))
61
69
  await dispatchStatic(req, response)
62
-
63
70
  else
64
71
  await dispatchMock(req, response)
65
72
  }
@@ -48,3 +48,21 @@ export function readBody(req, parser = a => a) {
48
48
  }
49
49
  })
50
50
  }
51
+
52
+ export const reControlAndDelChars = /[\x00-\x1f\x7f]/
53
+ export function isControlCharFree(url) {
54
+ try {
55
+ const decoded = decode(url)
56
+ return decoded && !reControlAndDelChars.test(decoded)
57
+ }
58
+ catch {
59
+ return false
60
+ }
61
+ }
62
+
63
+ export function decode(url) {
64
+ const candidate = decodeURIComponent(url)
65
+ return candidate === decodeURIComponent(candidate)
66
+ ? candidate
67
+ : '' // reject multiple encodings
68
+ }
@@ -28,21 +28,33 @@ export function sendNoContent(response) {
28
28
  }
29
29
 
30
30
 
31
+ export function sendBadRequest(response) {
32
+ response.statusCode = 400
33
+ logger.access(response)
34
+ response.end()
35
+ }
36
+
31
37
  export function sendNotFound(response) {
32
38
  response.statusCode = 404
33
39
  logger.access(response)
34
40
  response.end()
35
41
  }
36
42
 
43
+ export function sendTooLongURI(response) {
44
+ response.statusCode = 414
45
+ logger.access(response)
46
+ response.end()
47
+ }
48
+
37
49
  export function sendUnprocessableContent(response, error) {
38
- logger.warn(error)
50
+ logger.access(response, error)
39
51
  response.statusCode = 422
40
52
  response.end(error)
41
53
  }
42
54
 
43
55
 
44
56
  export function sendInternalServerError(response, error) {
45
- logger.error(error?.message || error, error?.stack || undefined)
57
+ logger.error(500, response.req.url, error?.message || error, error?.stack || '')
46
58
  response.statusCode = 500
47
59
  response.end()
48
60
  }
@@ -1,3 +1,6 @@
1
+ import { decode, reControlAndDelChars } from './http-request.js'
2
+
3
+
1
4
  export const logger = new class {
2
5
  #level = 'normal'
3
6
 
@@ -12,16 +15,17 @@ export const logger = new class {
12
15
 
13
16
  accessMock(url, ...msg) {
14
17
  if (this.#level !== 'quiet')
15
- console.log(this.#msg('MOCK', this.#sanitizeURL(url), ...msg))
18
+ console.log(this.#msg('MOCK', url, ...msg))
16
19
  }
17
20
 
18
- access(response) {
21
+ access(response, error = '') {
19
22
  if (this.#level === 'verbose')
20
23
  console.log(this.#msg(
21
24
  'ACCESS',
22
25
  response.req.method,
23
26
  response.statusCode,
24
- this.#sanitizeURL(response.req.url)))
27
+ response.req.url,
28
+ error))
25
29
  }
26
30
 
27
31
  warn(...msg) {
@@ -33,10 +37,22 @@ export const logger = new class {
33
37
  }
34
38
 
35
39
  #msg(...msg) {
36
- return [new Date().toISOString(), ...msg].join('::')
40
+ if (!msg.at(-1))
41
+ msg.pop()
42
+ return [new Date().toISOString(), ...msg.map(this.#sanitize)].join('::')
37
43
  }
38
-
39
- #sanitizeURL(url) {
40
- return decodeURIComponent(url).replace(/[\x00-\x1F\x7F\x9B]/g, '')
44
+
45
+ #sanitize(url) {
46
+ try {
47
+ const decoded = decode(url)
48
+ if (!decoded)
49
+ return '__MULTI_ENCODED__'
50
+ return decoded
51
+ .replace(reControlAndDelChars, '')
52
+ .slice(0, 200)
53
+ }
54
+ catch {
55
+ return '__NON_DECODABLE__'
56
+ }
41
57
  }
42
58
  }