mockaton 8.7.7 → 8.8.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/README.md +25 -10
- 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 +71 -35
- 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,16 @@ 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
|
-
##
|
|
18
|
+
## No Need to Mock Everything
|
|
19
|
+
Mockaton can fallback to your real backend on routes you don’t have mocks
|
|
20
|
+
for. For that, type your backend address in the **Fallback Backend** field.
|
|
21
|
+
|
|
22
|
+
Similarly, if already have mocks for a route you can check the ☁️ **Cloud
|
|
23
|
+
checkbox** and Mockaton will request it from your backend.
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
check **Save Mocks**,
|
|
25
|
+
|
|
26
|
+
## Scrapping Mocks from you Backend
|
|
27
|
+
If you check **Save Mocks**, Mockaton will collect the responses that hit your backend.
|
|
25
28
|
Those mocks will be saved to your `config.mocksDir` following the filename convention.
|
|
26
29
|
|
|
27
30
|
|
|
@@ -31,7 +34,9 @@ Those mocks will be saved to your `config.mocksDir` following the filename conve
|
|
|
31
34
|
Want to mock a locked-out user or an invalid login attempt?
|
|
32
35
|
Add a comment to the filename in parentheses. For example:
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
<pre>
|
|
38
|
+
api/login<b>(locked out user)</b>.POST.423.json
|
|
39
|
+
</pre>
|
|
35
40
|
|
|
36
41
|
### Different response status code
|
|
37
42
|
For instance, you can have mocks with a `4xx` or `5xx` status code for triggering
|
|
@@ -295,9 +300,11 @@ checkbox, the delay amount is globally configurable.
|
|
|
295
300
|
|
|
296
301
|
|
|
297
302
|
### `proxyFallback?: string`
|
|
298
|
-
Lets you specify a target server for serving routes you don’t have mocks for.
|
|
299
303
|
For example, `config.proxyFallback = 'http://example.com'`
|
|
300
304
|
|
|
305
|
+
Lets you specify a target server for serving routes you don’t have mocks for,
|
|
306
|
+
or that you manually picked with the ☁️ **Cloud Checkbox**.
|
|
307
|
+
|
|
301
308
|
### `collectProxied?: boolean`
|
|
302
309
|
Defaults to `false`. With this flag you can save mocks that hit
|
|
303
310
|
your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
|
|
@@ -308,7 +315,10 @@ URL, the filename will have `[id]` in their place. For example,
|
|
|
308
315
|
my-mocks-dir/api/user/[id]/likes.GET.200.json
|
|
309
316
|
```
|
|
310
317
|
|
|
311
|
-
Your existing mocks won’t be overwritten
|
|
318
|
+
Your existing mocks won’t be overwritten. That is, the routes you manually
|
|
319
|
+
selected for using your backend with the ☁️ **Cloud Checkbox**, will have
|
|
320
|
+
a unique filename comment.
|
|
321
|
+
|
|
312
322
|
|
|
313
323
|
<details>
|
|
314
324
|
<summary>Extension Details</summary>
|
|
@@ -495,6 +505,11 @@ first mock in alphabetical order that matches.
|
|
|
495
505
|
await mockaton.setRouteIsDelayed('GET', '/api/foo', true)
|
|
496
506
|
```
|
|
497
507
|
|
|
508
|
+
### Set Route is Proxied
|
|
509
|
+
```js
|
|
510
|
+
await mockaton.setRouteIsProxied('GET', '/api/foo', true)
|
|
511
|
+
```
|
|
512
|
+
|
|
498
513
|
### Select a cookie
|
|
499
514
|
In `config.cookies`, each key is the label used for selecting it.
|
|
500
515
|
```js
|
|
@@ -521,5 +536,5 @@ await mockaton.reset()
|
|
|
521
536
|
|
|
522
537
|
<div style="display: flex; align-items: center; gap: 20px">
|
|
523
538
|
<img src="fixtures-mocks/api/user/avatar.GET.200.png" width="170"/>
|
|
524
|
-
<p style="font-size: 18px">“
|
|
539
|
+
<p style="font-size: 18px">“Mockaton”</p>
|
|
525
540
|
</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',
|
|
@@ -24,7 +24,9 @@ const Strings = {
|
|
|
24
24
|
internal_server_error: 'Internal Server Error',
|
|
25
25
|
mock: 'Mock',
|
|
26
26
|
no_mocks_found: 'No mocks found',
|
|
27
|
-
|
|
27
|
+
pick_comment: 'Pick Comment…',
|
|
28
|
+
proxied: 'Proxied',
|
|
29
|
+
proxy_toggler: 'Proxy Toggler',
|
|
28
30
|
reset: 'Reset',
|
|
29
31
|
save_proxied: 'Save Mocks',
|
|
30
32
|
static_get: 'Static GET'
|
|
@@ -42,6 +44,7 @@ const CSS = {
|
|
|
42
44
|
PayloadViewer: 'PayloadViewer',
|
|
43
45
|
PreviewLink: 'PreviewLink',
|
|
44
46
|
ProgressBar: 'ProgressBar',
|
|
47
|
+
ProxyToggler: 'ProxyToggler',
|
|
45
48
|
ResetButton: 'ResetButton',
|
|
46
49
|
SaveProxiedCheckbox: 'SaveProxiedCheckbox',
|
|
47
50
|
StaticFilesList: 'StaticFilesList',
|
|
@@ -73,7 +76,7 @@ init()
|
|
|
73
76
|
function App([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
|
|
74
77
|
return [
|
|
75
78
|
r(Header, { cookies, comments, fallbackAddress, collectProxied }),
|
|
76
|
-
r(MockList, { brokersByMethod }),
|
|
79
|
+
r(MockList, { brokersByMethod, canProxy: Boolean(fallbackAddress) }),
|
|
77
80
|
r(StaticFilesList, { staticFiles })
|
|
78
81
|
]
|
|
79
82
|
}
|
|
@@ -121,7 +124,7 @@ function CookieSelector({ cookies }) {
|
|
|
121
124
|
function BulkSelector({ comments }) {
|
|
122
125
|
// UX wise this should be a menu instead of this `select`.
|
|
123
126
|
// But this way is easier to implement, with a few hacks.
|
|
124
|
-
const firstOption = Strings.
|
|
127
|
+
const firstOption = Strings.pick_comment
|
|
125
128
|
function onChange() {
|
|
126
129
|
const value = this.value
|
|
127
130
|
this.value = firstOption // Hack
|
|
@@ -135,19 +138,19 @@ function BulkSelector({ comments }) {
|
|
|
135
138
|
: [firstOption].concat(comments)
|
|
136
139
|
return (
|
|
137
140
|
r('label', { className: CSS.Field },
|
|
138
|
-
r('span', null, Strings.
|
|
141
|
+
r('span', null, Strings.bulk_select),
|
|
139
142
|
r('select', {
|
|
140
143
|
className: CSS.BulkSelector,
|
|
141
144
|
'data-qaid': 'BulkSelector',
|
|
142
145
|
autocomplete: 'off',
|
|
143
146
|
disabled,
|
|
144
|
-
title: disabled ? Strings.
|
|
147
|
+
title: disabled ? Strings.bulk_select_disabled_title : '',
|
|
145
148
|
onChange
|
|
146
149
|
}, list.map(value =>
|
|
147
150
|
r('option', { value }, value)))))
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
function ProxyFallbackField({ fallbackAddress
|
|
153
|
+
function ProxyFallbackField({ fallbackAddress, collectProxied }) {
|
|
151
154
|
function onChange() {
|
|
152
155
|
const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
|
|
153
156
|
saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
|
|
@@ -155,12 +158,16 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
|
155
158
|
if (!this.validity.valid)
|
|
156
159
|
this.reportValidity()
|
|
157
160
|
else
|
|
158
|
-
mockaton.setProxyFallback(this.value.trim())
|
|
161
|
+
mockaton.setProxyFallback(this.value.trim())
|
|
162
|
+
.then(init)
|
|
163
|
+
.catch(onError)
|
|
159
164
|
}
|
|
160
165
|
return (
|
|
161
166
|
r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
|
|
162
167
|
r('label', null,
|
|
163
|
-
r('span', null,
|
|
168
|
+
r('span', null,
|
|
169
|
+
r(CloudIcon),
|
|
170
|
+
Strings.fallback_server),
|
|
164
171
|
r('input', {
|
|
165
172
|
type: 'url',
|
|
166
173
|
autocomplete: 'none',
|
|
@@ -205,7 +212,7 @@ function ResetButton() {
|
|
|
205
212
|
|
|
206
213
|
// MockList ===============
|
|
207
214
|
|
|
208
|
-
function MockList({ brokersByMethod }) {
|
|
215
|
+
function MockList({ brokersByMethod, canProxy }) {
|
|
209
216
|
const hasMocks = Object.keys(brokersByMethod).length
|
|
210
217
|
if (!hasMocks)
|
|
211
218
|
return (
|
|
@@ -214,12 +221,12 @@ function MockList({ brokersByMethod }) {
|
|
|
214
221
|
return (
|
|
215
222
|
r('main', { className: CSS.MockList },
|
|
216
223
|
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
217
|
-
r(SectionByMethod, { method, brokers }))),
|
|
224
|
+
r(SectionByMethod, { method, brokers, canProxy }))),
|
|
218
225
|
r(PayloadViewer)))
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
|
|
222
|
-
function SectionByMethod({ method, brokers }) {
|
|
229
|
+
function SectionByMethod({ method, brokers, canProxy }) {
|
|
223
230
|
return (
|
|
224
231
|
r('tbody', null,
|
|
225
232
|
r('th', null, method),
|
|
@@ -231,7 +238,8 @@ function SectionByMethod({ method, brokers }) {
|
|
|
231
238
|
r('td', null, r(PreviewLink, { method, urlMask })),
|
|
232
239
|
r('td', null, r(MockSelector, { broker })),
|
|
233
240
|
r('td', null, r(DelayRouteToggler, { broker })),
|
|
234
|
-
r('td', null, r(InternalServerErrorToggler, { broker }))
|
|
241
|
+
r('td', null, r(InternalServerErrorToggler, { broker })),
|
|
242
|
+
r('td', null, r(ProxyToggler, { broker, disabled: !canProxy }))))))
|
|
235
243
|
}
|
|
236
244
|
|
|
237
245
|
|
|
@@ -257,38 +265,35 @@ function PreviewLink({ method, urlMask }) {
|
|
|
257
265
|
|
|
258
266
|
|
|
259
267
|
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
268
|
function onChange() {
|
|
268
|
-
const {
|
|
269
|
+
const { urlMask, method } = parseFilename(this.value)
|
|
269
270
|
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
270
271
|
? 'normal'
|
|
271
272
|
: 'bold'
|
|
272
273
|
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
|
-
})
|
|
274
|
+
.then(init)
|
|
275
|
+
.then(() => linkFor(method, urlMask)?.click())
|
|
278
276
|
.catch(onError)
|
|
279
277
|
}
|
|
280
278
|
|
|
281
|
-
|
|
279
|
+
let selected = broker.currentMock.file
|
|
282
280
|
const { status, urlMask } = parseFilename(selected)
|
|
283
281
|
const files = broker.mocks.filter(item =>
|
|
284
282
|
status === 500 ||
|
|
285
283
|
!item.includes(DEFAULT_500_COMMENT))
|
|
284
|
+
if (!selected) {
|
|
285
|
+
selected = Strings.proxied
|
|
286
|
+
files.push(selected)
|
|
287
|
+
}
|
|
286
288
|
|
|
287
289
|
return (
|
|
288
290
|
r('select', {
|
|
289
291
|
'data-qaid': urlMask,
|
|
290
292
|
autocomplete: 'off',
|
|
291
|
-
className:
|
|
293
|
+
className: cssClass(
|
|
294
|
+
CSS.MockSelector,
|
|
295
|
+
selected !== files[0] && CSS.bold,
|
|
296
|
+
status >= 400 && status < 500 && CSS.status4xx),
|
|
292
297
|
disabled: files.length <= 1,
|
|
293
298
|
onChange
|
|
294
299
|
}, files.map(file =>
|
|
@@ -317,12 +322,6 @@ function DelayRouteToggler({ broker }) {
|
|
|
317
322
|
onChange
|
|
318
323
|
}),
|
|
319
324
|
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
325
|
}
|
|
327
326
|
|
|
328
327
|
|
|
@@ -353,6 +352,30 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
353
352
|
)
|
|
354
353
|
}
|
|
355
354
|
|
|
355
|
+
function ProxyToggler({ broker, disabled }) {
|
|
356
|
+
function onChange() {
|
|
357
|
+
const { urlMask, method } = parseFilename(this.name)
|
|
358
|
+
mockaton.setRouteIsProxied(method, urlMask, this.checked)
|
|
359
|
+
.then(init)
|
|
360
|
+
.then(() => linkFor(method, urlMask)?.click())
|
|
361
|
+
.catch(onError)
|
|
362
|
+
}
|
|
363
|
+
return (
|
|
364
|
+
r('label', {
|
|
365
|
+
className: CSS.ProxyToggler,
|
|
366
|
+
title: Strings.proxy_toggler
|
|
367
|
+
},
|
|
368
|
+
r('input', {
|
|
369
|
+
type: 'checkbox',
|
|
370
|
+
disabled,
|
|
371
|
+
name: broker.currentMock.file,
|
|
372
|
+
checked: !broker.currentMock.file,
|
|
373
|
+
onChange
|
|
374
|
+
}),
|
|
375
|
+
r(CloudIcon)))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
356
379
|
|
|
357
380
|
// Payload Preview ===============
|
|
358
381
|
|
|
@@ -458,6 +481,19 @@ function onError(error) {
|
|
|
458
481
|
}
|
|
459
482
|
|
|
460
483
|
|
|
484
|
+
function TimerIcon() {
|
|
485
|
+
return (
|
|
486
|
+
r('svg', { viewBox: '0 0 24 24' },
|
|
487
|
+
r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function CloudIcon() {
|
|
491
|
+
return (
|
|
492
|
+
r('svg', { viewBox: '0 0 24 24' },
|
|
493
|
+
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' })))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
|
|
461
497
|
|
|
462
498
|
// Utils ============
|
|
463
499
|
|
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
|
}
|