mockaton 8.11.2 → 8.11.3
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 +19 -22
- 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.3",
|
|
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,6 +1,6 @@
|
|
|
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:
|
|
3
|
+
--radius: 28px;
|
|
4
4
|
--radiusSmall: 2px;
|
|
5
5
|
}
|
|
6
6
|
|
|
@@ -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: 28px;
|
|
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;
|
|
@@ -182,10 +183,6 @@ select {
|
|
|
182
183
|
position: relative;
|
|
183
184
|
width: 210px;
|
|
184
185
|
|
|
185
|
-
input[type=url] {
|
|
186
|
-
padding: 0 6px;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
186
|
.SaveProxiedCheckbox {
|
|
190
187
|
position: absolute;
|
|
191
188
|
top: 0;
|
|
@@ -301,14 +298,14 @@ select {
|
|
|
301
298
|
.MockSelector {
|
|
302
299
|
width: 260px;
|
|
303
300
|
height: 30px;
|
|
304
|
-
padding-right:
|
|
305
|
-
padding-left:
|
|
301
|
+
padding-right: 11px;
|
|
302
|
+
padding-left: 17px;
|
|
306
303
|
border: 0;
|
|
307
304
|
text-align: right;
|
|
308
305
|
direction: rtl;
|
|
309
306
|
text-overflow: ellipsis;
|
|
310
307
|
font-size: 12px;
|
|
311
|
-
background-position:
|
|
308
|
+
background-position: 5px center;
|
|
312
309
|
|
|
313
310
|
&.nonDefault {
|
|
314
311
|
font-weight: bold;
|
|
@@ -318,7 +315,7 @@ select {
|
|
|
318
315
|
background: var(--color4xxBackground);
|
|
319
316
|
}
|
|
320
317
|
&:disabled {
|
|
321
|
-
padding-right:
|
|
318
|
+
padding-right: 10px;
|
|
322
319
|
appearance: none;
|
|
323
320
|
background: transparent;
|
|
324
321
|
cursor: default;
|
|
@@ -367,8 +364,8 @@ select {
|
|
|
367
364
|
}
|
|
368
365
|
|
|
369
366
|
> svg {
|
|
370
|
-
width:
|
|
371
|
-
height:
|
|
367
|
+
width: 20px;
|
|
368
|
+
height: 20px;
|
|
372
369
|
stroke-width: 2.5px;
|
|
373
370
|
border-radius: 50%;
|
|
374
371
|
background: var(--colorSecondaryButtonBackground);
|
|
@@ -376,7 +373,7 @@ select {
|
|
|
376
373
|
}
|
|
377
374
|
|
|
378
375
|
.ProxyToggler {
|
|
379
|
-
padding: 1px
|
|
376
|
+
padding: 1px 4px;
|
|
380
377
|
background: var(--colorSecondaryButtonBackground);
|
|
381
378
|
border-radius: var(--radiusSmall);
|
|
382
379
|
|
|
@@ -415,8 +412,8 @@ select {
|
|
|
415
412
|
}
|
|
416
413
|
|
|
417
414
|
> svg {
|
|
418
|
-
width:
|
|
419
|
-
height:
|
|
415
|
+
width: 18px;
|
|
416
|
+
height: 18px;
|
|
420
417
|
stroke-width: 2px;
|
|
421
418
|
border-radius: var(--radiusSmall);
|
|
422
419
|
}
|
|
@@ -444,7 +441,7 @@ select {
|
|
|
444
441
|
}
|
|
445
442
|
|
|
446
443
|
> span {
|
|
447
|
-
padding: 4px;
|
|
444
|
+
padding: 5px 4px;
|
|
448
445
|
font-size: 10px;
|
|
449
446
|
font-weight: bold;
|
|
450
447
|
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
|