mockaton 10.6.8 → 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 +24 -8
- package/src/Dashboard.js +57 -41
- package/src/DashboardStore.js +125 -104
- 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
|
@@ -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
|
|
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
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,13 +319,12 @@ 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
|
-
|
|
326
|
+
const row = store.brokerAsRow(method, urlMask)
|
|
327
|
+
trFor(row.key).replaceWith(Row(row))
|
|
320
328
|
previewMock()
|
|
321
329
|
})
|
|
322
330
|
|
|
@@ -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)),
|
|
433
445
|
|
|
434
|
-
|
|
435
|
-
|
|
446
|
+
r('td', null,
|
|
447
|
+
NotFoundToggler(row.urlMask, row.status === 404)),
|
|
448
|
+
|
|
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) {
|
|
@@ -610,7 +626,7 @@ async function previewMock() {
|
|
|
610
626
|
signal: previewMock.controller.signal
|
|
611
627
|
})
|
|
612
628
|
clearTimeout(spinnerTimer)
|
|
613
|
-
const { proxied, file } = store.brokerFor(method, urlMask)
|
|
629
|
+
const { proxied, file } = store.brokerFor(method, urlMask)
|
|
614
630
|
if (proxied || file)
|
|
615
631
|
await updatePayloadViewer(proxied, file, response)
|
|
616
632
|
}
|
package/src/DashboardStore.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
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) {},
|
|
@@ -25,11 +24,11 @@ export const store = {
|
|
|
25
24
|
return Boolean(store.proxyFallback)
|
|
26
25
|
},
|
|
27
26
|
|
|
28
|
-
getSyncVersion:
|
|
27
|
+
getSyncVersion: api.getSyncVersion,
|
|
29
28
|
|
|
30
29
|
async fetchState() {
|
|
31
30
|
try {
|
|
32
|
-
const response = await
|
|
31
|
+
const response = await api.getState()
|
|
33
32
|
if (!response.ok) throw response
|
|
34
33
|
Object.assign(store, await response.json())
|
|
35
34
|
store.render()
|
|
@@ -59,11 +58,11 @@ export const store = {
|
|
|
59
58
|
setChosenLink(method, urlMask) {
|
|
60
59
|
store.chosenLink = { method, urlMask }
|
|
61
60
|
},
|
|
62
|
-
|
|
61
|
+
|
|
63
62
|
|
|
64
63
|
async reset() {
|
|
65
64
|
try {
|
|
66
|
-
const response = await
|
|
65
|
+
const response = await api.reset()
|
|
67
66
|
if (!response.ok) throw response
|
|
68
67
|
store.setChosenLink('', '')
|
|
69
68
|
await store.fetchState()
|
|
@@ -73,17 +72,17 @@ export const store = {
|
|
|
73
72
|
|
|
74
73
|
async bulkSelectByComment(value) {
|
|
75
74
|
try {
|
|
76
|
-
const response = await
|
|
75
|
+
const response = await api.bulkSelectByComment(value)
|
|
77
76
|
if (!response.ok) throw response
|
|
78
77
|
await store.fetchState()
|
|
79
78
|
}
|
|
80
79
|
catch (error) { store.onError(error) }
|
|
81
80
|
},
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
async setGlobalDelay(value) {
|
|
85
84
|
try {
|
|
86
|
-
const response = await
|
|
85
|
+
const response = await api.setGlobalDelay(value)
|
|
87
86
|
if (!response.ok) throw response
|
|
88
87
|
store.delay = value
|
|
89
88
|
}
|
|
@@ -92,7 +91,7 @@ export const store = {
|
|
|
92
91
|
|
|
93
92
|
async selectCookie(name) {
|
|
94
93
|
try {
|
|
95
|
-
const response = await
|
|
94
|
+
const response = await api.selectCookie(name)
|
|
96
95
|
if (!response.ok) throw response
|
|
97
96
|
store.cookies = await response.json()
|
|
98
97
|
}
|
|
@@ -101,7 +100,7 @@ export const store = {
|
|
|
101
100
|
|
|
102
101
|
async setProxyFallback(value) {
|
|
103
102
|
try {
|
|
104
|
-
const response = await
|
|
103
|
+
const response = await api.setProxyFallback(value)
|
|
105
104
|
if (!response.ok) throw response
|
|
106
105
|
store.proxyFallback = value
|
|
107
106
|
store.render()
|
|
@@ -111,7 +110,7 @@ export const store = {
|
|
|
111
110
|
|
|
112
111
|
async setCollectProxied(checked) {
|
|
113
112
|
try {
|
|
114
|
-
const response = await
|
|
113
|
+
const response = await api.setCollectProxied(checked)
|
|
115
114
|
if (!response.ok) throw response
|
|
116
115
|
store.collectProxied = checked
|
|
117
116
|
}
|
|
@@ -123,35 +122,54 @@ export const store = {
|
|
|
123
122
|
return store.brokersByMethod[method]?.[urlMask]
|
|
124
123
|
},
|
|
125
124
|
|
|
125
|
+
setBroker(broker) {
|
|
126
|
+
const { method, urlMask } = parseFilename(broker.file)
|
|
127
|
+
store.brokersByMethod[method] ??= {}
|
|
128
|
+
store.brokersByMethod[method][urlMask] = broker
|
|
129
|
+
},
|
|
130
|
+
|
|
126
131
|
_dittoCache: new Map(),
|
|
127
132
|
|
|
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
|
|
139
|
+
},
|
|
140
|
+
|
|
128
141
|
brokersAsRowsByMethod(method) {
|
|
129
142
|
const rows = store._brokersAsArray(method)
|
|
130
143
|
.map(b => new BrokerRowModel(b, store.canProxy))
|
|
131
144
|
.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
132
145
|
const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
|
|
133
146
|
rows.forEach((r, i) => {
|
|
134
|
-
store._dittoCache.set(r.method + '::' + r.urlMask, urlMasksDittoed[i])
|
|
135
147
|
r.setUrlMaskDittoed(urlMasksDittoed[i])
|
|
148
|
+
r.setIsNew(!store._dittoCache.has(r.key))
|
|
149
|
+
store._dittoCache.set(r.key, urlMasksDittoed[i])
|
|
136
150
|
})
|
|
137
151
|
return rows
|
|
138
152
|
},
|
|
139
153
|
|
|
140
154
|
brokerAsRow(method, urlMask) {
|
|
141
|
-
const
|
|
142
|
-
r
|
|
155
|
+
const b = store.brokerFor(method, urlMask)
|
|
156
|
+
const r = new BrokerRowModel(b, store.canProxy)
|
|
157
|
+
r.setUrlMaskDittoed(store._dittoCache.get(r.key))
|
|
143
158
|
return r
|
|
144
159
|
},
|
|
145
|
-
|
|
146
|
-
_brokersAsArray(byMethod = '*') {
|
|
147
|
-
const arr = []
|
|
148
|
-
for (const [method, brokers] of Object.entries(store.brokersByMethod))
|
|
149
|
-
if (byMethod === '*' || byMethod === method)
|
|
150
|
-
for (const broker of Object.values(brokers))
|
|
151
|
-
arr.push(broker)
|
|
152
|
-
return arr
|
|
153
|
-
},
|
|
154
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
|
|
172
|
+
},
|
|
155
173
|
|
|
156
174
|
previewLink(method, urlMask) {
|
|
157
175
|
store.setChosenLink(method, urlMask)
|
|
@@ -160,10 +178,10 @@ export const store = {
|
|
|
160
178
|
|
|
161
179
|
async selectFile(file) {
|
|
162
180
|
try {
|
|
163
|
-
const response = await
|
|
181
|
+
const response = await api.select(file)
|
|
164
182
|
if (!response.ok) throw response
|
|
165
183
|
const { method, urlMask } = parseFilename(file)
|
|
166
|
-
store.
|
|
184
|
+
store.setBroker(await response.json())
|
|
167
185
|
store.setChosenLink(method, urlMask)
|
|
168
186
|
store.renderRow(method, urlMask)
|
|
169
187
|
}
|
|
@@ -172,9 +190,9 @@ export const store = {
|
|
|
172
190
|
|
|
173
191
|
async toggle500(method, urlMask) {
|
|
174
192
|
try {
|
|
175
|
-
const response = await
|
|
193
|
+
const response = await api.toggle500(method, urlMask)
|
|
176
194
|
if (!response.ok) throw response
|
|
177
|
-
store.
|
|
195
|
+
store.setBroker(await response.json())
|
|
178
196
|
store.setChosenLink(method, urlMask)
|
|
179
197
|
store.renderRow(method, urlMask)
|
|
180
198
|
}
|
|
@@ -183,9 +201,9 @@ export const store = {
|
|
|
183
201
|
|
|
184
202
|
async setProxied(method, urlMask, checked) {
|
|
185
203
|
try {
|
|
186
|
-
const response = await
|
|
204
|
+
const response = await api.setRouteIsProxied(method, urlMask, checked)
|
|
187
205
|
if (!response.ok) throw response
|
|
188
|
-
store.
|
|
206
|
+
store.setBroker(await response.json())
|
|
189
207
|
store.setChosenLink(method, urlMask)
|
|
190
208
|
store.renderRow(method, urlMask)
|
|
191
209
|
}
|
|
@@ -194,9 +212,9 @@ export const store = {
|
|
|
194
212
|
|
|
195
213
|
async setDelayed(method, urlMask, checked) {
|
|
196
214
|
try {
|
|
197
|
-
const response = await
|
|
215
|
+
const response = await api.setRouteIsDelayed(method, urlMask, checked)
|
|
198
216
|
if (!response.ok) throw response
|
|
199
|
-
store.
|
|
217
|
+
store.setBroker(await response.json())
|
|
200
218
|
}
|
|
201
219
|
catch (error) { store.onError(error) }
|
|
202
220
|
},
|
|
@@ -204,7 +222,7 @@ export const store = {
|
|
|
204
222
|
|
|
205
223
|
async setDelayedStatic(route, checked) {
|
|
206
224
|
try {
|
|
207
|
-
const response = await
|
|
225
|
+
const response = await api.setStaticRouteIsDelayed(route, checked)
|
|
208
226
|
if (!response.ok) throw response
|
|
209
227
|
store.staticBrokers[route].delayed = checked
|
|
210
228
|
}
|
|
@@ -213,7 +231,7 @@ export const store = {
|
|
|
213
231
|
|
|
214
232
|
async setStaticRouteStatus(route, status) {
|
|
215
233
|
try {
|
|
216
|
-
const response = await
|
|
234
|
+
const response = await api.setStaticRouteStatus(route, status)
|
|
217
235
|
if (!response.ok) throw response
|
|
218
236
|
store.staticBrokers[route].status = status
|
|
219
237
|
}
|
|
@@ -309,9 +327,10 @@ dittoSplitPaths.test = function () {
|
|
|
309
327
|
deferred(dittoSplitPaths.test)
|
|
310
328
|
|
|
311
329
|
|
|
312
|
-
|
|
313
330
|
export class BrokerRowModel {
|
|
314
331
|
opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
|
|
332
|
+
isNew = false
|
|
333
|
+
key = ''
|
|
315
334
|
method = ''
|
|
316
335
|
urlMask = ''
|
|
317
336
|
urlMaskDittoed = ['', '']
|
|
@@ -325,7 +344,8 @@ export class BrokerRowModel {
|
|
|
325
344
|
constructor(broker, canProxy) {
|
|
326
345
|
this.#broker = broker
|
|
327
346
|
this.#canProxy = canProxy
|
|
328
|
-
const { method, urlMask } = parseFilename(broker.
|
|
347
|
+
const { method, urlMask } = parseFilename(broker.file)
|
|
348
|
+
this.key = 'brm' + '::' + method + '::' + urlMask
|
|
329
349
|
this.method = method
|
|
330
350
|
this.urlMask = urlMask
|
|
331
351
|
this.opts = this.#makeOptions()
|
|
@@ -334,41 +354,36 @@ export class BrokerRowModel {
|
|
|
334
354
|
setUrlMaskDittoed(urlMaskDittoed) {
|
|
335
355
|
this.urlMaskDittoed = urlMaskDittoed
|
|
336
356
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return this.#broker.currentMock.delayed
|
|
340
|
-
}
|
|
341
|
-
get proxied() {
|
|
342
|
-
return this.#canProxy && this.#broker.currentMock.proxied
|
|
357
|
+
setIsNew(isNew) {
|
|
358
|
+
this.isNew = isNew
|
|
343
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 }
|
|
344
366
|
get selectedIdx() {
|
|
345
367
|
return this.opts.findIndex(([, , selected]) => selected)
|
|
346
368
|
}
|
|
347
|
-
get selectedFile() {
|
|
348
|
-
return this.#broker.currentMock.file
|
|
349
|
-
}
|
|
350
369
|
get selectedFileIs4xx() {
|
|
351
|
-
|
|
352
|
-
return status >= 400 && status < 500
|
|
353
|
-
}
|
|
354
|
-
get selectedFileIs500() {
|
|
355
|
-
const { status } = parseFilename(this.selectedFile)
|
|
356
|
-
return status === 500
|
|
370
|
+
return this.status >= 400 && this.status < 500
|
|
357
371
|
}
|
|
358
372
|
|
|
359
373
|
#makeOptions() {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
.map(f => [
|
|
366
|
-
f,
|
|
367
|
-
this.#optionLabelFor(f),
|
|
368
|
-
!proxied && f === this.selectedFile
|
|
369
|
-
])
|
|
374
|
+
const opts = this.#broker.mocks.map(f => [
|
|
375
|
+
f,
|
|
376
|
+
this.#optionLabelFor(f),
|
|
377
|
+
!this.auto500 && !this.proxied && f === this.selectedFile
|
|
378
|
+
])
|
|
370
379
|
|
|
371
|
-
if (
|
|
380
|
+
if (this.auto500)
|
|
381
|
+
opts.push([
|
|
382
|
+
'__AUTO_500__',
|
|
383
|
+
t`Auto500`,
|
|
384
|
+
true
|
|
385
|
+
])
|
|
386
|
+
else if (this.proxied)
|
|
372
387
|
opts.push([
|
|
373
388
|
'__PROXIED__',
|
|
374
389
|
t`Proxied`,
|
|
@@ -380,49 +395,32 @@ export class BrokerRowModel {
|
|
|
380
395
|
|
|
381
396
|
#optionLabelFor(file) {
|
|
382
397
|
const { status, ext } = parseFilename(file)
|
|
383
|
-
const comments = extractComments(file)
|
|
384
|
-
const isAutogen500 = comments.includes(AUTO_500_COMMENT)
|
|
385
398
|
return [
|
|
386
|
-
|
|
399
|
+
status,
|
|
387
400
|
ext === 'empty' || ext === 'unknown' ? '' : ext,
|
|
388
|
-
|
|
401
|
+
extractComments(file).join(' ')
|
|
389
402
|
].filter(Boolean).join(' ')
|
|
390
403
|
}
|
|
391
404
|
}
|
|
392
405
|
|
|
393
|
-
const
|
|
394
|
-
'
|
|
395
|
-
const broker = {
|
|
396
|
-
currentMock: { file: 'api/other' },
|
|
397
|
-
mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
|
|
398
|
-
}
|
|
399
|
-
const row = new BrokerRowModel(broker, false)
|
|
400
|
-
console.assert(row.opts.length === 0)
|
|
401
|
-
},
|
|
402
|
-
|
|
403
|
-
'keeps non-autogen500 when unselected'() {
|
|
404
|
-
const broker = {
|
|
405
|
-
currentMock: { file: 'api/other' },
|
|
406
|
-
mocks: [`api/user.GET.500.txt`]
|
|
407
|
-
}
|
|
408
|
-
const row = new BrokerRowModel(broker, false)
|
|
409
|
-
console.assert(row.opts.length === 1)
|
|
410
|
-
console.assert(row.opts[0][1] === t`500 txt`)
|
|
411
|
-
},
|
|
412
|
-
|
|
413
|
-
'renames autogen file to Auto500'() {
|
|
406
|
+
const TestBrokerRowModel = {
|
|
407
|
+
'has Auto500 when is autogenerated 500'() {
|
|
414
408
|
const broker = {
|
|
415
|
-
|
|
416
|
-
|
|
409
|
+
auto500: true,
|
|
410
|
+
file: 'api/user.GET.200.json',
|
|
411
|
+
mocks: ['api/user.GET.200.json']
|
|
417
412
|
}
|
|
418
413
|
const row = new BrokerRowModel(broker, false)
|
|
419
|
-
|
|
420
|
-
console.assert(
|
|
414
|
+
const opts = row.opts.map(([, n, selected]) => [n, selected])
|
|
415
|
+
console.assert(deepEqual(opts, [
|
|
416
|
+
['200 json', false],
|
|
417
|
+
[t`Auto500`, true],
|
|
418
|
+
]))
|
|
421
419
|
},
|
|
422
420
|
|
|
423
421
|
'filename has extension except when empty or unknown'() {
|
|
424
422
|
const broker = {
|
|
425
|
-
|
|
423
|
+
file: `api/user0.GET.200.empty`,
|
|
426
424
|
mocks: [
|
|
427
425
|
`api/user0.GET.200.empty`,
|
|
428
426
|
`api/user1.GET.200.unknown`,
|
|
@@ -431,22 +429,21 @@ const TestBrokerRowModelOptions = {
|
|
|
431
429
|
]
|
|
432
430
|
}
|
|
433
431
|
const row = new BrokerRowModel(broker, false)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
'200',
|
|
438
|
-
'200',
|
|
439
|
-
'200 json',
|
|
440
|
-
'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]
|
|
441
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
|
|
442
441
|
},
|
|
443
442
|
|
|
444
443
|
'appends "Proxied" label iff current is proxied'() {
|
|
445
444
|
const broker = {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
proxied: true
|
|
449
|
-
},
|
|
445
|
+
file: 'api/foo',
|
|
446
|
+
proxied: true,
|
|
450
447
|
mocks: [`api/foo.GET.200.json`]
|
|
451
448
|
}
|
|
452
449
|
const row = new BrokerRowModel(broker, true)
|
|
@@ -456,7 +453,31 @@ const TestBrokerRowModelOptions = {
|
|
|
456
453
|
]))
|
|
457
454
|
}
|
|
458
455
|
}
|
|
459
|
-
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
|
+
}
|
|
460
481
|
|
|
461
482
|
function deepEqual(a, b) {
|
|
462
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
|
|