mockaton 7.2.0 → 7.3.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-dashboard-dark.png +0 -0
- package/README-dashboard-light.png +0 -0
- package/README.md +39 -32
- package/package.json +1 -1
- package/sample-mocks/api/user/edit-name.PATCH.422.json +3 -0
- package/src/Api.js +17 -2
- package/src/ApiConstants.js +2 -1
- package/src/Commander.js +11 -0
- package/src/Config.js +0 -3
- package/src/Dashboard.css +81 -50
- package/src/Dashboard.js +51 -49
- package/src/Mockaton.test.js +7 -8
- package/src/mockaton-logo.svg +9 -1
- package/src/utils/http-cors.js +13 -14
- package/src/utils/http-cors.test.js +60 -60
- package/src/utils/validate.js +1 -1
- package/README-dashboard.png +0 -0
|
Binary file
|
|
Binary file
|
package/README.md
CHANGED
|
@@ -12,21 +12,41 @@ my-mocks-dir/api/user/[user-id].GET.200.json
|
|
|
12
12
|
[This browser extension](https://github.com/ericfortis/devtools-ext-tar-http-requests)
|
|
13
13
|
can be used for downloading a TAR of your XHR requests following that convention.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
## Getting Started Demo
|
|
17
|
+
Checkout this repo and run `npm run demo`, which will open the following
|
|
18
|
+
dashboard. Then, explore the [sample-mocks/](./sample-mocks) directory.
|
|
19
|
+
|
|
20
|
+
<picture>
|
|
21
|
+
<source media="(prefers-color-scheme: light)" srcset="./README-dashboard-light.png">
|
|
22
|
+
<source media="(prefers-color-scheme: dark)" srcset="./README-dashboard-dark.png">
|
|
23
|
+
<img alt="Mockaton Dashboard Demo" src="./README-dashboard-light.png" style="max-width: 860px">
|
|
24
|
+
</picture>
|
|
25
|
+
|
|
26
|
+
Then, pick a mock variant from the Mock dropdown (we’ll discuss
|
|
27
|
+
them later). At its right, note the _Delay_ toggler, and the button
|
|
28
|
+
for sending _500 - Internal Server Error_ on that endpoint.
|
|
29
|
+
|
|
30
|
+
Then edit a mock file. You don’t need to restart Mockaton for that, the
|
|
31
|
+
_Reset_ button is for registering newly added, removed, or renamed mocks.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Use Cases
|
|
35
|
+
- Test empty responses
|
|
36
|
+
- Test spinners by delaying responses
|
|
37
|
+
- Test errors such as _Bad Request_ and _Internal Server Error_
|
|
38
|
+
- Trigger notifications and alerts
|
|
39
|
+
- Prototyping before the backend API is developed
|
|
40
|
+
- Setting up tests
|
|
41
|
+
- As API documentation
|
|
42
|
+
- If you commit the mocks in the repo, when bisecting a bug, you don’t
|
|
43
|
+
have to sync the frontend with many backend repos
|
|
44
|
+
- Similarly, I can check out long-lived branches that have old API contracts
|
|
45
|
+
|
|
46
|
+
## Motivation
|
|
16
47
|
- Avoids having to spin up and maintain hefty or complex backends when developing UIs.
|
|
17
48
|
- For a deterministic and comprehensive backend state. For example, having all the possible
|
|
18
|
-
state variants of a
|
|
19
|
-
assorted responses are not easy to trigger from the backend.
|
|
20
|
-
- Testing empty responses.
|
|
21
|
-
- Testing spinners by delaying responses.
|
|
22
|
-
- Testing errors such as _Bad Request_ and _Internal Server Error_.
|
|
23
|
-
- Triggering notifications and alerts.
|
|
24
|
-
- Prototyping before the backend API is developed.
|
|
25
|
-
- Setting up tests.
|
|
26
|
-
- If you commit the mocks in the repo, when bisecting a bug, you don’t
|
|
27
|
-
have to sync the frontend with many backend repos.
|
|
28
|
-
- Similarly, I can check out long-lived branches that have old API contracts.
|
|
29
|
-
- As API documentation.
|
|
49
|
+
state variants of a collection helps for spotting inadvertent bugs.
|
|
30
50
|
|
|
31
51
|
## Alternatives
|
|
32
52
|
- Chrome DevTools allows for [overriding responses](https://developer.chrome.com/docs/devtools/overrides)
|
|
@@ -37,19 +57,6 @@ can be used for downloading a TAR of your XHR requests following that convention
|
|
|
37
57
|
- Syncing the mocks, but the browser extension mentioned above helps.
|
|
38
58
|
|
|
39
59
|
|
|
40
|
-
## Getting Started
|
|
41
|
-
The best way to learn _Mockaton_ is by checking out this repo and
|
|
42
|
-
exploring its [sample-mocks/](./sample-mocks) directory. Then, run
|
|
43
|
-
[`./_usage_example.js`](./_usage_example.js) and you’ll see the dashboard.
|
|
44
|
-
|
|
45
|
-
You can select mock files without resetting Mockaton. The _Reset_
|
|
46
|
-
button is for when you add, remove, or rename a mock file.
|
|
47
|
-
|
|
48
|
-
The dropdown lets you pick a mock variant, details in the next section. Next to it is a
|
|
49
|
-
_Delay_ toggler, and a button for sending _500 - Internal Server Error_ on that endpoint.
|
|
50
|
-
|
|
51
|
-
<img src="./README-dashboard.png" style="max-width:820px"/>
|
|
52
|
-
|
|
53
60
|
## Basic Usage
|
|
54
61
|
```
|
|
55
62
|
npm install mockaton
|
|
@@ -89,7 +96,7 @@ interface Config {
|
|
|
89
96
|
extraHeaders?: []
|
|
90
97
|
|
|
91
98
|
corsAllowed?: boolean, // Defaults to false
|
|
92
|
-
//
|
|
99
|
+
// cors* customization options are explained below
|
|
93
100
|
|
|
94
101
|
onReady?: (dashboardUrl: string) => void // Defaults to trying to open macOS and Win default browser.
|
|
95
102
|
}
|
|
@@ -271,8 +278,8 @@ Config.corsOrigins = ['*']
|
|
|
271
278
|
Config.corsMethods = ['GET', 'PUT', 'DELETE', 'POST', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']
|
|
272
279
|
Config.corsHeaders = ['content-type']
|
|
273
280
|
Config.corsCredentials = true
|
|
274
|
-
Config.corsMaxAge = 0
|
|
275
|
-
Config.corsExposedHeaders = []
|
|
281
|
+
Config.corsMaxAge = 0 // seconds to cache the preflight req
|
|
282
|
+
Config.corsExposedHeaders = [] // headers you need to access in client-side JS
|
|
276
283
|
```
|
|
277
284
|
|
|
278
285
|
## `Config.onReady`
|
|
@@ -342,9 +349,9 @@ await mockaton.setProxyFallback('http://example.com')
|
|
|
342
349
|
Pass an empty string to disable it.
|
|
343
350
|
|
|
344
351
|
### Reset
|
|
345
|
-
Re-initialize the collection. So if you added or removed mocks they
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
Re-initialize the collection. So if you added or removed mocks they will
|
|
353
|
+
be considered. The selected mocks, cookies, and delays go back to default,
|
|
354
|
+
but `Config.proxyFallback` and `Config.corsAllowed` are not affected.
|
|
348
355
|
```js
|
|
349
356
|
await mockaton.reset()
|
|
350
357
|
```
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -18,10 +18,12 @@ export const apiGetRequests = new Map([
|
|
|
18
18
|
['/Dashboard.js', serveDashboardAsset],
|
|
19
19
|
['/Dashboard.css', serveDashboardAsset],
|
|
20
20
|
['/ApiConstants.js', serveDashboardAsset],
|
|
21
|
+
['/Commander.js', serveDashboardAsset],
|
|
21
22
|
['/mockaton-logo.svg', serveDashboardAsset],
|
|
22
23
|
[API.mocks, listMockBrokers],
|
|
23
24
|
[API.cookies, listCookies],
|
|
24
|
-
[API.comments, listComments]
|
|
25
|
+
[API.comments, listComments],
|
|
26
|
+
[API.cors, getIsCorsAllowed]
|
|
25
27
|
])
|
|
26
28
|
|
|
27
29
|
export const apiPatchRequests = new Map([
|
|
@@ -30,7 +32,8 @@ export const apiPatchRequests = new Map([
|
|
|
30
32
|
[API.reset, reinitialize],
|
|
31
33
|
[API.cookies, selectCookie],
|
|
32
34
|
[API.fallback, updateProxyFallback],
|
|
33
|
-
[API.bulkSelect, bulkUpdateBrokersByCommentTag]
|
|
35
|
+
[API.bulkSelect, bulkUpdateBrokersByCommentTag],
|
|
36
|
+
[API.cors, setCorsAllowed]
|
|
34
37
|
])
|
|
35
38
|
|
|
36
39
|
function serveDashboard(_, response) { sendFile(response, join(import.meta.dirname, 'Dashboard.html')) }
|
|
@@ -39,6 +42,7 @@ function serveDashboardAsset(req, response) { sendFile(response, join(import.met
|
|
|
39
42
|
function listCookies(_, response) { sendJSON(response, cookie.list()) }
|
|
40
43
|
function listComments(_, response) { sendJSON(response, mockBrokersCollection.extractAllComments()) }
|
|
41
44
|
function listMockBrokers(_, response) { sendJSON(response, mockBrokersCollection.getAll()) }
|
|
45
|
+
function getIsCorsAllowed(_, response) { sendJSON(response, Config.corsAllowed) }
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
function reinitialize(_, response) {
|
|
@@ -113,3 +117,14 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
|
113
117
|
sendBadRequest(response, error)
|
|
114
118
|
}
|
|
115
119
|
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async function setCorsAllowed(req, response) {
|
|
123
|
+
try {
|
|
124
|
+
Config.corsAllowed = await parseJSON(req)
|
|
125
|
+
sendOK(response)
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
sendBadRequest(response, error)
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/ApiConstants.js
CHANGED
package/src/Commander.js
CHANGED
|
@@ -7,6 +7,10 @@ export class Commander {
|
|
|
7
7
|
this.#addr = addr
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
listMocks() {
|
|
11
|
+
return this.#get(API.mocks)
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
select(file) {
|
|
11
15
|
return this.#patch(API.select, file)
|
|
12
16
|
}
|
|
@@ -41,6 +45,13 @@ export class Commander {
|
|
|
41
45
|
return this.#patch(API.reset)
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
getCorsAllowed() {
|
|
49
|
+
return this.#get(API.cors)
|
|
50
|
+
}
|
|
51
|
+
setCorsAllowed(value) {
|
|
52
|
+
return this.#patch(API.cors, value)
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
|
|
45
56
|
#get(api) {
|
|
46
57
|
return fetch(this.#addr + api)
|
package/src/Config.js
CHANGED
package/src/Dashboard.css
CHANGED
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--
|
|
3
|
-
--colorHover: #dfefff;
|
|
4
|
-
--colorRed: #da0f00;
|
|
5
|
-
--colorLightRed: #ffe4ee;
|
|
6
|
-
--colorLightOrange: #ffedd1;
|
|
7
|
-
--colorLightGrey: rgba(0, 0, 0, 0.02);
|
|
2
|
+
--boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
|
8
3
|
}
|
|
4
|
+
|
|
5
|
+
@media (prefers-color-scheme: light) {
|
|
6
|
+
:root {
|
|
7
|
+
--color4xxBackground: #ffedd1;
|
|
8
|
+
--colorAccent: #0171dd;
|
|
9
|
+
--colorBackground: #fff;
|
|
10
|
+
--colorCodeBackground: #fafafa;
|
|
11
|
+
--colorComboBoxBackground: #fafafa;
|
|
12
|
+
--colorDisabled: #222;
|
|
13
|
+
--colorHover: #dfefff;
|
|
14
|
+
--colorLabel: #777;
|
|
15
|
+
--colorLightRed: #ffe4ee;
|
|
16
|
+
--colorRed: #da0f00;
|
|
17
|
+
--colorSecondaryButtonBackground: #f0f0f0;
|
|
18
|
+
--colorText: #000;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
@media (prefers-color-scheme: dark) {
|
|
22
|
+
:root {
|
|
23
|
+
--color4xxBackground: #403630;
|
|
24
|
+
--colorAccent: #0682fa;
|
|
25
|
+
--colorBackground: #141414;
|
|
26
|
+
--colorCodeBackground: #333;
|
|
27
|
+
--colorComboBoxBackground: #252525;
|
|
28
|
+
--colorDisabled: #aaa;
|
|
29
|
+
--colorHover: #023661;
|
|
30
|
+
--colorLabel: #aaa;
|
|
31
|
+
--colorLightRed: #ffe4ee;
|
|
32
|
+
--colorRed: #f41606;
|
|
33
|
+
--colorSecondaryButtonBackground: #444;
|
|
34
|
+
--colorText: #fff;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
9
38
|
html, body {
|
|
10
39
|
margin: 0;
|
|
11
40
|
font-size: 12px;
|
|
12
41
|
}
|
|
13
42
|
body {
|
|
14
43
|
padding: 16px;
|
|
44
|
+
background: var(--colorBackground);
|
|
45
|
+
color: var(--colorText);
|
|
15
46
|
}
|
|
16
47
|
* {
|
|
17
48
|
padding: 0;
|
|
@@ -21,19 +52,24 @@ body {
|
|
|
21
52
|
font-size: 100%;
|
|
22
53
|
}
|
|
23
54
|
|
|
55
|
+
select {
|
|
56
|
+
background: var(--colorComboBoxBackground);
|
|
57
|
+
color: var(--colorText);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
outline: 0;
|
|
24
61
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
62
|
+
&:enabled {
|
|
63
|
+
box-shadow: var(--boxShadow1);
|
|
64
|
+
}
|
|
65
|
+
&:enabled:hover {
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
background: var(--colorHover);
|
|
68
|
+
}
|
|
69
|
+
&:disabled {
|
|
70
|
+
background: transparent;
|
|
71
|
+
cursor: not-allowed;
|
|
72
|
+
color: var(--colorDisabled);
|
|
37
73
|
}
|
|
38
74
|
}
|
|
39
75
|
|
|
@@ -61,26 +97,25 @@ menu {
|
|
|
61
97
|
display: flex;
|
|
62
98
|
align-items: flex-end;
|
|
63
99
|
margin-bottom: 12px;
|
|
64
|
-
gap:
|
|
100
|
+
gap: 16px;
|
|
65
101
|
|
|
66
102
|
img {
|
|
67
|
-
|
|
103
|
+
width: 140px;
|
|
104
|
+
margin-right: 24px;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
107
|
label {
|
|
71
108
|
span {
|
|
72
109
|
display: block;
|
|
73
|
-
|
|
110
|
+
padding-left: 7px;
|
|
111
|
+
color: var(--colorLabel);
|
|
74
112
|
font-size: 11px;
|
|
75
113
|
}
|
|
76
114
|
|
|
77
115
|
select {
|
|
78
|
-
width:
|
|
79
|
-
padding:
|
|
80
|
-
|
|
81
|
-
margin-top: 1px;
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
border-radius: 4px;
|
|
116
|
+
width: 150px;
|
|
117
|
+
padding: 4px;
|
|
118
|
+
margin-top: 2px;
|
|
84
119
|
font-size: 11px;
|
|
85
120
|
}
|
|
86
121
|
}
|
|
@@ -98,6 +133,13 @@ menu {
|
|
|
98
133
|
color: white;
|
|
99
134
|
}
|
|
100
135
|
}
|
|
136
|
+
|
|
137
|
+
.CorsCheckbox {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
margin-bottom: 4px;
|
|
141
|
+
gap: 4px;
|
|
142
|
+
}
|
|
101
143
|
}
|
|
102
144
|
|
|
103
145
|
.PayloadViewer {
|
|
@@ -118,7 +160,7 @@ menu {
|
|
|
118
160
|
max-height: calc(100vh - 160px);
|
|
119
161
|
padding: 12px;
|
|
120
162
|
margin-top: 6px;
|
|
121
|
-
background: var(--
|
|
163
|
+
background: var(--colorCodeBackground);
|
|
122
164
|
font-family: monospace;
|
|
123
165
|
}
|
|
124
166
|
}
|
|
@@ -147,23 +189,13 @@ menu {
|
|
|
147
189
|
padding: 8px 1px;
|
|
148
190
|
border: 0;
|
|
149
191
|
border-radius: 4px;
|
|
150
|
-
background: #eee;
|
|
151
192
|
text-align: right;
|
|
152
193
|
direction: rtl;
|
|
153
194
|
text-overflow: ellipsis;
|
|
154
195
|
font-size: 12px;
|
|
155
196
|
|
|
156
|
-
&:disabled {
|
|
157
|
-
background: transparent;
|
|
158
|
-
color: #222;
|
|
159
|
-
cursor: not-allowed
|
|
160
|
-
}
|
|
161
|
-
&:enabled:hover {
|
|
162
|
-
cursor: pointer;
|
|
163
|
-
background: var(--colorHover);
|
|
164
|
-
}
|
|
165
197
|
&.status4xx {
|
|
166
|
-
background: var(--
|
|
198
|
+
background: var(--color4xxBackground);
|
|
167
199
|
}
|
|
168
200
|
}
|
|
169
201
|
|
|
@@ -176,23 +208,22 @@ menu {
|
|
|
176
208
|
display: none;
|
|
177
209
|
|
|
178
210
|
&:checked ~ svg {
|
|
179
|
-
|
|
180
|
-
|
|
211
|
+
background: var(--colorAccent);
|
|
212
|
+
fill: white;
|
|
181
213
|
}
|
|
182
214
|
}
|
|
183
215
|
|
|
184
216
|
> svg {
|
|
185
|
-
width:
|
|
186
|
-
height:
|
|
187
|
-
border: 1px solid #000;
|
|
217
|
+
width: 16px;
|
|
218
|
+
height: 16px;
|
|
188
219
|
vertical-align: bottom;
|
|
189
|
-
fill:
|
|
220
|
+
fill: var(--colorText);
|
|
190
221
|
border-radius: 50%;
|
|
191
|
-
background:
|
|
222
|
+
background: var(--colorSecondaryButtonBackground);
|
|
192
223
|
|
|
193
224
|
&:hover {
|
|
194
|
-
background:
|
|
195
|
-
fill:
|
|
225
|
+
background: var(--colorHover);
|
|
226
|
+
fill: var(--colorText);
|
|
196
227
|
}
|
|
197
228
|
}
|
|
198
229
|
}
|
|
@@ -214,9 +245,9 @@ menu {
|
|
|
214
245
|
> span {
|
|
215
246
|
padding: 4px;
|
|
216
247
|
font-size: 10px;
|
|
217
|
-
color:
|
|
248
|
+
color: var(--colorText);
|
|
218
249
|
border-radius: 2px;
|
|
219
|
-
background:
|
|
250
|
+
background: var(--colorSecondaryButtonBackground);
|
|
220
251
|
|
|
221
252
|
&:hover {
|
|
222
253
|
background: var(--colorLightRed);
|
package/src/Dashboard.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { parseFilename } from '
|
|
2
|
-
import {
|
|
1
|
+
import { parseFilename } from '/Filename.js'
|
|
2
|
+
import { Commander } from '/Commander.js'
|
|
3
|
+
import { DEFAULT_500_COMMENT } from '/ApiConstants.js'
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
const Strings = {
|
|
7
|
+
allow_cors: 'Allow CORS',
|
|
6
8
|
bulk_select_by_comment: 'Bulk Select by Comment',
|
|
7
9
|
click_link_to_preview: 'Click a link to preview it',
|
|
8
10
|
cookie: 'Cookie',
|
|
@@ -17,6 +19,7 @@ const Strings = {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const CSS = {
|
|
22
|
+
CorsCheckbox: 'CorsCheckbox',
|
|
20
23
|
DelayToggler: 'DelayToggler',
|
|
21
24
|
InternalServerErrorToggler: 'InternalServerErrorToggler',
|
|
22
25
|
MockSelector: 'MockSelector',
|
|
@@ -32,24 +35,27 @@ const r = createElement
|
|
|
32
35
|
const refPayloadViewer = useRef()
|
|
33
36
|
const refPayloadFile = useRef()
|
|
34
37
|
|
|
38
|
+
const mockaton = new Commander(window.location.origin)
|
|
39
|
+
|
|
35
40
|
function init() {
|
|
36
41
|
Promise.all([
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
mockaton.listMocks(),
|
|
43
|
+
mockaton.listCookies(),
|
|
44
|
+
mockaton.listComments(),
|
|
45
|
+
mockaton.getCorsAllowed()
|
|
46
|
+
].map(api => api.then(response => response.ok && response.json())))
|
|
41
47
|
.then(App)
|
|
42
48
|
.catch(console.error)
|
|
43
49
|
}
|
|
44
50
|
init()
|
|
45
51
|
|
|
46
|
-
function App([brokersByMethod, cookies, comments]) {
|
|
52
|
+
function App([brokersByMethod, cookies, comments, corsAllowed]) {
|
|
47
53
|
empty(document.body)
|
|
48
54
|
createRoot(document.body).render(
|
|
49
|
-
DevPanel(brokersByMethod, cookies, comments))
|
|
55
|
+
DevPanel(brokersByMethod, cookies, comments, corsAllowed))
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
function DevPanel(brokersByMethod, cookies, comments) {
|
|
58
|
+
function DevPanel(brokersByMethod, cookies, comments, corsAllowed) {
|
|
53
59
|
document.title = Strings.title
|
|
54
60
|
return (
|
|
55
61
|
r('div', null,
|
|
@@ -57,6 +63,7 @@ function DevPanel(brokersByMethod, cookies, comments) {
|
|
|
57
63
|
r('img', { src: 'mockaton-logo.svg', width: 160 }),
|
|
58
64
|
r(CookieSelector, { list: cookies }),
|
|
59
65
|
r(BulkSelector, { comments }),
|
|
66
|
+
r(CorsCheckbox, { corsAllowed }),
|
|
60
67
|
r(ResetButton)),
|
|
61
68
|
r('main', null,
|
|
62
69
|
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
@@ -66,19 +73,6 @@ function DevPanel(brokersByMethod, cookies, comments) {
|
|
|
66
73
|
r('pre', { ref: refPayloadViewer }, Strings.click_link_to_preview)))))
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
function ResetButton() {
|
|
71
|
-
return (
|
|
72
|
-
r('button', {
|
|
73
|
-
onClick() {
|
|
74
|
-
fetch(API.reset, { method: 'PATCH' })
|
|
75
|
-
.then(init)
|
|
76
|
-
.catch(console.error)
|
|
77
|
-
}
|
|
78
|
-
}, Strings.reset)
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
76
|
function CookieSelector({ list }) {
|
|
83
77
|
return (
|
|
84
78
|
r('label', null,
|
|
@@ -87,10 +81,7 @@ function CookieSelector({ list }) {
|
|
|
87
81
|
autocomplete: 'off',
|
|
88
82
|
disabled: list.length <= 1,
|
|
89
83
|
onChange() {
|
|
90
|
-
|
|
91
|
-
method: 'PATCH',
|
|
92
|
-
body: JSON.stringify(this.value)
|
|
93
|
-
})
|
|
84
|
+
mockaton.selectCookie(this.value)
|
|
94
85
|
.catch(console.error)
|
|
95
86
|
}
|
|
96
87
|
}, list.map(([key, selected]) =>
|
|
@@ -100,7 +91,6 @@ function CookieSelector({ list }) {
|
|
|
100
91
|
}, key)))))
|
|
101
92
|
}
|
|
102
93
|
|
|
103
|
-
|
|
104
94
|
function BulkSelector({ comments }) {
|
|
105
95
|
return (
|
|
106
96
|
r('label', null,
|
|
@@ -109,10 +99,7 @@ function BulkSelector({ comments }) {
|
|
|
109
99
|
autocomplete: 'off',
|
|
110
100
|
disabled: comments.length <= 1,
|
|
111
101
|
onChange() {
|
|
112
|
-
|
|
113
|
-
method: 'PATCH',
|
|
114
|
-
body: JSON.stringify(this.value)
|
|
115
|
-
})
|
|
102
|
+
mockaton.bulkSelectByComment(this.value)
|
|
116
103
|
.then(init)
|
|
117
104
|
.catch(console.error)
|
|
118
105
|
}
|
|
@@ -122,6 +109,33 @@ function BulkSelector({ comments }) {
|
|
|
122
109
|
}, item)))))
|
|
123
110
|
}
|
|
124
111
|
|
|
112
|
+
function CorsCheckbox({ corsAllowed }) {
|
|
113
|
+
return (
|
|
114
|
+
r('label', { className: CSS.CorsCheckbox },
|
|
115
|
+
r('input', {
|
|
116
|
+
type: 'checkbox',
|
|
117
|
+
checked: corsAllowed,
|
|
118
|
+
onChange(event) {
|
|
119
|
+
mockaton.setCorsAllowed(event.currentTarget.checked)
|
|
120
|
+
.catch(console.error)
|
|
121
|
+
}
|
|
122
|
+
}),
|
|
123
|
+
Strings.allow_cors))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ResetButton() {
|
|
127
|
+
return (
|
|
128
|
+
r('button', {
|
|
129
|
+
onClick() {
|
|
130
|
+
mockaton.reset()
|
|
131
|
+
.then(init)
|
|
132
|
+
.catch(console.error)
|
|
133
|
+
}
|
|
134
|
+
}, Strings.reset)
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
125
139
|
|
|
126
140
|
function SectionByMethod({ method, brokers }) {
|
|
127
141
|
return (
|
|
@@ -188,10 +202,7 @@ function MockSelector({ broker }) {
|
|
|
188
202
|
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
189
203
|
? 'normal'
|
|
190
204
|
: 'bold'
|
|
191
|
-
|
|
192
|
-
method: 'PATCH',
|
|
193
|
-
body: JSON.stringify(this.value)
|
|
194
|
-
})
|
|
205
|
+
mockaton.select(this.value)
|
|
195
206
|
.then(() => {
|
|
196
207
|
this.closest('tr').querySelector('a').click()
|
|
197
208
|
this.closest('tr').querySelector(`.${CSS.InternalServerErrorToggler}>[type=checkbox]`).checked = status === 500
|
|
@@ -220,14 +231,8 @@ function DelayRouteToggler({ broker }) {
|
|
|
220
231
|
checked,
|
|
221
232
|
onChange(event) {
|
|
222
233
|
const { method, urlMask } = parseFilename(this.name)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
body: JSON.stringify({
|
|
226
|
-
[DF.routeMethod]: method,
|
|
227
|
-
[DF.routeUrlMask]: urlMask,
|
|
228
|
-
[DF.delayed]: event.currentTarget.checked
|
|
229
|
-
})
|
|
230
|
-
})
|
|
234
|
+
mockaton.setRouteIsDelayed(method, urlMask, event.currentTarget.checked)
|
|
235
|
+
.catch(console.error)
|
|
231
236
|
}
|
|
232
237
|
}),
|
|
233
238
|
TimerIcon()))
|
|
@@ -254,12 +259,9 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
254
259
|
name,
|
|
255
260
|
checked,
|
|
256
261
|
onChange(event) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
? items.find(f => parseFilename(f).status === 500)
|
|
261
|
-
: items[0])
|
|
262
|
-
})
|
|
262
|
+
mockaton.select(event.currentTarget.checked
|
|
263
|
+
? items.find(f => parseFilename(f).status === 500)
|
|
264
|
+
: items[0])
|
|
263
265
|
.then(init)
|
|
264
266
|
.catch(console.error)
|
|
265
267
|
}
|
package/src/Mockaton.test.js
CHANGED
|
@@ -11,7 +11,7 @@ import { mimeFor } from './utils/mime.js'
|
|
|
11
11
|
import { Mockaton } from './Mockaton.js'
|
|
12
12
|
import { Commander } from './Commander.js'
|
|
13
13
|
import { parseFilename } from './Filename.js'
|
|
14
|
-
import {
|
|
14
|
+
import { CorsHeader } from './utils/http-cors.js'
|
|
15
15
|
import { API, DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
16
16
|
|
|
17
17
|
|
|
@@ -157,7 +157,6 @@ const server = Mockaton({
|
|
|
157
157
|
extraMimes: {
|
|
158
158
|
my_custom_extension: 'my_custom_mime'
|
|
159
159
|
},
|
|
160
|
-
corsAllowed: true,
|
|
161
160
|
corsOrigins: ['http://example.com']
|
|
162
161
|
})
|
|
163
162
|
server.on('listening', runTests)
|
|
@@ -224,11 +223,11 @@ async function runTests() {
|
|
|
224
223
|
await testMockDispatching(...fixtureCustomMime, 'my_custom_mime')
|
|
225
224
|
await testJsFunctionMocks()
|
|
226
225
|
|
|
227
|
-
await testCorsAllowed()
|
|
228
226
|
await testItUpdatesUserRole()
|
|
229
227
|
await testStaticFileServing()
|
|
230
228
|
await testInvalidFilenamesAreIgnored()
|
|
231
229
|
await testEnableFallbackSoRoutesWithoutMocksGetRelayed()
|
|
230
|
+
await testCorsAllowed()
|
|
232
231
|
|
|
233
232
|
server.close()
|
|
234
233
|
}
|
|
@@ -438,19 +437,19 @@ async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
|
|
|
438
437
|
})
|
|
439
438
|
}
|
|
440
439
|
|
|
441
|
-
// TODO make API for changing CORS? so we can automate testing?
|
|
442
440
|
async function testCorsAllowed() {
|
|
443
441
|
await it('cors', async () => {
|
|
442
|
+
await commander.setCorsAllowed(true)
|
|
444
443
|
const res = await request('/does-not-matter', {
|
|
445
444
|
method: 'OPTIONS',
|
|
446
445
|
headers: {
|
|
447
|
-
[
|
|
448
|
-
[
|
|
446
|
+
[CorsHeader.Origin]: 'http://example.com',
|
|
447
|
+
[CorsHeader.AccessControlRequestMethod]: 'GET'
|
|
449
448
|
}
|
|
450
449
|
})
|
|
451
450
|
equal(res.status, 204)
|
|
452
|
-
equal(res.headers.get(
|
|
453
|
-
equal(res.headers.get(
|
|
451
|
+
equal(res.headers.get(CorsHeader.AccessControlAllowOrigin), 'http://example.com')
|
|
452
|
+
equal(res.headers.get(CorsHeader.AccessControlAllowMethods), 'GET')
|
|
454
453
|
})
|
|
455
454
|
}
|
|
456
455
|
|
package/src/mockaton-logo.svg
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<svg version="1.1" viewBox="0 0 570 100" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
-
|
|
3
|
+
<style>
|
|
4
|
+
:root { --color: #000000; }
|
|
5
|
+
@media (prefers-color-scheme: light) { :root { --color: #000000 } }
|
|
6
|
+
@media (prefers-color-scheme: dark) { :root { --color: #fff } }
|
|
7
|
+
path { fill: var(--color) }
|
|
8
|
+
</style>
|
|
9
|
+
<path
|
|
10
|
+
d="m88.6 11.5v82.2c0 3.75-2.37 5.72-7.1 5.72s-7.1-1.97-7.1-5.72v-61.5c-0.0468-1.83-2.74-2.4-3.45-0.355l-19.7 63.1c-0.907 2.86-3.15 4.14-6.51 4.04-1.48-0.0779-2.86-0.463-3.94-1.18-1.18-0.7-1.97-1.77-2.27-3.15-4.54-15.1-9.2-30.3-13.9-45.4-1.77-5.72-3.65-12.8-5.72-19.7-0.741-1.97-3.33-2.17-3.45 0.473v63.6c0 3.75-2.37 5.72-7.1 5.72-4.73 0-7.1-1.97-7.1-5.72v-84.3c0-2.34 1.07-5.37 3.83-6.65 3.06-1.28 6.12-1.65 8.88-1.55 1.28 0 2.46 0.28 3.65 0.596 1.28 0.325 2.46 0.808 3.55 1.48 1.08 0.562 2.17 1.28 3.06 2.27 0.907 0.907 1.58 1.97 2.07 3.15l18.7 64.1c1.89-6.49 13.2-47.7 13.6-49.3 1.68-5.62 3.15-10.8 4.44-15.8 0.473-1.08 1.18-2.17 2.07-3.06 0.877-0.877 1.87-1.68 3.06-2.17 1.08-0.641 2.27-1.18 3.45-1.48 1.28-0.325 3.85-0.403 4.94-0.403 4.75 0.438 12 3.73 12 11zm21.1 18.6c2.07-1.38 4.14-2.56 6.41-3.45 2.27-0.877 4.44-1.38 6.51-1.38h19.7c2.07 0 4.14 0.454 6.41 1.38 2.27 0.907 4.44 2.07 6.45 3.5 2.01 1.43 3.59 3.4 4.67 5.96 1.08 2.46 1.68 5.23 1.68 8.48v34.5c0 6.51-2.17 11.8-6.51 14.8-4.24 3.06-8.48 4.54-12.8 4.54h-19.7c-2.07 0-4.14-0.355-6.41-1.08-2.27-0.71-4.34-1.87-6.41-3.45-4.34-3.06-6.51-7.89-6.51-14.8v-34.5c0-3.25 0.552-6.01 1.68-8.48 1.08-2.46 2.76-4.44 4.85-6.01zm30.4 56.2c3.06 0 5.03-0.848 6.21-2.56 1.28-1.68 1.97-3.45 1.97-5.42v-34.5c0-2.07-0.651-3.75-1.97-5.42-1.28-1.68-3.35-2.56-6.21-2.56h-14.8c-3.06 0-5.13 0.848-6.41 2.56-1.28 1.68-1.87 3.45-1.87 5.42v34.5c0 1.97 0.611 3.75 1.87 5.42 1.28 1.68 3.45 2.56 6.41 2.56zm94.6-7.79c0 6.51-2.17 11.8-6.51 14.8-4.24 3.06-8.48 4.54-12.8 4.54h-20.7c-4.24 0-8.48-1.58-12.8-4.54-1.97-1.68-3.45-3.55-4.44-6.01-0.986-2.46-1.58-5.32-1.58-8.58v-34.5c0-6.51 2.07-11.8 6.01-14.8 2.07-1.48 4.04-2.56 6.11-3.45 2.17-0.907 4.14-1.38 6.21-1.38h20.7c1.97 0 4.04 0.384 6.41 1.18 2.37 0.71 4.44 1.77 6.51 3.06 1.97 1.38 3.45 3.25 4.54 5.62 1.28 2.46 1.87 5.23 1.87 8.58v6.01c0 3.75-2.27 5.72-6.61 5.72s-6.61-1.97-6.61-5.72v-5.13c0-2.07-0.651-3.75-1.97-5.42-1.28-1.68-3.35-2.56-6.21-2.56h-14.8c-3.06 0-5.13 0.848-6.41 2.56-1.28 1.68-1.87 3.45-1.87 5.42v34.5c0 1.97 0.611 3.75 1.87 5.42 1.28 1.68 3.45 2.56 6.41 2.56h14.8c3.06 0 5.03-0.848 6.21-2.56 1.28-1.68 1.97-3.45 1.97-5.42v-5.52c0-3.75 2.27-5.62 6.61-5.62s6.61 1.87 6.61 5.62zm60.5-54.7c0.779 0 1.58 0.207 2.27 0.601 0.779 0.325 1.68 0.808 3.05 1.98s2.36 2.75 2.36 4.23c0 0.562-0.118 1.18-0.355 1.97-0.246 0.641-0.631 1.18-1.18 1.68l-37.5 43.4v15.8c0 3.75-2.27 5.62-6.61 5.62s-6.61-1.87-6.61-5.62v-85.8c0-3.75 2.27-5.72 6.61-5.72s6.61 1.97 6.61 5.72v50.3c2.07-2.46 4.14-4.93 6.61-7.69 2.46-2.86 4.83-5.62 7.3-8.48 2.46-2.96 4.83-5.72 7.3-8.48 2.46-2.86 4.63-5.32 6.62-7.8 0.656-0.789 2.13-1.68 3.51-1.68zm8.4 64.8c0.562 1.08 0.848 2.17 0.848 3.25 0 2.07-1.18 3.55-3.55 4.63-1.68 0.976-3.15 1.48-4.34 1.48-0.907 0-1.77-0.325-2.56-0.966-0.779-0.562-1.48-1.28-1.97-2.17l-11.8-23.7c-0.71-1.28-1.08-2.46-1.08-3.55 0-2.07 1.08-3.65 3.15-5.03 0.562-0.394 1.08-0.759 1.68-1.08 0.641-0.325 1.38-0.552 2.07-0.72 0.907-0.168 1.77-0.0033 2.56 0.473 0.779 0.473 1.48 1.18 2.07 1.97zm29.6 8.08c-2.46 0-4.83-0.325-7.1-0.966-2.27-0.71-4.44-1.77-7.44-3.59-3.01-1.81-4.98-6.35-4.98-13.2v-5.92c0-3.35 0.552-6.11 1.68-8.58 1.28-2.46 2.96-4.44 4.92-5.87 1.96-1.43 4.03-2.42 6.3-3.11 2.27-0.71 4.44-1.08 6.51-1.08h26.6v-10.8c0-2.07-0.651-3.75-1.97-5.42-1.28-1.68-3.35-2.56-6.21-2.56h-13.8c-3.06 0-5.13 0.749-6.41 2.27-1.08 1.48-1.68 3.15-1.68 5.13v0.966c0 4.14-2.27 6.21-6.7 6.21s-6.7-2.07-6.7-6.21v-1.08c0-3.35 0.552-6.01 1.68-8.28 1.28-2.27 2.86-4.14 4.73-5.62h-0.118c2.07-1.38 4.24-2.56 6.51-3.45 2.37-0.907 4.54-1.38 6.61-1.38h18.7c2.07 0 4.14 0.454 6.41 1.38 2.37 0.907 4.44 2.07 6.51 3.45h-0.118c2.07 1.48 3.65 3.45 4.73 6.01 1.08 2.46 1.68 5.23 1.68 8.48v48.3c0 3.75-2.27 5.72-6.61 5.72-0.907 0-1.77-0.0394-2.56-0.118-0.71-0.0779-1.38-0.286-2.07-0.601-0.562-0.394-1.08-0.927-1.48-1.58-0.325-0.71-0.473-1.68-0.473-2.76l-0.118-26.6h-24.6c-1.77 0-3.15 0.207-4.04 0.601-4.09 1.83-4.1 7.7-4.1 7.79v4.63c0 2.07 0.611 3.65 1.87 4.73 1.28 1.08 3.45 1.58 6.51 1.58h13.8c3.06 0 4.44 1.87 4.44 5.62 0 1.68-0.355 3.06-1.08 4.04-0.71 1.08-1.87 1.58-3.45 1.58zm71-61.1v43.4c0 2.07 0.454 3.65 1.38 4.73 0.907 1.08 2.86 1.68 5.62 1.68h3.15c3.15 0 4.54 1.87 4.44 5.52 0 3.75-1.48 5.62-4.44 5.62h-3.94c-2.66 0-4.93-0.325-7.2-0.966-2.27-0.71-4.44-1.77-6.51-3.06v0.118c-3.94-2.66-5.92-7.1-5.92-13.8v-43.4h-4.63c-2.96 0-4.44-1.87-4.54-5.62 0-3.75 1.58-5.62 4.54-5.62h4.63v-12.8c0-3.75 2.27-5.72 6.8-5.72 4.34 0 6.61 1.97 6.61 5.72v12.8h9.76c3.15 0 4.54 1.87 4.44 5.62 0 3.75-1.48 5.62-4.44 5.62zm37.8-10.4c2.27-0.877 4.44-1.38 6.51-1.38h19.7c2.07 0 4.14 0.454 6.41 1.38 2.27 0.907 4.44 2.07 6.45 3.5 2.01 1.43 3.59 3.4 4.67 5.96 1.08 2.46 1.68 5.23 1.68 8.48v34.5c0 6.51-2.17 11.8-6.51 14.8-4.24 3.06-8.48 4.54-12.8 4.54h-19.7c-2.07 0-4.14-0.355-6.41-1.08-2.27-0.71-4.34-1.87-6.41-3.45-4.34-3.06-6.51-7.89-6.51-14.8v-34.5c0-3.25 0.552-6.01 1.68-8.48 1.08-2.46 2.76-4.44 4.84-5.92s4.15-2.66 6.42-3.55zm24 59.6c3.06 0 5.03-0.848 6.21-2.56 1.28-1.68 1.97-3.45 1.97-5.42v-34.5c0-2.07-0.651-3.75-1.97-5.42-1.28-1.68-3.35-2.56-6.21-2.56h-14.8c-3.06 0-5.13 0.848-6.41 2.56-1.28 1.68-1.87 3.45-1.87 5.42v34.5c0 1.97 0.611 3.75 1.87 5.42 1.28 1.68 3.45 2.56 6.41 2.56zm72.6-50.2h-14.8c-3.35 0-5.42 0.976-6.51 2.96-1.08 1.68-1.68 3.65-1.68 5.92l-0.315 47.3c0 3.75-2.27 5.72-6.61 5.72-4.34 0-6.61-1.97-6.61-5.72v-48.3c0-3.35 0.513-6.11 1.58-8.58 1.08-2.46 2.76-4.34 4.93-5.92 4.24-3.06 8.48-4.54 12.8-4.54h19.7c4.04 0 8.28 1.58 12.8 4.54 4.34 3.06 6.51 7.89 6.51 14.8v48.3c0 3.75-2.27 5.72-6.61 5.72s-6.61-1.97-6.61-5.72v-47.3c0-2.07-0.651-4.04-1.99-6.11-1.34-2.07-3.51-3.06-6.66-3.06z"
|
|
11
|
+
aria-label="Mockaton"/>
|
|
4
12
|
</svg>
|
package/src/utils/http-cors.js
CHANGED
|
@@ -2,7 +2,7 @@ import { StandardMethods } from './http-request.js'
|
|
|
2
2
|
|
|
3
3
|
// https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-processing-model
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const CorsHeader = {
|
|
6
6
|
// request
|
|
7
7
|
Origin: 'origin',
|
|
8
8
|
AccessControlRequestMethod: 'access-control-request-method',
|
|
@@ -16,13 +16,13 @@ export const PreflightHeader = {
|
|
|
16
16
|
AccessControlExposeHeaders: 'Access-Control-Expose-Headers', // '*' | Comma delimited
|
|
17
17
|
AccessControlAllowCredentials: 'Access-Control-Allow-Credentials' // 'true'
|
|
18
18
|
}
|
|
19
|
-
const
|
|
19
|
+
const CH = CorsHeader
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
export function isPreflight(req) {
|
|
23
23
|
return req.method === 'OPTIONS'
|
|
24
|
-
&& URL.canParse(req.headers[
|
|
25
|
-
&& StandardMethods.includes(req.headers[
|
|
24
|
+
&& URL.canParse(req.headers[CH.Origin])
|
|
25
|
+
&& StandardMethods.includes(req.headers[CH.AccessControlRequestMethod])
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
|
|
@@ -34,16 +34,16 @@ export function setCorsHeaders(req, response, {
|
|
|
34
34
|
credentials = false,
|
|
35
35
|
maxAge = 0
|
|
36
36
|
}) {
|
|
37
|
-
const reqOrigin = req.headers[
|
|
37
|
+
const reqOrigin = req.headers[CH.Origin]
|
|
38
38
|
const hasWildcard = origins.some(ao => ao === '*')
|
|
39
39
|
if (!reqOrigin || (!hasWildcard && !origins.includes(reqOrigin)))
|
|
40
40
|
return
|
|
41
|
-
response.setHeader(
|
|
41
|
+
response.setHeader(CH.AccessControlAllowOrigin, reqOrigin) // Never '*', so no need to `Vary` it
|
|
42
42
|
|
|
43
43
|
if (credentials)
|
|
44
|
-
response.setHeader(
|
|
44
|
+
response.setHeader(CH.AccessControlAllowCredentials, 'true')
|
|
45
45
|
|
|
46
|
-
if (req.headers[
|
|
46
|
+
if (req.headers[CH.AccessControlRequestMethod])
|
|
47
47
|
setPreflightSpecificHeaders(req, response, methods, headers, maxAge)
|
|
48
48
|
else
|
|
49
49
|
setActualRequestHeaders(response, exposedHeaders)
|
|
@@ -51,20 +51,19 @@ export function setCorsHeaders(req, response, {
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
function setPreflightSpecificHeaders(req, response, methods, headers, maxAge) {
|
|
54
|
-
const methodAskingFor = req.headers[
|
|
54
|
+
const methodAskingFor = req.headers[CH.AccessControlRequestMethod]
|
|
55
55
|
if (!methods.includes(methodAskingFor))
|
|
56
56
|
return
|
|
57
57
|
|
|
58
|
-
response.setHeader(
|
|
58
|
+
response.setHeader(CH.AccessControlMaxAge, maxAge)
|
|
59
|
+
response.setHeader(CH.AccessControlAllowMethods, methodAskingFor)
|
|
59
60
|
if (headers.length)
|
|
60
|
-
response.setHeader(
|
|
61
|
-
|
|
62
|
-
response.setHeader(PH.AccessControlMaxAge, maxAge)
|
|
61
|
+
response.setHeader(CH.AccessControlAllowHeaders, headers.join(','))
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
|
|
66
65
|
function setActualRequestHeaders(response, exposedHeaders) {
|
|
67
66
|
// Exposed means the client-side JavaScript can read them
|
|
68
67
|
if (exposedHeaders.length)
|
|
69
|
-
response.setHeader(
|
|
68
|
+
response.setHeader(CH.AccessControlExposeHeaders, exposedHeaders.join(','))
|
|
70
69
|
}
|
|
@@ -2,7 +2,7 @@ import { equal } from 'node:assert/strict'
|
|
|
2
2
|
import { promisify } from 'node:util'
|
|
3
3
|
import { createServer } from 'node:http'
|
|
4
4
|
import { describe, it, after } from 'node:test'
|
|
5
|
-
import { isPreflight, setCorsHeaders,
|
|
5
|
+
import { isPreflight, setCorsHeaders, CorsHeader as CH } from './http-cors.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
function headerIs(response, header, value) {
|
|
@@ -40,8 +40,8 @@ await describe('CORS', async () => {
|
|
|
40
40
|
|
|
41
41
|
await describe('Identifies Preflight Requests', async () => {
|
|
42
42
|
const requiredRequestHeaders = {
|
|
43
|
-
[
|
|
44
|
-
[
|
|
43
|
+
[CH.Origin]: 'http://locahost:9999',
|
|
44
|
+
[CH.AccessControlRequestMethod]: 'POST'
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
await it('Ignores non-OPTIONS requests', async () => {
|
|
@@ -49,26 +49,26 @@ await describe('CORS', async () => {
|
|
|
49
49
|
equal(await res.text(), 'NON_PREFLIGHT')
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
await it(`Ignores non-parseable req ${
|
|
52
|
+
await it(`Ignores non-parseable req ${CH.Origin} header`, async () => {
|
|
53
53
|
const headers = {
|
|
54
54
|
...requiredRequestHeaders,
|
|
55
|
-
[
|
|
55
|
+
[CH.Origin]: 'non-url'
|
|
56
56
|
}
|
|
57
57
|
const res = await preflight(headers)
|
|
58
58
|
equal(await res.text(), 'NON_PREFLIGHT')
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
await it(`Ignores missing method in ${
|
|
61
|
+
await it(`Ignores missing method in ${CH.AccessControlRequestMethod} header`, async () => {
|
|
62
62
|
const headers = { ...requiredRequestHeaders }
|
|
63
|
-
delete headers[
|
|
63
|
+
delete headers[CH.AccessControlRequestMethod]
|
|
64
64
|
const res = await preflight(headers)
|
|
65
65
|
equal(await res.text(), 'NON_PREFLIGHT')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
await it(`Ignores non-standard method in ${
|
|
68
|
+
await it(`Ignores non-standard method in ${CH.AccessControlRequestMethod} header`, async () => {
|
|
69
69
|
const headers = {
|
|
70
70
|
...requiredRequestHeaders,
|
|
71
|
-
[
|
|
71
|
+
[CH.AccessControlRequestMethod]: 'NON_STANDARD'
|
|
72
72
|
}
|
|
73
73
|
const res = await preflight(headers)
|
|
74
74
|
equal(await res.text(), 'NON_PREFLIGHT')
|
|
@@ -87,14 +87,14 @@ await describe('CORS', async () => {
|
|
|
87
87
|
methods: ['GET']
|
|
88
88
|
}
|
|
89
89
|
const p = await preflight({
|
|
90
|
-
[
|
|
91
|
-
[
|
|
90
|
+
[CH.Origin]: FooDotCom,
|
|
91
|
+
[CH.AccessControlRequestMethod]: 'GET'
|
|
92
92
|
})
|
|
93
|
-
headerIs(p,
|
|
94
|
-
headerIs(p,
|
|
95
|
-
headerIs(p,
|
|
96
|
-
headerIs(p,
|
|
97
|
-
headerIs(p,
|
|
93
|
+
headerIs(p, CH.AccessControlAllowOrigin, null)
|
|
94
|
+
headerIs(p, CH.AccessControlAllowMethods, null)
|
|
95
|
+
headerIs(p, CH.AccessControlAllowCredentials, null)
|
|
96
|
+
headerIs(p, CH.AccessControlAllowHeaders, null)
|
|
97
|
+
headerIs(p, CH.AccessControlMaxAge, null)
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
await it('not in allowed origins', async () => {
|
|
@@ -103,13 +103,13 @@ await describe('CORS', async () => {
|
|
|
103
103
|
methods: ['GET']
|
|
104
104
|
}
|
|
105
105
|
const p = await preflight({
|
|
106
|
-
[
|
|
107
|
-
[
|
|
106
|
+
[CH.Origin]: NotAllowedDotCom,
|
|
107
|
+
[CH.AccessControlRequestMethod]: 'GET'
|
|
108
108
|
})
|
|
109
|
-
headerIs(p,
|
|
110
|
-
headerIs(p,
|
|
111
|
-
headerIs(p,
|
|
112
|
-
headerIs(p,
|
|
109
|
+
headerIs(p, CH.AccessControlAllowOrigin, null)
|
|
110
|
+
headerIs(p, CH.AccessControlAllowMethods, null)
|
|
111
|
+
headerIs(p, CH.AccessControlAllowCredentials, null)
|
|
112
|
+
headerIs(p, CH.AccessControlAllowHeaders, null)
|
|
113
113
|
})
|
|
114
114
|
|
|
115
115
|
await it('origin and method match', async () => {
|
|
@@ -118,13 +118,13 @@ await describe('CORS', async () => {
|
|
|
118
118
|
methods: ['GET']
|
|
119
119
|
}
|
|
120
120
|
const p = await preflight({
|
|
121
|
-
[
|
|
122
|
-
[
|
|
121
|
+
[CH.Origin]: AllowedDotCom,
|
|
122
|
+
[CH.AccessControlRequestMethod]: 'GET'
|
|
123
123
|
})
|
|
124
|
-
headerIs(p,
|
|
125
|
-
headerIs(p,
|
|
126
|
-
headerIs(p,
|
|
127
|
-
headerIs(p,
|
|
124
|
+
headerIs(p, CH.AccessControlAllowOrigin, AllowedDotCom)
|
|
125
|
+
headerIs(p, CH.AccessControlAllowMethods, 'GET')
|
|
126
|
+
headerIs(p, CH.AccessControlAllowCredentials, null)
|
|
127
|
+
headerIs(p, CH.AccessControlAllowHeaders, null)
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
await it('origin matches from multiple', async () => {
|
|
@@ -133,13 +133,13 @@ await describe('CORS', async () => {
|
|
|
133
133
|
methods: ['GET']
|
|
134
134
|
}
|
|
135
135
|
const p = await preflight({
|
|
136
|
-
[
|
|
137
|
-
[
|
|
136
|
+
[CH.Origin]: AllowedDotCom,
|
|
137
|
+
[CH.AccessControlRequestMethod]: 'GET'
|
|
138
138
|
})
|
|
139
|
-
headerIs(p,
|
|
140
|
-
headerIs(p,
|
|
141
|
-
headerIs(p,
|
|
142
|
-
headerIs(p,
|
|
139
|
+
headerIs(p, CH.AccessControlAllowOrigin, AllowedDotCom)
|
|
140
|
+
headerIs(p, CH.AccessControlAllowMethods, 'GET')
|
|
141
|
+
headerIs(p, CH.AccessControlAllowCredentials, null)
|
|
142
|
+
headerIs(p, CH.AccessControlAllowHeaders, null)
|
|
143
143
|
})
|
|
144
144
|
|
|
145
145
|
await it('wildcard origin', async () => {
|
|
@@ -148,13 +148,13 @@ await describe('CORS', async () => {
|
|
|
148
148
|
methods: ['GET']
|
|
149
149
|
}
|
|
150
150
|
const p = await preflight({
|
|
151
|
-
[
|
|
152
|
-
[
|
|
151
|
+
[CH.Origin]: FooDotCom,
|
|
152
|
+
[CH.AccessControlRequestMethod]: 'GET'
|
|
153
153
|
})
|
|
154
|
-
headerIs(p,
|
|
155
|
-
headerIs(p,
|
|
156
|
-
headerIs(p,
|
|
157
|
-
headerIs(p,
|
|
154
|
+
headerIs(p, CH.AccessControlAllowOrigin, FooDotCom)
|
|
155
|
+
headerIs(p, CH.AccessControlAllowMethods, 'GET')
|
|
156
|
+
headerIs(p, CH.AccessControlAllowCredentials, null)
|
|
157
|
+
headerIs(p, CH.AccessControlAllowHeaders, null)
|
|
158
158
|
})
|
|
159
159
|
|
|
160
160
|
await it(`wildcard and credentials`, async () => {
|
|
@@ -164,13 +164,13 @@ await describe('CORS', async () => {
|
|
|
164
164
|
credentials: true
|
|
165
165
|
}
|
|
166
166
|
const p = await preflight({
|
|
167
|
-
[
|
|
168
|
-
[
|
|
167
|
+
[CH.Origin]: FooDotCom,
|
|
168
|
+
[CH.AccessControlRequestMethod]: 'GET'
|
|
169
169
|
})
|
|
170
|
-
headerIs(p,
|
|
171
|
-
headerIs(p,
|
|
172
|
-
headerIs(p,
|
|
173
|
-
headerIs(p,
|
|
170
|
+
headerIs(p, CH.AccessControlAllowOrigin, FooDotCom)
|
|
171
|
+
headerIs(p, CH.AccessControlAllowMethods, 'GET')
|
|
172
|
+
headerIs(p, CH.AccessControlAllowCredentials, 'true')
|
|
173
|
+
headerIs(p, CH.AccessControlAllowHeaders, null)
|
|
174
174
|
})
|
|
175
175
|
|
|
176
176
|
await it(`wildcard, credentials, and headers`, async () => {
|
|
@@ -181,13 +181,13 @@ await describe('CORS', async () => {
|
|
|
181
181
|
headers: ['content-type', 'my-header']
|
|
182
182
|
}
|
|
183
183
|
const p = await preflight({
|
|
184
|
-
[
|
|
185
|
-
[
|
|
184
|
+
[CH.Origin]: FooDotCom,
|
|
185
|
+
[CH.AccessControlRequestMethod]: 'GET'
|
|
186
186
|
})
|
|
187
|
-
headerIs(p,
|
|
188
|
-
headerIs(p,
|
|
189
|
-
headerIs(p,
|
|
190
|
-
headerIs(p,
|
|
187
|
+
headerIs(p, CH.AccessControlAllowOrigin, FooDotCom)
|
|
188
|
+
headerIs(p, CH.AccessControlAllowMethods, 'GET')
|
|
189
|
+
headerIs(p, CH.AccessControlAllowCredentials, 'true')
|
|
190
|
+
headerIs(p, CH.AccessControlAllowHeaders, 'content-type,my-header')
|
|
191
191
|
})
|
|
192
192
|
})
|
|
193
193
|
|
|
@@ -198,12 +198,12 @@ await describe('CORS', async () => {
|
|
|
198
198
|
methods: ['GET']
|
|
199
199
|
}
|
|
200
200
|
const p = await request({
|
|
201
|
-
[
|
|
201
|
+
[CH.Origin]: NotAllowedDotCom
|
|
202
202
|
})
|
|
203
203
|
equal(p.status, 200)
|
|
204
|
-
headerIs(p,
|
|
205
|
-
headerIs(p,
|
|
206
|
-
headerIs(p,
|
|
204
|
+
headerIs(p, CH.AccessControlAllowOrigin, null)
|
|
205
|
+
headerIs(p, CH.AccessControlAllowCredentials, null)
|
|
206
|
+
headerIs(p, CH.AccessControlExposeHeaders, null)
|
|
207
207
|
})
|
|
208
208
|
|
|
209
209
|
await it('origin allowed', async () => {
|
|
@@ -214,12 +214,12 @@ await describe('CORS', async () => {
|
|
|
214
214
|
exposedHeaders: ['x-h1', 'x-h2']
|
|
215
215
|
}
|
|
216
216
|
const p = await request({
|
|
217
|
-
[
|
|
217
|
+
[CH.Origin]: AllowedDotCom
|
|
218
218
|
})
|
|
219
219
|
equal(p.status, 200)
|
|
220
|
-
headerIs(p,
|
|
221
|
-
headerIs(p,
|
|
222
|
-
headerIs(p,
|
|
220
|
+
headerIs(p, CH.AccessControlAllowOrigin, AllowedDotCom)
|
|
221
|
+
headerIs(p, CH.AccessControlAllowCredentials, 'true')
|
|
222
|
+
headerIs(p, CH.AccessControlExposeHeaders, 'x-h1,x-h2')
|
|
223
223
|
})
|
|
224
224
|
})
|
|
225
225
|
})
|
package/src/utils/validate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function validate(obj, shape) {
|
|
2
2
|
for (const [field, value] of Object.entries(obj))
|
|
3
3
|
if (!shape[field](value))
|
|
4
|
-
throw new TypeError(
|
|
4
|
+
throw new TypeError(`Config.${field}=${JSON.stringify(value)} is invalid`)
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export const is = ctor => val => val.constructor === ctor
|
package/README-dashboard.png
DELETED
|
Binary file
|