mockaton 8.11.2 → 8.11.4
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 +48 -42
- package/package.json +2 -5
- package/src/Dashboard.css +28 -27
- package/src/Dashboard.js +7 -11
- package/src/MockBroker.js +68 -51
- package/src/MockDispatcher.js +1 -1
- package/src/Mockaton.js +1 -8
- package/src/utils/http-cors.js +12 -12
- package/src/utils/http-cors.test.js +34 -34
package/README.md
CHANGED
|
@@ -3,59 +3,66 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
your frontend development and testing easier—and a lot more fun.
|
|
6
|
+
HTTP mock server for developing and testing frontends.
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
Instead, just place your mocks in a directory and it will be scanned
|
|
11
|
-
for filenames following a convention similar to the URLs.
|
|
8
|
+
## Convention over Code
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
my-mocks-dir/api/user/[user-id].GET.200.json
|
|
16
|
-
```
|
|
10
|
+
With Mockaton you don’t need to write code for wiring mocks. Instead, it scans a
|
|
11
|
+
given directory for mock filenames following a convention similar to the URLs.
|
|
17
12
|
|
|
13
|
+
For example, for <code>/<b>api/user</b>/1234</code> the mock filename would be:
|
|
14
|
+
<pre>
|
|
15
|
+
<code>my-mocks-dir/<b>api/user</b>/[user-id].GET.200.json</code>
|
|
16
|
+
</pre>
|
|
18
17
|
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`500` (Internal Server Error). Nonetheless, there’s a programmatic API,
|
|
23
|
-
which is handy for setting up tests (see **Commander API** below).
|
|
18
|
+
## Multiple Mock Variants
|
|
19
|
+
You can have different mocks for a particular route by adding
|
|
20
|
+
comments and/or using different status codes.
|
|
24
21
|
|
|
25
|
-
<picture>
|
|
26
|
-
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp830x800.light.gold.png">
|
|
27
|
-
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp830x800.dark.gold.png">
|
|
28
|
-
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp830x800.light.gold.png">
|
|
29
|
-
</picture>
|
|
30
22
|
|
|
23
|
+
### Adding comments in filenames
|
|
24
|
+
Comments are anything within parentheses, including them. They
|
|
25
|
+
are ignored for URL purposes, so you can add as many as you want.
|
|
31
26
|
|
|
27
|
+
<pre>
|
|
28
|
+
api/login<b>(locked out user)</b>.POST.423.json
|
|
29
|
+
api/login<b>(invalid login attempt)</b>.POST.401.json
|
|
30
|
+
</pre>
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
### Different response status code
|
|
33
|
+
For instance, using a `4xx` or `5xx` status code for triggering error
|
|
34
|
+
responses. Or `2xx` such as `204` (No Content) for testing collections without records.
|
|
35
|
+
|
|
36
|
+
<pre>
|
|
37
|
+
api/videos.GET.<b>204</b>.json
|
|
38
|
+
api/videos.GET.<b>403</b>.json
|
|
39
|
+
api/videos.GET.<b>500</b>.empty
|
|
40
|
+
</pre>
|
|
36
41
|
|
|
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
42
|
|
|
43
|
+
## Fallback to your Backend
|
|
44
|
+
Mockaton can fallback to your real backend on routes you don’t have
|
|
45
|
+
mocks for, or on routes that have the ☁️ **Cloud Checkbox** checked.
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
|
|
48
|
+
### Scrapping Mocks from your Backend
|
|
42
49
|
If you check **Save Mocks**, Mockaton will collect the responses that hit your backend.
|
|
43
|
-
|
|
50
|
+
They will be saved on your `config.mocksDir` following the filename convention.
|
|
44
51
|
|
|
45
52
|
|
|
46
|
-
## Multiple Mock Variants
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
## Dashboard
|
|
55
|
+
On the dashboard you can select a mock variant for a particular route, delaying responses,
|
|
56
|
+
and triggering an autogenerated `500` (Internal Server Error), among other features.
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</pre>
|
|
58
|
+
Nonetheless, there’s a programmatic API see
|
|
59
|
+
**Commander API** below, which is handy setting up tests.
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
<picture>
|
|
62
|
+
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp830x800.light.gold.png">
|
|
63
|
+
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp830x800.dark.gold.png">
|
|
64
|
+
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp830x800.light.gold.png">
|
|
65
|
+
</picture>
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
|
|
@@ -82,24 +89,23 @@ node --import=tsx my-mockaton.js
|
|
|
82
89
|
```
|
|
83
90
|
|
|
84
91
|
|
|
85
|
-
##
|
|
92
|
+
## Demo App (Vite)
|
|
86
93
|
|
|
87
94
|
This is a minimal React + Vite + Mockaton app. It’s mainly a list of
|
|
88
95
|
colors, which contains all of their possible states. For example,
|
|
89
96
|
permutations for out-of-stock, new-arrival, and discontinued.
|
|
90
97
|
|
|
91
|
-
Also, if you select the **Admin User** from the Mockaton dashboard,
|
|
92
|
-
the color cards will have a Delete button as well.
|
|
93
|
-
|
|
94
98
|
```sh
|
|
95
99
|
git clone https://github.com/ericfortis/mockaton.git
|
|
96
100
|
cd mockaton/demo-app-vite
|
|
97
101
|
npm install
|
|
102
|
+
|
|
98
103
|
npm run mockaton
|
|
99
104
|
npm run start
|
|
105
|
+
# BTW, that directory has scripts for running Mockaton and Vite
|
|
106
|
+
# with one command in two terminals.
|
|
100
107
|
```
|
|
101
108
|
|
|
102
|
-
By the way, that directory has scripts for opening Mockaton and Vite in one command.
|
|
103
109
|
|
|
104
110
|
The app looks like this:
|
|
105
111
|
|
|
@@ -116,6 +122,7 @@ The app looks like this:
|
|
|
116
122
|
- alerts
|
|
117
123
|
- notifications
|
|
118
124
|
- slow to build resources
|
|
125
|
+
- Mocking third-party APIs
|
|
119
126
|
|
|
120
127
|
### Time Travel
|
|
121
128
|
If you commit the mocks to your repo, it’s straightforward to bisect bugs and
|
|
@@ -556,5 +563,4 @@ await mockaton.reset()
|
|
|
556
563
|
|
|
557
564
|
<div style="display: flex; align-items: center; gap: 20px">
|
|
558
565
|
<img src="fixtures-mocks/api/user/avatar.GET.200.png" width="170"/>
|
|
559
|
-
<p style="font-size: 18px">“Mockaton”</p>
|
|
560
566
|
</div>
|
package/package.json
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"name": "mockaton",
|
|
3
3
|
"description": "A deterministic server-side for developing and testing frontend clients",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.11.
|
|
5
|
+
"version": "8.11.4",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": "https://github.com/ericfortis/mockaton",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node --test src
|
|
11
|
+
"test": "node --test \"src/**/*.test.js\"",
|
|
12
12
|
"start": "node dev-mockaton.js",
|
|
13
13
|
"start:ts": "node --import=tsx dev-mockaton.js",
|
|
14
14
|
"pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none \"pixaton-tests/**/*.test.js\"",
|
|
@@ -17,8 +17,5 @@
|
|
|
17
17
|
"optionalDependencies": {
|
|
18
18
|
"pixaton": ">=1.0.2",
|
|
19
19
|
"puppeteer": ">=24.1.1"
|
|
20
|
-
},
|
|
21
|
-
"devDependencies": {
|
|
22
|
-
"pixaton": "1.0.2"
|
|
23
20
|
}
|
|
24
21
|
}
|
package/src/Dashboard.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
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
|
-
--radius:
|
|
4
|
-
--radiusSmall:
|
|
3
|
+
--radius: 28px;
|
|
4
|
+
--radiusSmall: 4px;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
@media (prefers-color-scheme: light) {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
--colorSecondaryAction: #555;
|
|
18
18
|
--colorDisabledMockSelector: #444;
|
|
19
19
|
--colorHover: #dfefff;
|
|
20
|
-
--colorLabel: #
|
|
20
|
+
--colorLabel: #555;
|
|
21
21
|
--colorLightRed: #ffe4ee;
|
|
22
22
|
--colorRed: #da0f00;
|
|
23
23
|
--colorText: #000;
|
|
@@ -89,7 +89,7 @@ select {
|
|
|
89
89
|
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888888'><path d='M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z'/></svg>") no-repeat;
|
|
90
90
|
background-color: var(--colorComboBoxBackground);
|
|
91
91
|
background-size: 16px;
|
|
92
|
-
background-position:
|
|
92
|
+
background-position: 98% center;
|
|
93
93
|
|
|
94
94
|
&:enabled {
|
|
95
95
|
box-shadow: var(--boxShadow1);
|
|
@@ -115,12 +115,12 @@ select {
|
|
|
115
115
|
border-bottom: 1px solid rgba(127, 127, 127, 0.1);
|
|
116
116
|
background: var(--colorHeaderBackground);
|
|
117
117
|
box-shadow: var(--boxShadow1);
|
|
118
|
-
gap:
|
|
118
|
+
gap: 6px;
|
|
119
119
|
|
|
120
120
|
img {
|
|
121
121
|
width: 130px;
|
|
122
122
|
align-self: center;
|
|
123
|
-
margin-right:
|
|
123
|
+
margin-right: 20px;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
.Field {
|
|
@@ -129,6 +129,7 @@ select {
|
|
|
129
129
|
span {
|
|
130
130
|
display: flex;
|
|
131
131
|
align-items: center;
|
|
132
|
+
margin-left: 6px;
|
|
132
133
|
color: var(--colorLabel);
|
|
133
134
|
font-size: 11px;
|
|
134
135
|
gap: 4px;
|
|
@@ -148,9 +149,9 @@ select {
|
|
|
148
149
|
select {
|
|
149
150
|
width: 100%;
|
|
150
151
|
height: 28px;
|
|
151
|
-
padding: 4px
|
|
152
|
+
padding: 4px 12px;
|
|
152
153
|
border-right: 3px solid transparent;
|
|
153
|
-
margin-top:
|
|
154
|
+
margin-top: 3px;
|
|
154
155
|
color: var(--colorText);
|
|
155
156
|
font-size: 11px;
|
|
156
157
|
box-shadow: var(--boxShadow1);
|
|
@@ -163,7 +164,7 @@ select {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
&.GlobalDelayField {
|
|
166
|
-
width:
|
|
167
|
+
width: 88px;
|
|
167
168
|
|
|
168
169
|
input[type=number] {
|
|
169
170
|
padding-right: 0;
|
|
@@ -180,11 +181,7 @@ select {
|
|
|
180
181
|
|
|
181
182
|
&.FallbackBackend {
|
|
182
183
|
position: relative;
|
|
183
|
-
width:
|
|
184
|
-
|
|
185
|
-
input[type=url] {
|
|
186
|
-
padding: 0 6px;
|
|
187
|
-
}
|
|
184
|
+
width: 212px;
|
|
188
185
|
|
|
189
186
|
.SaveProxiedCheckbox {
|
|
190
187
|
position: absolute;
|
|
@@ -197,6 +194,10 @@ select {
|
|
|
197
194
|
font-size: 11px;
|
|
198
195
|
gap: 4px;
|
|
199
196
|
|
|
197
|
+
span {
|
|
198
|
+
margin-left: 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
200
201
|
input:disabled + span {
|
|
201
202
|
opacity: 0.8;
|
|
202
203
|
}
|
|
@@ -281,10 +282,10 @@ select {
|
|
|
281
282
|
|
|
282
283
|
.PreviewLink {
|
|
283
284
|
position: relative;
|
|
284
|
-
left: -
|
|
285
|
+
left: -10px;
|
|
285
286
|
display: inline-block;
|
|
286
|
-
width:
|
|
287
|
-
padding: 8px
|
|
287
|
+
width: 282px;
|
|
288
|
+
padding: 8px 11px;
|
|
288
289
|
border-radius: var(--radius);
|
|
289
290
|
color: var(--colorAccent);
|
|
290
291
|
text-decoration: none;
|
|
@@ -301,14 +302,14 @@ select {
|
|
|
301
302
|
.MockSelector {
|
|
302
303
|
width: 260px;
|
|
303
304
|
height: 30px;
|
|
304
|
-
padding-right:
|
|
305
|
-
padding-left:
|
|
305
|
+
padding-right: 11px;
|
|
306
|
+
padding-left: 17px;
|
|
306
307
|
border: 0;
|
|
307
308
|
text-align: right;
|
|
308
309
|
direction: rtl;
|
|
309
310
|
text-overflow: ellipsis;
|
|
310
311
|
font-size: 12px;
|
|
311
|
-
background-position:
|
|
312
|
+
background-position: 5px center;
|
|
312
313
|
|
|
313
314
|
&.nonDefault {
|
|
314
315
|
font-weight: bold;
|
|
@@ -318,7 +319,7 @@ select {
|
|
|
318
319
|
background: var(--color4xxBackground);
|
|
319
320
|
}
|
|
320
321
|
&:disabled {
|
|
321
|
-
padding-right:
|
|
322
|
+
padding-right: 10px;
|
|
322
323
|
appearance: none;
|
|
323
324
|
background: transparent;
|
|
324
325
|
cursor: default;
|
|
@@ -367,8 +368,8 @@ select {
|
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
> svg {
|
|
370
|
-
width:
|
|
371
|
-
height:
|
|
371
|
+
width: 20px;
|
|
372
|
+
height: 20px;
|
|
372
373
|
stroke-width: 2.5px;
|
|
373
374
|
border-radius: 50%;
|
|
374
375
|
background: var(--colorSecondaryButtonBackground);
|
|
@@ -376,7 +377,7 @@ select {
|
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
.ProxyToggler {
|
|
379
|
-
padding: 1px
|
|
380
|
+
padding: 1px 4px;
|
|
380
381
|
background: var(--colorSecondaryButtonBackground);
|
|
381
382
|
border-radius: var(--radiusSmall);
|
|
382
383
|
|
|
@@ -415,8 +416,8 @@ select {
|
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
> svg {
|
|
418
|
-
width:
|
|
419
|
-
height:
|
|
419
|
+
width: 18px;
|
|
420
|
+
height: 18px;
|
|
420
421
|
stroke-width: 2px;
|
|
421
422
|
border-radius: var(--radiusSmall);
|
|
422
423
|
}
|
|
@@ -444,7 +445,7 @@ select {
|
|
|
444
445
|
}
|
|
445
446
|
|
|
446
447
|
> span {
|
|
447
|
-
padding: 4px;
|
|
448
|
+
padding: 5px 4px;
|
|
448
449
|
font-size: 10px;
|
|
449
450
|
font-weight: bold;
|
|
450
451
|
color: var(--colorSecondaryAction);
|
package/src/Dashboard.js
CHANGED
|
@@ -58,6 +58,7 @@ const CSS = {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const r = createElement
|
|
61
|
+
const s = createSvgElement
|
|
61
62
|
const mockaton = new Commander(window.location.origin)
|
|
62
63
|
|
|
63
64
|
const PROGRESS_BAR_DELAY = 180
|
|
@@ -519,16 +520,15 @@ function onError(error) {
|
|
|
519
520
|
|
|
520
521
|
function TimerIcon() {
|
|
521
522
|
return (
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
s('svg', { viewBox: '0 0 24 24' },
|
|
524
|
+
s('path', { d: 'm11 5.6 0.14 7.2 6 3.7' })))
|
|
524
525
|
}
|
|
525
526
|
|
|
526
527
|
function CloudIcon() {
|
|
527
528
|
return (
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
))
|
|
529
|
+
s('svg', { viewBox: '0 0 24 24' },
|
|
530
|
+
s('path', { d: 'm6.1 8.9c0.98-2.3 3.3-3.9 6-3.9 3.3-2e-7 6 2.5 6.4 5.7 0.018 0.15 0.024 0.18 0.026 0.23 0.0016 0.037 8.2e-4 0.084 0.098 0.14 0.097 0.054 0.29 0.05 0.48 0.05 2.2 0 4 1.8 4 4s-1.8 4-4 4c-4-0.038-9-0.038-13-0.018-2.8 0-5-2.2-5-5-2.2e-7 -2.8 2.2-5 5-5 2.8 2e-7 5 2.2 5 5' }),
|
|
531
|
+
s('path', { d: 'm6.1 9.1c2.8 0 5 2.3 5 5' })))
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
|
|
@@ -568,16 +568,12 @@ function cssClass(...args) {
|
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
|
|
571
|
-
// These are simplified React-compatible implementations
|
|
572
|
-
// IOW, for switching to React, remove the `createRoot`, `createElement`, `useRef`
|
|
571
|
+
// These are simplified React-compatible implementations
|
|
573
572
|
|
|
574
573
|
function createElement(elem, props = null, ...children) {
|
|
575
574
|
if (typeof elem === 'function')
|
|
576
575
|
return elem(props)
|
|
577
576
|
|
|
578
|
-
if (['svg', 'path'].includes(elem)) // Incomplete list
|
|
579
|
-
return createSvgElement(elem, props, children)
|
|
580
|
-
|
|
581
577
|
const node = document.createElement(elem)
|
|
582
578
|
if (props)
|
|
583
579
|
for (const [key, value] of Object.entries(props))
|
package/src/MockBroker.js
CHANGED
|
@@ -3,12 +3,10 @@ import { DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// MockBroker is a state for a particular route. It knows the available mock files
|
|
6
|
-
// that can be served for the route, the currently selected file, and
|
|
6
|
+
// that can be served for the route, the currently selected file, and if it’s delayed.
|
|
7
7
|
export class MockBroker {
|
|
8
|
-
#urlRegex
|
|
9
8
|
constructor(file) {
|
|
10
|
-
|
|
11
|
-
this.#urlRegex = new RegExp('^' + disregardVariables(removeQueryStringAndFragment(urlMask)) + '/*$')
|
|
9
|
+
this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
|
|
12
10
|
this.mocks = []
|
|
13
11
|
this.currentMock = {
|
|
14
12
|
file: '',
|
|
@@ -17,20 +15,44 @@ export class MockBroker {
|
|
|
17
15
|
this.register(file)
|
|
18
16
|
}
|
|
19
17
|
|
|
18
|
+
get file() { return this.currentMock.file }
|
|
19
|
+
get status() { return parseFilename(this.file).status }
|
|
20
|
+
get delayed() { return this.currentMock.delayed }
|
|
21
|
+
get proxied() { return !this.currentMock.file }
|
|
22
|
+
get temp500IsSelected() { return this.#isTemp500(this.file) }
|
|
23
|
+
|
|
24
|
+
hasMock(file) { return this.mocks.includes(file) }
|
|
25
|
+
|
|
20
26
|
register(file) {
|
|
21
27
|
if (parseFilename(file).status === 500) {
|
|
22
|
-
this.#deleteTemp500()
|
|
23
28
|
if (this.temp500IsSelected)
|
|
24
29
|
this.updateFile(file)
|
|
30
|
+
this.#deleteTemp500()
|
|
25
31
|
}
|
|
26
32
|
this.mocks.push(file)
|
|
27
33
|
this.#sortMocks()
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
#deleteTemp500() {
|
|
31
|
-
|
|
36
|
+
#deleteTemp500() { this.mocks = this.mocks.filter(file => !this.#isTemp500(file)) }
|
|
37
|
+
|
|
38
|
+
#isTemp500(file) { return includesComment(file, DEFAULT_500_COMMENT) }
|
|
39
|
+
|
|
40
|
+
#sortMocks() {
|
|
41
|
+
this.mocks.sort()
|
|
42
|
+
const defaults = this.mocks.filter(file => includesComment(file, DEFAULT_MOCK_COMMENT))
|
|
43
|
+
const temp500 = this.mocks.filter(file => includesComment(file, DEFAULT_500_COMMENT))
|
|
44
|
+
this.mocks = [
|
|
45
|
+
...defaults,
|
|
46
|
+
...this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file)),
|
|
47
|
+
...temp500
|
|
48
|
+
]
|
|
32
49
|
}
|
|
33
50
|
|
|
51
|
+
ensureItHas500() {
|
|
52
|
+
if (!this.#has500())
|
|
53
|
+
this.#registerTemp500()
|
|
54
|
+
}
|
|
55
|
+
#has500() { return this.mocks.some(mock => parseFilename(mock).status === 500) }
|
|
34
56
|
#registerTemp500() {
|
|
35
57
|
const { urlMask, method } = parseFilename(this.mocks[0])
|
|
36
58
|
const file = urlMask.replace(/^\//, '') // Removes leading slash
|
|
@@ -46,44 +68,18 @@ export class MockBroker {
|
|
|
46
68
|
return isEmpty
|
|
47
69
|
}
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// without it, the following regex would match both of these URLs:
|
|
52
|
-
// api/foo/[route_id] => api/foo/.* (wrong match because it’s too greedy)
|
|
53
|
-
// api/foo/[route_id]/suffix => api/foo/.*/suffix
|
|
54
|
-
// By the same token, the regex handles many trailing
|
|
55
|
-
// slashes. For instance, for routing api/foo/[id]?qs…
|
|
56
|
-
urlMaskMatches(url) {
|
|
57
|
-
return this.#urlRegex.test(removeQueryStringAndFragment(decodeURIComponent(url)) + '/')
|
|
71
|
+
selectDefaultFile() {
|
|
72
|
+
this.updateFile(this.mocks[0])
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
get proxied() { return !this.currentMock.file }
|
|
63
|
-
get status() { return parseFilename(this.file).status }
|
|
64
|
-
get temp500IsSelected() { return this.#isTemp500(this.file) }
|
|
65
|
-
|
|
66
|
-
#isTemp500(file) { return includesComment(file, DEFAULT_500_COMMENT) }
|
|
67
|
-
|
|
68
|
-
#sortMocks() {
|
|
69
|
-
this.mocks.sort()
|
|
70
|
-
const defaults = this.mocks.filter(file => includesComment(file, DEFAULT_MOCK_COMMENT))
|
|
71
|
-
const temp500 = this.mocks.filter(file => includesComment(file, DEFAULT_500_COMMENT))
|
|
72
|
-
this.mocks = [
|
|
73
|
-
...defaults,
|
|
74
|
-
...this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file)),
|
|
75
|
-
...temp500
|
|
76
|
-
]
|
|
75
|
+
updateFile(filename) {
|
|
76
|
+
this.currentMock.file = filename
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
this.
|
|
79
|
+
updateDelayed(delayed) {
|
|
80
|
+
this.currentMock.delayed = delayed
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
hasMock(file) { return this.mocks.includes(file) }
|
|
84
|
-
updateFile(filename) { this.currentMock.file = filename }
|
|
85
|
-
updateDelayed(delayed) { this.currentMock.delayed = delayed }
|
|
86
|
-
|
|
87
83
|
updateProxied(proxied) {
|
|
88
84
|
if (proxied)
|
|
89
85
|
this.updateFile('')
|
|
@@ -105,22 +101,43 @@ export class MockBroker {
|
|
|
105
101
|
comments.push(...extractComments(file))
|
|
106
102
|
return comments
|
|
107
103
|
}
|
|
104
|
+
}
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class UrlMatcher {
|
|
109
|
+
#urlRegex
|
|
110
|
+
constructor(file) {
|
|
111
|
+
this.#urlRegex = this.#buildUrlRegex(file)
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
|
|
114
|
+
#buildUrlRegex(file) {
|
|
115
|
+
let { urlMask } = parseFilename(file)
|
|
116
|
+
urlMask = this.#removeQueryStringAndFragment(urlMask)
|
|
117
|
+
urlMask = this.#disregardVariables(urlMask)
|
|
118
|
+
return new RegExp('^' + urlMask + '/*$')
|
|
115
119
|
}
|
|
116
|
-
}
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
121
|
+
#removeQueryStringAndFragment(str) {
|
|
122
|
+
return str.replace(/[?#].*/, '')
|
|
123
|
+
}
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
#disregardVariables(str) { // Stars out all parts that are in square brackets
|
|
126
|
+
return str.replace(/\[.*?]/g, '[^/]*')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Appending a '/' so URLs ending with variables don't match
|
|
130
|
+
// URLs that have a path after that variable. For example,
|
|
131
|
+
// without it, the following regex would match both of these URLs:
|
|
132
|
+
// api/foo/[route_id] => api/foo/.* (wrong match because it’s too greedy)
|
|
133
|
+
// api/foo/[route_id]/suffix => api/foo/.*/suffix
|
|
134
|
+
// By the same token, the regex handles many trailing
|
|
135
|
+
// slashes. For instance, for routing api/foo/[id]?qs…
|
|
136
|
+
urlMaskMatches = (url) => {
|
|
137
|
+
let u = decodeURIComponent(url)
|
|
138
|
+
u = this.#removeQueryStringAndFragment(u)
|
|
139
|
+
u += '/'
|
|
140
|
+
return this.#urlRegex.test(u)
|
|
141
|
+
}
|
|
125
142
|
}
|
|
126
143
|
|
package/src/MockDispatcher.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function dispatchMock(req, response) {
|
|
|
34
34
|
: await applyPlugins(join(config.mocksDir, broker.file), req, response)
|
|
35
35
|
|
|
36
36
|
response.setHeader('Content-Type', mime)
|
|
37
|
-
setTimeout(() => response.end(body),
|
|
37
|
+
setTimeout(() => response.end(body), config.delay * broker.delayed)
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
40
|
if (error instanceof BodyReaderError)
|
package/src/Mockaton.js
CHANGED
|
@@ -38,14 +38,7 @@ async function onRequest(req, response) {
|
|
|
38
38
|
response.setHeader('Server', 'Mockaton')
|
|
39
39
|
|
|
40
40
|
if (config.corsAllowed)
|
|
41
|
-
setCorsHeaders(req, response,
|
|
42
|
-
origins: config.corsOrigins,
|
|
43
|
-
headers: config.corsHeaders,
|
|
44
|
-
methods: config.corsMethods,
|
|
45
|
-
maxAge: config.corsMaxAge,
|
|
46
|
-
credentials: config.corsCredentials,
|
|
47
|
-
exposedHeaders: config.corsExposedHeaders
|
|
48
|
-
})
|
|
41
|
+
setCorsHeaders(req, response, config)
|
|
49
42
|
|
|
50
43
|
const { url, method } = req
|
|
51
44
|
|
package/src/utils/http-cors.js
CHANGED
|
@@ -40,26 +40,26 @@ export function isPreflight(req) {
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
export function setCorsHeaders(req, response, {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
corsOrigins = [],
|
|
44
|
+
corsMethods = [],
|
|
45
|
+
corsHeaders = [],
|
|
46
|
+
corsExposedHeaders = [],
|
|
47
|
+
corsCredentials = false,
|
|
48
|
+
corsMaxAge = 0
|
|
49
49
|
}) {
|
|
50
50
|
const reqOrigin = req.headers[CH.Origin]
|
|
51
|
-
const hasWildcard =
|
|
52
|
-
if (!reqOrigin || (!hasWildcard && !
|
|
51
|
+
const hasWildcard = corsOrigins.some(ao => ao === '*')
|
|
52
|
+
if (!reqOrigin || (!hasWildcard && !corsOrigins.includes(reqOrigin)))
|
|
53
53
|
return
|
|
54
54
|
response.setHeader(CH.AccessControlAllowOrigin, reqOrigin) // Never '*', so no need to `Vary` it
|
|
55
55
|
|
|
56
|
-
if (
|
|
56
|
+
if (corsCredentials)
|
|
57
57
|
response.setHeader(CH.AccessControlAllowCredentials, 'true')
|
|
58
58
|
|
|
59
59
|
if (req.headers[CH.AccessControlRequestMethod])
|
|
60
|
-
setPreflightSpecificHeaders(req, response,
|
|
61
|
-
else if (
|
|
62
|
-
response.setHeader(CH.AccessControlExposeHeaders,
|
|
60
|
+
setPreflightSpecificHeaders(req, response, corsMethods, corsHeaders, corsMaxAge)
|
|
61
|
+
else if (corsExposedHeaders.length)
|
|
62
|
+
response.setHeader(CH.AccessControlExposeHeaders, corsExposedHeaders.join(','))
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function setPreflightSpecificHeaders(req, response, methods, headers, maxAge) {
|
|
@@ -14,10 +14,10 @@ const AllowedDotCom = 'http://allowed.com'
|
|
|
14
14
|
const NotAllowedDotCom = 'http://not-allowed.com'
|
|
15
15
|
|
|
16
16
|
await describe('CORS', async () => {
|
|
17
|
-
let
|
|
17
|
+
let corsConfig = {}
|
|
18
18
|
|
|
19
19
|
const server = createServer((req, response) => {
|
|
20
|
-
setCorsHeaders(req, response,
|
|
20
|
+
setCorsHeaders(req, response, corsConfig)
|
|
21
21
|
if (isPreflight(req)) {
|
|
22
22
|
response.statusCode = 204
|
|
23
23
|
response.end()
|
|
@@ -82,9 +82,9 @@ await describe('CORS', async () => {
|
|
|
82
82
|
|
|
83
83
|
await describe('Preflight Response Headers', async () => {
|
|
84
84
|
await it('no origins allowed', async () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
corsConfig = {
|
|
86
|
+
corsOrigins: [],
|
|
87
|
+
corsMethods: ['GET']
|
|
88
88
|
}
|
|
89
89
|
const p = await preflight({
|
|
90
90
|
[CH.Origin]: FooDotCom,
|
|
@@ -98,9 +98,9 @@ await describe('CORS', async () => {
|
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
await it('not in allowed origins', async () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
corsConfig = {
|
|
102
|
+
corsOrigins: [AllowedDotCom],
|
|
103
|
+
corsMethods: ['GET']
|
|
104
104
|
}
|
|
105
105
|
const p = await preflight({
|
|
106
106
|
[CH.Origin]: NotAllowedDotCom,
|
|
@@ -113,9 +113,9 @@ await describe('CORS', async () => {
|
|
|
113
113
|
})
|
|
114
114
|
|
|
115
115
|
await it('origin and method match', async () => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
corsConfig = {
|
|
117
|
+
corsOrigins: [AllowedDotCom],
|
|
118
|
+
corsMethods: ['GET']
|
|
119
119
|
}
|
|
120
120
|
const p = await preflight({
|
|
121
121
|
[CH.Origin]: AllowedDotCom,
|
|
@@ -128,9 +128,9 @@ await describe('CORS', async () => {
|
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
await it('origin matches from multiple', async () => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
corsConfig = {
|
|
132
|
+
corsOrigins: [AllowedDotCom, FooDotCom],
|
|
133
|
+
corsMethods: ['GET']
|
|
134
134
|
}
|
|
135
135
|
const p = await preflight({
|
|
136
136
|
[CH.Origin]: AllowedDotCom,
|
|
@@ -143,9 +143,9 @@ await describe('CORS', async () => {
|
|
|
143
143
|
})
|
|
144
144
|
|
|
145
145
|
await it('wildcard origin', async () => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
corsConfig = {
|
|
147
|
+
corsOrigins: ['*'],
|
|
148
|
+
corsMethods: ['GET']
|
|
149
149
|
}
|
|
150
150
|
const p = await preflight({
|
|
151
151
|
[CH.Origin]: FooDotCom,
|
|
@@ -158,10 +158,10 @@ await describe('CORS', async () => {
|
|
|
158
158
|
})
|
|
159
159
|
|
|
160
160
|
await it(`wildcard and credentials`, async () => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
corsConfig = {
|
|
162
|
+
corsOrigins: ['*'],
|
|
163
|
+
corsMethods: ['GET'],
|
|
164
|
+
corsCredentials: true
|
|
165
165
|
}
|
|
166
166
|
const p = await preflight({
|
|
167
167
|
[CH.Origin]: FooDotCom,
|
|
@@ -174,11 +174,11 @@ await describe('CORS', async () => {
|
|
|
174
174
|
})
|
|
175
175
|
|
|
176
176
|
await it(`wildcard, credentials, and headers`, async () => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
corsConfig = {
|
|
178
|
+
corsOrigins: ['*'],
|
|
179
|
+
corsMethods: ['GET'],
|
|
180
|
+
corsCredentials: true,
|
|
181
|
+
corsHeaders: ['content-type', 'my-header']
|
|
182
182
|
}
|
|
183
183
|
const p = await preflight({
|
|
184
184
|
[CH.Origin]: FooDotCom,
|
|
@@ -193,9 +193,9 @@ await describe('CORS', async () => {
|
|
|
193
193
|
|
|
194
194
|
await describe('Non-Preflight (Actual Response) Headers', async () => {
|
|
195
195
|
await it('no origins allowed', async () => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
corsConfig = {
|
|
197
|
+
corsOrigins: [],
|
|
198
|
+
corsMethods: ['GET']
|
|
199
199
|
}
|
|
200
200
|
const p = await request({
|
|
201
201
|
[CH.Origin]: NotAllowedDotCom
|
|
@@ -207,11 +207,11 @@ await describe('CORS', async () => {
|
|
|
207
207
|
})
|
|
208
208
|
|
|
209
209
|
await it('origin allowed', async () => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
corsConfig = {
|
|
211
|
+
corsOrigins: [AllowedDotCom],
|
|
212
|
+
corsMethods: ['GET'],
|
|
213
|
+
corsCredentials: true,
|
|
214
|
+
corsExposedHeaders: ['x-h1', 'x-h2']
|
|
215
215
|
}
|
|
216
216
|
const p = await request({
|
|
217
217
|
[CH.Origin]: AllowedDotCom
|