mockaton 8.7.3 → 8.7.5
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 +19 -14
- package/package.json +1 -1
- package/src/Api.js +23 -12
- package/src/Dashboard.css +27 -18
- package/src/Dashboard.js +76 -69
- package/src/Filename.js +1 -0
- package/src/MockDispatcher.js +1 -1
- package/src/Mockaton.js +5 -1
- package/src/Mockaton.test.js +7 -6
- package/src/StaticDispatcher.js +44 -2
- package/src/mockBrokersCollection.js +5 -2
- package/src/utils/fs.js +1 -3
- package/src/utils/http-cors.test.js +1 -1
- package/src/utils/http-response.js +9 -36
- package/src/utils/mime.js +8 -2
package/README.md
CHANGED
|
@@ -9,27 +9,27 @@ Mockaton is an HTTP mock server with the goal of making
|
|
|
9
9
|
your frontend development and testing easier—and a lot more fun.
|
|
10
10
|
|
|
11
11
|
With Mockaton you don’t need to write code for wiring your mocks.
|
|
12
|
-
Instead, just place your mocks in a directory
|
|
13
|
-
for filenames
|
|
12
|
+
Instead, just place your mocks in a directory and it will be scanned
|
|
13
|
+
for filenames following a convention similar to the URLs.
|
|
14
14
|
|
|
15
|
-
For example, for
|
|
15
|
+
For example, for `/api/user/1234` the mock filename would be:
|
|
16
16
|
```
|
|
17
17
|
my-mocks-dir/api/user/[user-id].GET.200.json
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Scrapping Mocks from you Backend
|
|
21
21
|
|
|
22
|
-
Mockaton can fallback to your real backend on routes you don’t have mocks for.
|
|
23
|
-
|
|
24
|
-
check **Save Mocks**,
|
|
22
|
+
Mockaton can fallback to your real backend on routes you don’t have mocks for. For that,
|
|
23
|
+
type your backend address in the **Fallback Backend** field. And if you
|
|
24
|
+
check **Save Mocks**, it will collect those responses that hit your backend.
|
|
25
25
|
Those mocks will be saved to your `config.mocksDir` following the filename convention.
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
## Multiple Mock Variants
|
|
29
29
|
|
|
30
30
|
### Adding comments in filenames
|
|
31
|
-
Want to mock a locked-out user or an invalid login attempt?
|
|
32
|
-
|
|
31
|
+
Want to mock a locked-out user or an invalid login attempt?
|
|
32
|
+
Add a comment to the filename in parentheses. For example:
|
|
33
33
|
|
|
34
34
|
`api/login(locked out user).POST.423.json`
|
|
35
35
|
|
|
@@ -214,8 +214,8 @@ Response Status Code, and File Extension.
|
|
|
214
214
|
api/user.GET.200.json
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
You can also use `.empty` if you don’t
|
|
218
|
-
`Content-Type` header.
|
|
217
|
+
You can also use `.empty` or `.unknown` if you don’t
|
|
218
|
+
want a `Content-Type` header in the response.
|
|
219
219
|
|
|
220
220
|
|
|
221
221
|
### Dynamic Parameters
|
|
@@ -305,10 +305,10 @@ URL, the filename will have `[id]` in their place. For example,
|
|
|
305
305
|
|
|
306
306
|
```
|
|
307
307
|
/api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
|
|
308
|
-
my-mocks-dir/api/user/[id]/likes.GET.200.json
|
|
308
|
+
my-mocks-dir/api/user/[id]/likes.GET.200.json
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
-
Your existing mocks won’t be overwritten (they don’t hit the fallback server).
|
|
311
|
+
Your existing mocks won’t be overwritten (because they don’t hit the fallback server).
|
|
312
312
|
|
|
313
313
|
<details>
|
|
314
314
|
<summary>Extension Details</summary>
|
|
@@ -464,7 +464,7 @@ If you don’t want to open a browser, pass a noop:
|
|
|
464
464
|
config.onReady = () => {}
|
|
465
465
|
```
|
|
466
466
|
|
|
467
|
-
|
|
467
|
+
At any rate, you can trigger any command besides opening a browser.
|
|
468
468
|
|
|
469
469
|
---
|
|
470
470
|
|
|
@@ -507,9 +507,14 @@ await mockaton.setProxyFallback('http://example.com')
|
|
|
507
507
|
```
|
|
508
508
|
Pass an empty string to disable it.
|
|
509
509
|
|
|
510
|
+
### Set Save Proxied Mocks
|
|
511
|
+
```js
|
|
512
|
+
await mockaton.setCollectProxied(true)
|
|
513
|
+
```
|
|
514
|
+
|
|
510
515
|
### Reset
|
|
511
516
|
Re-initialize the collection. The selected mocks, cookies, and delays go back to
|
|
512
|
-
default, but `
|
|
517
|
+
default, but the `proxyFallback`, `colledProxied`, and `corsAllowed` are not affected.
|
|
513
518
|
```js
|
|
514
519
|
await mockaton.reset()
|
|
515
520
|
```
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -10,17 +10,22 @@ import { DF, API } from './ApiConstants.js'
|
|
|
10
10
|
import { parseJSON } from './utils/http-request.js'
|
|
11
11
|
import { listFilesRecursively } from './utils/fs.js'
|
|
12
12
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
13
|
-
import { sendOK, sendBadRequest, sendJSON,
|
|
13
|
+
import { sendOK, sendBadRequest, sendJSON, sendUnprocessableContent, sendDashboardFile, sendForbidden } from './utils/http-response.js'
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
const dashboardAssets = [
|
|
17
|
+
'/ApiConstants.js',
|
|
18
|
+
'/Commander.js',
|
|
19
|
+
'/Dashboard.css',
|
|
20
|
+
'/Dashboard.js',
|
|
21
|
+
'/Filename.js',
|
|
22
|
+
'/mockaton-logo.svg'
|
|
23
|
+
]
|
|
24
|
+
|
|
16
25
|
export const apiGetRequests = new Map([
|
|
17
26
|
[API.dashboard, serveDashboard],
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
[API.dashboard + '/Dashboard.css', serveDashboardAsset],
|
|
21
|
-
[API.dashboard + '/Dashboard.js', serveDashboardAsset],
|
|
22
|
-
[API.dashboard + '/Filename.js', serveDashboardAsset],
|
|
23
|
-
[API.dashboard + '/mockaton-logo.svg', serveDashboardAsset],
|
|
27
|
+
...dashboardAssets.map(f =>
|
|
28
|
+
[API.dashboard + f, serveDashboardAsset]),
|
|
24
29
|
[API.mocks, listMockBrokers],
|
|
25
30
|
[API.cookies, listCookies],
|
|
26
31
|
[API.comments, listComments],
|
|
@@ -44,10 +49,14 @@ export const apiPatchRequests = new Map([
|
|
|
44
49
|
/* === GET === */
|
|
45
50
|
|
|
46
51
|
function serveDashboard(_, response) {
|
|
47
|
-
|
|
52
|
+
sendDashboardFile(response, join(import.meta.dirname, 'Dashboard.html'))
|
|
48
53
|
}
|
|
49
54
|
function serveDashboardAsset(req, response) {
|
|
50
|
-
|
|
55
|
+
const f = req.url.replace(API.dashboard, '')
|
|
56
|
+
if (dashboardAssets.includes(f))
|
|
57
|
+
sendDashboardFile(response, join(import.meta.dirname, f))
|
|
58
|
+
else
|
|
59
|
+
sendForbidden(response)
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
function listCookies(_, response) { sendJSON(response, cookie.list()) }
|
|
@@ -95,9 +104,11 @@ async function selectMock(req, response) {
|
|
|
95
104
|
const file = await parseJSON(req)
|
|
96
105
|
const broker = mockBrokersCollection.getBrokerByFilename(file)
|
|
97
106
|
if (!broker || !broker.hasMock(file))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
sendUnprocessableContent(response, `Missing Mock: ${file}`)
|
|
108
|
+
else {
|
|
109
|
+
broker.updateFile(file)
|
|
110
|
+
sendOK(response)
|
|
111
|
+
}
|
|
101
112
|
}
|
|
102
113
|
catch (error) {
|
|
103
114
|
sendBadRequest(response, error)
|
package/src/Dashboard.css
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.1), 0 1px 1px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
|
3
|
+
--radius: 6px
|
|
3
4
|
}
|
|
4
5
|
|
|
5
6
|
@media (prefers-color-scheme: light) {
|
|
6
7
|
:root {
|
|
7
8
|
--color4xxBackground: #ffedd1;
|
|
8
|
-
--colorAccent: #
|
|
9
|
-
--colorAccentAlt: #
|
|
9
|
+
--colorAccent: #0075db;
|
|
10
|
+
--colorAccentAlt: #008664;
|
|
10
11
|
--colorBackground: #fff;
|
|
11
|
-
--colorHeaderBackground: #f7f7f7;
|
|
12
|
-
--colorComboBoxBackground: #f7f7f7;
|
|
13
|
-
--colorSecondaryButtonBackground: #f5f5f5;
|
|
14
12
|
--colorComboBoxHeaderBackground: #fff;
|
|
13
|
+
--colorComboBoxBackground: #f7f7f7;
|
|
14
|
+
--colorHeaderBackground: #f3f3f3;
|
|
15
|
+
--colorSecondaryButtonBackground: #f3f3f3;
|
|
15
16
|
--colorDisabled: #444;
|
|
16
17
|
--colorHover: #dfefff;
|
|
17
18
|
--colorLabel: #444;
|
|
@@ -64,13 +65,21 @@ select, a, input, button, summary {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
a, button, input[type=checkbox] {
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
|
|
71
|
+
&:active {
|
|
72
|
+
cursor: grabbing;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
select {
|
|
68
77
|
font-size: 100%;
|
|
69
78
|
background: var(--colorComboBoxBackground);
|
|
70
79
|
color: var(--colorText);
|
|
71
80
|
cursor: pointer;
|
|
72
81
|
outline: 0;
|
|
73
|
-
border-radius:
|
|
82
|
+
border-radius: var(--radius);
|
|
74
83
|
|
|
75
84
|
&:enabled {
|
|
76
85
|
box-shadow: var(--boxShadow1);
|
|
@@ -122,7 +131,11 @@ select {
|
|
|
122
131
|
margin-top: 4px;
|
|
123
132
|
font-size: 11px;
|
|
124
133
|
background: var(--colorComboBoxHeaderBackground);
|
|
125
|
-
border-radius:
|
|
134
|
+
border-radius: var(--radius);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
select:enabled:hover {
|
|
138
|
+
background: var(--colorHover);
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
&.FallbackBackend {
|
|
@@ -165,7 +178,6 @@ select {
|
|
|
165
178
|
background: transparent;
|
|
166
179
|
color: var(--colorRed);
|
|
167
180
|
border-radius: 50px;
|
|
168
|
-
cursor: pointer;
|
|
169
181
|
|
|
170
182
|
&:hover {
|
|
171
183
|
background: var(--colorRed);
|
|
@@ -234,7 +246,7 @@ select {
|
|
|
234
246
|
display: inline-block;
|
|
235
247
|
width: 280px;
|
|
236
248
|
padding: 8px 6px;
|
|
237
|
-
border-radius:
|
|
249
|
+
border-radius: var(--radius);
|
|
238
250
|
color: var(--colorAccent);
|
|
239
251
|
text-decoration: none;
|
|
240
252
|
|
|
@@ -292,8 +304,8 @@ select {
|
|
|
292
304
|
}
|
|
293
305
|
|
|
294
306
|
> svg {
|
|
295
|
-
width:
|
|
296
|
-
height:
|
|
307
|
+
width: 20px;
|
|
308
|
+
height: 20px;
|
|
297
309
|
vertical-align: bottom;
|
|
298
310
|
fill: var(--colorText);
|
|
299
311
|
border-radius: 50%;
|
|
@@ -329,7 +341,7 @@ select {
|
|
|
329
341
|
}
|
|
330
342
|
|
|
331
343
|
> span {
|
|
332
|
-
padding: 4px;
|
|
344
|
+
padding: 5px 4px;
|
|
333
345
|
box-shadow: var(--boxShadow1);
|
|
334
346
|
font-size: 10px;
|
|
335
347
|
color: var(--colorText);
|
|
@@ -374,13 +386,10 @@ select {
|
|
|
374
386
|
}
|
|
375
387
|
|
|
376
388
|
.StaticFilesList {
|
|
377
|
-
margin-top:
|
|
389
|
+
margin-top: 20px;
|
|
378
390
|
|
|
379
|
-
|
|
380
|
-
width: max-content;
|
|
391
|
+
h2 {
|
|
381
392
|
margin-bottom: 8px;
|
|
382
|
-
cursor: pointer;
|
|
383
|
-
font-weight: bold;
|
|
384
393
|
}
|
|
385
394
|
|
|
386
395
|
ul {
|
|
@@ -395,7 +404,7 @@ select {
|
|
|
395
404
|
a {
|
|
396
405
|
display: inline-block;
|
|
397
406
|
padding: 6px;
|
|
398
|
-
border-radius:
|
|
407
|
+
border-radius: var(--radius);
|
|
399
408
|
color: var(--colorAccentAlt);
|
|
400
409
|
text-decoration: none;
|
|
401
410
|
|
package/src/Dashboard.js
CHANGED
|
@@ -3,6 +3,14 @@ import { Commander } from './Commander.js'
|
|
|
3
3
|
import { DEFAULT_500_COMMENT } from './ApiConstants.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
function syntaxHighlightJson(textBody) {
|
|
7
|
+
const prism = window.Prism
|
|
8
|
+
return prism?.highlight && prism?.languages?.json
|
|
9
|
+
? prism.highlight(textBody, prism.languages.json, 'json')
|
|
10
|
+
: false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
const Strings = {
|
|
7
15
|
bulk_select_by_comment: 'Bulk Select by Comment',
|
|
8
16
|
bulk_select_by_comment_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
|
|
@@ -19,7 +27,7 @@ const Strings = {
|
|
|
19
27
|
pick: 'Pick…',
|
|
20
28
|
reset: 'Reset',
|
|
21
29
|
save_proxied: 'Save Mocks',
|
|
22
|
-
|
|
30
|
+
static_get: 'Static GET'
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
const CSS = {
|
|
@@ -45,8 +53,6 @@ const CSS = {
|
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
const r = createElement
|
|
48
|
-
const refPayloadViewer = useRef()
|
|
49
|
-
const refPayloadViewerFileTitle = useRef()
|
|
50
56
|
|
|
51
57
|
const mockaton = new Commander(window.location.origin)
|
|
52
58
|
|
|
@@ -59,10 +65,7 @@ function init() {
|
|
|
59
65
|
mockaton.getProxyFallback(),
|
|
60
66
|
mockaton.listStaticFiles()
|
|
61
67
|
].map(api => api.then(response => response.ok && response.json())))
|
|
62
|
-
.then(data =>
|
|
63
|
-
empty(document.body)
|
|
64
|
-
document.body.append(...App(data))
|
|
65
|
-
})
|
|
68
|
+
.then(data => document.body.replaceChildren(...App(data)))
|
|
66
69
|
.catch(onError)
|
|
67
70
|
}
|
|
68
71
|
init()
|
|
@@ -118,9 +121,10 @@ function CookieSelector({ cookies }) {
|
|
|
118
121
|
function BulkSelector({ comments }) {
|
|
119
122
|
// UX wise this should be a menu instead of this `select`.
|
|
120
123
|
// But this way is easier to implement, with a few hacks.
|
|
124
|
+
const firstOption = Strings.pick
|
|
121
125
|
function onChange() {
|
|
122
126
|
const value = this.value
|
|
123
|
-
this.value =
|
|
127
|
+
this.value = firstOption // Hack
|
|
124
128
|
mockaton.bulkSelectByComment(value)
|
|
125
129
|
.then(init)
|
|
126
130
|
.catch(onError)
|
|
@@ -128,7 +132,7 @@ function BulkSelector({ comments }) {
|
|
|
128
132
|
const disabled = !comments.length
|
|
129
133
|
const list = disabled
|
|
130
134
|
? []
|
|
131
|
-
: [
|
|
135
|
+
: [firstOption].concat(comments)
|
|
132
136
|
return (
|
|
133
137
|
r('label', { className: CSS.Field },
|
|
134
138
|
r('span', null, Strings.bulk_select_by_comment),
|
|
@@ -144,14 +148,14 @@ function BulkSelector({ comments }) {
|
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (!
|
|
152
|
-
|
|
151
|
+
function onChange() {
|
|
152
|
+
const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
|
|
153
|
+
saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
|
|
154
|
+
|
|
155
|
+
if (!this.validity.valid)
|
|
156
|
+
this.reportValidity()
|
|
153
157
|
else
|
|
154
|
-
mockaton.setProxyFallback(
|
|
158
|
+
mockaton.setProxyFallback(this.value.trim()).catch(onError)
|
|
155
159
|
}
|
|
156
160
|
return (
|
|
157
161
|
r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
|
|
@@ -166,20 +170,17 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
|
|
|
166
170
|
})),
|
|
167
171
|
r(SaveProxiedCheckbox, {
|
|
168
172
|
collectProxied,
|
|
169
|
-
disabled: !fallbackAddress
|
|
170
|
-
ref: refSaveProxiedCheckbox
|
|
173
|
+
disabled: !fallbackAddress
|
|
171
174
|
})))
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
function SaveProxiedCheckbox({
|
|
175
|
-
function onChange(
|
|
176
|
-
mockaton.setCollectProxied(
|
|
177
|
-
.catch(onError)
|
|
177
|
+
function SaveProxiedCheckbox({ disabled, collectProxied }) {
|
|
178
|
+
function onChange() {
|
|
179
|
+
mockaton.setCollectProxied(this.checked).catch(onError)
|
|
178
180
|
}
|
|
179
181
|
return (
|
|
180
182
|
r('label', { className: CSS.SaveProxiedCheckbox },
|
|
181
183
|
r('input', {
|
|
182
|
-
ref,
|
|
183
184
|
type: 'checkbox',
|
|
184
185
|
disabled,
|
|
185
186
|
checked: collectProxied,
|
|
@@ -238,14 +239,7 @@ function PreviewLink({ method, urlMask }) {
|
|
|
238
239
|
async function onClick(event) {
|
|
239
240
|
event.preventDefault()
|
|
240
241
|
try {
|
|
241
|
-
|
|
242
|
-
empty(refPayloadViewer.current)
|
|
243
|
-
refPayloadViewer.current.append(PayloadViewerProgressBar())
|
|
244
|
-
}, 180)
|
|
245
|
-
|
|
246
|
-
const response = await fetch(this.href, { method })
|
|
247
|
-
clearTimeout(preloader)
|
|
248
|
-
await updatePayloadViewer(method, urlMask, this.href, response)
|
|
242
|
+
await previewMock(method, urlMask, this.href)
|
|
249
243
|
document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
|
|
250
244
|
this.classList.add(CSS.chosen)
|
|
251
245
|
}
|
|
@@ -306,9 +300,9 @@ function MockSelector({ broker }) {
|
|
|
306
300
|
|
|
307
301
|
|
|
308
302
|
function DelayRouteToggler({ broker }) {
|
|
309
|
-
function onChange(
|
|
303
|
+
function onChange() {
|
|
310
304
|
const { method, urlMask } = parseFilename(this.name)
|
|
311
|
-
mockaton.setRouteIsDelayed(method, urlMask,
|
|
305
|
+
mockaton.setRouteIsDelayed(method, urlMask, this.checked)
|
|
312
306
|
.catch(onError)
|
|
313
307
|
}
|
|
314
308
|
return (
|
|
@@ -333,11 +327,12 @@ function DelayRouteToggler({ broker }) {
|
|
|
333
327
|
|
|
334
328
|
|
|
335
329
|
function InternalServerErrorToggler({ broker }) {
|
|
336
|
-
function onChange(
|
|
330
|
+
function onChange() {
|
|
337
331
|
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
338
|
-
mockaton.select(
|
|
339
|
-
|
|
340
|
-
|
|
332
|
+
mockaton.select(
|
|
333
|
+
this.checked
|
|
334
|
+
? broker.mocks.find(f => parseFilename(f).status === 500)
|
|
335
|
+
: broker.mocks[0])
|
|
341
336
|
.then(init)
|
|
342
337
|
.then(() => linkFor(method, urlMask)?.click())
|
|
343
338
|
.catch(onError)
|
|
@@ -358,18 +353,24 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
358
353
|
)
|
|
359
354
|
}
|
|
360
355
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
356
|
+
|
|
357
|
+
// Payload Preview ===============
|
|
358
|
+
|
|
359
|
+
const payloadViewerTitleRef = useRef()
|
|
360
|
+
const payloadViewerRef = useRef()
|
|
366
361
|
|
|
367
362
|
function PayloadViewer() {
|
|
368
363
|
return (
|
|
369
364
|
r('div', { className: CSS.PayloadViewer },
|
|
370
|
-
r('h2', { ref:
|
|
365
|
+
r('h2', { ref: payloadViewerTitleRef }, Strings.mock),
|
|
371
366
|
r('pre', null,
|
|
372
|
-
r('code', { ref:
|
|
367
|
+
r('code', { ref: payloadViewerRef }, Strings.click_link_to_preview))))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function PayloadViewerProgressBar() {
|
|
371
|
+
return (
|
|
372
|
+
r('div', { className: CSS.ProgressBar },
|
|
373
|
+
r('div', { style: { animationDuration: '1000ms' } }))) // TODO from Config.delay - 180
|
|
373
374
|
}
|
|
374
375
|
|
|
375
376
|
function PayloadViewerTitle({ file }) {
|
|
@@ -381,27 +382,38 @@ function PayloadViewerTitle({ file }) {
|
|
|
381
382
|
'.' + ext))
|
|
382
383
|
}
|
|
383
384
|
|
|
384
|
-
async function
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
385
|
+
async function previewMock(method, urlMask, href) {
|
|
386
|
+
const timer = setTimeout(renderProgressBar, 180)
|
|
387
|
+
const response = await fetch(href, { method })
|
|
388
|
+
clearTimeout(timer)
|
|
389
|
+
await updatePayloadViewer(method, urlMask, response)
|
|
390
|
+
|
|
391
|
+
function renderProgressBar() {
|
|
392
|
+
payloadViewerRef.current.replaceChildren(PayloadViewerProgressBar())
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function updatePayloadViewer(method, urlMask, response) {
|
|
397
|
+
payloadViewerTitleRef.current.replaceChildren(
|
|
398
|
+
PayloadViewerTitle({ file: mockSelectorFor(method, urlMask).value }))
|
|
389
399
|
|
|
390
400
|
const mime = response.headers.get('content-type') || ''
|
|
391
|
-
if (mime.startsWith('image/')) //
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
function renderPayloadImage(src) {
|
|
397
|
-
empty(refPayloadViewer.current)
|
|
398
|
-
refPayloadViewer.current.append(r('img', { src }))
|
|
401
|
+
if (mime.startsWith('image/')) { // Naively assumes GET.200
|
|
402
|
+
payloadViewerRef.current.replaceChildren(
|
|
403
|
+
r('img', {
|
|
404
|
+
src: URL.createObjectURL(await response.blob())
|
|
405
|
+
}))
|
|
399
406
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
407
|
+
else {
|
|
408
|
+
const body = await response.text() || Strings.empty_response_body
|
|
409
|
+
if (mime === 'application/json') {
|
|
410
|
+
const hBody = syntaxHighlightJson(body)
|
|
411
|
+
if (hBody) {
|
|
412
|
+
payloadViewerRef.current.innerHTML = hBody
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
payloadViewerRef.current.innerText = body
|
|
405
417
|
}
|
|
406
418
|
}
|
|
407
419
|
|
|
@@ -427,11 +439,11 @@ function StaticFilesList({ staticFiles }) {
|
|
|
427
439
|
if (!staticFiles.length)
|
|
428
440
|
return null
|
|
429
441
|
return (
|
|
430
|
-
r('
|
|
442
|
+
r('section', {
|
|
431
443
|
open: true,
|
|
432
444
|
className: CSS.StaticFilesList
|
|
433
445
|
},
|
|
434
|
-
r('
|
|
446
|
+
r('h2', null, Strings.static_get),
|
|
435
447
|
r('ul', null, staticFiles.map(f =>
|
|
436
448
|
r('li', null,
|
|
437
449
|
r('a', { href: f, target: '_blank' }, f))))))
|
|
@@ -453,11 +465,6 @@ function cssClass(...args) {
|
|
|
453
465
|
return args.filter(a => a).join(' ')
|
|
454
466
|
}
|
|
455
467
|
|
|
456
|
-
function empty(node) {
|
|
457
|
-
while (node.firstChild)
|
|
458
|
-
node.removeChild(node.firstChild)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
468
|
|
|
462
469
|
// These are simplified React-compatible implementations.
|
|
463
470
|
// IOW, for switching to React, remove the `createRoot`, `createElement`, `useRef`
|
package/src/Filename.js
CHANGED
package/src/MockDispatcher.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function dispatchMock(req, response) {
|
|
|
20
20
|
return
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
console.log(decodeURIComponent(req.url),
|
|
23
|
+
console.log('%s → %s', decodeURIComponent(req.url), broker.file)
|
|
24
24
|
response.statusCode = broker.status
|
|
25
25
|
|
|
26
26
|
if (cookie.getCurrent())
|
package/src/Mockaton.js
CHANGED
|
@@ -12,6 +12,8 @@ import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
|
12
12
|
import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
process.on('unhandledRejection', error => { throw error })
|
|
16
|
+
|
|
15
17
|
export function Mockaton(options) {
|
|
16
18
|
setup(options)
|
|
17
19
|
mockBrokerCollection.init()
|
|
@@ -21,7 +23,7 @@ export function Mockaton(options) {
|
|
|
21
23
|
if (!file)
|
|
22
24
|
return
|
|
23
25
|
if (existsSync(join(config.mocksDir, file)))
|
|
24
|
-
mockBrokerCollection.registerMock(file, '
|
|
26
|
+
mockBrokerCollection.registerMock(file, 'isFromWatcher')
|
|
25
27
|
else
|
|
26
28
|
mockBrokerCollection.unregisterMock(file)
|
|
27
29
|
})
|
|
@@ -39,6 +41,8 @@ export function Mockaton(options) {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
async function onRequest(req, response) {
|
|
44
|
+
req.on('error', console.error)
|
|
45
|
+
response.on('error', console.error)
|
|
42
46
|
response.setHeader('Server', 'Mockaton')
|
|
43
47
|
|
|
44
48
|
if (config.corsAllowed)
|
package/src/Mockaton.test.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createServer } from 'node:http'
|
|
|
5
5
|
import { dirname, join } from 'node:path'
|
|
6
6
|
import { randomUUID } from 'node:crypto'
|
|
7
7
|
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
8
|
-
import { writeFileSync, mkdtempSync, mkdirSync, unlinkSync } from 'node:fs'
|
|
8
|
+
import { writeFileSync, mkdtempSync, mkdirSync, unlinkSync, readFileSync } from 'node:fs'
|
|
9
9
|
|
|
10
10
|
import { config } from './config.js'
|
|
11
11
|
import { mimeFor } from './utils/mime.js'
|
|
@@ -14,7 +14,7 @@ import { readBody } from './utils/http-request.js'
|
|
|
14
14
|
import { Commander } from './Commander.js'
|
|
15
15
|
import { CorsHeader } from './utils/http-cors.js'
|
|
16
16
|
import { parseFilename } from './Filename.js'
|
|
17
|
-
import { listFilesRecursively
|
|
17
|
+
import { listFilesRecursively } from './utils/fs.js'
|
|
18
18
|
import { API, DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
19
19
|
|
|
20
20
|
|
|
@@ -351,7 +351,7 @@ async function testRegistering() {
|
|
|
351
351
|
fixtureForRegisteringPutA500[1]
|
|
352
352
|
])
|
|
353
353
|
deepEqual(currentMock, {
|
|
354
|
-
file:
|
|
354
|
+
file: fixtureForRegisteringPutA[1],
|
|
355
355
|
delay: 0
|
|
356
356
|
})
|
|
357
357
|
})
|
|
@@ -409,6 +409,7 @@ async function testItUpdatesRouteDelay(url, file, expectedBody) {
|
|
|
409
409
|
await describe('url: ' + url, () => {
|
|
410
410
|
it('body is: ' + expectedBody, () => equal(body, JSON.stringify(expectedBody)))
|
|
411
411
|
it('delay', () => equal((new Date()).getTime() - now.getTime() > config.delay, true))
|
|
412
|
+
// TODO flaky test ^
|
|
412
413
|
})
|
|
413
414
|
}
|
|
414
415
|
|
|
@@ -416,7 +417,7 @@ async function testBadRequestWhenUpdatingNonExistingMockAlternative() {
|
|
|
416
417
|
await it('There are mocks for /api/the-route but not this one', async () => {
|
|
417
418
|
const missingFile = 'api/the-route(non-existing-variant).GET.200.json'
|
|
418
419
|
const res = await commander.select(missingFile)
|
|
419
|
-
equal(res.status,
|
|
420
|
+
equal(res.status, 422)
|
|
420
421
|
equal(await res.text(), `Missing Mock: ${missingFile}`)
|
|
421
422
|
})
|
|
422
423
|
}
|
|
@@ -549,7 +550,7 @@ async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
|
|
|
549
550
|
const fallbackServer = createServer(async (req, response) => {
|
|
550
551
|
response.writeHead(423, {
|
|
551
552
|
'custom_header': 'my_custom_header',
|
|
552
|
-
'content-type': mimeFor('txt'),
|
|
553
|
+
'content-type': mimeFor('.txt'),
|
|
553
554
|
'set-cookie': [
|
|
554
555
|
'cookieA=A',
|
|
555
556
|
'cookieB=B'
|
|
@@ -573,7 +574,7 @@ async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
|
|
|
573
574
|
equal(res.headers.get('set-cookie'), ['cookieA=A', 'cookieB=B'].join(', '))
|
|
574
575
|
equal(await res.text(), reqBodyPayload)
|
|
575
576
|
|
|
576
|
-
const savedBody =
|
|
577
|
+
const savedBody = readFileSync(join(tmpDir, 'api/non-existing-mock/[id].POST.423.txt'), 'utf8')
|
|
577
578
|
equal(savedBody, reqBodyPayload)
|
|
578
579
|
|
|
579
580
|
fallbackServer.close()
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
1
|
+
import { join, isAbsolute } from 'node:path'
|
|
2
|
+
import fs, { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
2
4
|
import { config } from './config.js'
|
|
5
|
+
import { mimeFor } from './utils/mime.js'
|
|
3
6
|
import { isDirectory, isFile } from './utils/fs.js'
|
|
4
|
-
import {
|
|
7
|
+
import { sendNotFound, sendInternalServerError } from './utils/http-response.js'
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
export function isStatic(req) {
|
|
8
11
|
if (!config.staticDir)
|
|
9
12
|
return false
|
|
13
|
+
if (!isAbsolute(req.url)) // prevent sandbox escape
|
|
14
|
+
return false
|
|
10
15
|
const f = resolvePath(req.url)
|
|
11
16
|
return !config.ignore.test(f) && Boolean(f)
|
|
12
17
|
}
|
|
@@ -29,4 +34,41 @@ function resolvePath(url) {
|
|
|
29
34
|
return candidate
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
function sendFile(response, file) {
|
|
38
|
+
if (!isFile(file))
|
|
39
|
+
sendNotFound(response)
|
|
40
|
+
else {
|
|
41
|
+
response.setHeader('Content-Type', mimeFor(file))
|
|
42
|
+
response.end(readFileSync(file, 'utf8'))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function sendPartialContent(response, range, file) {
|
|
47
|
+
const { size } = await fs.promises.lstat(file)
|
|
48
|
+
let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
49
|
+
if (isNaN(end)) end = size - 1
|
|
50
|
+
if (isNaN(start)) start = size - end
|
|
51
|
+
|
|
52
|
+
if (start < 0 || start > end || start >= size || end >= size) {
|
|
53
|
+
response.statusCode = 416 // Range Not Satisfiable
|
|
54
|
+
response.setHeader('Content-Range', `bytes */${size}`)
|
|
55
|
+
response.end()
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
response.statusCode = 206 // Partial Content
|
|
59
|
+
response.setHeader('Accept-Ranges', 'bytes')
|
|
60
|
+
response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
61
|
+
response.setHeader('Content-Type', mimeFor(file))
|
|
62
|
+
const reader = fs.createReadStream(file, { start, end })
|
|
63
|
+
reader.on('open', function () {
|
|
64
|
+
this.pipe(response)
|
|
65
|
+
})
|
|
66
|
+
reader.on('error', function (error) {
|
|
67
|
+
sendInternalServerError(response, error)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
32
74
|
|
|
@@ -35,7 +35,7 @@ export function init() {
|
|
|
35
35
|
})
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function registerMock(file,
|
|
38
|
+
export function registerMock(file, isFromWatcher) {
|
|
39
39
|
if (getBrokerByFilename(file)?.hasMock(file)
|
|
40
40
|
|| config.ignore.test(file)
|
|
41
41
|
|| !filenameIsValid(file))
|
|
@@ -48,8 +48,11 @@ export function registerMock(file, shouldEnsure500) {
|
|
|
48
48
|
else
|
|
49
49
|
collection[method][urlMask].register(file)
|
|
50
50
|
|
|
51
|
-
if (
|
|
51
|
+
if (isFromWatcher) {
|
|
52
|
+
if (!this.file)
|
|
53
|
+
collection[method][urlMask].selectDefaultFile()
|
|
52
54
|
collection[method][urlMask].ensureItHas500()
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export function unregisterMock(file) {
|
package/src/utils/fs.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { join, dirname, sep, posix } from 'node:path'
|
|
2
|
-
import { lstatSync,
|
|
2
|
+
import { lstatSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
|
|
6
6
|
export const isDirectory = path => lstatSync(path, { throwIfNoEntry: false })?.isDirectory()
|
|
7
7
|
|
|
8
|
-
export const read = path => readFileSync(path, 'utf8')
|
|
9
|
-
|
|
10
8
|
/** @returns {Array<string>} paths relative to `dir` */
|
|
11
9
|
export const listFilesRecursively = dir => {
|
|
12
10
|
const files = readdirSync(dir, { recursive: true }).filter(f => isFile(join(dir, f)))
|
|
@@ -40,7 +40,7 @@ await describe('CORS', async () => {
|
|
|
40
40
|
|
|
41
41
|
await describe('Identifies Preflight Requests', async () => {
|
|
42
42
|
const requiredRequestHeaders = {
|
|
43
|
-
[CH.Origin]: 'http://
|
|
43
|
+
[CH.Origin]: 'http://localhost:9999',
|
|
44
44
|
[CH.AccessControlRequestMethod]: 'POST'
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { mimeFor } from './mime.js'
|
|
3
|
-
import { isFile, read } from './fs.js'
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
export function sendOK(response) {
|
|
@@ -17,46 +16,20 @@ export function sendJSON(response, payload) {
|
|
|
17
16
|
response.end(JSON.stringify(payload))
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
else {
|
|
24
|
-
response.setHeader('Content-Type', mimeFor(filePath))
|
|
25
|
-
response.end(read(filePath))
|
|
26
|
-
}
|
|
19
|
+
export function sendForbidden(response) {
|
|
20
|
+
response.statusCode = 403
|
|
21
|
+
response.end()
|
|
27
22
|
}
|
|
28
23
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (isNaN(end)) end = size - 1
|
|
33
|
-
if (isNaN(start)) start = size - end
|
|
34
|
-
|
|
35
|
-
if (start < 0 || start > end || start >= size || end >= size) {
|
|
36
|
-
response.statusCode = 416 // Range Not Satisfiable
|
|
37
|
-
response.setHeader('Content-Range', `bytes */${size}`)
|
|
38
|
-
response.end()
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
response.statusCode = 206 // Partial Content
|
|
42
|
-
response.setHeader('Accept-Ranges', 'bytes')
|
|
43
|
-
response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
44
|
-
response.setHeader('Content-Type', mimeFor(file))
|
|
45
|
-
const reader = fs.createReadStream(file, { start, end })
|
|
46
|
-
reader.on('open', function () {
|
|
47
|
-
this.pipe(response)
|
|
48
|
-
})
|
|
49
|
-
reader.on('error', function (error) {
|
|
50
|
-
sendInternalServerError(response, error)
|
|
51
|
-
})
|
|
52
|
-
}
|
|
24
|
+
export function sendDashboardFile(response, file) {
|
|
25
|
+
response.setHeader('Content-Type', mimeFor(file))
|
|
26
|
+
response.end(readFileSync(file, 'utf8'))
|
|
53
27
|
}
|
|
54
28
|
|
|
55
|
-
|
|
56
29
|
export function sendBadRequest(response, error) {
|
|
57
30
|
console.error(error)
|
|
58
31
|
response.statusCode = 400
|
|
59
|
-
response.end(
|
|
32
|
+
response.end()
|
|
60
33
|
}
|
|
61
34
|
|
|
62
35
|
export function sendNotFound(response) {
|
|
@@ -73,5 +46,5 @@ export function sendUnprocessableContent(response, error) {
|
|
|
73
46
|
export function sendInternalServerError(response, error) {
|
|
74
47
|
console.error(error)
|
|
75
48
|
response.statusCode = 500
|
|
76
|
-
response.end(
|
|
49
|
+
response.end()
|
|
77
50
|
}
|
package/src/utils/mime.js
CHANGED
|
@@ -88,16 +88,22 @@ const mimes = {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function mimeFor(filename) {
|
|
91
|
-
const ext = filename
|
|
91
|
+
const ext = extname(filename).toLowerCase()
|
|
92
92
|
return config.extraMimes[ext] || mimes[ext] || ''
|
|
93
93
|
}
|
|
94
|
+
function extname(filename) {
|
|
95
|
+
const i = filename.lastIndexOf('.')
|
|
96
|
+
return i >= 0
|
|
97
|
+
? filename.substring(i + 1)
|
|
98
|
+
: ''
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
|
|
95
102
|
export function extFor(mime) {
|
|
96
103
|
return mime
|
|
97
104
|
? findExt(mime)
|
|
98
105
|
: 'empty'
|
|
99
106
|
}
|
|
100
|
-
|
|
101
107
|
function findExt(targetMime) {
|
|
102
108
|
for (const [ext, mime] of Object.entries(config.extraMimes))
|
|
103
109
|
if (targetMime === mime)
|