mockaton 10.6.7 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +14 -14
- package/package.json +2 -2
- package/src/Api.js +5 -6
- package/src/ApiCommander.js +11 -11
- package/src/ApiConstants.js +1 -6
- package/src/Dashboard.css +27 -9
- package/src/Dashboard.js +63 -46
- package/src/DashboardStore.js +132 -114
- package/src/Filename.js +3 -3
- package/src/MockBroker.js +46 -63
- package/src/MockDispatcher.js +2 -2
- package/src/Mockaton.js +17 -15
- package/src/Watcher.js +19 -6
- package/src/mockBrokersCollection.js +2 -10
- package/src/utils/http-request.js +3 -3
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Server, IncomingMessage, OutgoingMessage } from 'node:http'
|
|
1
|
+
import { Server, IncomingMessage, OutgoingMessage } from 'node:http'
|
|
2
2
|
|
|
3
3
|
type Plugin = (
|
|
4
4
|
filePath: string,
|
|
@@ -16,7 +16,7 @@ interface Config {
|
|
|
16
16
|
|
|
17
17
|
host?: string,
|
|
18
18
|
port?: number
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
logLevel?: 'normal' | 'verbose' | 'quiet'
|
|
21
21
|
|
|
22
22
|
delay?: number
|
|
@@ -37,8 +37,8 @@ interface Config {
|
|
|
37
37
|
corsExposedHeaders?: string[]
|
|
38
38
|
corsCredentials?: boolean
|
|
39
39
|
corsMaxAge?: number
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
|
|
41
|
+
|
|
42
42
|
plugins?: [filenameTester: RegExp, plugin: Plugin][]
|
|
43
43
|
|
|
44
44
|
onReady?: (address: string) => void
|
|
@@ -64,11 +64,11 @@ export type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
|
|
|
64
64
|
|
|
65
65
|
export type ClientMockBroker = {
|
|
66
66
|
mocks: string[]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
file: string
|
|
68
|
+
status: number
|
|
69
|
+
auto500: boolean
|
|
70
|
+
delayed: boolean
|
|
71
|
+
proxied: boolean
|
|
72
72
|
}
|
|
73
73
|
export type ClientBrokersByMethod = {
|
|
74
74
|
[method: string]: {
|
|
@@ -89,14 +89,14 @@ export type ClientStaticBrokers = {
|
|
|
89
89
|
export interface State {
|
|
90
90
|
brokersByMethod: ClientBrokersByMethod
|
|
91
91
|
staticBrokers: ClientStaticBrokers
|
|
92
|
-
|
|
93
|
-
cookies: [label:string, selected:boolean][]
|
|
92
|
+
|
|
93
|
+
cookies: [label: string, selected: boolean][]
|
|
94
94
|
comments: string[]
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
delay: number
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
collectProxied: boolean
|
|
99
99
|
proxyFallback: string
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
corsAllowed?: boolean
|
|
102
102
|
}
|
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.
|
|
5
|
+
"version": "10.7.0",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"start": "node src/cli.js",
|
|
24
24
|
"watch": "node --watch src/cli.js",
|
|
25
25
|
"pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none 'pixaton-tests/**/*.test.js'",
|
|
26
|
-
"outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i
|
|
26
|
+
"outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"pixaton": "1.1.3",
|
package/src/Api.js
CHANGED
|
@@ -21,7 +21,7 @@ export const apiGetRequests = new Map([
|
|
|
21
21
|
...[
|
|
22
22
|
'Dashboard.css',
|
|
23
23
|
'Dashboard.js',
|
|
24
|
-
'
|
|
24
|
+
'ApiCommander.js', 'ApiConstants.js', 'DashboardDom.js', 'DashboardStore.js', 'Filename.js',
|
|
25
25
|
'Logo.svg'
|
|
26
26
|
].map(f => [API.dashboard + '/' + f, serveStatic(f)]),
|
|
27
27
|
|
|
@@ -117,7 +117,7 @@ async function selectMock(req, response) {
|
|
|
117
117
|
sendUnprocessableContent(response, `Missing Mock: ${file}`)
|
|
118
118
|
else {
|
|
119
119
|
broker.selectFile(file)
|
|
120
|
-
sendJSON(response, broker
|
|
120
|
+
sendJSON(response, broker)
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -130,7 +130,7 @@ async function toggle500(req, response) {
|
|
|
130
130
|
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
|
|
131
131
|
else {
|
|
132
132
|
broker.toggle500()
|
|
133
|
-
sendJSON(response, broker
|
|
133
|
+
sendJSON(response, broker)
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -147,7 +147,7 @@ async function setRouteIsDelayed(req, response) {
|
|
|
147
147
|
sendUnprocessableContent(response, `Expected boolean for "delayed"`)
|
|
148
148
|
else {
|
|
149
149
|
broker.setDelayed(delayed)
|
|
150
|
-
|
|
150
|
+
sendJSON(response, broker)
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -166,7 +166,7 @@ async function setRouteIsProxied(req, response) {
|
|
|
166
166
|
sendUnprocessableContent(response, `There’s no proxy fallback`)
|
|
167
167
|
else {
|
|
168
168
|
broker.setProxied(proxied)
|
|
169
|
-
|
|
169
|
+
sendJSON(response, broker)
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -231,7 +231,6 @@ async function setStaticRouteStatusCode(req, response) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
235
234
|
async function setStaticRouteIsDelayed(req, response) {
|
|
236
235
|
const body = await parseJSON(req)
|
|
237
236
|
const delayed = body[DF.delayed]
|
package/src/ApiCommander.js
CHANGED
|
@@ -9,27 +9,27 @@ export class Commander {
|
|
|
9
9
|
this.#addr = addr
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
#patch = (api, body) =>
|
|
13
|
-
|
|
12
|
+
#patch = (api, body) =>
|
|
13
|
+
fetch(this.#addr + api, {
|
|
14
14
|
method: 'PATCH',
|
|
15
15
|
body: JSON.stringify(body)
|
|
16
16
|
})
|
|
17
|
-
}
|
|
18
17
|
|
|
19
18
|
/** @returns {JsonPromise<State>} */
|
|
20
|
-
getState = () =>
|
|
21
|
-
|
|
22
|
-
}
|
|
19
|
+
getState = () =>
|
|
20
|
+
fetch(this.#addr + API.state)
|
|
23
21
|
|
|
24
22
|
/** @returns {JsonPromise<number>} */
|
|
25
|
-
getSyncVersion = (
|
|
26
|
-
|
|
27
|
-
signal: AbortSignal.any([
|
|
23
|
+
getSyncVersion = (currSyncVer, abortSignal) =>
|
|
24
|
+
fetch(this.#addr + API.syncVersion, {
|
|
25
|
+
signal: AbortSignal.any([
|
|
26
|
+
abortSignal,
|
|
27
|
+
AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
|
|
28
|
+
]),
|
|
28
29
|
headers: {
|
|
29
|
-
[DF.syncVersion]:
|
|
30
|
+
[DF.syncVersion]: currSyncVer
|
|
30
31
|
}
|
|
31
32
|
})
|
|
32
|
-
}
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
reset() {
|
package/src/ApiConstants.js
CHANGED
|
@@ -28,12 +28,7 @@ export const DF = { // Dashboard Fields (XHR)
|
|
|
28
28
|
syncVersion: 'last_received_sync_version'
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
// e.g. 'ton' would match
|
|
33
|
-
export const AUTO_500_COMMENT = '(Mockaton 500)'
|
|
31
|
+
export const HEADER_FOR_502 = 'Mockaton502'
|
|
34
32
|
export const DEFAULT_MOCK_COMMENT = '(default)'
|
|
35
|
-
|
|
36
33
|
export const EXT_FOR_UNKNOWN_MIME = 'unknown'
|
|
37
34
|
export const LONG_POLL_SERVER_TIMEOUT = 8_000
|
|
38
|
-
|
|
39
|
-
export const HEADER_FOR_502 = 'Mockaton502'
|
package/src/Dashboard.css
CHANGED
|
@@ -78,7 +78,7 @@ body {
|
|
|
78
78
|
|
|
79
79
|
select, a, input, button {
|
|
80
80
|
cursor: pointer;
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
&:focus-visible {
|
|
83
83
|
outline: 2px solid var(--colorAccent);
|
|
84
84
|
}
|
|
@@ -369,6 +369,19 @@ table {
|
|
|
369
369
|
> tr:first-child > th {
|
|
370
370
|
border-top: 0;
|
|
371
371
|
}
|
|
372
|
+
|
|
373
|
+
tr.animIn {
|
|
374
|
+
opacity: 0;
|
|
375
|
+
transform: scaleY(0);
|
|
376
|
+
animation: _kfAnimIn 180ms ease-in-out forwards;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@keyframes _kfAnimIn {
|
|
381
|
+
to {
|
|
382
|
+
opacity: 1;
|
|
383
|
+
transform: scaleY(1);
|
|
384
|
+
}
|
|
372
385
|
}
|
|
373
386
|
|
|
374
387
|
.Method {
|
|
@@ -544,11 +557,22 @@ table {
|
|
|
544
557
|
}
|
|
545
558
|
}
|
|
546
559
|
|
|
560
|
+
&:disabled + span {
|
|
561
|
+
cursor: not-allowed;
|
|
562
|
+
opacity: 0.7;
|
|
563
|
+
}
|
|
547
564
|
&:checked + span {
|
|
548
565
|
border-color: var(--colorRed);
|
|
549
566
|
color: white;
|
|
550
567
|
background: var(--colorRed);
|
|
551
568
|
}
|
|
569
|
+
&:not(:checked):enabled:hover + span {
|
|
570
|
+
border-color: var(--colorRed);
|
|
571
|
+
color: var(--colorRed);
|
|
572
|
+
}
|
|
573
|
+
&:enabled:active + span {
|
|
574
|
+
cursor: grabbing;
|
|
575
|
+
}
|
|
552
576
|
}
|
|
553
577
|
|
|
554
578
|
> span {
|
|
@@ -558,14 +582,6 @@ table {
|
|
|
558
582
|
font-weight: bold;
|
|
559
583
|
color: var(--colorSecondaryAction);
|
|
560
584
|
border-radius: var(--radius);
|
|
561
|
-
|
|
562
|
-
&:hover {
|
|
563
|
-
border-color: var(--colorRed);
|
|
564
|
-
color: var(--colorRed);
|
|
565
|
-
}
|
|
566
|
-
&:active {
|
|
567
|
-
cursor: grabbing;
|
|
568
|
-
}
|
|
569
585
|
}
|
|
570
586
|
}
|
|
571
587
|
|
|
@@ -628,6 +644,8 @@ table {
|
|
|
628
644
|
background: var(--colorAccent);
|
|
629
645
|
animation-name: _kfProgress;
|
|
630
646
|
animation-timing-function: linear;
|
|
647
|
+
animation-iteration-count: infinite;
|
|
648
|
+
animation-direction: alternate;
|
|
631
649
|
/* duration in JavaScript */
|
|
632
650
|
}
|
|
633
651
|
}
|
package/src/Dashboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement as r, createSvgElement as s, className, restoreFocus, Defer, Fragment, useRef } from './DashboardDom.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { store, BrokerRowModel } from './DashboardStore.js'
|
|
3
|
+
import { HEADER_FOR_502 } from './ApiConstants.js'
|
|
4
4
|
import { parseFilename } from './Filename.js'
|
|
5
5
|
|
|
6
6
|
|
|
@@ -28,6 +28,7 @@ const CSS = {
|
|
|
28
28
|
SaveProxiedCheckbox: null,
|
|
29
29
|
SettingsMenu: null,
|
|
30
30
|
|
|
31
|
+
animIn: null,
|
|
31
32
|
chosen: null,
|
|
32
33
|
dittoDir: null,
|
|
33
34
|
leftSide: null,
|
|
@@ -63,10 +64,12 @@ initRealTimeUpdates()
|
|
|
63
64
|
initKeyboardNavigation()
|
|
64
65
|
|
|
65
66
|
function render() {
|
|
67
|
+
render.count++
|
|
66
68
|
restoreFocus(() => document.body.replaceChildren(...App()))
|
|
67
69
|
if (store.hasChosenLink)
|
|
68
|
-
previewMock(
|
|
70
|
+
previewMock()
|
|
69
71
|
}
|
|
72
|
+
render.count = 0
|
|
70
73
|
|
|
71
74
|
const t = translation => translation[0]
|
|
72
75
|
|
|
@@ -156,9 +159,7 @@ function BulkSelector() {
|
|
|
156
159
|
},
|
|
157
160
|
r('option', { value: firstOption }, firstOption),
|
|
158
161
|
r('hr'),
|
|
159
|
-
comments.map(value => r('option', { value }, value))
|
|
160
|
-
r('hr'),
|
|
161
|
-
r('option', { value: AUTO_500_COMMENT }, t`Auto500`)
|
|
162
|
+
comments.map(value => r('option', { value }, value))
|
|
162
163
|
)))
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -278,7 +279,7 @@ function MockList() {
|
|
|
278
279
|
return Object.keys(store.brokersByMethod).map(method => Fragment(
|
|
279
280
|
r('tr', null,
|
|
280
281
|
r('th', { colspan: 2 + Number(store.canProxy) }),
|
|
281
|
-
r('th',
|
|
282
|
+
r('th', { colspan: 2 }, method)),
|
|
282
283
|
store.brokersAsRowsByMethod(method).map(Row)))
|
|
283
284
|
|
|
284
285
|
return store.brokersAsRowsByMethod('*').map(Row)
|
|
@@ -289,9 +290,12 @@ function MockList() {
|
|
|
289
290
|
* @param {number} i
|
|
290
291
|
*/
|
|
291
292
|
function Row(row, i) {
|
|
292
|
-
const { method, urlMask } = row
|
|
293
|
+
const { key, method, urlMask } = row
|
|
293
294
|
return (
|
|
294
|
-
r('tr', {
|
|
295
|
+
r('tr', {
|
|
296
|
+
key,
|
|
297
|
+
...className(render.count > 1 && row.isNew && CSS.animIn)
|
|
298
|
+
},
|
|
295
299
|
store.canProxy && r('td', null,
|
|
296
300
|
ProxyToggler(method, urlMask, row.proxied)),
|
|
297
301
|
|
|
@@ -299,7 +303,12 @@ function Row(row, i) {
|
|
|
299
303
|
DelayRouteToggler(method, urlMask, row.delayed)),
|
|
300
304
|
|
|
301
305
|
r('td', null,
|
|
302
|
-
InternalServerErrorToggler(
|
|
306
|
+
InternalServerErrorToggler(
|
|
307
|
+
method,
|
|
308
|
+
urlMask,
|
|
309
|
+
!row.proxied && row.status === 500, // checked
|
|
310
|
+
row.opts.length === 1 && row.status === 500 // disabled
|
|
311
|
+
)),
|
|
303
312
|
|
|
304
313
|
!store.groupByMethod && r('td', className(CSS.Method),
|
|
305
314
|
method),
|
|
@@ -310,14 +319,13 @@ function Row(row, i) {
|
|
|
310
319
|
r('td', null,
|
|
311
320
|
MockSelector(row))))
|
|
312
321
|
}
|
|
313
|
-
Row.key = (method, urlMask) => method + '::' + urlMask
|
|
314
322
|
|
|
315
323
|
function renderRow(method, urlMask) {
|
|
316
324
|
restoreFocus(() => {
|
|
317
325
|
unChooseOld()
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
previewMock(
|
|
326
|
+
const row = store.brokerAsRow(method, urlMask)
|
|
327
|
+
trFor(row.key).replaceWith(Row(row))
|
|
328
|
+
previewMock()
|
|
321
329
|
})
|
|
322
330
|
|
|
323
331
|
function trFor(key) {
|
|
@@ -362,7 +370,7 @@ function MockSelector(row) {
|
|
|
362
370
|
CSS.MockSelector,
|
|
363
371
|
row.selectedIdx > 0 && CSS.nonDefault,
|
|
364
372
|
row.selectedFileIs4xx && CSS.status4xx)
|
|
365
|
-
}, row.opts.map(([value, label, selected]) =>
|
|
373
|
+
}, row.opts.map(([value, label, selected]) =>
|
|
366
374
|
r('option', { value, selected }, label))))
|
|
367
375
|
}
|
|
368
376
|
|
|
@@ -375,7 +383,7 @@ function DelayRouteToggler(method, urlMask, checked) {
|
|
|
375
383
|
})
|
|
376
384
|
}
|
|
377
385
|
|
|
378
|
-
function InternalServerErrorToggler(method, urlMask, checked) {
|
|
386
|
+
function InternalServerErrorToggler(method, urlMask, checked, disabled) {
|
|
379
387
|
return (
|
|
380
388
|
r('label', {
|
|
381
389
|
className: CSS.InternalServerErrorToggler,
|
|
@@ -383,6 +391,7 @@ function InternalServerErrorToggler(method, urlMask, checked) {
|
|
|
383
391
|
},
|
|
384
392
|
r('input', {
|
|
385
393
|
type: 'checkbox',
|
|
394
|
+
disabled,
|
|
386
395
|
checked,
|
|
387
396
|
onChange() { store.toggle500(method, urlMask) },
|
|
388
397
|
'data-focus-group': FocusGroup.StatusToggler
|
|
@@ -410,38 +419,45 @@ function ProxyToggler(method, urlMask, checked) {
|
|
|
410
419
|
/** # StaticFilesList */
|
|
411
420
|
|
|
412
421
|
function StaticFilesList() {
|
|
413
|
-
const {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
? [r('span', className(CSS.dittoDir), ditto), tail]
|
|
419
|
-
: tail)
|
|
420
|
-
return (
|
|
421
|
-
Fragment(
|
|
422
|
+
const { canProxy, groupByMethod } = store
|
|
423
|
+
const rows = store.staticBrokersAsRows()
|
|
424
|
+
return !rows.length
|
|
425
|
+
? null
|
|
426
|
+
: Fragment(
|
|
422
427
|
r('tr', null,
|
|
423
428
|
r('th', { colspan: (2 + Number(!groupByMethod)) + Number(canProxy) }),
|
|
424
|
-
r('th',
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
canProxy && r('td'),
|
|
428
|
-
r('td', null,
|
|
429
|
-
DelayStaticRouteToggler(route, delayed)),
|
|
429
|
+
r('th', { colspan: 2 }, t`Static GET`)),
|
|
430
|
+
rows.map(StaticRow))
|
|
431
|
+
}
|
|
430
432
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
+
/** @param {StaticBrokerRowModel} row */
|
|
434
|
+
function StaticRow(row) {
|
|
435
|
+
const { canProxy, groupByMethod } = store
|
|
436
|
+
const [ditto, tail] = row.urlMaskDittoed
|
|
437
|
+
return (
|
|
438
|
+
r('tr', {
|
|
439
|
+
key: row.key,
|
|
440
|
+
...className(render.count > 1 && row.isNew && CSS.animIn)
|
|
441
|
+
},
|
|
442
|
+
canProxy && r('td'),
|
|
443
|
+
r('td', null,
|
|
444
|
+
DelayStaticRouteToggler(row.urlMask, row.delayed)),
|
|
445
|
+
|
|
446
|
+
r('td', null,
|
|
447
|
+
NotFoundToggler(row.urlMask, row.status === 404)),
|
|
433
448
|
|
|
434
|
-
|
|
435
|
-
|
|
449
|
+
!groupByMethod && r('td', className(CSS.Method),
|
|
450
|
+
'GET'),
|
|
436
451
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
452
|
+
r('td', null,
|
|
453
|
+
r('a', {
|
|
454
|
+
href: row.urlMask,
|
|
455
|
+
target: '_blank',
|
|
456
|
+
className: CSS.PreviewLink,
|
|
457
|
+
'data-focus-group': FocusGroup.PreviewLink
|
|
458
|
+
}, ditto
|
|
459
|
+
? [r('span', className(CSS.dittoDir), ditto), tail]
|
|
460
|
+
: tail))))
|
|
445
461
|
}
|
|
446
462
|
|
|
447
463
|
function DelayStaticRouteToggler(route, checked) {
|
|
@@ -586,7 +602,6 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
|
|
|
586
602
|
' ' + mime))
|
|
587
603
|
}
|
|
588
604
|
|
|
589
|
-
// TODO indeterminate when there's store.delayJitter
|
|
590
605
|
const SPINNER_DELAY = 80
|
|
591
606
|
function PayloadViewerProgressBar() {
|
|
592
607
|
return (
|
|
@@ -594,7 +609,9 @@ function PayloadViewerProgressBar() {
|
|
|
594
609
|
r('div', { style: { animationDuration: store.delay - SPINNER_DELAY + 'ms' } })))
|
|
595
610
|
}
|
|
596
611
|
|
|
597
|
-
async function previewMock(
|
|
612
|
+
async function previewMock() {
|
|
613
|
+
const { method, urlMask } = store.chosenLink
|
|
614
|
+
|
|
598
615
|
previewMock.controller?.abort()
|
|
599
616
|
previewMock.controller = new AbortController
|
|
600
617
|
|
|
@@ -609,7 +626,7 @@ async function previewMock(method, urlMask) {
|
|
|
609
626
|
signal: previewMock.controller.signal
|
|
610
627
|
})
|
|
611
628
|
clearTimeout(spinnerTimer)
|
|
612
|
-
const { proxied, file } = store.brokerFor(method, urlMask)
|
|
629
|
+
const { proxied, file } = store.brokerFor(method, urlMask)
|
|
613
630
|
if (proxied || file)
|
|
614
631
|
await updatePayloadViewer(proxied, file, response)
|
|
615
632
|
}
|
package/src/DashboardStore.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import { deferred } from './DashboardDom.js'
|
|
2
2
|
import { Commander } from './ApiCommander.js'
|
|
3
3
|
import { parseFilename, extractComments } from './Filename.js'
|
|
4
|
-
import { AUTO_500_COMMENT } from './ApiConstants.js'
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
const t = translation => translation[0]
|
|
8
|
-
const
|
|
7
|
+
const api = new Commander(location.origin)
|
|
9
8
|
|
|
10
9
|
export const store = {
|
|
11
10
|
onError(err) {},
|
|
12
11
|
render() {},
|
|
13
12
|
renderRow(method, urlMask) {},
|
|
14
13
|
|
|
15
|
-
/** @type {State.brokersByMethod} */
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/** @type {State.staticBrokers} */
|
|
19
|
-
staticBrokers: {},
|
|
14
|
+
brokersByMethod: /** @type {State.brokersByMethod} */ {},
|
|
15
|
+
staticBrokers: /** @type {State.staticBrokers} */ {},
|
|
20
16
|
|
|
21
17
|
cookies: [],
|
|
22
18
|
comments: [],
|
|
@@ -28,11 +24,11 @@ export const store = {
|
|
|
28
24
|
return Boolean(store.proxyFallback)
|
|
29
25
|
},
|
|
30
26
|
|
|
31
|
-
getSyncVersion:
|
|
27
|
+
getSyncVersion: api.getSyncVersion,
|
|
32
28
|
|
|
33
29
|
async fetchState() {
|
|
34
30
|
try {
|
|
35
|
-
const response = await
|
|
31
|
+
const response = await api.getState()
|
|
36
32
|
if (!response.ok) throw response
|
|
37
33
|
Object.assign(store, await response.json())
|
|
38
34
|
store.render()
|
|
@@ -63,9 +59,10 @@ export const store = {
|
|
|
63
59
|
store.chosenLink = { method, urlMask }
|
|
64
60
|
},
|
|
65
61
|
|
|
62
|
+
|
|
66
63
|
async reset() {
|
|
67
64
|
try {
|
|
68
|
-
const response = await
|
|
65
|
+
const response = await api.reset()
|
|
69
66
|
if (!response.ok) throw response
|
|
70
67
|
store.setChosenLink('', '')
|
|
71
68
|
await store.fetchState()
|
|
@@ -75,16 +72,17 @@ export const store = {
|
|
|
75
72
|
|
|
76
73
|
async bulkSelectByComment(value) {
|
|
77
74
|
try {
|
|
78
|
-
const response = await
|
|
75
|
+
const response = await api.bulkSelectByComment(value)
|
|
79
76
|
if (!response.ok) throw response
|
|
80
77
|
await store.fetchState()
|
|
81
78
|
}
|
|
82
79
|
catch (error) { store.onError(error) }
|
|
83
80
|
},
|
|
84
81
|
|
|
82
|
+
|
|
85
83
|
async setGlobalDelay(value) {
|
|
86
84
|
try {
|
|
87
|
-
const response = await
|
|
85
|
+
const response = await api.setGlobalDelay(value)
|
|
88
86
|
if (!response.ok) throw response
|
|
89
87
|
store.delay = value
|
|
90
88
|
}
|
|
@@ -93,7 +91,7 @@ export const store = {
|
|
|
93
91
|
|
|
94
92
|
async selectCookie(name) {
|
|
95
93
|
try {
|
|
96
|
-
const response = await
|
|
94
|
+
const response = await api.selectCookie(name)
|
|
97
95
|
if (!response.ok) throw response
|
|
98
96
|
store.cookies = await response.json()
|
|
99
97
|
}
|
|
@@ -102,7 +100,7 @@ export const store = {
|
|
|
102
100
|
|
|
103
101
|
async setProxyFallback(value) {
|
|
104
102
|
try {
|
|
105
|
-
const response = await
|
|
103
|
+
const response = await api.setProxyFallback(value)
|
|
106
104
|
if (!response.ok) throw response
|
|
107
105
|
store.proxyFallback = value
|
|
108
106
|
store.render()
|
|
@@ -112,7 +110,7 @@ export const store = {
|
|
|
112
110
|
|
|
113
111
|
async setCollectProxied(checked) {
|
|
114
112
|
try {
|
|
115
|
-
const response = await
|
|
113
|
+
const response = await api.setCollectProxied(checked)
|
|
116
114
|
if (!response.ok) throw response
|
|
117
115
|
store.collectProxied = checked
|
|
118
116
|
}
|
|
@@ -124,36 +122,53 @@ export const store = {
|
|
|
124
122
|
return store.brokersByMethod[method]?.[urlMask]
|
|
125
123
|
},
|
|
126
124
|
|
|
125
|
+
setBroker(broker) {
|
|
126
|
+
const { method, urlMask } = parseFilename(broker.file)
|
|
127
|
+
store.brokersByMethod[method] ??= {}
|
|
128
|
+
store.brokersByMethod[method][urlMask] = broker
|
|
129
|
+
},
|
|
130
|
+
|
|
127
131
|
_dittoCache: new Map(),
|
|
128
132
|
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
_brokersAsArray(byMethod = '*') {
|
|
134
|
+
const arr = []
|
|
135
|
+
for (const [method, brokers] of Object.entries(store.brokersByMethod))
|
|
136
|
+
if (byMethod === '*' || byMethod === method)
|
|
137
|
+
arr.push(...Object.values(brokers))
|
|
138
|
+
return arr
|
|
131
139
|
},
|
|
132
140
|
|
|
133
141
|
brokersAsRowsByMethod(method) {
|
|
134
142
|
const rows = store._brokersAsArray(method)
|
|
143
|
+
.map(b => new BrokerRowModel(b, store.canProxy))
|
|
144
|
+
.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
135
145
|
const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
|
|
136
|
-
|
|
137
|
-
const r = rows[i]
|
|
146
|
+
rows.forEach((r, i) => {
|
|
138
147
|
r.setUrlMaskDittoed(urlMasksDittoed[i])
|
|
139
|
-
store._dittoCache.
|
|
140
|
-
|
|
148
|
+
r.setIsNew(!store._dittoCache.has(r.key))
|
|
149
|
+
store._dittoCache.set(r.key, urlMasksDittoed[i])
|
|
150
|
+
})
|
|
141
151
|
return rows
|
|
142
152
|
},
|
|
143
153
|
|
|
144
|
-
_brokersAsArray(byMethod = '*') {
|
|
145
|
-
const rows = []
|
|
146
|
-
for (const [method, brokers] of Object.entries(store.brokersByMethod))
|
|
147
|
-
if (byMethod === '*' || byMethod === method)
|
|
148
|
-
for (const broker of Object.values(brokers))
|
|
149
|
-
rows.push(new BrokerRowModel(broker, store.canProxy))
|
|
150
|
-
return rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
151
|
-
},
|
|
152
|
-
|
|
153
154
|
brokerAsRow(method, urlMask) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
const b = store.brokerFor(method, urlMask)
|
|
156
|
+
const r = new BrokerRowModel(b, store.canProxy)
|
|
157
|
+
r.setUrlMaskDittoed(store._dittoCache.get(r.key))
|
|
158
|
+
return r
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
staticBrokersAsRows() {
|
|
162
|
+
const rows = Object.values(store.staticBrokers)
|
|
163
|
+
.map(b => new StaticBrokerRowModel(b))
|
|
164
|
+
.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
165
|
+
const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
|
|
166
|
+
rows.forEach((r, i) => {
|
|
167
|
+
r.setUrlMaskDittoed(urlMasksDittoed[i])
|
|
168
|
+
r.setIsNew(!store._dittoCache.has(r.key))
|
|
169
|
+
store._dittoCache.set(r.key, urlMasksDittoed[i])
|
|
170
|
+
})
|
|
171
|
+
return rows
|
|
157
172
|
},
|
|
158
173
|
|
|
159
174
|
previewLink(method, urlMask) {
|
|
@@ -163,10 +178,10 @@ export const store = {
|
|
|
163
178
|
|
|
164
179
|
async selectFile(file) {
|
|
165
180
|
try {
|
|
166
|
-
const response = await
|
|
181
|
+
const response = await api.select(file)
|
|
167
182
|
if (!response.ok) throw response
|
|
168
183
|
const { method, urlMask } = parseFilename(file)
|
|
169
|
-
store.
|
|
184
|
+
store.setBroker(await response.json())
|
|
170
185
|
store.setChosenLink(method, urlMask)
|
|
171
186
|
store.renderRow(method, urlMask)
|
|
172
187
|
}
|
|
@@ -175,9 +190,9 @@ export const store = {
|
|
|
175
190
|
|
|
176
191
|
async toggle500(method, urlMask) {
|
|
177
192
|
try {
|
|
178
|
-
const response = await
|
|
193
|
+
const response = await api.toggle500(method, urlMask)
|
|
179
194
|
if (!response.ok) throw response
|
|
180
|
-
store.
|
|
195
|
+
store.setBroker(await response.json())
|
|
181
196
|
store.setChosenLink(method, urlMask)
|
|
182
197
|
store.renderRow(method, urlMask)
|
|
183
198
|
}
|
|
@@ -186,9 +201,9 @@ export const store = {
|
|
|
186
201
|
|
|
187
202
|
async setProxied(method, urlMask, checked) {
|
|
188
203
|
try {
|
|
189
|
-
const response = await
|
|
204
|
+
const response = await api.setRouteIsProxied(method, urlMask, checked)
|
|
190
205
|
if (!response.ok) throw response
|
|
191
|
-
store.
|
|
206
|
+
store.setBroker(await response.json())
|
|
192
207
|
store.setChosenLink(method, urlMask)
|
|
193
208
|
store.renderRow(method, urlMask)
|
|
194
209
|
}
|
|
@@ -197,9 +212,9 @@ export const store = {
|
|
|
197
212
|
|
|
198
213
|
async setDelayed(method, urlMask, checked) {
|
|
199
214
|
try {
|
|
200
|
-
const response = await
|
|
215
|
+
const response = await api.setRouteIsDelayed(method, urlMask, checked)
|
|
201
216
|
if (!response.ok) throw response
|
|
202
|
-
store.
|
|
217
|
+
store.setBroker(await response.json())
|
|
203
218
|
}
|
|
204
219
|
catch (error) { store.onError(error) }
|
|
205
220
|
},
|
|
@@ -207,7 +222,7 @@ export const store = {
|
|
|
207
222
|
|
|
208
223
|
async setDelayedStatic(route, checked) {
|
|
209
224
|
try {
|
|
210
|
-
const response = await
|
|
225
|
+
const response = await api.setStaticRouteIsDelayed(route, checked)
|
|
211
226
|
if (!response.ok) throw response
|
|
212
227
|
store.staticBrokers[route].delayed = checked
|
|
213
228
|
}
|
|
@@ -216,7 +231,7 @@ export const store = {
|
|
|
216
231
|
|
|
217
232
|
async setStaticRouteStatus(route, status) {
|
|
218
233
|
try {
|
|
219
|
-
const response = await
|
|
234
|
+
const response = await api.setStaticRouteStatus(route, status)
|
|
220
235
|
if (!response.ok) throw response
|
|
221
236
|
store.staticBrokers[route].status = status
|
|
222
237
|
}
|
|
@@ -312,9 +327,10 @@ dittoSplitPaths.test = function () {
|
|
|
312
327
|
deferred(dittoSplitPaths.test)
|
|
313
328
|
|
|
314
329
|
|
|
315
|
-
|
|
316
330
|
export class BrokerRowModel {
|
|
317
331
|
opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
|
|
332
|
+
isNew = false
|
|
333
|
+
key = ''
|
|
318
334
|
method = ''
|
|
319
335
|
urlMask = ''
|
|
320
336
|
urlMaskDittoed = ['', '']
|
|
@@ -328,50 +344,46 @@ export class BrokerRowModel {
|
|
|
328
344
|
constructor(broker, canProxy) {
|
|
329
345
|
this.#broker = broker
|
|
330
346
|
this.#canProxy = canProxy
|
|
331
|
-
const { method, urlMask } = parseFilename(broker.
|
|
347
|
+
const { method, urlMask } = parseFilename(broker.file)
|
|
348
|
+
this.key = 'brm' + '::' + method + '::' + urlMask
|
|
332
349
|
this.method = method
|
|
333
350
|
this.urlMask = urlMask
|
|
334
351
|
this.opts = this.#makeOptions()
|
|
335
352
|
}
|
|
336
|
-
|
|
353
|
+
|
|
337
354
|
setUrlMaskDittoed(urlMaskDittoed) {
|
|
338
355
|
this.urlMaskDittoed = urlMaskDittoed
|
|
339
356
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
return this.#broker.currentMock.delayed
|
|
343
|
-
}
|
|
344
|
-
get proxied() {
|
|
345
|
-
return this.#canProxy && this.#broker.currentMock.proxied
|
|
357
|
+
setIsNew(isNew) {
|
|
358
|
+
this.isNew = isNew
|
|
346
359
|
}
|
|
360
|
+
|
|
361
|
+
get status() { return this.#broker.status }
|
|
362
|
+
get auto500() { return this.#broker.auto500 }
|
|
363
|
+
get delayed() { return this.#broker.delayed }
|
|
364
|
+
get proxied() { return this.#canProxy && this.#broker.proxied }
|
|
365
|
+
get selectedFile() { return this.#broker.file }
|
|
347
366
|
get selectedIdx() {
|
|
348
367
|
return this.opts.findIndex(([, , selected]) => selected)
|
|
349
368
|
}
|
|
350
|
-
get selectedFile() {
|
|
351
|
-
return this.#broker.currentMock.file
|
|
352
|
-
}
|
|
353
369
|
get selectedFileIs4xx() {
|
|
354
|
-
|
|
355
|
-
return status >= 400 && status < 500
|
|
356
|
-
}
|
|
357
|
-
get selectedFileIs500() {
|
|
358
|
-
const { status } = parseFilename(this.selectedFile)
|
|
359
|
-
return status === 500
|
|
370
|
+
return this.status >= 400 && this.status < 500
|
|
360
371
|
}
|
|
361
372
|
|
|
362
373
|
#makeOptions() {
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
.map(f => [
|
|
369
|
-
f,
|
|
370
|
-
this.#optionLabelFor(f),
|
|
371
|
-
!proxied && f === this.selectedFile
|
|
372
|
-
])
|
|
374
|
+
const opts = this.#broker.mocks.map(f => [
|
|
375
|
+
f,
|
|
376
|
+
this.#optionLabelFor(f),
|
|
377
|
+
!this.auto500 && !this.proxied && f === this.selectedFile
|
|
378
|
+
])
|
|
373
379
|
|
|
374
|
-
if (
|
|
380
|
+
if (this.auto500)
|
|
381
|
+
opts.push([
|
|
382
|
+
'__AUTO_500__',
|
|
383
|
+
t`Auto500`,
|
|
384
|
+
true
|
|
385
|
+
])
|
|
386
|
+
else if (this.proxied)
|
|
375
387
|
opts.push([
|
|
376
388
|
'__PROXIED__',
|
|
377
389
|
t`Proxied`,
|
|
@@ -383,49 +395,32 @@ export class BrokerRowModel {
|
|
|
383
395
|
|
|
384
396
|
#optionLabelFor(file) {
|
|
385
397
|
const { status, ext } = parseFilename(file)
|
|
386
|
-
const comments = extractComments(file)
|
|
387
|
-
const isAutogen500 = comments.includes(AUTO_500_COMMENT)
|
|
388
398
|
return [
|
|
389
|
-
|
|
399
|
+
status,
|
|
390
400
|
ext === 'empty' || ext === 'unknown' ? '' : ext,
|
|
391
|
-
|
|
401
|
+
extractComments(file).join(' ')
|
|
392
402
|
].filter(Boolean).join(' ')
|
|
393
403
|
}
|
|
394
404
|
}
|
|
395
405
|
|
|
396
|
-
const
|
|
397
|
-
'
|
|
398
|
-
const broker = {
|
|
399
|
-
currentMock: { file: 'api/other' },
|
|
400
|
-
mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
|
|
401
|
-
}
|
|
402
|
-
const row = new BrokerRowModel(broker, false)
|
|
403
|
-
console.assert(row.opts.length === 0)
|
|
404
|
-
},
|
|
405
|
-
|
|
406
|
-
'keeps non-autogen500 when unselected'() {
|
|
406
|
+
const TestBrokerRowModel = {
|
|
407
|
+
'has Auto500 when is autogenerated 500'() {
|
|
407
408
|
const broker = {
|
|
408
|
-
|
|
409
|
-
|
|
409
|
+
auto500: true,
|
|
410
|
+
file: 'api/user.GET.200.json',
|
|
411
|
+
mocks: ['api/user.GET.200.json']
|
|
410
412
|
}
|
|
411
413
|
const row = new BrokerRowModel(broker, false)
|
|
412
|
-
|
|
413
|
-
console.assert(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const broker = {
|
|
418
|
-
currentMock: { file: `api/user${AUTO_500_COMMENT}.GET.500.empty` },
|
|
419
|
-
mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
|
|
420
|
-
}
|
|
421
|
-
const row = new BrokerRowModel(broker, false)
|
|
422
|
-
console.assert(row.opts.length === 1)
|
|
423
|
-
console.assert(row.opts[0][1] === t`Auto500`)
|
|
414
|
+
const opts = row.opts.map(([, n, selected]) => [n, selected])
|
|
415
|
+
console.assert(deepEqual(opts, [
|
|
416
|
+
['200 json', false],
|
|
417
|
+
[t`Auto500`, true],
|
|
418
|
+
]))
|
|
424
419
|
},
|
|
425
420
|
|
|
426
421
|
'filename has extension except when empty or unknown'() {
|
|
427
422
|
const broker = {
|
|
428
|
-
|
|
423
|
+
file: `api/user0.GET.200.empty`,
|
|
429
424
|
mocks: [
|
|
430
425
|
`api/user0.GET.200.empty`,
|
|
431
426
|
`api/user1.GET.200.unknown`,
|
|
@@ -434,22 +429,21 @@ const TestBrokerRowModelOptions = {
|
|
|
434
429
|
]
|
|
435
430
|
}
|
|
436
431
|
const row = new BrokerRowModel(broker, false)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
'200',
|
|
441
|
-
'200',
|
|
442
|
-
'200 json',
|
|
443
|
-
'200 json (another json)',
|
|
432
|
+
const opts = row.opts.map(([, n, selected]) => [n, selected])
|
|
433
|
+
console.assert(deepEqual(opts, [
|
|
434
|
+
['200', true],
|
|
435
|
+
['200', false],
|
|
436
|
+
['200 json', false],
|
|
437
|
+
['200 json (another json)', false]
|
|
444
438
|
]))
|
|
439
|
+
// TODO Think about, in cases like this, the only option the user has
|
|
440
|
+
// for discerning empty and unknown is on the Previewer Title
|
|
445
441
|
},
|
|
446
442
|
|
|
447
443
|
'appends "Proxied" label iff current is proxied'() {
|
|
448
444
|
const broker = {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
proxied: true
|
|
452
|
-
},
|
|
445
|
+
file: 'api/foo',
|
|
446
|
+
proxied: true,
|
|
453
447
|
mocks: [`api/foo.GET.200.json`]
|
|
454
448
|
}
|
|
455
449
|
const row = new BrokerRowModel(broker, true)
|
|
@@ -459,7 +453,31 @@ const TestBrokerRowModelOptions = {
|
|
|
459
453
|
]))
|
|
460
454
|
}
|
|
461
455
|
}
|
|
462
|
-
deferred(() => Object.values(
|
|
456
|
+
deferred(() => Object.values(TestBrokerRowModel).forEach(t => t()))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
export class StaticBrokerRowModel {
|
|
460
|
+
isNew = false
|
|
461
|
+
key = ''
|
|
462
|
+
method = 'GET'
|
|
463
|
+
urlMaskDittoed = ['', '']
|
|
464
|
+
#broker = /** @type {ClientStaticBroker} */ {}
|
|
465
|
+
|
|
466
|
+
/** @param {ClientStaticBroker} broker */
|
|
467
|
+
constructor(broker) {
|
|
468
|
+
this.#broker = broker
|
|
469
|
+
this.key = 'sbrm' + '::' + this.method + '::' + broker.route
|
|
470
|
+
}
|
|
471
|
+
setUrlMaskDittoed(urlMaskDittoed) {
|
|
472
|
+
this.urlMaskDittoed = urlMaskDittoed
|
|
473
|
+
}
|
|
474
|
+
setIsNew(isNew) {
|
|
475
|
+
this.isNew = isNew
|
|
476
|
+
}
|
|
477
|
+
get urlMask() { return this.#broker.route }
|
|
478
|
+
get delayed() { return this.#broker.delayed }
|
|
479
|
+
get status() { return this.#broker.status }
|
|
480
|
+
}
|
|
463
481
|
|
|
464
482
|
function deepEqual(a, b) {
|
|
465
483
|
return JSON.stringify(a) === JSON.stringify(b)
|
package/src/Filename.js
CHANGED
|
@@ -22,7 +22,6 @@ export const includesComment = (filename, search) =>
|
|
|
22
22
|
extractComments(filename).some(comment => comment.includes(search))
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
// TODO ThinkAbout 206 (reject, handle, or send in full?)
|
|
26
25
|
export function validateFilename(file) {
|
|
27
26
|
const tokens = file.replace(reComments, '').split('.')
|
|
28
27
|
if (tokens.length < 4)
|
|
@@ -34,7 +33,8 @@ export function validateFilename(file) {
|
|
|
34
33
|
|
|
35
34
|
if (!httpMethods.includes(method))
|
|
36
35
|
return `Unrecognized HTTP Method: "${method}"`
|
|
37
|
-
}
|
|
36
|
+
}
|
|
37
|
+
// TODO ThinkAbout 206 (reject, handle, or send in full?)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
export function parseFilename(file) {
|
|
@@ -54,12 +54,12 @@ function removeTrailingSlash(url = '') {
|
|
|
54
54
|
.replace('/#', '#')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// TODO ThinkAbout allowing custom status codes
|
|
58
57
|
function responseStatusIsValid(status) {
|
|
59
58
|
return Number.isInteger(status)
|
|
60
59
|
&& status >= 100
|
|
61
60
|
&& status <= 599
|
|
62
61
|
}
|
|
62
|
+
// TODO ThinkAbout allowing custom status codes
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
export function makeMockFilename(url, method, status, ext) {
|
package/src/MockBroker.js
CHANGED
|
@@ -1,99 +1,82 @@
|
|
|
1
1
|
import { includesComment, extractComments, parseFilename } from './Filename.js'
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
5
|
+
/**
|
|
6
|
+
* MockBroker is a state for a particular route. It knows the available mock
|
|
7
|
+
* files that can be served for the route, and the currently selected file, etc.
|
|
8
|
+
*/
|
|
7
9
|
export class MockBroker {
|
|
8
10
|
constructor(file) {
|
|
11
|
+
this.file = '' // selected mock filename
|
|
12
|
+
this.mocks = [] // filenames
|
|
13
|
+
this.status = -1
|
|
14
|
+
this.delayed = false
|
|
15
|
+
this.proxied = false
|
|
16
|
+
this.auto500 = false
|
|
9
17
|
this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
|
|
10
|
-
this.mocks = []
|
|
11
|
-
this.currentMock = {
|
|
12
|
-
file: '',
|
|
13
|
-
delayed: false,
|
|
14
|
-
proxied: false
|
|
15
|
-
}
|
|
16
18
|
this.register(file)
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
get delayed() { return this.currentMock.delayed }
|
|
21
|
-
get proxied() { return this.currentMock.proxied }
|
|
22
|
-
|
|
23
|
-
get status() { return parseFilename(this.file).status }
|
|
24
|
-
get temp500IsSelected() { return this.#isTemp500(this.file) }
|
|
25
|
-
|
|
26
|
-
hasMock(file) { return this.mocks.includes(file) }
|
|
27
|
-
|
|
28
|
-
register(file) {
|
|
29
|
-
if (this.#is500(file)) {
|
|
30
|
-
if (this.temp500IsSelected)
|
|
31
|
-
this.selectFile(file)
|
|
32
|
-
this.#deleteTemp500()
|
|
33
|
-
}
|
|
34
|
-
this.mocks.push(file)
|
|
35
|
-
this.#sortMocks()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
#is500(file) {
|
|
39
|
-
return parseFilename(file).status === 500
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
#deleteTemp500() {
|
|
43
|
-
this.mocks = this.mocks.filter(file => !this.#isTemp500(file))
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
#isTemp500(file) {
|
|
47
|
-
return includesComment(file, AUTO_500_COMMENT)
|
|
48
|
-
}
|
|
21
|
+
#is500 = file => parseFilename(file).status === 500
|
|
49
22
|
|
|
50
23
|
#sortMocks() {
|
|
51
24
|
this.mocks.sort()
|
|
52
25
|
const defaults = this.mocks.filter(file => includesComment(file, DEFAULT_MOCK_COMMENT))
|
|
53
|
-
|
|
54
|
-
this.mocks = [
|
|
55
|
-
...defaults,
|
|
56
|
-
...this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file)),
|
|
57
|
-
...temp500
|
|
58
|
-
]
|
|
26
|
+
this.mocks = Array.from(new Set(defaults).union(new Set(this.mocks)))
|
|
59
27
|
}
|
|
60
28
|
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
this
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const { urlMask, method } = parseFilename(this.mocks[0])
|
|
67
|
-
const file = urlMask.replace(/^\//, '') // Removes leading slash
|
|
68
|
-
this.mocks.push(`${file}${AUTO_500_COMMENT}.${method}.500.empty`)
|
|
29
|
+
register(file) {
|
|
30
|
+
if (this.auto500 && this.#is500(file)) // TESTME
|
|
31
|
+
this.selectFile(file)
|
|
32
|
+
this.mocks.push(file)
|
|
33
|
+
this.#sortMocks()
|
|
69
34
|
}
|
|
70
35
|
|
|
71
36
|
unregister(file) {
|
|
72
37
|
this.mocks = this.mocks.filter(f => f !== file)
|
|
73
38
|
const isEmpty = !this.mocks.length
|
|
74
|
-
|| this.mocks.length === 1 && this.#isTemp500(this.mocks[0])
|
|
75
39
|
if (!isEmpty && this.file === file)
|
|
76
40
|
this.selectDefaultFile()
|
|
77
41
|
return isEmpty
|
|
78
42
|
}
|
|
79
43
|
|
|
44
|
+
hasMock = file => this.mocks.includes(file)
|
|
45
|
+
|
|
46
|
+
selectFile(filename) {
|
|
47
|
+
this.file = filename
|
|
48
|
+
this.proxied = false
|
|
49
|
+
this.auto500 = false
|
|
50
|
+
this.status = parseFilename(filename).status
|
|
51
|
+
}
|
|
52
|
+
|
|
80
53
|
selectDefaultFile() {
|
|
81
54
|
this.selectFile(this.mocks[0])
|
|
82
55
|
}
|
|
83
56
|
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
57
|
+
toggle500() {
|
|
58
|
+
this.proxied = false // TESTME
|
|
59
|
+
if (this.auto500 || this.status === 500)
|
|
60
|
+
this.selectDefaultFile()
|
|
61
|
+
else {
|
|
62
|
+
const f = this.mocks.find(this.#is500) // TESTME
|
|
63
|
+
if (f)
|
|
64
|
+
this.selectFile(f)
|
|
65
|
+
else {
|
|
66
|
+
this.auto500 = true
|
|
67
|
+
this.status = 500 // TESTME
|
|
68
|
+
}
|
|
69
|
+
}
|
|
87
70
|
}
|
|
88
71
|
|
|
89
|
-
|
|
90
|
-
this
|
|
91
|
-
? this.selectDefaultFile()
|
|
92
|
-
: this.selectFile(this.mocks.find(this.#is500))
|
|
72
|
+
setDelayed(delayed) {
|
|
73
|
+
this.delayed = delayed
|
|
93
74
|
}
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
|
|
76
|
+
setProxied(proxied) {
|
|
77
|
+
this.auto500 = false // TESTME
|
|
78
|
+
this.proxied = proxied
|
|
79
|
+
}
|
|
97
80
|
|
|
98
81
|
setByMatchingComment(comment) {
|
|
99
82
|
for (const file of this.mocks)
|
package/src/MockDispatcher.js
CHANGED
|
@@ -35,8 +35,8 @@ export async function dispatchMock(req, response) {
|
|
|
35
35
|
for (let i = 0; i < config.extraHeaders.length; i += 2)
|
|
36
36
|
response.setHeader(config.extraHeaders[i], config.extraHeaders[i + 1])
|
|
37
37
|
|
|
38
|
-
response.statusCode = broker.status // TESTME plugins can change it
|
|
39
|
-
const { mime, body } = broker.
|
|
38
|
+
response.statusCode = broker.auto500 ? 500 : broker.status // TESTME plugins can change it
|
|
39
|
+
const { mime, body } = broker.auto500
|
|
40
40
|
? { mime: '', body: '' }
|
|
41
41
|
: await applyPlugins(join(config.mocksDir, broker.file), req, response)
|
|
42
42
|
|
package/src/Mockaton.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
|
10
10
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
11
11
|
import { watchMocksDir, watchStaticDir } from './Watcher.js'
|
|
12
12
|
import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
13
|
-
import { BodyReaderError,
|
|
13
|
+
import { BodyReaderError, hasControlChars } from './utils/http-request.js'
|
|
14
14
|
import { sendNoContent, sendInternalServerError, sendUnprocessableContent, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
|
|
15
15
|
|
|
16
16
|
|
|
@@ -26,8 +26,7 @@ export function Mockaton(options) {
|
|
|
26
26
|
const server = createServer(onRequest)
|
|
27
27
|
server.on('error', reject)
|
|
28
28
|
server.listen(config.port, config.host, () => {
|
|
29
|
-
const
|
|
30
|
-
const url = `http://${address}:${port}`
|
|
29
|
+
const url = `http://${server.address().address}:${server.address().port}`
|
|
31
30
|
const dashboardUrl = url + API.dashboard
|
|
32
31
|
logger.info('Listening', url)
|
|
33
32
|
logger.info('Dashboard', dashboardUrl)
|
|
@@ -47,27 +46,30 @@ async function onRequest(req, response) {
|
|
|
47
46
|
sendTooLongURI(response)
|
|
48
47
|
return
|
|
49
48
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
sendBadRequest(response)
|
|
49
|
+
if (hasControlChars(url)) {
|
|
50
|
+
sendBadRequest(response)
|
|
53
51
|
return
|
|
54
52
|
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const route = new URL(url, 'http://_').pathname
|
|
58
|
-
const { method } = req
|
|
59
53
|
|
|
54
|
+
try {
|
|
60
55
|
if (config.corsAllowed)
|
|
61
56
|
setCorsHeaders(req, response, config)
|
|
62
57
|
|
|
58
|
+
const { method } = req
|
|
59
|
+
const { pathname } = new URL(url, 'http://_')
|
|
60
|
+
|
|
63
61
|
if (isPreflight(req))
|
|
64
62
|
sendNoContent(response)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
else if (method === 'GET' &&
|
|
63
|
+
|
|
64
|
+
else if (method === 'PATCH' && apiPatchRequests.has(pathname))
|
|
65
|
+
await apiPatchRequests.get(pathname)(req, response)
|
|
66
|
+
|
|
67
|
+
else if (method === 'GET' && apiGetRequests.has(pathname))
|
|
68
|
+
apiGetRequests.get(pathname)(req, response)
|
|
69
|
+
|
|
70
|
+
else if (method === 'GET' && staticCollection.brokerByRoute(pathname))
|
|
70
71
|
await dispatchStatic(req, response)
|
|
72
|
+
|
|
71
73
|
else
|
|
72
74
|
await dispatchMock(req, response)
|
|
73
75
|
}
|
package/src/Watcher.js
CHANGED
|
@@ -8,20 +8,34 @@ import * as staticCollection from './staticCollection.js'
|
|
|
8
8
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* ARR = Add, Remove, or Rename Mock Event
|
|
13
|
+
*
|
|
14
|
+
* The emitter is debounced so it handles e.g. bulk deletes,
|
|
15
|
+
* and also renames, which are two events (delete + add).
|
|
16
|
+
*/
|
|
12
17
|
export const uiSyncVersion = new class extends EventEmitter {
|
|
13
18
|
version = 0
|
|
14
19
|
|
|
15
|
-
increment() {
|
|
20
|
+
increment = this.#debounce(() => {
|
|
16
21
|
this.version++
|
|
17
22
|
super.emit('ARR')
|
|
18
|
-
}
|
|
23
|
+
})
|
|
24
|
+
|
|
19
25
|
subscribe(listener) {
|
|
20
26
|
this.once('ARR', listener)
|
|
21
27
|
}
|
|
22
28
|
unsubscribe(listener) {
|
|
23
29
|
this.removeListener('ARR', listener)
|
|
24
30
|
}
|
|
31
|
+
|
|
32
|
+
#debounce(fn) {
|
|
33
|
+
let timer
|
|
34
|
+
return () => {
|
|
35
|
+
clearTimeout(timer)
|
|
36
|
+
timer = setTimeout(fn, 80)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
export function watchMocksDir() {
|
|
@@ -29,7 +43,7 @@ export function watchMocksDir() {
|
|
|
29
43
|
watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
30
44
|
if (!file)
|
|
31
45
|
return
|
|
32
|
-
|
|
46
|
+
|
|
33
47
|
const path = join(dir, file)
|
|
34
48
|
|
|
35
49
|
if (isDirectory(path)) {
|
|
@@ -56,7 +70,7 @@ export function watchStaticDir() {
|
|
|
56
70
|
watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
57
71
|
if (!file)
|
|
58
72
|
return
|
|
59
|
-
|
|
73
|
+
|
|
60
74
|
const path = join(dir, file)
|
|
61
75
|
|
|
62
76
|
if (isDirectory(path)) {
|
|
@@ -77,4 +91,3 @@ export function watchStaticDir() {
|
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
// TODO config changes
|
|
80
|
-
// TODO think about throttling e.g. bulk deletes/remove files
|
|
@@ -6,7 +6,6 @@ import { MockBroker } from './MockBroker.js'
|
|
|
6
6
|
import { listFilesRecursively } from './utils/fs.js'
|
|
7
7
|
import { config, isFileAllowed } from './config.js'
|
|
8
8
|
import { parseFilename, validateFilename } from './Filename.js'
|
|
9
|
-
import { AUTO_500_COMMENT } from './ApiConstants.js'
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
/**
|
|
@@ -36,10 +35,7 @@ export function init() {
|
|
|
36
35
|
.sort()
|
|
37
36
|
.forEach(f => registerMock(f))
|
|
38
37
|
|
|
39
|
-
forEachBroker(
|
|
40
|
-
broker.ensureItHas500()
|
|
41
|
-
broker.selectDefaultFile()
|
|
42
|
-
})
|
|
38
|
+
forEachBroker(b => b.selectDefaultFile())
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
/** @returns {boolean} registered */
|
|
@@ -57,12 +53,9 @@ export function registerMock(file, isFromWatcher = false) {
|
|
|
57
53
|
else
|
|
58
54
|
collection[method][urlMask].register(file)
|
|
59
55
|
|
|
60
|
-
if (isFromWatcher && !
|
|
56
|
+
if (isFromWatcher && !collection[method][urlMask].file) // TESTME e.g. auto500 is selected and adding a new one
|
|
61
57
|
collection[method][urlMask].selectDefaultFile()
|
|
62
58
|
|
|
63
|
-
if (isFromWatcher)
|
|
64
|
-
collection[method][urlMask].ensureItHas500()
|
|
65
|
-
|
|
66
59
|
return true
|
|
67
60
|
}
|
|
68
61
|
|
|
@@ -117,7 +110,6 @@ export function extractAllComments() {
|
|
|
117
110
|
comments.add(c)
|
|
118
111
|
})
|
|
119
112
|
return Array.from(comments)
|
|
120
|
-
.filter(c => c !== AUTO_500_COMMENT)
|
|
121
113
|
}
|
|
122
114
|
|
|
123
115
|
export function setMocksMatchingComment(comment) {
|
|
@@ -50,13 +50,13 @@ export function readBody(req, parser = a => a) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export const reControlAndDelChars = /[\x00-\x1f\x7f]/
|
|
53
|
-
export function
|
|
53
|
+
export function hasControlChars(url) {
|
|
54
54
|
try {
|
|
55
55
|
const decoded = decode(url)
|
|
56
|
-
return decoded
|
|
56
|
+
return !decoded || reControlAndDelChars.test(decoded)
|
|
57
57
|
}
|
|
58
58
|
catch {
|
|
59
|
-
return
|
|
59
|
+
return true
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|