mockaton 8.7.7 → 8.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -23
- package/package.json +1 -1
- package/src/Api.js +40 -7
- package/src/ApiConstants.js +3 -1
- package/src/Commander.js +8 -0
- package/src/Dashboard.css +47 -22
- package/src/Dashboard.js +97 -132
- package/src/MockBroker.js +8 -0
- package/src/MockDispatcher.js +1 -1
- package/src/Mockaton.test.js +1 -1
- package/src/ProxyRelay.js +5 -1
- package/src/StaticDispatcher.js +16 -23
- package/src/mockBrokersCollection.js +8 -1
- package/src/utils/fs.js +7 -2
package/README.md
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
## Mock your APIs, Enhance your Development Workflow
|
|
7
|
-
|
|
8
6
|
Mockaton is an HTTP mock server with the goal of making
|
|
9
7
|
your frontend development and testing easier—and a lot more fun.
|
|
10
8
|
|
|
@@ -17,11 +15,31 @@ For example, for `/api/user/1234` the mock filename would be:
|
|
|
17
15
|
my-mocks-dir/api/user/[user-id].GET.200.json
|
|
18
16
|
```
|
|
19
17
|
|
|
20
|
-
## Scrapping Mocks from you Backend
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
## Dashboard
|
|
20
|
+
In the dashboard you can select a mock variant for a particular route, among
|
|
21
|
+
other features such as delaying responses, or triggering an autogenerated
|
|
22
|
+
`500` (Internal Server Error). Nonetheless, there’s a programmatic API,
|
|
23
|
+
which is handy for setting up tests (see **Commander API** below).
|
|
24
|
+
|
|
25
|
+
<picture>
|
|
26
|
+
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp860x800.light.gold.png">
|
|
27
|
+
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp860x800.dark.gold.png">
|
|
28
|
+
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp860x800.light.gold.png">
|
|
29
|
+
</picture>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## No Need to Mock Everything
|
|
34
|
+
Mockaton can fallback to your real backend on routes you don’t have mocks
|
|
35
|
+
for. For that, type your backend address in the **Fallback Backend** field.
|
|
36
|
+
|
|
37
|
+
Similarly, if you already have mocks for a route you can check the
|
|
38
|
+
☁️ **Cloud checkbox** and that route will be requested from your backend.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Scrapping Mocks from you Backend
|
|
42
|
+
If you check **Save Mocks**, Mockaton will collect the responses that hit your backend.
|
|
25
43
|
Those mocks will be saved to your `config.mocksDir` following the filename convention.
|
|
26
44
|
|
|
27
45
|
|
|
@@ -31,26 +49,15 @@ Those mocks will be saved to your `config.mocksDir` following the filename conve
|
|
|
31
49
|
Want to mock a locked-out user or an invalid login attempt?
|
|
32
50
|
Add a comment to the filename in parentheses. For example:
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+
<pre>
|
|
53
|
+
api/login<b>(locked out user)</b>.POST.423.json
|
|
54
|
+
</pre>
|
|
35
55
|
|
|
36
56
|
### Different response status code
|
|
37
57
|
For instance, you can have mocks with a `4xx` or `5xx` status code for triggering
|
|
38
58
|
error responses. Or with a `204` (No Content) for testing empty collections.
|
|
39
59
|
|
|
40
60
|
|
|
41
|
-
## Dashboard
|
|
42
|
-
In the dashboard you can select a mock variant for a particular route, among
|
|
43
|
-
other features such as delaying responses, or triggering an autogenerated
|
|
44
|
-
`500` (Internal Server Error). Nonetheless, there’s a programmatic API,
|
|
45
|
-
which is handy for setting up tests (see **Commander API** below).
|
|
46
|
-
|
|
47
|
-
<picture>
|
|
48
|
-
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp860x800.light.gold.png">
|
|
49
|
-
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp860x800.dark.gold.png">
|
|
50
|
-
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp860x800.light.gold.png">
|
|
51
|
-
</picture>
|
|
52
|
-
|
|
53
|
-
|
|
54
61
|
|
|
55
62
|
## Basic Usage
|
|
56
63
|
`tsx` is only needed if you want to write mocks in TypeScript.
|
|
@@ -295,9 +302,11 @@ checkbox, the delay amount is globally configurable.
|
|
|
295
302
|
|
|
296
303
|
|
|
297
304
|
### `proxyFallback?: string`
|
|
298
|
-
Lets you specify a target server for serving routes you don’t have mocks for.
|
|
299
305
|
For example, `config.proxyFallback = 'http://example.com'`
|
|
300
306
|
|
|
307
|
+
Lets you specify a target server for serving routes you don’t have mocks for,
|
|
308
|
+
or that you manually picked with the ☁️ **Cloud Checkbox**.
|
|
309
|
+
|
|
301
310
|
### `collectProxied?: boolean`
|
|
302
311
|
Defaults to `false`. With this flag you can save mocks that hit
|
|
303
312
|
your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
|
|
@@ -308,7 +317,10 @@ URL, the filename will have `[id]` in their place. For example,
|
|
|
308
317
|
my-mocks-dir/api/user/[id]/likes.GET.200.json
|
|
309
318
|
```
|
|
310
319
|
|
|
311
|
-
Your existing mocks won’t be overwritten
|
|
320
|
+
Your existing mocks won’t be overwritten. That is, the routes you manually
|
|
321
|
+
selected for using your backend with the ☁️ **Cloud Checkbox**, will have
|
|
322
|
+
a unique filename comment.
|
|
323
|
+
|
|
312
324
|
|
|
313
325
|
<details>
|
|
314
326
|
<summary>Extension Details</summary>
|
|
@@ -495,6 +507,11 @@ first mock in alphabetical order that matches.
|
|
|
495
507
|
await mockaton.setRouteIsDelayed('GET', '/api/foo', true)
|
|
496
508
|
```
|
|
497
509
|
|
|
510
|
+
### Set Route is Proxied
|
|
511
|
+
```js
|
|
512
|
+
await mockaton.setRouteIsProxied('GET', '/api/foo', true)
|
|
513
|
+
```
|
|
514
|
+
|
|
498
515
|
### Select a cookie
|
|
499
516
|
In `config.cookies`, each key is the label used for selecting it.
|
|
500
517
|
```js
|
|
@@ -521,5 +538,5 @@ await mockaton.reset()
|
|
|
521
538
|
|
|
522
539
|
<div style="display: flex; align-items: center; gap: 20px">
|
|
523
540
|
<img src="fixtures-mocks/api/user/avatar.GET.200.png" width="170"/>
|
|
524
|
-
<p style="font-size: 18px">“
|
|
541
|
+
<p style="font-size: 18px">“Mockaton”</p>
|
|
525
542
|
</div>
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -38,6 +38,7 @@ export const apiGetRequests = new Map([
|
|
|
38
38
|
export const apiPatchRequests = new Map([
|
|
39
39
|
[API.select, selectMock],
|
|
40
40
|
[API.delay, setRouteIsDelayed],
|
|
41
|
+
[API.proxied, setRouteIsProxied],
|
|
41
42
|
[API.reset, reinitialize],
|
|
42
43
|
[API.cookies, selectCookie],
|
|
43
44
|
[API.fallback, updateProxyFallback],
|
|
@@ -118,13 +119,43 @@ async function selectMock(req, response) {
|
|
|
118
119
|
async function setRouteIsDelayed(req, response) {
|
|
119
120
|
try {
|
|
120
121
|
const body = await parseJSON(req)
|
|
122
|
+
const delayed = body[DF.delayed]
|
|
121
123
|
const broker = mockBrokersCollection.getBrokerForUrl(
|
|
122
124
|
body[DF.routeMethod],
|
|
123
125
|
body[DF.routeUrlMask])
|
|
126
|
+
|
|
127
|
+
if (!broker) // TESTME
|
|
128
|
+
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`)
|
|
129
|
+
else if (typeof delayed !== 'boolean')
|
|
130
|
+
sendUnprocessableContent(response, `Expected a boolean for "delayed"`) // TESTME
|
|
131
|
+
else {
|
|
132
|
+
broker.updateDelay(body[DF.delayed])
|
|
133
|
+
sendOK(response)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
sendBadRequest(response, error)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function setRouteIsProxied(req, response) { // TESTME
|
|
142
|
+
try {
|
|
143
|
+
const body = await parseJSON(req)
|
|
144
|
+
const proxied = body[DF.proxied]
|
|
145
|
+
const broker = mockBrokersCollection.getBrokerForUrl(
|
|
146
|
+
body[DF.routeMethod],
|
|
147
|
+
body[DF.routeUrlMask])
|
|
148
|
+
|
|
124
149
|
if (!broker)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
150
|
+
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`)
|
|
151
|
+
else if (typeof proxied !== 'boolean')
|
|
152
|
+
sendUnprocessableContent(response, `Expected a boolean for "proxied"`)
|
|
153
|
+
else if (proxied && !config.proxyFallback)
|
|
154
|
+
sendUnprocessableContent(response, `There’s no proxy fallback`)
|
|
155
|
+
else {
|
|
156
|
+
broker.updateProxied(proxied)
|
|
157
|
+
sendOK(response)
|
|
158
|
+
}
|
|
128
159
|
}
|
|
129
160
|
catch (error) {
|
|
130
161
|
sendBadRequest(response, error)
|
|
@@ -134,12 +165,14 @@ async function setRouteIsDelayed(req, response) {
|
|
|
134
165
|
async function updateProxyFallback(req, response) {
|
|
135
166
|
try {
|
|
136
167
|
const fallback = await parseJSON(req)
|
|
137
|
-
if (fallback && !URL.canParse(fallback))
|
|
168
|
+
if (fallback && !URL.canParse(fallback)) {
|
|
138
169
|
sendUnprocessableContent(response)
|
|
139
|
-
|
|
140
|
-
config.proxyFallback = fallback
|
|
141
|
-
sendOK(response)
|
|
170
|
+
return
|
|
142
171
|
}
|
|
172
|
+
if (!fallback) // TESTME
|
|
173
|
+
mockBrokersCollection.ensureAllRoutesHaveSelectedMock()
|
|
174
|
+
config.proxyFallback = fallback
|
|
175
|
+
sendOK(response)
|
|
143
176
|
}
|
|
144
177
|
catch (error) {
|
|
145
178
|
sendBadRequest(response, error)
|
package/src/ApiConstants.js
CHANGED
|
@@ -10,6 +10,7 @@ export const API = {
|
|
|
10
10
|
cookies: MOUNT + '/cookies',
|
|
11
11
|
fallback: MOUNT + '/fallback',
|
|
12
12
|
collectProxied: MOUNT + '/collect-proxied',
|
|
13
|
+
proxied: MOUNT + '/proxied',
|
|
13
14
|
cors: MOUNT + '/cors',
|
|
14
15
|
static: MOUNT + '/static'
|
|
15
16
|
}
|
|
@@ -17,7 +18,8 @@ export const API = {
|
|
|
17
18
|
export const DF = { // Dashboard Fields (XHR)
|
|
18
19
|
routeMethod: 'route_method',
|
|
19
20
|
routeUrlMask: 'route_url_mask',
|
|
20
|
-
delayed: 'delayed'
|
|
21
|
+
delayed: 'delayed',
|
|
22
|
+
proxied: 'proxied'
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export const DEFAULT_500_COMMENT = '(Mockaton 500)'
|
package/src/Commander.js
CHANGED
|
@@ -37,6 +37,14 @@ export class Commander {
|
|
|
37
37
|
})
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
setRouteIsProxied(routeMethod, routeUrlMask, proxied) {
|
|
41
|
+
return this.#patch(API.proxied, {
|
|
42
|
+
[DF.routeMethod]: routeMethod,
|
|
43
|
+
[DF.routeUrlMask]: routeUrlMask,
|
|
44
|
+
[DF.proxied]: proxied
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
listCookies() {
|
|
41
49
|
return this.#get(API.cookies)
|
|
42
50
|
}
|
package/src/Dashboard.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.
|
|
2
|
+
--boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.15), 0 1px 1px 0 rgba(0, 0, 0, 0.15), 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
|
3
3
|
--radius: 6px
|
|
4
4
|
}
|
|
5
5
|
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
:root {
|
|
8
8
|
--color4xxBackground: #ffedd1;
|
|
9
9
|
--colorAccent: #0075db;
|
|
10
|
-
--colorAccentAlt: #
|
|
10
|
+
--colorAccentAlt: #068564;
|
|
11
11
|
--colorBackground: #fff;
|
|
12
12
|
--colorComboBoxHeaderBackground: #fff;
|
|
13
13
|
--colorComboBoxBackground: #f7f7f7;
|
|
14
14
|
--colorHeaderBackground: #f3f3f3;
|
|
15
|
-
--colorSecondaryButtonBackground:
|
|
16
|
-
--
|
|
15
|
+
--colorSecondaryButtonBackground: transparent;
|
|
16
|
+
--colorSecondaryAction: #555;
|
|
17
|
+
--colorDisabledMockSelector: #444;
|
|
17
18
|
--colorHover: #dfefff;
|
|
18
19
|
--colorLabel: #444;
|
|
19
20
|
--colorLightRed: #ffe4ee;
|
|
@@ -29,9 +30,10 @@
|
|
|
29
30
|
--colorBackground: #161616;
|
|
30
31
|
--colorHeaderBackground: #090909;
|
|
31
32
|
--colorComboBoxBackground: #252525;
|
|
32
|
-
--colorSecondaryButtonBackground: #
|
|
33
|
+
--colorSecondaryButtonBackground: #202020;
|
|
34
|
+
--colorSecondaryAction: #bbb;
|
|
33
35
|
--colorComboBoxHeaderBackground: #222;
|
|
34
|
-
--
|
|
36
|
+
--colorDisabledMockSelector: #b9b9b9;
|
|
35
37
|
--colorHover: #023661;
|
|
36
38
|
--colorLabel: #aaa;
|
|
37
39
|
--colorLightRed: #ffe4ee;
|
|
@@ -101,11 +103,11 @@ select {
|
|
|
101
103
|
display: flex;
|
|
102
104
|
width: 100%;
|
|
103
105
|
align-items: flex-end;
|
|
104
|
-
padding:
|
|
106
|
+
padding: 16px;
|
|
105
107
|
border-bottom: 1px solid rgba(127, 127, 127, 0.1);
|
|
106
108
|
background: var(--colorHeaderBackground);
|
|
107
109
|
box-shadow: var(--boxShadow1);
|
|
108
|
-
gap:
|
|
110
|
+
gap: 8px;
|
|
109
111
|
|
|
110
112
|
img {
|
|
111
113
|
width: 130px;
|
|
@@ -114,12 +116,14 @@ select {
|
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
.Field {
|
|
117
|
-
|
|
119
|
+
width: 120px;
|
|
118
120
|
|
|
119
121
|
span {
|
|
120
|
-
display:
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
121
124
|
color: var(--colorLabel);
|
|
122
125
|
font-size: 11px;
|
|
126
|
+
gap: 4px;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
input[type=url],
|
|
@@ -140,7 +144,14 @@ select {
|
|
|
140
144
|
|
|
141
145
|
&.FallbackBackend {
|
|
142
146
|
position: relative;
|
|
143
|
-
width:
|
|
147
|
+
width: 210px;
|
|
148
|
+
|
|
149
|
+
svg {
|
|
150
|
+
width: 14px;
|
|
151
|
+
height: 14px;
|
|
152
|
+
fill: var(--colorLabel);
|
|
153
|
+
opacity: 0.6;
|
|
154
|
+
}
|
|
144
155
|
|
|
145
156
|
input[type=url] {
|
|
146
157
|
padding: 0 6px;
|
|
@@ -244,7 +255,7 @@ select {
|
|
|
244
255
|
position: relative;
|
|
245
256
|
left: -6px;
|
|
246
257
|
display: inline-block;
|
|
247
|
-
width:
|
|
258
|
+
width: 300px;
|
|
248
259
|
padding: 8px 6px;
|
|
249
260
|
border-radius: var(--radius);
|
|
250
261
|
color: var(--colorAccent);
|
|
@@ -260,7 +271,7 @@ select {
|
|
|
260
271
|
}
|
|
261
272
|
|
|
262
273
|
.MockSelector {
|
|
263
|
-
width:
|
|
274
|
+
width: 260px;
|
|
264
275
|
height: 30px;
|
|
265
276
|
border: 0;
|
|
266
277
|
border-left: 3px solid transparent;
|
|
@@ -277,12 +288,13 @@ select {
|
|
|
277
288
|
appearance: none;
|
|
278
289
|
background: transparent;
|
|
279
290
|
cursor: default;
|
|
280
|
-
color: var(--
|
|
291
|
+
color: var(--colorDisabledMockSelector);
|
|
281
292
|
opacity: 1;
|
|
282
293
|
}
|
|
283
294
|
}
|
|
284
295
|
|
|
285
|
-
.DelayToggler
|
|
296
|
+
.DelayToggler,
|
|
297
|
+
.ProxyToggler {
|
|
286
298
|
display: flex;
|
|
287
299
|
margin-left: 8px;
|
|
288
300
|
cursor: pointer;
|
|
@@ -297,25 +309,38 @@ select {
|
|
|
297
309
|
}
|
|
298
310
|
}
|
|
299
311
|
|
|
312
|
+
&:enabled:hover:not(:checked) ~ svg {
|
|
313
|
+
background: var(--colorHover);
|
|
314
|
+
fill: var(--colorText);
|
|
315
|
+
}
|
|
316
|
+
|
|
300
317
|
&:checked ~ svg {
|
|
301
318
|
background: var(--colorAccent);
|
|
302
319
|
fill: white;
|
|
303
320
|
}
|
|
321
|
+
|
|
322
|
+
&:disabled ~ svg {
|
|
323
|
+
opacity: .5;
|
|
324
|
+
cursor: not-allowed;
|
|
325
|
+
}
|
|
304
326
|
}
|
|
305
327
|
|
|
306
328
|
> svg {
|
|
307
329
|
width: 20px;
|
|
308
330
|
height: 20px;
|
|
309
331
|
vertical-align: bottom;
|
|
310
|
-
fill: var(--
|
|
332
|
+
fill: var(--colorSecondaryAction);
|
|
311
333
|
border-radius: 50%;
|
|
312
334
|
background: var(--colorSecondaryButtonBackground);
|
|
313
335
|
box-shadow: var(--boxShadow1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
314
338
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
339
|
+
.ProxyToggler {
|
|
340
|
+
> svg {
|
|
341
|
+
width: 24px;
|
|
342
|
+
padding: 3px;
|
|
343
|
+
border-radius: 4px;
|
|
319
344
|
}
|
|
320
345
|
}
|
|
321
346
|
|
|
@@ -344,8 +369,8 @@ select {
|
|
|
344
369
|
padding: 5px 4px;
|
|
345
370
|
box-shadow: var(--boxShadow1);
|
|
346
371
|
font-size: 10px;
|
|
347
|
-
color: var(--
|
|
348
|
-
border-radius:
|
|
372
|
+
color: var(--colorSecondaryAction);
|
|
373
|
+
border-radius: 4px;
|
|
349
374
|
background: var(--colorSecondaryButtonBackground);
|
|
350
375
|
|
|
351
376
|
&:hover {
|
package/src/Dashboard.js
CHANGED
|
@@ -12,8 +12,8 @@ function syntaxHighlightJson(textBody) {
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
const Strings = {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
bulk_select: 'Bulk Select',
|
|
16
|
+
bulk_select_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
|
|
17
17
|
click_link_to_preview: 'Click a link to preview it',
|
|
18
18
|
cookie: 'Cookie',
|
|
19
19
|
cookie_disabled_title: 'No cookies specified in Config.cookies',
|
|
@@ -21,10 +21,13 @@ const Strings = {
|
|
|
21
21
|
empty_response_body: '/* Empty Response Body */',
|
|
22
22
|
fallback_server: 'Fallback Backend',
|
|
23
23
|
fallback_server_placeholder: 'Type Server Address',
|
|
24
|
+
got: 'Got',
|
|
24
25
|
internal_server_error: 'Internal Server Error',
|
|
25
26
|
mock: 'Mock',
|
|
26
27
|
no_mocks_found: 'No mocks found',
|
|
27
|
-
|
|
28
|
+
pick_comment: 'Pick Comment…',
|
|
29
|
+
proxied: 'Proxied',
|
|
30
|
+
proxy_toggler: 'Proxy Toggler',
|
|
28
31
|
reset: 'Reset',
|
|
29
32
|
save_proxied: 'Save Mocks',
|
|
30
33
|
static_get: 'Static GET'
|
|
@@ -42,6 +45,7 @@ const CSS = {
|
|
|
42
45
|
PayloadViewer: 'PayloadViewer',
|
|
43
46
|
PreviewLink: 'PreviewLink',
|
|
44
47
|
ProgressBar: 'ProgressBar',
|
|
48
|
+
ProxyToggler: 'ProxyToggler',
|
|
45
49
|
ResetButton: 'ResetButton',
|
|
46
50
|
SaveProxiedCheckbox: 'SaveProxiedCheckbox',
|
|
47
51
|
StaticFilesList: 'StaticFilesList',
|
|
@@ -73,7 +77,7 @@ init()
|
|
|
73
77
|
function App([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
|
|
74
78
|
return [
|
|
75
79
|
r(Header, { cookies, comments, fallbackAddress, collectProxied }),
|
|
76
|
-
r(MockList, { brokersByMethod }),
|
|
80
|
+
r(MockList, { brokersByMethod, canProxy: Boolean(fallbackAddress) }),
|
|
77
81
|
r(StaticFilesList, { staticFiles })
|
|
78
82
|
]
|
|
79
83
|
}
|
|
@@ -121,7 +125,7 @@ function CookieSelector({ cookies }) {
|
|
|
121
125
|
function BulkSelector({ comments }) {
|
|
122
126
|
// UX wise this should be a menu instead of this `select`.
|
|
123
127
|
// But this way is easier to implement, with a few hacks.
|
|
124
|
-
const firstOption = Strings.
|
|
128
|
+
const firstOption = Strings.pick_comment
|
|
125
129
|
function onChange() {
|
|
126
130
|
const value = this.value
|
|
127
131
|
this.value = firstOption // Hack
|
|
@@ -135,19 +139,19 @@ function BulkSelector({ comments }) {
|
|
|
135
139
|
: [firstOption].concat(comments)
|
|
136
140
|
return (
|
|
137
141
|
r('label', { className: CSS.Field },
|
|
138
|
-
r('span', null, Strings.
|
|
142
|
+
r('span', null, Strings.bulk_select),
|
|
139
143
|
r('select', {
|
|
140
144
|
className: CSS.BulkSelector,
|
|
141
145
|
'data-qaid': 'BulkSelector',
|
|
142
146
|
autocomplete: 'off',
|
|
143
147
|
disabled,
|
|
144
|
-
title: disabled ? Strings.
|
|
148
|
+
title: disabled ? Strings.bulk_select_disabled_title : '',
|
|
145
149
|
onChange
|
|
146
150
|
}, list.map(value =>
|
|
147
151
|
r('option', { value }, value)))))
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
function ProxyFallbackField({ fallbackAddress
|
|
154
|
+
function ProxyFallbackField({ fallbackAddress, collectProxied }) {
|
|
151
155
|
function onChange() {
|
|
152
156
|
const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
|
|
153
157
|
saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
|
|
@@ -155,12 +159,16 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
|
155
159
|
if (!this.validity.valid)
|
|
156
160
|
this.reportValidity()
|
|
157
161
|
else
|
|
158
|
-
mockaton.setProxyFallback(this.value.trim())
|
|
162
|
+
mockaton.setProxyFallback(this.value.trim())
|
|
163
|
+
.then(init)
|
|
164
|
+
.catch(onError)
|
|
159
165
|
}
|
|
160
166
|
return (
|
|
161
167
|
r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
|
|
162
168
|
r('label', null,
|
|
163
|
-
r('span', null,
|
|
169
|
+
r('span', null,
|
|
170
|
+
r(CloudIcon),
|
|
171
|
+
Strings.fallback_server),
|
|
164
172
|
r('input', {
|
|
165
173
|
type: 'url',
|
|
166
174
|
autocomplete: 'none',
|
|
@@ -205,7 +213,7 @@ function ResetButton() {
|
|
|
205
213
|
|
|
206
214
|
// MockList ===============
|
|
207
215
|
|
|
208
|
-
function MockList({ brokersByMethod }) {
|
|
216
|
+
function MockList({ brokersByMethod, canProxy }) {
|
|
209
217
|
const hasMocks = Object.keys(brokersByMethod).length
|
|
210
218
|
if (!hasMocks)
|
|
211
219
|
return (
|
|
@@ -214,12 +222,12 @@ function MockList({ brokersByMethod }) {
|
|
|
214
222
|
return (
|
|
215
223
|
r('main', { className: CSS.MockList },
|
|
216
224
|
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
217
|
-
r(SectionByMethod, { method, brokers }))),
|
|
225
|
+
r(SectionByMethod, { method, brokers, canProxy }))),
|
|
218
226
|
r(PayloadViewer)))
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
|
|
222
|
-
function SectionByMethod({ method, brokers }) {
|
|
230
|
+
function SectionByMethod({ method, brokers, canProxy }) {
|
|
223
231
|
return (
|
|
224
232
|
r('tbody', null,
|
|
225
233
|
r('th', null, method),
|
|
@@ -231,7 +239,8 @@ function SectionByMethod({ method, brokers }) {
|
|
|
231
239
|
r('td', null, r(PreviewLink, { method, urlMask })),
|
|
232
240
|
r('td', null, r(MockSelector, { broker })),
|
|
233
241
|
r('td', null, r(DelayRouteToggler, { broker })),
|
|
234
|
-
r('td', null, r(InternalServerErrorToggler, { broker }))
|
|
242
|
+
r('td', null, r(InternalServerErrorToggler, { broker })),
|
|
243
|
+
r('td', null, r(ProxyToggler, { broker, disabled: !canProxy }))))))
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
|
|
@@ -257,38 +266,35 @@ function PreviewLink({ method, urlMask }) {
|
|
|
257
266
|
|
|
258
267
|
|
|
259
268
|
function MockSelector({ broker }) {
|
|
260
|
-
function className(defaultIsSelected, status) {
|
|
261
|
-
return cssClass(
|
|
262
|
-
CSS.MockSelector,
|
|
263
|
-
!defaultIsSelected && CSS.bold,
|
|
264
|
-
status >= 400 && status < 500 && CSS.status4xx)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
269
|
function onChange() {
|
|
268
|
-
const {
|
|
270
|
+
const { urlMask, method } = parseFilename(this.value)
|
|
269
271
|
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
270
272
|
? 'normal'
|
|
271
273
|
: 'bold'
|
|
272
274
|
mockaton.select(this.value)
|
|
273
|
-
.then(
|
|
274
|
-
|
|
275
|
-
checkbox500For(method, urlMask).checked = status === 500
|
|
276
|
-
this.className = className(this.value === this.options[0].value, status)
|
|
277
|
-
})
|
|
275
|
+
.then(init)
|
|
276
|
+
.then(() => linkFor(method, urlMask)?.click())
|
|
278
277
|
.catch(onError)
|
|
279
278
|
}
|
|
280
279
|
|
|
281
|
-
|
|
280
|
+
let selected = broker.currentMock.file
|
|
282
281
|
const { status, urlMask } = parseFilename(selected)
|
|
283
282
|
const files = broker.mocks.filter(item =>
|
|
284
283
|
status === 500 ||
|
|
285
284
|
!item.includes(DEFAULT_500_COMMENT))
|
|
285
|
+
if (!selected) {
|
|
286
|
+
selected = Strings.proxied
|
|
287
|
+
files.push(selected)
|
|
288
|
+
}
|
|
286
289
|
|
|
287
290
|
return (
|
|
288
291
|
r('select', {
|
|
289
292
|
'data-qaid': urlMask,
|
|
290
293
|
autocomplete: 'off',
|
|
291
|
-
className:
|
|
294
|
+
className: cssClass(
|
|
295
|
+
CSS.MockSelector,
|
|
296
|
+
selected !== files[0] && CSS.bold,
|
|
297
|
+
status >= 400 && status < 500 && CSS.status4xx),
|
|
292
298
|
disabled: files.length <= 1,
|
|
293
299
|
onChange
|
|
294
300
|
}, files.map(file =>
|
|
@@ -317,12 +323,6 @@ function DelayRouteToggler({ broker }) {
|
|
|
317
323
|
onChange
|
|
318
324
|
}),
|
|
319
325
|
TimerIcon()))
|
|
320
|
-
|
|
321
|
-
function TimerIcon() {
|
|
322
|
-
return (
|
|
323
|
-
r('svg', { viewBox: '0 0 24 24' },
|
|
324
|
-
r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
|
|
325
|
-
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
|
|
@@ -353,6 +353,30 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
353
353
|
)
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
function ProxyToggler({ broker, disabled }) {
|
|
357
|
+
function onChange() {
|
|
358
|
+
const { urlMask, method } = parseFilename(this.name)
|
|
359
|
+
mockaton.setRouteIsProxied(method, urlMask, this.checked)
|
|
360
|
+
.then(init)
|
|
361
|
+
.then(() => linkFor(method, urlMask)?.click())
|
|
362
|
+
.catch(onError)
|
|
363
|
+
}
|
|
364
|
+
return (
|
|
365
|
+
r('label', {
|
|
366
|
+
className: CSS.ProxyToggler,
|
|
367
|
+
title: Strings.proxy_toggler
|
|
368
|
+
},
|
|
369
|
+
r('input', {
|
|
370
|
+
type: 'checkbox',
|
|
371
|
+
disabled,
|
|
372
|
+
name: broker.currentMock.file,
|
|
373
|
+
checked: !broker.currentMock.file,
|
|
374
|
+
onChange
|
|
375
|
+
}),
|
|
376
|
+
r(CloudIcon)))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
356
380
|
|
|
357
381
|
// Payload Preview ===============
|
|
358
382
|
|
|
@@ -373,14 +397,21 @@ function PayloadViewerProgressBar() {
|
|
|
373
397
|
r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
|
|
374
398
|
}
|
|
375
399
|
|
|
376
|
-
function PayloadViewerTitle({ file }) {
|
|
377
|
-
const { urlMask, method,
|
|
400
|
+
function PayloadViewerTitle({ file, status, statusText }) {
|
|
401
|
+
const { urlMask, method, ext } = parseFilename(file)
|
|
378
402
|
return (
|
|
379
403
|
r('span', null,
|
|
380
404
|
urlMask + '.' + method + '.',
|
|
381
|
-
r('abbr', { title:
|
|
405
|
+
r('abbr', { title: statusText }, status),
|
|
382
406
|
'.' + ext))
|
|
383
407
|
}
|
|
408
|
+
function PayloadViewerTitleWhenProxied({ mime, status, statusText }) {
|
|
409
|
+
return (
|
|
410
|
+
r('span', null,
|
|
411
|
+
Strings.got + ' ',
|
|
412
|
+
r('abbr', { title: statusText }, status),
|
|
413
|
+
' ' + mime))
|
|
414
|
+
}
|
|
384
415
|
|
|
385
416
|
async function previewMock(method, urlMask, href) {
|
|
386
417
|
const timer = setTimeout(renderProgressBar, 180)
|
|
@@ -394,10 +425,22 @@ async function previewMock(method, urlMask, href) {
|
|
|
394
425
|
}
|
|
395
426
|
|
|
396
427
|
async function updatePayloadViewer(method, urlMask, response) {
|
|
397
|
-
payloadViewerTitleRef.current.replaceChildren(
|
|
398
|
-
PayloadViewerTitle({ file: mockSelectorFor(method, urlMask).value }))
|
|
399
|
-
|
|
400
428
|
const mime = response.headers.get('content-type') || ''
|
|
429
|
+
|
|
430
|
+
const file = mockSelectorFor(method, urlMask).value
|
|
431
|
+
if (file === Strings.proxied)
|
|
432
|
+
payloadViewerTitleRef.current.replaceChildren(PayloadViewerTitleWhenProxied({
|
|
433
|
+
status: response.status,
|
|
434
|
+
statusText: response.statusText,
|
|
435
|
+
mime
|
|
436
|
+
}))
|
|
437
|
+
else
|
|
438
|
+
payloadViewerTitleRef.current.replaceChildren(PayloadViewerTitle({
|
|
439
|
+
status: response.status,
|
|
440
|
+
statusText: response.statusText,
|
|
441
|
+
file
|
|
442
|
+
}))
|
|
443
|
+
|
|
401
444
|
if (mime.startsWith('image/')) { // Naively assumes GET.200
|
|
402
445
|
payloadViewerRef.current.replaceChildren(
|
|
403
446
|
r('img', {
|
|
@@ -424,9 +467,6 @@ function trFor(method, urlMask) {
|
|
|
424
467
|
function linkFor(method, urlMask) {
|
|
425
468
|
return trFor(method, urlMask)?.querySelector(`a.${CSS.PreviewLink}`)
|
|
426
469
|
}
|
|
427
|
-
function checkbox500For(method, urlMask) {
|
|
428
|
-
return trFor(method, urlMask)?.querySelector(`.${CSS.InternalServerErrorToggler} > input`)
|
|
429
|
-
}
|
|
430
470
|
function mockSelectorFor(method, urlMask) {
|
|
431
471
|
return trFor(method, urlMask)?.querySelector(`select.${CSS.MockSelector}`)
|
|
432
472
|
}
|
|
@@ -458,6 +498,19 @@ function onError(error) {
|
|
|
458
498
|
}
|
|
459
499
|
|
|
460
500
|
|
|
501
|
+
function TimerIcon() {
|
|
502
|
+
return (
|
|
503
|
+
r('svg', { viewBox: '0 0 24 24' },
|
|
504
|
+
r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function CloudIcon() {
|
|
508
|
+
return (
|
|
509
|
+
r('svg', { viewBox: '0 0 24 24' },
|
|
510
|
+
r('path', { d: 'M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.61 5.64 5.36 8.04 2.35 8.36 0 10.9 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96M19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4h2c0-2.76-1.86-5.08-4.4-5.78C8.61 6.88 10.2 6 12 6c3.03 0 5.5 2.47 5.5 5.5v.5H19c1.65 0 3 1.35 3 3s-1.35 3-3 3' })))
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
461
514
|
|
|
462
515
|
// Utils ============
|
|
463
516
|
|
|
@@ -504,91 +557,3 @@ function createSvgElement(tagName, props, ...children) {
|
|
|
504
557
|
function useRef() {
|
|
505
558
|
return { current: null }
|
|
506
559
|
}
|
|
507
|
-
|
|
508
|
-
const HttpStatus = {
|
|
509
|
-
100: 'Continue',
|
|
510
|
-
101: 'Switching Protocols',
|
|
511
|
-
102: 'Processing',
|
|
512
|
-
103: 'Early Hints',
|
|
513
|
-
200: 'OK',
|
|
514
|
-
201: 'Created',
|
|
515
|
-
202: 'Accepted',
|
|
516
|
-
203: 'Non-Authoritative Information',
|
|
517
|
-
204: 'No Content',
|
|
518
|
-
205: 'Reset Content',
|
|
519
|
-
206: 'Partial Content',
|
|
520
|
-
207: 'Multi-Status',
|
|
521
|
-
208: 'Already Reported',
|
|
522
|
-
218: 'This is fine (Apache Web Server)',
|
|
523
|
-
226: 'IM Used',
|
|
524
|
-
300: 'Multiple Choices',
|
|
525
|
-
301: 'Moved Permanently',
|
|
526
|
-
302: 'Found',
|
|
527
|
-
303: 'See Other',
|
|
528
|
-
304: 'Not Modified',
|
|
529
|
-
306: 'Switch Proxy',
|
|
530
|
-
307: 'Temporary Redirect',
|
|
531
|
-
308: 'Resume Incomplete',
|
|
532
|
-
400: 'Bad Request',
|
|
533
|
-
401: 'Unauthorized',
|
|
534
|
-
402: 'Payment Required',
|
|
535
|
-
403: 'Forbidden',
|
|
536
|
-
404: 'Not Found',
|
|
537
|
-
405: 'Method Not Allowed',
|
|
538
|
-
406: 'Not Acceptable',
|
|
539
|
-
407: 'Proxy Authentication Required',
|
|
540
|
-
408: 'Request Timeout',
|
|
541
|
-
409: 'Conflict',
|
|
542
|
-
410: 'Gone',
|
|
543
|
-
411: 'Length Required',
|
|
544
|
-
412: 'Precondition Failed',
|
|
545
|
-
413: 'Request Entity Too Large',
|
|
546
|
-
414: 'Request-URI Too Long',
|
|
547
|
-
415: 'Unsupported Media Type',
|
|
548
|
-
416: 'Requested Range Not Satisfiable',
|
|
549
|
-
417: 'Expectation Failed',
|
|
550
|
-
418: 'I’m a teapot',
|
|
551
|
-
419: 'Page Expired (Laravel Framework)',
|
|
552
|
-
420: 'Method Failure (Spring Framework)',
|
|
553
|
-
421: 'Misdirected Request',
|
|
554
|
-
422: 'Unprocessable Entity',
|
|
555
|
-
423: 'Locked',
|
|
556
|
-
424: 'Failed Dependency',
|
|
557
|
-
426: 'Upgrade Required',
|
|
558
|
-
428: 'Precondition Required',
|
|
559
|
-
429: 'Too Many Requests',
|
|
560
|
-
431: 'Request Header Fields Too Large',
|
|
561
|
-
440: 'Login Time-out',
|
|
562
|
-
444: 'Connection Closed Without Response',
|
|
563
|
-
449: 'Retry With',
|
|
564
|
-
450: 'Blocked by Windows Parental Controls',
|
|
565
|
-
451: 'Unavailable For Legal Reasons',
|
|
566
|
-
494: 'Request Header Too Large',
|
|
567
|
-
495: 'SSL Certificate Error',
|
|
568
|
-
496: 'SSL Certificate Required',
|
|
569
|
-
497: 'HTTP Request Sent to HTTPS Port',
|
|
570
|
-
498: 'Invalid Token (Esri)',
|
|
571
|
-
499: 'Client Closed Request',
|
|
572
|
-
500: 'Internal Server Error',
|
|
573
|
-
501: 'Not Implemented',
|
|
574
|
-
502: 'Bad Gateway',
|
|
575
|
-
503: 'Service Unavailable',
|
|
576
|
-
504: 'Gateway Timeout',
|
|
577
|
-
505: 'HTTP Version Not Supported',
|
|
578
|
-
506: 'Variant Also Negotiates',
|
|
579
|
-
507: 'Insufficient Storage',
|
|
580
|
-
508: 'Loop Detected',
|
|
581
|
-
509: 'Bandwidth Limit Exceeded',
|
|
582
|
-
510: 'Not Extended',
|
|
583
|
-
511: 'Network Authentication Required',
|
|
584
|
-
520: 'Unknown Error',
|
|
585
|
-
521: 'Web Server Is Down',
|
|
586
|
-
522: 'Connection Timed Out',
|
|
587
|
-
523: 'Origin Is Unreachable',
|
|
588
|
-
524: 'A Timeout Occurred',
|
|
589
|
-
525: 'SSL Handshake Failed',
|
|
590
|
-
526: 'Invalid SSL Certificate',
|
|
591
|
-
527: 'Railgun Listener to Origin Error',
|
|
592
|
-
530: 'Origin DNS Error',
|
|
593
|
-
598: 'Network Read Timeout Error'
|
|
594
|
-
}
|
package/src/MockBroker.js
CHANGED
|
@@ -60,6 +60,7 @@ export class MockBroker {
|
|
|
60
60
|
|
|
61
61
|
get file() { return this.currentMock.file }
|
|
62
62
|
get delay() { return this.currentMock.delay }
|
|
63
|
+
get proxied() { return !this.currentMock.file }
|
|
63
64
|
get status() { return parseFilename(this.file).status }
|
|
64
65
|
get temp500IsSelected() { return this.#isTemp500(this.file) }
|
|
65
66
|
|
|
@@ -85,6 +86,13 @@ export class MockBroker {
|
|
|
85
86
|
updateFile(filename) { this.currentMock.file = filename }
|
|
86
87
|
updateDelay(delayed) { this.currentMock.delay = Number(delayed) * config.delay }
|
|
87
88
|
|
|
89
|
+
updateProxied(proxied) {
|
|
90
|
+
if (proxied)
|
|
91
|
+
this.updateFile('')
|
|
92
|
+
else
|
|
93
|
+
this.selectDefaultFile()
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
setByMatchingComment(comment) {
|
|
89
97
|
for (const file of this.mocks)
|
|
90
98
|
if (includesComment(file, comment)) {
|
package/src/MockDispatcher.js
CHANGED
|
@@ -12,7 +12,7 @@ import { sendInternalServerError, sendNotFound, sendBadRequest } from './utils/h
|
|
|
12
12
|
export async function dispatchMock(req, response) {
|
|
13
13
|
try {
|
|
14
14
|
const broker = mockBrokerCollection.getBrokerForUrl(req.method, req.url)
|
|
15
|
-
if (!broker) {
|
|
15
|
+
if (!broker || broker.proxied) {
|
|
16
16
|
if (config.proxyFallback)
|
|
17
17
|
await proxy(req, response)
|
|
18
18
|
else
|
package/src/Mockaton.test.js
CHANGED
package/src/ProxyRelay.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
2
4
|
import { write } from './utils/fs.js'
|
|
3
5
|
import { config } from './config.js'
|
|
4
6
|
import { extFor } from './utils/mime.js'
|
|
@@ -23,7 +25,9 @@ export async function proxy(req, response) {
|
|
|
23
25
|
|
|
24
26
|
if (config.collectProxied) {
|
|
25
27
|
const ext = extFor(proxyResponse.headers.get('content-type'))
|
|
26
|
-
|
|
28
|
+
let filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
|
|
29
|
+
if (existsSync(join(config.mocksDir, filename))) // TESTME
|
|
30
|
+
filename = makeMockFilename(req.url + `(${randomUUID()})`, req.method, proxyResponse.status, ext)
|
|
27
31
|
write(join(config.mocksDir, filename), body)
|
|
28
32
|
}
|
|
29
33
|
}
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { join
|
|
1
|
+
import { join } from 'node:path'
|
|
2
2
|
import fs, { readFileSync, realpathSync } from 'node:fs'
|
|
3
3
|
|
|
4
4
|
import { config } from './config.js'
|
|
5
5
|
import { mimeFor } from './utils/mime.js'
|
|
6
6
|
import { isDirectory, isFile } from './utils/fs.js'
|
|
7
|
-
import {
|
|
7
|
+
import { sendInternalServerError } from './utils/http-response.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
export function isStatic(req) {
|
|
@@ -16,34 +16,27 @@ export function isStatic(req) {
|
|
|
16
16
|
|
|
17
17
|
export async function dispatchStatic(req, response) {
|
|
18
18
|
const file = resolvedAllowedPath(req.url)
|
|
19
|
-
if (
|
|
20
|
-
sendNotFound(response)
|
|
21
|
-
else if (req.headers.range)
|
|
19
|
+
if (req.headers.range)
|
|
22
20
|
await sendPartialContent(response, req.headers.range, file)
|
|
23
|
-
else
|
|
24
|
-
sendFile(response, file)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function resolvedAllowedPath(url) {
|
|
28
|
-
let candidate = resolve(join(config.staticDir, url))
|
|
29
|
-
if (isDirectory(candidate))
|
|
30
|
-
candidate = join(candidate, 'index.html')
|
|
31
|
-
if (!isFile(candidate))
|
|
32
|
-
return false
|
|
33
|
-
candidate = realpathSync(candidate)
|
|
34
|
-
if (candidate.startsWith(config.staticDir))
|
|
35
|
-
return candidate
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function sendFile(response, file) {
|
|
39
|
-
if (!isFile(file))
|
|
40
|
-
sendNotFound(response)
|
|
41
21
|
else {
|
|
42
22
|
response.setHeader('Content-Type', mimeFor(file))
|
|
43
23
|
response.end(readFileSync(file, 'utf8'))
|
|
44
24
|
}
|
|
45
25
|
}
|
|
46
26
|
|
|
27
|
+
function resolvedAllowedPath(url) {
|
|
28
|
+
try {
|
|
29
|
+
let candidate = realpathSync(join(config.staticDir, url))
|
|
30
|
+
if (!candidate.startsWith(config.staticDir))
|
|
31
|
+
return false
|
|
32
|
+
if (isDirectory(candidate))
|
|
33
|
+
candidate = join(candidate, 'index.html')
|
|
34
|
+
if (isFile(candidate))
|
|
35
|
+
return candidate
|
|
36
|
+
}
|
|
37
|
+
catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
47
40
|
async function sendPartialContent(response, range, file) {
|
|
48
41
|
const { size } = await fs.promises.lstat(file)
|
|
49
42
|
let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
@@ -104,9 +104,16 @@ export function extractAllComments() {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export function setMocksMatchingComment(comment) {
|
|
107
|
-
forEachBroker(broker =>
|
|
107
|
+
forEachBroker(broker =>
|
|
108
|
+
broker.setByMatchingComment(comment))
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
export function ensureAllRoutesHaveSelectedMock() {
|
|
112
|
+
forEachBroker(broker => {
|
|
113
|
+
if (broker.proxied)
|
|
114
|
+
broker.selectDefaultFile()
|
|
115
|
+
})
|
|
116
|
+
}
|
|
110
117
|
|
|
111
118
|
function forEachBroker(fn) {
|
|
112
119
|
for (const brokers of Object.values(collection))
|
package/src/utils/fs.js
CHANGED
|
@@ -14,6 +14,11 @@ export const listFilesRecursively = dir => {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export const write = (path, body) => {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
try {
|
|
18
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
19
|
+
writeFileSync(path, body)
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
console.error('Write access denied', err)
|
|
23
|
+
}
|
|
19
24
|
}
|