mockaton 8.13.1 → 8.14.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/package.json +1 -1
- package/src/Api.js +42 -12
- package/src/ApiConstants.js +3 -0
- package/src/Commander.js +14 -0
- package/src/Dashboard.css +18 -30
- package/src/Dashboard.js +112 -58
- package/src/Mockaton.js +2 -1
- package/src/StaticDispatcher.js +68 -44
- package/src/utils/fs.js +9 -4
- package/src/utils/http-response.js +28 -1
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { cookie } from './cookie.js'
|
|
8
|
-
import { uiSyncVersion } from './Watcher.js'
|
|
9
8
|
import { parseJSON } from './utils/http-request.js'
|
|
10
|
-
import {
|
|
9
|
+
import { uiSyncVersion } from './Watcher.js'
|
|
11
10
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
12
|
-
import { config,
|
|
11
|
+
import { config, ConfigValidator } from './config.js'
|
|
12
|
+
import { getStaticFilesCollection, findStaticBrokerByRoute } from './StaticDispatcher.js'
|
|
13
13
|
import { DF, API, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
|
|
14
14
|
import { sendOK, sendJSON, sendUnprocessableContent, sendFile } from './utils/http-response.js'
|
|
15
15
|
|
|
@@ -47,7 +47,9 @@ export const apiPatchRequests = new Map([
|
|
|
47
47
|
[API.fallback, updateProxyFallback],
|
|
48
48
|
[API.bulkSelect, bulkUpdateBrokersByCommentTag],
|
|
49
49
|
[API.globalDelay, setGlobalDelay],
|
|
50
|
-
[API.collectProxied, setCollectProxied]
|
|
50
|
+
[API.collectProxied, setCollectProxied],
|
|
51
|
+
[API.delayStatic, setStaticRouteIsDelayed],
|
|
52
|
+
[API.notFoundStatic, setStaticRouteIsNotFound]
|
|
51
53
|
])
|
|
52
54
|
|
|
53
55
|
|
|
@@ -64,18 +66,13 @@ function serveDashboardAsset(f) {
|
|
|
64
66
|
|
|
65
67
|
function listCookies(_, response) { sendJSON(response, cookie.list()) }
|
|
66
68
|
function listComments(_, response) { sendJSON(response, mockBrokersCollection.extractAllComments()) }
|
|
69
|
+
function listStaticFiles(req, response) { sendJSON(response, getStaticFilesCollection()) }
|
|
67
70
|
function getGlobalDelay(_, response) { sendJSON(response, config.delay) }
|
|
68
71
|
function listMockBrokers(_, response) { sendJSON(response, mockBrokersCollection.getAll()) }
|
|
69
72
|
function getProxyFallback(_, response) { sendJSON(response, config.proxyFallback) }
|
|
70
73
|
function getIsCorsAllowed(_, response) { sendJSON(response, config.corsAllowed) }
|
|
71
74
|
function getCollectProxied(_, response) { sendJSON(response, config.collectProxied) }
|
|
72
75
|
|
|
73
|
-
function listStaticFiles(req, response) {
|
|
74
|
-
sendJSON(response, config.staticDir
|
|
75
|
-
? listFilesRecursively(config.staticDir).filter(isFileAllowed)
|
|
76
|
-
: [])
|
|
77
|
-
}
|
|
78
|
-
|
|
79
76
|
function longPollClientSyncVersion(req, response) {
|
|
80
77
|
if (uiSyncVersion.version !== Number(req.headers[DF.syncVersion])) {
|
|
81
78
|
// e.g., tab was hidden while new mocks were added or removed
|
|
@@ -134,7 +131,7 @@ async function setRouteIsDelayed(req, response) {
|
|
|
134
131
|
body[DF.routeUrlMask])
|
|
135
132
|
|
|
136
133
|
if (!broker) // TESTME
|
|
137
|
-
sendUnprocessableContent(response, `Route does not exist: ${body[DF.
|
|
134
|
+
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
|
|
138
135
|
else if (typeof delayed !== 'boolean')
|
|
139
136
|
sendUnprocessableContent(response, `Expected a boolean for "delayed"`) // TESTME
|
|
140
137
|
else {
|
|
@@ -151,7 +148,7 @@ async function setRouteIsProxied(req, response) { // TESTME
|
|
|
151
148
|
body[DF.routeUrlMask])
|
|
152
149
|
|
|
153
150
|
if (!broker)
|
|
154
|
-
sendUnprocessableContent(response, `Route does not exist: ${body[DF.
|
|
151
|
+
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
|
|
155
152
|
else if (typeof proxied !== 'boolean')
|
|
156
153
|
sendUnprocessableContent(response, `Expected a boolean for "proxied"`)
|
|
157
154
|
else if (proxied && !config.proxyFallback)
|
|
@@ -198,3 +195,36 @@ async function setGlobalDelay(req, response) { // TESTME
|
|
|
198
195
|
config.delay = parseInt(await parseJSON(req), 10)
|
|
199
196
|
sendOK(response)
|
|
200
197
|
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async function setStaticRouteIsNotFound(req, response) {
|
|
201
|
+
const body = await parseJSON(req)
|
|
202
|
+
const shouldBeNotFound = body[DF.shouldBeNotFound]
|
|
203
|
+
const broker = findStaticBrokerByRoute(body[DF.routeUrlMask])
|
|
204
|
+
|
|
205
|
+
if (!broker) // TESTME
|
|
206
|
+
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]}`)
|
|
207
|
+
else if (typeof shouldBeNotFound !== 'boolean')
|
|
208
|
+
sendUnprocessableContent(response, `Expected a boolean for "not found"`) // TESTME
|
|
209
|
+
else {
|
|
210
|
+
broker.updateNotFound(body[DF.shouldBeNotFound])
|
|
211
|
+
sendOK(response)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async function setStaticRouteIsDelayed(req, response) {
|
|
217
|
+
const body = await parseJSON(req)
|
|
218
|
+
const shouldBeNotFound = body[DF.delayed]
|
|
219
|
+
const broker = findStaticBrokerByRoute(body[DF.routeUrlMask])
|
|
220
|
+
|
|
221
|
+
if (!broker) // TESTME
|
|
222
|
+
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]}`)
|
|
223
|
+
else if (typeof shouldBeNotFound !== 'boolean')
|
|
224
|
+
sendUnprocessableContent(response, `Expected a boolean for "delayed"`) // TESTME
|
|
225
|
+
else {
|
|
226
|
+
broker.updateDelayed(body[DF.delayed])
|
|
227
|
+
sendOK(response)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
package/src/ApiConstants.js
CHANGED
|
@@ -7,9 +7,11 @@ export const API = {
|
|
|
7
7
|
cookies: MOUNT + '/cookies',
|
|
8
8
|
cors: MOUNT + '/cors',
|
|
9
9
|
delay: MOUNT + '/delay',
|
|
10
|
+
delayStatic: MOUNT + '/delay-static',
|
|
10
11
|
fallback: MOUNT + '/fallback',
|
|
11
12
|
globalDelay: MOUNT + '/global-delay',
|
|
12
13
|
mocks: MOUNT + '/mocks',
|
|
14
|
+
notFoundStatic: MOUNT + '/not-found-static',
|
|
13
15
|
proxied: MOUNT + '/proxied',
|
|
14
16
|
reset: MOUNT + '/reset',
|
|
15
17
|
select: MOUNT + '/select',
|
|
@@ -22,6 +24,7 @@ export const DF = { // Dashboard Fields (XHR)
|
|
|
22
24
|
routeUrlMask: 'route_url_mask',
|
|
23
25
|
delayed: 'delayed',
|
|
24
26
|
proxied: 'proxied',
|
|
27
|
+
shouldBeNotFound: 'should_be_not_found',
|
|
25
28
|
syncVersion: 'last_received_sync_version'
|
|
26
29
|
}
|
|
27
30
|
|
package/src/Commander.js
CHANGED
|
@@ -37,6 +37,20 @@ export class Commander {
|
|
|
37
37
|
})
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
setStaticRouteIsDelayed(routeUrlMask, delayed) {
|
|
41
|
+
return this.#patch(API.delayStatic, {
|
|
42
|
+
[DF.routeUrlMask]: routeUrlMask,
|
|
43
|
+
[DF.delayed]: delayed
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setStaticRouteIs404(routeUrlMask, shouldBeNotFound) {
|
|
48
|
+
return this.#patch(API.notFoundStatic, {
|
|
49
|
+
[DF.routeUrlMask]: routeUrlMask,
|
|
50
|
+
[DF.shouldBeNotFound]: shouldBeNotFound
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
setRouteIsProxied(routeMethod, routeUrlMask, proxied) {
|
|
41
55
|
return this.#patch(API.proxied, {
|
|
42
56
|
[DF.routeMethod]: routeMethod,
|
package/src/Dashboard.css
CHANGED
|
@@ -230,26 +230,26 @@ select {
|
|
|
230
230
|
display: flex;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
table {
|
|
234
|
+
border-collapse: collapse;
|
|
235
|
+
|
|
236
|
+
th {
|
|
237
|
+
padding-top: 20px;
|
|
238
|
+
padding-bottom: 2px;
|
|
239
|
+
padding-left: 99px;
|
|
240
|
+
text-align: left;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
tr {
|
|
244
|
+
border-top: 2px solid transparent;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
233
247
|
|
|
234
248
|
.MockList {
|
|
235
249
|
display: flex;
|
|
236
250
|
align-items: flex-start;
|
|
237
251
|
margin-top: 64px;
|
|
238
252
|
|
|
239
|
-
> table {
|
|
240
|
-
border-collapse: collapse;
|
|
241
|
-
|
|
242
|
-
th {
|
|
243
|
-
padding-top: 20px;
|
|
244
|
-
padding-bottom: 2px;
|
|
245
|
-
text-align: left;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
tr {
|
|
249
|
-
border-top: 2px solid transparent;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
253
|
&.empty {
|
|
254
254
|
margin-top: 80px;
|
|
255
255
|
}
|
|
@@ -340,7 +340,6 @@ select {
|
|
|
340
340
|
.DelayToggler,
|
|
341
341
|
.ProxyToggler {
|
|
342
342
|
display: flex;
|
|
343
|
-
margin-left: 8px;
|
|
344
343
|
|
|
345
344
|
> input {
|
|
346
345
|
appearance: none;
|
|
@@ -361,6 +360,7 @@ select {
|
|
|
361
360
|
}
|
|
362
361
|
|
|
363
362
|
.DelayToggler {
|
|
363
|
+
margin-left: 8px;
|
|
364
364
|
> input {
|
|
365
365
|
&:checked ~ svg {
|
|
366
366
|
fill: var(--colorAccent);
|
|
@@ -437,6 +437,7 @@ select {
|
|
|
437
437
|
.InternalServerErrorToggler {
|
|
438
438
|
display: flex;
|
|
439
439
|
margin-left: 8px;
|
|
440
|
+
margin-right: 12px;
|
|
440
441
|
cursor: pointer;
|
|
441
442
|
|
|
442
443
|
> input {
|
|
@@ -498,24 +499,11 @@ select {
|
|
|
498
499
|
}
|
|
499
500
|
|
|
500
501
|
.StaticFilesList {
|
|
501
|
-
margin-top:
|
|
502
|
-
|
|
503
|
-
h2 {
|
|
504
|
-
margin-bottom: 8px;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
ul {
|
|
508
|
-
position: relative;
|
|
509
|
-
left: -6px;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
li {
|
|
513
|
-
list-style: none;
|
|
514
|
-
}
|
|
502
|
+
margin-top: 8px;
|
|
515
503
|
|
|
516
504
|
a {
|
|
517
505
|
display: inline-block;
|
|
518
|
-
padding: 6px;
|
|
506
|
+
padding: 6px 0;
|
|
519
507
|
border-radius: var(--radius);
|
|
520
508
|
color: var(--colorAccent);
|
|
521
509
|
text-decoration: none;
|
package/src/Dashboard.js
CHANGED
|
@@ -26,6 +26,7 @@ const Strings = {
|
|
|
26
26
|
internal_server_error: 'Internal Server Error',
|
|
27
27
|
mock: 'Mock',
|
|
28
28
|
no_mocks_found: 'No mocks found',
|
|
29
|
+
not_found: 'Not Found',
|
|
29
30
|
pick_comment: 'Pick Comment…',
|
|
30
31
|
proxied: 'Proxied',
|
|
31
32
|
proxy_toggler: 'Proxy Toggler',
|
|
@@ -264,14 +265,15 @@ function SectionByMethod({ method, brokers, canProxy }) {
|
|
|
264
265
|
const urlMasksDittoed = dittoSplitPaths(urlMasks)
|
|
265
266
|
return (
|
|
266
267
|
r('tbody', null,
|
|
267
|
-
r('th',
|
|
268
|
+
r('th', { colspan: 4 }, method),
|
|
268
269
|
brokersSorted.map(([urlMask, broker], i) =>
|
|
269
270
|
r('tr', { 'data-method': method, 'data-urlMask': urlMask },
|
|
270
|
-
r('td', null, r(
|
|
271
|
-
r('td', null, r(MockSelector, { broker })),
|
|
272
|
-
r('td', null, r(InternalServerErrorToggler, { broker })),
|
|
271
|
+
r('td', null, r(ProxyToggler, { broker, disabled: !canProxy })),
|
|
273
272
|
r('td', null, r(DelayRouteToggler, { broker })),
|
|
274
|
-
r('td', null, r(
|
|
273
|
+
r('td', null, r(InternalServerErrorToggler, { broker })),
|
|
274
|
+
r('td', null, r(PreviewLink, { method, urlMask, urlMaskDittoed: urlMasksDittoed[i] })),
|
|
275
|
+
r('td', null, r(MockSelector, { broker }))
|
|
276
|
+
))))
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
function PreviewLink({ method, urlMask, urlMaskDittoed }) {
|
|
@@ -399,6 +401,84 @@ function ProxyToggler({ broker, disabled }) {
|
|
|
399
401
|
}
|
|
400
402
|
|
|
401
403
|
|
|
404
|
+
|
|
405
|
+
/** # StaticFilesList */
|
|
406
|
+
|
|
407
|
+
function StaticFilesList({ staticFiles }) {
|
|
408
|
+
if (!Object.keys(staticFiles).length)
|
|
409
|
+
return null
|
|
410
|
+
const paths = dittoSplitPaths(Object.keys(staticFiles)).map(([ditto, tail]) => ditto
|
|
411
|
+
? [r('span', null, ditto), tail]
|
|
412
|
+
: tail)
|
|
413
|
+
return (
|
|
414
|
+
r('table', { className: CSS.StaticFilesList },
|
|
415
|
+
r('tbody', null,
|
|
416
|
+
r('th', { colspan: 4 }, Strings.static_get),
|
|
417
|
+
Object.values(staticFiles).map((broker, i) =>
|
|
418
|
+
r('tr', null,
|
|
419
|
+
r('td', null, r(ProxyStaticToggler, {})),
|
|
420
|
+
r('td', null, r(DelayStaticRouteToggler, { broker })),
|
|
421
|
+
r('td', null, r(NotFoundToggler, { broker })),
|
|
422
|
+
r('td', null, r('a', { href: broker.file, target: '_blank' }, paths[i]))
|
|
423
|
+
)))))
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
function DelayStaticRouteToggler({ broker }) {
|
|
428
|
+
function onChange() {
|
|
429
|
+
mockaton.setStaticRouteIsDelayed(broker.file, this.checked)
|
|
430
|
+
.catch(onError)
|
|
431
|
+
}
|
|
432
|
+
return (
|
|
433
|
+
r('label', {
|
|
434
|
+
className: CSS.DelayToggler,
|
|
435
|
+
title: Strings.delay
|
|
436
|
+
},
|
|
437
|
+
r('input', {
|
|
438
|
+
type: 'checkbox',
|
|
439
|
+
checked: broker.delayed,
|
|
440
|
+
onChange
|
|
441
|
+
}),
|
|
442
|
+
TimerIcon()))
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function NotFoundToggler({ broker }) {
|
|
446
|
+
function onChange() {
|
|
447
|
+
mockaton.setStaticRouteIs404(broker.file, this.checked)
|
|
448
|
+
.catch(onError)
|
|
449
|
+
}
|
|
450
|
+
return (
|
|
451
|
+
r('label', {
|
|
452
|
+
className: CSS.InternalServerErrorToggler, // TODO rename
|
|
453
|
+
title: Strings.not_found
|
|
454
|
+
},
|
|
455
|
+
r('input', {
|
|
456
|
+
type: 'checkbox',
|
|
457
|
+
checked: broker.should404,
|
|
458
|
+
onChange
|
|
459
|
+
}),
|
|
460
|
+
r('span', null, '404')))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
// TODO
|
|
465
|
+
function ProxyStaticToggler({}) {
|
|
466
|
+
function onChange() {
|
|
467
|
+
}
|
|
468
|
+
return (
|
|
469
|
+
r('label', {
|
|
470
|
+
style: { visibility: 'hidden' },
|
|
471
|
+
className: CSS.ProxyToggler,
|
|
472
|
+
title: Strings.proxy_toggler
|
|
473
|
+
},
|
|
474
|
+
r('input', {
|
|
475
|
+
type: 'checkbox',
|
|
476
|
+
disabled: true,
|
|
477
|
+
onChange
|
|
478
|
+
}),
|
|
479
|
+
r(CloudIcon)))
|
|
480
|
+
}
|
|
481
|
+
|
|
402
482
|
/** # Payload Preview */
|
|
403
483
|
|
|
404
484
|
const payloadViewerTitleRef = useRef()
|
|
@@ -496,29 +576,6 @@ function mockSelectorFor(method, urlMask) {
|
|
|
496
576
|
}
|
|
497
577
|
|
|
498
578
|
|
|
499
|
-
/** # StaticFilesList */
|
|
500
|
-
|
|
501
|
-
function StaticFilesList({ staticFiles }) {
|
|
502
|
-
if (!staticFiles.length)
|
|
503
|
-
return null
|
|
504
|
-
const paths = dittoSplitPaths(staticFiles).map(([ditto, tail]) => ditto
|
|
505
|
-
? [r('span', null, ditto), tail]
|
|
506
|
-
: tail)
|
|
507
|
-
return (
|
|
508
|
-
r('section', {
|
|
509
|
-
open: true,
|
|
510
|
-
className: CSS.StaticFilesList
|
|
511
|
-
},
|
|
512
|
-
r('h2', null, Strings.static_get),
|
|
513
|
-
r('ul', null, staticFiles.map((f, i) =>
|
|
514
|
-
r('li', null,
|
|
515
|
-
r('a', {
|
|
516
|
-
href: f,
|
|
517
|
-
target: '_blank'
|
|
518
|
-
}, paths[i]))))))
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
579
|
/** # Misc */
|
|
523
580
|
|
|
524
581
|
function onError(error) {
|
|
@@ -626,39 +683,32 @@ function useRef() {
|
|
|
626
683
|
|
|
627
684
|
|
|
628
685
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
686
|
+
* Think of this as a way of printing a directory tree in which
|
|
687
|
+
* the repeated folder paths are kept but styled differently.
|
|
688
|
+
* @param {string[]} paths - sorted
|
|
632
689
|
*/
|
|
633
690
|
function dittoSplitPaths(paths) {
|
|
634
|
-
const result = []
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (k > dittoParts.length)
|
|
651
|
-
dittoParts = currParts.slice(0, k)
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
if (!dittoParts.length)
|
|
655
|
-
result.push(['', path])
|
|
691
|
+
const result = [['', paths[0]]]
|
|
692
|
+
const pathsInParts = paths.map(p => p.split('/').filter(Boolean))
|
|
693
|
+
|
|
694
|
+
for (let i = 1; i < paths.length; i++) {
|
|
695
|
+
const prevParts = pathsInParts[i - 1]
|
|
696
|
+
const currParts = pathsInParts[i]
|
|
697
|
+
|
|
698
|
+
let j = 0
|
|
699
|
+
while (
|
|
700
|
+
j < currParts.length &&
|
|
701
|
+
j < prevParts.length &&
|
|
702
|
+
currParts[j] === prevParts[j])
|
|
703
|
+
j++
|
|
704
|
+
|
|
705
|
+
if (!j) // no common dirs
|
|
706
|
+
result.push(['', paths[i]])
|
|
656
707
|
else {
|
|
657
|
-
const ditto =
|
|
658
|
-
result.push([ditto,
|
|
708
|
+
const ditto = '/' + currParts.slice(0, j).join('/') + '/'
|
|
709
|
+
result.push([ditto, paths[i].slice(ditto.length)])
|
|
659
710
|
}
|
|
660
711
|
}
|
|
661
|
-
|
|
662
712
|
return result
|
|
663
713
|
}
|
|
664
714
|
|
|
@@ -669,7 +719,9 @@ function dittoSplitPaths(paths) {
|
|
|
669
719
|
'/api/user/friends',
|
|
670
720
|
'/api/vid',
|
|
671
721
|
'/api/video/id',
|
|
672
|
-
'/api/video/stats'
|
|
722
|
+
'/api/video/stats',
|
|
723
|
+
'/v2/foo',
|
|
724
|
+
'/v2/foo/bar'
|
|
673
725
|
]
|
|
674
726
|
const expected = [
|
|
675
727
|
['', '/api/user'],
|
|
@@ -677,7 +729,9 @@ function dittoSplitPaths(paths) {
|
|
|
677
729
|
['/api/user/', 'friends'],
|
|
678
730
|
['/api/', 'vid'],
|
|
679
731
|
['/api/', 'video/id'],
|
|
680
|
-
['/api/video/', 'stats']
|
|
732
|
+
['/api/video/', 'stats'],
|
|
733
|
+
['', '/v2/foo'],
|
|
734
|
+
['/v2/foo/', 'bar']
|
|
681
735
|
]
|
|
682
736
|
console.assert(JSON.stringify(dittoSplitPaths(input)) === JSON.stringify(expected))
|
|
683
737
|
}())
|
package/src/Mockaton.js
CHANGED
|
@@ -6,9 +6,9 @@ import { dispatchMock } from './MockDispatcher.js'
|
|
|
6
6
|
import { watchMocksDir } from './Watcher.js'
|
|
7
7
|
import { BodyReaderError } from './utils/http-request.js'
|
|
8
8
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
9
|
-
import { dispatchStatic, isStatic } from './StaticDispatcher.js'
|
|
10
9
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
11
10
|
import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
11
|
+
import { dispatchStatic, isStatic, initStaticCollection } from './StaticDispatcher.js'
|
|
12
12
|
import { sendNoContent, sendInternalServerError, sendUnprocessableContent } from './utils/http-response.js'
|
|
13
13
|
|
|
14
14
|
|
|
@@ -18,6 +18,7 @@ export function Mockaton(options) {
|
|
|
18
18
|
setup(options)
|
|
19
19
|
mockBrokerCollection.init()
|
|
20
20
|
watchMocksDir()
|
|
21
|
+
initStaticCollection()
|
|
21
22
|
|
|
22
23
|
return createServer(onRequest).listen(config.port, config.host, function (error) {
|
|
23
24
|
if (error) {
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,59 +1,83 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
3
|
|
|
4
4
|
import { mimeFor } from './utils/mime.js'
|
|
5
|
-
import { isDirectory, isFile } from './utils/fs.js'
|
|
6
5
|
import { config, isFileAllowed } from './config.js'
|
|
7
|
-
import {
|
|
6
|
+
import { sendPartialContent, sendNotFound } from './utils/http-response.js'
|
|
7
|
+
import { isDirectory, isFile, listFilesRecursively } from './utils/fs.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
class StaticBroker {
|
|
11
|
+
constructor(file) {
|
|
12
|
+
this.file = file
|
|
13
|
+
this.delayed = false
|
|
14
|
+
this.should404 = false
|
|
15
|
+
this.resolvedPath = this.#staticFilePath()
|
|
16
|
+
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
#staticFilePath() { // url is absolute e.g. /home/../.. => /
|
|
19
|
+
let candidate = join(config.staticDir, this.file)
|
|
20
|
+
if (isDirectory(candidate))
|
|
21
|
+
candidate = join(candidate, 'index.html')
|
|
22
|
+
if (isFile(candidate))
|
|
23
|
+
return candidate
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
updateDelayed(value) {
|
|
27
|
+
this.delayed = value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
updateNotFound(value) {
|
|
31
|
+
this.should404 = value
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
let collection = {}
|
|
36
|
+
|
|
37
|
+
export function initStaticCollection() {
|
|
38
|
+
collection = {}
|
|
39
|
+
listFilesRecursively(config.staticDir)
|
|
40
|
+
.filter(isFileAllowed)
|
|
41
|
+
.sort()
|
|
42
|
+
.forEach(f => registerStatic(f))
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (isNaN(start)) start = size - end
|
|
45
|
+
function registerStatic(file) {
|
|
46
|
+
file = '/' + file
|
|
47
|
+
collection[file] = new StaticBroker(file)
|
|
48
|
+
}
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
export function findStaticBrokerByRoute(route) {
|
|
51
|
+
return collection[route] || collection[join(route, 'index.html')]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getStaticFilesCollection() {
|
|
55
|
+
return collection
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isStatic(req) {
|
|
59
|
+
return req.url in collection || join(req.url, 'index.html') in collection
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// TODO improve
|
|
63
|
+
export async function dispatchStatic(req, response) {
|
|
64
|
+
let broker = collection[join(req.url, 'index.html')]
|
|
65
|
+
if (!broker && req.url in collection)
|
|
66
|
+
broker = collection[req.url]
|
|
67
|
+
|
|
68
|
+
if (broker?.should404) { // TESTME
|
|
69
|
+
sendNotFound(response)
|
|
70
|
+
return
|
|
58
71
|
}
|
|
72
|
+
|
|
73
|
+
const file = broker.resolvedPath
|
|
74
|
+
setTimeout(async () => {
|
|
75
|
+
if (req.headers.range)
|
|
76
|
+
await sendPartialContent(response, req.headers.range, file)
|
|
77
|
+
else {
|
|
78
|
+
response.setHeader('Content-Type', mimeFor(file))
|
|
79
|
+
response.end(readFileSync(file))
|
|
80
|
+
}
|
|
81
|
+
}, broker.delayed * config.delay)
|
|
59
82
|
}
|
|
83
|
+
|
package/src/utils/fs.js
CHANGED
|
@@ -7,10 +7,15 @@ export const isDirectory = path => lstatSync(path, { throwIfNoEntry: false })?.i
|
|
|
7
7
|
|
|
8
8
|
/** @returns {Array<string>} paths relative to `dir` */
|
|
9
9
|
export const listFilesRecursively = dir => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
try {
|
|
11
|
+
const files = readdirSync(dir, { recursive: true }).filter(f => isFile(join(dir, f)))
|
|
12
|
+
return process.platform === 'win32'
|
|
13
|
+
? files.map(f => f.replaceAll(sep, posix.sep))
|
|
14
|
+
: files
|
|
15
|
+
}
|
|
16
|
+
catch (err) { // e.g. ENOENT
|
|
17
|
+
return []
|
|
18
|
+
}
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export const write = (path, body) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
1
|
+
import fs, { readFileSync } from 'node:fs'
|
|
2
2
|
import { mimeFor } from './mime.js'
|
|
3
3
|
import { HEADER_FOR_502 } from '../ApiConstants.js'
|
|
4
4
|
|
|
@@ -45,3 +45,30 @@ export function sendBadGateway(response, error) {
|
|
|
45
45
|
response.setHeader(HEADER_FOR_502, 1)
|
|
46
46
|
response.end()
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
export async function sendPartialContent(response, range, file) {
|
|
51
|
+
const { size } = await fs.promises.lstat(file)
|
|
52
|
+
let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
53
|
+
if (isNaN(end)) end = size - 1
|
|
54
|
+
if (isNaN(start)) start = size - end
|
|
55
|
+
|
|
56
|
+
if (start < 0 || start > end || start >= size || end >= size) {
|
|
57
|
+
response.statusCode = 416 // Range Not Satisfiable
|
|
58
|
+
response.setHeader('Content-Range', `bytes */${size}`)
|
|
59
|
+
response.end()
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
response.statusCode = 206 // Partial Content
|
|
63
|
+
response.setHeader('Accept-Ranges', 'bytes')
|
|
64
|
+
response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
65
|
+
response.setHeader('Content-Type', mimeFor(file))
|
|
66
|
+
const reader = fs.createReadStream(file, { start, end })
|
|
67
|
+
reader.on('open', function () {
|
|
68
|
+
this.pipe(response)
|
|
69
|
+
})
|
|
70
|
+
reader.on('error', function (error) {
|
|
71
|
+
sendInternalServerError(response, error)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|