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 +7 -0
- package/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/Api.js +15 -1
- package/src/ApiCommander.js +8 -1
- package/src/ApiConstants.js +3 -3
- package/src/Dashboard.css +5 -5
- package/src/Dashboard.html +1 -1
- package/src/Dashboard.js +70 -69
- package/src/MockBroker.js +9 -3
- package/src/MockDispatcher.js +1 -1
- package/src/Mockaton.js +16 -9
- package/src/utils/http-request.js +18 -0
- package/src/utils/http-response.js +14 -2
- package/src/utils/logger.js +23 -7
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
package/package.json
CHANGED
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]
|
package/src/ApiCommander.js
CHANGED
|
@@ -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
|
}
|
package/src/ApiConstants.js
CHANGED
|
@@ -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
|
|
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.
|
|
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:
|
|
530
|
-
height:
|
|
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
|
|
546
|
+
&:focus {
|
|
547
547
|
outline: 0;
|
|
548
548
|
& ~ span {
|
|
549
549
|
outline: 2px solid var(--colorAccent)
|
package/src/Dashboard.html
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
139
|
-
|
|
142
|
+
MockList(),
|
|
143
|
+
StaticFilesList()),
|
|
140
144
|
r('div', { className: CSS.rightSide },
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
GlobalDelayField(),
|
|
161
|
+
CookieSelector(),
|
|
162
|
+
BulkSelector(),
|
|
163
|
+
ProxyFallbackField(),
|
|
164
|
+
ResetButton()),
|
|
161
165
|
r('button', {
|
|
162
166
|
className: CSS.MenuTrigger,
|
|
163
167
|
popovertarget: 'Menu'
|
|
164
|
-
},
|
|
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].
|
|
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 },
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
291
|
-
collectProxied,
|
|
292
|
-
disabled: !proxyFallback
|
|
293
|
-
})))
|
|
297
|
+
SaveProxiedCheckbox()))
|
|
294
298
|
}
|
|
295
299
|
|
|
296
|
-
function SaveProxiedCheckbox(
|
|
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
|
|
361
|
+
const { canProxy } = state
|
|
359
362
|
return (
|
|
360
363
|
r('tr', { 'data-method': method, 'data-urlMask': urlMask },
|
|
361
|
-
canProxy && r('td', null,
|
|
362
|
-
r('td', null,
|
|
363
|
-
r('td', null,
|
|
364
|
-
r('td', null,
|
|
365
|
-
r('td', null,
|
|
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(
|
|
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 {
|
|
412
|
-
function MockSelector(
|
|
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(
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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 {
|
|
461
|
-
function DelayRouteToggler(
|
|
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 {
|
|
476
|
-
function InternalServerErrorToggler(
|
|
483
|
+
/** @param {ClientMockBroker} broker */
|
|
484
|
+
function InternalServerErrorToggler(broker) {
|
|
477
485
|
function onChange() {
|
|
478
|
-
const {
|
|
479
|
-
mockaton.
|
|
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 {
|
|
503
|
-
function ProxyToggler(
|
|
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
|
-
|
|
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,
|
|
547
|
-
r('td', null,
|
|
548
|
-
r('td', null,
|
|
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 {
|
|
554
|
-
function DelayStaticRouteToggler(
|
|
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 {
|
|
567
|
-
function NotFoundToggler(
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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}${
|
|
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
|
package/src/MockDispatcher.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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 ||
|
|
57
|
+
logger.error(500, response.req.url, error?.message || error, error?.stack || '')
|
|
46
58
|
response.statusCode = 500
|
|
47
59
|
response.end()
|
|
48
60
|
}
|
package/src/utils/logger.js
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
if (!msg.at(-1))
|
|
41
|
+
msg.pop()
|
|
42
|
+
return [new Date().toISOString(), ...msg.map(this.#sanitize)].join('::')
|
|
37
43
|
}
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
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
|
}
|