mockaton 11.3.0 → 11.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/ApiCommander.js +3 -0
- package/src/client/ApiConstants.js +1 -0
- package/src/client/app-store.js +10 -0
- package/src/client/app.js +46 -5
- package/src/client/dom-utils.js +1 -0
- package/src/client/indexHtml.js +1 -0
- package/src/client/styles.css +33 -7
- package/src/server/Api.js +12 -0
- package/src/server/ApiConstants.js +1 -0
- package/src/server/Mockaton.test.js +23 -9
- package/src/server/Watcher.js +1 -2
- package/src/server/WatcherDevClient.js +2 -3
- package/src/server/config.js +1 -3
- package/src/server/utils/http-cors.test.js +22 -22
- package/src/server/utils/jwt.js +1 -6
- package/src/server/utils/mime.js +32 -3
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
An HTTP mock server for simulating APIs with minimal setup — ideal
|
|
8
8
|
for testing difficult to reproduce backend states.
|
|
9
9
|
|
|
10
|
-
## https://mockaton.com
|
|
10
|
+
## [mockaton.com ↗](https://mockaton.com)
|
|
11
|
+
## [Changelog ↗](https://mockaton.com/changelog)
|
|
11
12
|
|
|
12
13
|
## Overview
|
|
13
14
|
With Mockaton, you don’t need to write code for wiring up your
|
|
@@ -49,9 +50,9 @@ curl localhost:2020/api/user
|
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
<picture>
|
|
52
|
-
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.
|
|
53
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.
|
|
54
|
-
<img alt="Mockaton Dashboard" src="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.
|
|
53
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp822x740.light.gold.png">
|
|
54
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp822x740.dark.gold.png">
|
|
55
|
+
<img alt="Mockaton Dashboard" src="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp822x740.dark.gold.png">
|
|
55
56
|
</picture>
|
|
56
57
|
|
|
57
58
|
On the dashboard you can:
|
|
@@ -64,7 +65,5 @@ Nonetheless, there’s a programmatic API, which is handy for
|
|
|
64
65
|
setting up tests (see **Commander API** section below).
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
## [Changelog ↗](https://mockaton.com/changelog)
|
|
68
|
-
|
|
69
68
|
## License
|
|
70
69
|
MIT
|
package/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface Config {
|
|
|
14
14
|
staticDir?: string
|
|
15
15
|
ignore?: RegExp
|
|
16
16
|
watcherEnabled?: boolean
|
|
17
|
+
watcherDebounceMs?: number
|
|
17
18
|
|
|
18
19
|
host?: string,
|
|
19
20
|
port?: number
|
|
@@ -98,6 +99,7 @@ export interface State {
|
|
|
98
99
|
comments: string[]
|
|
99
100
|
|
|
100
101
|
delay: number
|
|
102
|
+
delayJitter: number
|
|
101
103
|
|
|
102
104
|
collectProxied: boolean
|
|
103
105
|
proxyFallback: string
|
package/package.json
CHANGED
|
@@ -23,6 +23,9 @@ export class Commander {
|
|
|
23
23
|
|
|
24
24
|
/** @returns {Promise<Response>} */
|
|
25
25
|
setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
|
|
26
|
+
|
|
27
|
+
/** @returns {Promise<Response>} */
|
|
28
|
+
setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
|
|
26
29
|
|
|
27
30
|
/** @returns {Promise<Response>} */
|
|
28
31
|
setCorsAllowed = value => this.#patch(API.cors, value)
|
|
@@ -12,6 +12,7 @@ export const API = {
|
|
|
12
12
|
delayStatic: MOUNT + '/delay-static',
|
|
13
13
|
fallback: MOUNT + '/fallback',
|
|
14
14
|
globalDelay: MOUNT + '/global-delay',
|
|
15
|
+
globalDelayJitter: MOUNT + '/global-delay-jitter',
|
|
15
16
|
proxied: MOUNT + '/proxied',
|
|
16
17
|
reset: MOUNT + '/reset',
|
|
17
18
|
select: MOUNT + '/select',
|
package/src/client/app-store.js
CHANGED
|
@@ -16,6 +16,7 @@ export const store = {
|
|
|
16
16
|
cookies: [],
|
|
17
17
|
comments: [],
|
|
18
18
|
delay: 0,
|
|
19
|
+
delayJitter: 0,
|
|
19
20
|
|
|
20
21
|
collectProxied: false,
|
|
21
22
|
proxyFallback: '',
|
|
@@ -84,6 +85,15 @@ export const store = {
|
|
|
84
85
|
}
|
|
85
86
|
catch (error) { store.onError(error) }
|
|
86
87
|
},
|
|
88
|
+
|
|
89
|
+
async setGlobalDelayJitter(value) {
|
|
90
|
+
try {
|
|
91
|
+
const response = await api.setGlobalDelayJitter(value)
|
|
92
|
+
if (!response.ok) throw response
|
|
93
|
+
store.delayJitter = value
|
|
94
|
+
}
|
|
95
|
+
catch (error) { store.onError(error) }
|
|
96
|
+
},
|
|
87
97
|
|
|
88
98
|
async selectCookie(name) {
|
|
89
99
|
try {
|
package/src/client/app.js
CHANGED
|
@@ -17,12 +17,13 @@ const CSS = {
|
|
|
17
17
|
FallbackBackend: null,
|
|
18
18
|
Field: null,
|
|
19
19
|
GlobalDelayField: null,
|
|
20
|
+
GlobalDelayJitterField: null,
|
|
21
|
+
GlobalDelayWrap: null,
|
|
20
22
|
GroupByMethod: null,
|
|
21
23
|
InternalServerErrorToggler: null,
|
|
22
24
|
Logo: null,
|
|
23
25
|
MenuTrigger: null,
|
|
24
26
|
Method: null,
|
|
25
|
-
MockList: null,
|
|
26
27
|
MockSelector: null,
|
|
27
28
|
NotFoundToggler: null,
|
|
28
29
|
PayloadViewer: null,
|
|
@@ -110,7 +111,7 @@ function Header() {
|
|
|
110
111
|
r('header', null,
|
|
111
112
|
r('a', {
|
|
112
113
|
className: CSS.Logo,
|
|
113
|
-
href: 'https://mockaton.com'
|
|
114
|
+
href: 'https://mockaton.com'
|
|
114
115
|
},
|
|
115
116
|
r('object', {
|
|
116
117
|
data: 'logo.svg',
|
|
@@ -119,14 +120,18 @@ function Header() {
|
|
|
119
120
|
height: 22
|
|
120
121
|
})),
|
|
121
122
|
r('div', null,
|
|
122
|
-
|
|
123
|
+
r('div', className(CSS.GlobalDelayWrap),
|
|
124
|
+
GlobalDelayField(),
|
|
125
|
+
GlobalDelayJitterField()),
|
|
123
126
|
BulkSelector(),
|
|
124
127
|
CookieSelector(),
|
|
125
128
|
ProxyFallbackField(),
|
|
126
129
|
ResetButton(),
|
|
127
|
-
SettingsMenuTrigger()
|
|
130
|
+
SettingsMenuTrigger()
|
|
131
|
+
)))
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
|
|
130
135
|
function GlobalDelayField() {
|
|
131
136
|
function onChange() {
|
|
132
137
|
store.setGlobalDelay(this.valueAsNumber)
|
|
@@ -143,6 +148,7 @@ function GlobalDelayField() {
|
|
|
143
148
|
r('label', className(CSS.Field, CSS.GlobalDelayField),
|
|
144
149
|
r('span', null, t`Delay (ms)`),
|
|
145
150
|
r('input', {
|
|
151
|
+
name: 'delay',
|
|
146
152
|
type: 'number',
|
|
147
153
|
min: 0,
|
|
148
154
|
step: 100,
|
|
@@ -153,6 +159,38 @@ function GlobalDelayField() {
|
|
|
153
159
|
})))
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
function GlobalDelayJitterField() {
|
|
163
|
+
function onChange() {
|
|
164
|
+
this.value = this.valueAsNumber.toFixed(0)
|
|
165
|
+
this.value = Math.max(0, this.valueAsNumber)
|
|
166
|
+
this.value = Math.min(300, this.valueAsNumber)
|
|
167
|
+
store.setGlobalDelayJitter(this.valueAsNumber / 100)
|
|
168
|
+
}
|
|
169
|
+
function onWheel(event) {
|
|
170
|
+
if (event.deltaY > 0)
|
|
171
|
+
this.stepUp()
|
|
172
|
+
else
|
|
173
|
+
this.stepDown()
|
|
174
|
+
clearTimeout(onWheel.timer)
|
|
175
|
+
onWheel.timer = setTimeout(onChange.bind(this), 300)
|
|
176
|
+
}
|
|
177
|
+
return (
|
|
178
|
+
r('label', className(CSS.Field, CSS.GlobalDelayJitterField),
|
|
179
|
+
r('span', null, t`Max Jitter %`),
|
|
180
|
+
r('input', {
|
|
181
|
+
name: 'delay-jitter',
|
|
182
|
+
type: 'number',
|
|
183
|
+
min: 0,
|
|
184
|
+
max: 300,
|
|
185
|
+
step: 10,
|
|
186
|
+
autocomplete: 'none',
|
|
187
|
+
value: (store.delayJitter * 100).toFixed(0),
|
|
188
|
+
onChange,
|
|
189
|
+
onWheel: [onWheel, { passive: true }]
|
|
190
|
+
})))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
156
194
|
function BulkSelector() {
|
|
157
195
|
const { comments } = store
|
|
158
196
|
const firstOption = t`Pick Comment…`
|
|
@@ -209,6 +247,7 @@ function ProxyFallbackField() {
|
|
|
209
247
|
r('label', null,
|
|
210
248
|
r('span', null, t`Fallback`),
|
|
211
249
|
r('input', {
|
|
250
|
+
name: 'fallback',
|
|
212
251
|
type: 'url',
|
|
213
252
|
autocomplete: 'none',
|
|
214
253
|
placeholder: t`Type backend address`,
|
|
@@ -222,6 +261,7 @@ function SaveProxiedCheckbox(ref) {
|
|
|
222
261
|
return (
|
|
223
262
|
r('label', className(CSS.SaveProxiedCheckbox),
|
|
224
263
|
r('input', {
|
|
264
|
+
name: 'save-proxied',
|
|
225
265
|
ref,
|
|
226
266
|
type: 'checkbox',
|
|
227
267
|
disabled: !store.canProxy,
|
|
@@ -269,6 +309,7 @@ function SettingsMenu(id) {
|
|
|
269
309
|
r('div', null,
|
|
270
310
|
r('label', className(CSS.GroupByMethod),
|
|
271
311
|
r('input', {
|
|
312
|
+
name: 'group-by-method',
|
|
272
313
|
ref: firstInputRef,
|
|
273
314
|
type: 'checkbox',
|
|
274
315
|
checked: store.groupByMethod,
|
|
@@ -281,7 +322,7 @@ function SettingsMenu(id) {
|
|
|
281
322
|
target: '_blank',
|
|
282
323
|
rel: 'noopener noreferrer'
|
|
283
324
|
}, t`Website`),
|
|
284
|
-
|
|
325
|
+
|
|
285
326
|
r('a', {
|
|
286
327
|
href: 'https://github.com/ericfortis/mockaton',
|
|
287
328
|
target: '_blank',
|
package/src/client/dom-utils.js
CHANGED
package/src/client/indexHtml.js
CHANGED
package/src/client/styles.css
CHANGED
|
@@ -137,7 +137,7 @@ header {
|
|
|
137
137
|
align-self: end;
|
|
138
138
|
margin-right: 22px;
|
|
139
139
|
margin-bottom: 3px;
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
object {
|
|
142
142
|
pointer-events: none;
|
|
143
143
|
}
|
|
@@ -151,10 +151,10 @@ header {
|
|
|
151
151
|
gap: 16px 8px;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
@media (max-width:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
align-self:
|
|
154
|
+
@media (max-width: 820px) {
|
|
155
|
+
.Logo {
|
|
156
|
+
margin-top: 16px;
|
|
157
|
+
align-self: start;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
> div {
|
|
@@ -170,6 +170,10 @@ header {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
.GlobalDelayWrap {
|
|
174
|
+
display: flex;
|
|
175
|
+
}
|
|
176
|
+
|
|
173
177
|
.Field {
|
|
174
178
|
width: 116px;
|
|
175
179
|
|
|
@@ -202,10 +206,13 @@ header {
|
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
&.GlobalDelayField {
|
|
209
|
+
position: relative;
|
|
205
210
|
width: 76px;
|
|
206
211
|
|
|
207
|
-
input
|
|
212
|
+
input {
|
|
208
213
|
padding-right: 6px;
|
|
214
|
+
border-bottom-right-radius: 0;
|
|
215
|
+
border-top-right-radius: 0;
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
svg {
|
|
@@ -215,6 +222,25 @@ header {
|
|
|
215
222
|
stroke-width: 3px;
|
|
216
223
|
border-radius: 50%;
|
|
217
224
|
}
|
|
225
|
+
|
|
226
|
+
&:focus-within {
|
|
227
|
+
z-index: 100;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
&.GlobalDelayJitterField {
|
|
232
|
+
width: 70px;
|
|
233
|
+
span {
|
|
234
|
+
margin-left: 0;
|
|
235
|
+
}
|
|
236
|
+
input {
|
|
237
|
+
border-left: 2px solid transparent;
|
|
238
|
+
border-bottom-left-radius: 0;
|
|
239
|
+
border-top-left-radius: 0;
|
|
240
|
+
}
|
|
241
|
+
&:focus-within {
|
|
242
|
+
z-index: 100;
|
|
243
|
+
}
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
&.CookieSelector {
|
|
@@ -223,7 +249,7 @@ header {
|
|
|
223
249
|
|
|
224
250
|
&.FallbackBackend {
|
|
225
251
|
position: relative;
|
|
226
|
-
width:
|
|
252
|
+
width: 150px;
|
|
227
253
|
|
|
228
254
|
.SaveProxiedCheckbox {
|
|
229
255
|
position: absolute;
|
package/src/server/Api.js
CHANGED
|
@@ -39,6 +39,7 @@ export const apiPatchReqs = new Map([
|
|
|
39
39
|
[API.reset, reinitialize],
|
|
40
40
|
[API.cookies, selectCookie],
|
|
41
41
|
[API.globalDelay, setGlobalDelay],
|
|
42
|
+
[API.globalDelayJitter, setGlobalDelayJitter],
|
|
42
43
|
|
|
43
44
|
[API.fallback, setProxyFallback],
|
|
44
45
|
[API.collectProxied, setCollectProxied],
|
|
@@ -117,6 +118,17 @@ async function setGlobalDelay(req, response) {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
async function setGlobalDelayJitter(req, response) {
|
|
122
|
+
const jitter = await req.json()
|
|
123
|
+
|
|
124
|
+
if (!ConfigValidator.delayJitter(jitter))
|
|
125
|
+
response.unprocessable(`Expected 0 to 3 float for "delayJitter"`)
|
|
126
|
+
else {
|
|
127
|
+
config.delayJitter = jitter
|
|
128
|
+
response.ok()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
|
|
121
133
|
async function selectCookie(req, response) {
|
|
122
134
|
const cookieKey = await req.json()
|
|
@@ -12,6 +12,7 @@ export const API = {
|
|
|
12
12
|
delayStatic: MOUNT + '/delay-static',
|
|
13
13
|
fallback: MOUNT + '/fallback',
|
|
14
14
|
globalDelay: MOUNT + '/global-delay',
|
|
15
|
+
globalDelayJitter: MOUNT + '/global-delay-jitter',
|
|
15
16
|
proxied: MOUNT + '/proxied',
|
|
16
17
|
reset: MOUNT + '/reset',
|
|
17
18
|
select: MOUNT + '/select',
|
|
@@ -39,7 +39,7 @@ const CUSTOM_EXT = 'custom_extension'
|
|
|
39
39
|
const CUSTOM_MIME = 'custom_mime'
|
|
40
40
|
const CUSTOM_HEADER_NAME = 'custom_header_name'
|
|
41
41
|
const CUSTOM_HEADER_VAL = 'custom_header_val'
|
|
42
|
-
const ALLOWED_ORIGIN = '
|
|
42
|
+
const ALLOWED_ORIGIN = 'https://example.test'
|
|
43
43
|
|
|
44
44
|
const server = await Mockaton({
|
|
45
45
|
mocksDir,
|
|
@@ -51,7 +51,8 @@ const server = await Mockaton({
|
|
|
51
51
|
logLevel: 'verbose',
|
|
52
52
|
corsOrigins: [ALLOWED_ORIGIN],
|
|
53
53
|
corsExposedHeaders: ['Content-Encoding'],
|
|
54
|
-
watcherEnabled: false,
|
|
54
|
+
watcherEnabled: false, // But we enable it at run-time
|
|
55
|
+
watcherDebounceMs: 0
|
|
55
56
|
})
|
|
56
57
|
after(() => server?.close())
|
|
57
58
|
|
|
@@ -308,7 +309,7 @@ describe('Cookie', () => {
|
|
|
308
309
|
|
|
309
310
|
describe('Delay', () => {
|
|
310
311
|
describe('Set Global Delay', () => {
|
|
311
|
-
test('422 for invalid
|
|
312
|
+
test('422 for invalid value', async () => {
|
|
312
313
|
const r = await api.setGlobalDelay('not-a-number')
|
|
313
314
|
equal(r.status, 422)
|
|
314
315
|
equal(await r.text(), 'Expected non-negative integer for "delay"')
|
|
@@ -320,6 +321,19 @@ describe('Delay', () => {
|
|
|
320
321
|
})
|
|
321
322
|
})
|
|
322
323
|
|
|
324
|
+
describe('Set Global Delay Jitter', () => {
|
|
325
|
+
test('422 for invalid value', async () => {
|
|
326
|
+
const r = await api.setGlobalDelayJitter('not-a-number')
|
|
327
|
+
equal(r.status, 422)
|
|
328
|
+
equal(await r.text(), 'Expected 0 to 3 float for "delayJitter"')
|
|
329
|
+
})
|
|
330
|
+
test('200 for valid value', async () => {
|
|
331
|
+
const r = await api.setGlobalDelayJitter(0.1)
|
|
332
|
+
equal(r.status, 200)
|
|
333
|
+
equal((await fetchState()).delayJitter, 0.1)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
323
337
|
test('updates route delay', async () => {
|
|
324
338
|
const fx = new Fixture('route-delay.GET.200.json')
|
|
325
339
|
await fx.sync()
|
|
@@ -404,9 +418,9 @@ describe('Proxy Fallback', () => {
|
|
|
404
418
|
})
|
|
405
419
|
|
|
406
420
|
test('sets fallback', async () => {
|
|
407
|
-
const r = await api.setProxyFallback('
|
|
421
|
+
const r = await api.setProxyFallback('https://example.test')
|
|
408
422
|
equal(r.status, 200)
|
|
409
|
-
equal((await fetchState()).proxyFallback, '
|
|
423
|
+
equal((await fetchState()).proxyFallback, 'https://example.test')
|
|
410
424
|
})
|
|
411
425
|
|
|
412
426
|
test('unsets fallback', async () => {
|
|
@@ -461,7 +475,7 @@ describe('Proxy Fallback', () => {
|
|
|
461
475
|
})
|
|
462
476
|
|
|
463
477
|
test('200 when setting', async () => {
|
|
464
|
-
await api.setProxyFallback('https://example.
|
|
478
|
+
await api.setProxyFallback('https://example.test')
|
|
465
479
|
const r0 = await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
466
480
|
equal(r0.status, 200)
|
|
467
481
|
equal((await r0.json()).proxied, true)
|
|
@@ -480,7 +494,7 @@ describe('Proxy Fallback', () => {
|
|
|
480
494
|
test('unsets auto500', async () => {
|
|
481
495
|
const fx = new Fixture('unset-500-on-proxy.GET.200.txt')
|
|
482
496
|
await fx.sync()
|
|
483
|
-
await api.setProxyFallback('https://example.
|
|
497
|
+
await api.setProxyFallback('https://example.test')
|
|
484
498
|
|
|
485
499
|
const r0 = await api.toggle500(fx.method, fx.urlMask)
|
|
486
500
|
const b0 = await r0.json()
|
|
@@ -500,7 +514,7 @@ describe('Proxy Fallback', () => {
|
|
|
500
514
|
test('updating selected mock resets proxied flag', async () => {
|
|
501
515
|
const fx = new Fixture('select-resets-proxied.GET.200.txt')
|
|
502
516
|
await fx.sync()
|
|
503
|
-
await api.setProxyFallback('
|
|
517
|
+
await api.setProxyFallback('https://example.test')
|
|
504
518
|
const r0 = await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
505
519
|
equal((await r0.json()).proxied, true)
|
|
506
520
|
|
|
@@ -789,7 +803,7 @@ describe('500', () => {
|
|
|
789
803
|
test('toggling ON 500 unsets `proxied` flag', async () => {
|
|
790
804
|
const fx = new Fixture('proxied-to-500.GET.200.txt')
|
|
791
805
|
await fx.sync()
|
|
792
|
-
await api.setProxyFallback('
|
|
806
|
+
await api.setProxyFallback('https://example.test')
|
|
793
807
|
await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
794
808
|
await api.toggle500(fx.method, fx.urlMask)
|
|
795
809
|
equal((await fx.fetchBroker()).proxied, false)
|
package/src/server/Watcher.js
CHANGED
|
@@ -21,7 +21,6 @@ import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
|
21
21
|
* and also renames, which are two events (delete + add).
|
|
22
22
|
*/
|
|
23
23
|
const uiSyncVersion = new class extends EventEmitter {
|
|
24
|
-
delay = Number(process.env.MOCKATON_WATCHER_DEBOUNCE_MS ?? 80)
|
|
25
24
|
version = 0
|
|
26
25
|
|
|
27
26
|
increment = /** @type {function} */ this.#debounce(() => {
|
|
@@ -40,7 +39,7 @@ const uiSyncVersion = new class extends EventEmitter {
|
|
|
40
39
|
let timer
|
|
41
40
|
return () => {
|
|
42
41
|
clearTimeout(timer)
|
|
43
|
-
timer = setTimeout(fn,
|
|
42
|
+
timer = setTimeout(fn, config.watcherDebounceMs)
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { EventEmitter } from 'node:events'
|
|
3
3
|
import { watch, readdirSync } from 'node:fs'
|
|
4
|
+
import { config } from './config.js'
|
|
4
5
|
import { LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
const DEV = process.env.NODE_ENV === 'development'
|
|
8
|
-
|
|
9
8
|
export const CLIENT_DIR = join(import.meta.dirname, '../client')
|
|
10
9
|
export const DASHBOARD_ASSETS = readdirSync(CLIENT_DIR)
|
|
11
10
|
|
|
@@ -28,7 +27,7 @@ export function watchDevSPA() {
|
|
|
28
27
|
|
|
29
28
|
/** Realtime notify Dev UI changes */
|
|
30
29
|
export function longPollDevClientHotReload(req, response) {
|
|
31
|
-
if (!
|
|
30
|
+
if (!config.hotReload) {
|
|
32
31
|
response.notFound()
|
|
33
32
|
return
|
|
34
33
|
}
|
package/src/server/config.js
CHANGED
|
@@ -21,6 +21,7 @@ const schema = {
|
|
|
21
21
|
staticDir: [resolve('mockaton-static-mocks'), optional(isDirectory)],
|
|
22
22
|
ignore: [/(\.DS_Store|~)$/, is(RegExp)],
|
|
23
23
|
watcherEnabled: [true, is(Boolean)],
|
|
24
|
+
watcherDebounceMs: [80, ms => Number.isInteger(ms) && ms >= 0],
|
|
24
25
|
|
|
25
26
|
host: ['127.0.0.1', is(String)],
|
|
26
27
|
port: [0, port => Number.isInteger(port) && port >= 0 && port < 2 ** 16], // 0 means auto-assigned
|
|
@@ -73,9 +74,6 @@ export const ConfigValidator = Object.freeze(validators)
|
|
|
73
74
|
|
|
74
75
|
/** @param {Partial<Config>} opts */
|
|
75
76
|
export function setup(opts) {
|
|
76
|
-
if (process.env.NODE_ENV !== 'development')
|
|
77
|
-
opts.hotReload = false
|
|
78
|
-
|
|
79
77
|
if (opts.mocksDir)
|
|
80
78
|
opts.mocksDir = resolve(opts.mocksDir)
|
|
81
79
|
|
|
@@ -9,9 +9,9 @@ function headerIs(response, header, value) {
|
|
|
9
9
|
equal(response.headers.get(header), value)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
12
|
+
const FooDotTest = 'https://foo.test'
|
|
13
|
+
const AllowedDotTest = 'https://allowed.test'
|
|
14
|
+
const NotAllowedDotTest = 'https://not-allowed.test'
|
|
15
15
|
|
|
16
16
|
await describe('CORS', async () => {
|
|
17
17
|
let corsConfig = {}
|
|
@@ -87,7 +87,7 @@ await describe('CORS', async () => {
|
|
|
87
87
|
corsMethods: ['GET']
|
|
88
88
|
}
|
|
89
89
|
const p = await preflight({
|
|
90
|
-
[CH.Origin]:
|
|
90
|
+
[CH.Origin]: FooDotTest,
|
|
91
91
|
[CH.AcRequestMethod]: 'GET'
|
|
92
92
|
})
|
|
93
93
|
headerIs(p, CH.AcAllowOrigin, null)
|
|
@@ -99,11 +99,11 @@ await describe('CORS', async () => {
|
|
|
99
99
|
|
|
100
100
|
await test('not in allowed origins', async () => {
|
|
101
101
|
corsConfig = {
|
|
102
|
-
corsOrigins: [
|
|
102
|
+
corsOrigins: [AllowedDotTest],
|
|
103
103
|
corsMethods: ['GET']
|
|
104
104
|
}
|
|
105
105
|
const p = await preflight({
|
|
106
|
-
[CH.Origin]:
|
|
106
|
+
[CH.Origin]: NotAllowedDotTest,
|
|
107
107
|
[CH.AcRequestMethod]: 'GET'
|
|
108
108
|
})
|
|
109
109
|
headerIs(p, CH.AcAllowOrigin, null)
|
|
@@ -114,14 +114,14 @@ await describe('CORS', async () => {
|
|
|
114
114
|
|
|
115
115
|
await test('origin and method match', async () => {
|
|
116
116
|
corsConfig = {
|
|
117
|
-
corsOrigins: [
|
|
117
|
+
corsOrigins: [AllowedDotTest],
|
|
118
118
|
corsMethods: ['GET']
|
|
119
119
|
}
|
|
120
120
|
const p = await preflight({
|
|
121
|
-
[CH.Origin]:
|
|
121
|
+
[CH.Origin]: AllowedDotTest,
|
|
122
122
|
[CH.AcRequestMethod]: 'GET'
|
|
123
123
|
})
|
|
124
|
-
headerIs(p, CH.AcAllowOrigin,
|
|
124
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotTest)
|
|
125
125
|
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
126
126
|
headerIs(p, CH.AcAllowCredentials, null)
|
|
127
127
|
headerIs(p, CH.AcAllowHeaders, null)
|
|
@@ -129,14 +129,14 @@ await describe('CORS', async () => {
|
|
|
129
129
|
|
|
130
130
|
await test('origin matches from multiple', async () => {
|
|
131
131
|
corsConfig = {
|
|
132
|
-
corsOrigins: [
|
|
132
|
+
corsOrigins: [AllowedDotTest, FooDotTest],
|
|
133
133
|
corsMethods: ['GET']
|
|
134
134
|
}
|
|
135
135
|
const p = await preflight({
|
|
136
|
-
[CH.Origin]:
|
|
136
|
+
[CH.Origin]: AllowedDotTest,
|
|
137
137
|
[CH.AcRequestMethod]: 'GET'
|
|
138
138
|
})
|
|
139
|
-
headerIs(p, CH.AcAllowOrigin,
|
|
139
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotTest)
|
|
140
140
|
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
141
141
|
headerIs(p, CH.AcAllowCredentials, null)
|
|
142
142
|
headerIs(p, CH.AcAllowHeaders, null)
|
|
@@ -148,10 +148,10 @@ await describe('CORS', async () => {
|
|
|
148
148
|
corsMethods: ['GET']
|
|
149
149
|
}
|
|
150
150
|
const p = await preflight({
|
|
151
|
-
[CH.Origin]:
|
|
151
|
+
[CH.Origin]: FooDotTest,
|
|
152
152
|
[CH.AcRequestMethod]: 'GET'
|
|
153
153
|
})
|
|
154
|
-
headerIs(p, CH.AcAllowOrigin,
|
|
154
|
+
headerIs(p, CH.AcAllowOrigin, FooDotTest)
|
|
155
155
|
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
156
156
|
headerIs(p, CH.AcAllowCredentials, null)
|
|
157
157
|
headerIs(p, CH.AcAllowHeaders, null)
|
|
@@ -164,10 +164,10 @@ await describe('CORS', async () => {
|
|
|
164
164
|
corsCredentials: true
|
|
165
165
|
}
|
|
166
166
|
const p = await preflight({
|
|
167
|
-
[CH.Origin]:
|
|
167
|
+
[CH.Origin]: FooDotTest,
|
|
168
168
|
[CH.AcRequestMethod]: 'GET'
|
|
169
169
|
})
|
|
170
|
-
headerIs(p, CH.AcAllowOrigin,
|
|
170
|
+
headerIs(p, CH.AcAllowOrigin, FooDotTest)
|
|
171
171
|
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
172
172
|
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
173
173
|
headerIs(p, CH.AcAllowHeaders, null)
|
|
@@ -181,10 +181,10 @@ await describe('CORS', async () => {
|
|
|
181
181
|
corsHeaders: ['content-type', 'my-header']
|
|
182
182
|
}
|
|
183
183
|
const p = await preflight({
|
|
184
|
-
[CH.Origin]:
|
|
184
|
+
[CH.Origin]: FooDotTest,
|
|
185
185
|
[CH.AcRequestMethod]: 'GET'
|
|
186
186
|
})
|
|
187
|
-
headerIs(p, CH.AcAllowOrigin,
|
|
187
|
+
headerIs(p, CH.AcAllowOrigin, FooDotTest)
|
|
188
188
|
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
189
189
|
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
190
190
|
headerIs(p, CH.AcAllowHeaders, 'content-type,my-header')
|
|
@@ -198,7 +198,7 @@ await describe('CORS', async () => {
|
|
|
198
198
|
corsMethods: ['GET']
|
|
199
199
|
}
|
|
200
200
|
const p = await request({
|
|
201
|
-
[CH.Origin]:
|
|
201
|
+
[CH.Origin]: NotAllowedDotTest
|
|
202
202
|
})
|
|
203
203
|
equal(p.status, 200)
|
|
204
204
|
headerIs(p, CH.AcAllowOrigin, null)
|
|
@@ -208,16 +208,16 @@ await describe('CORS', async () => {
|
|
|
208
208
|
|
|
209
209
|
await test('origin allowed', async () => {
|
|
210
210
|
corsConfig = {
|
|
211
|
-
corsOrigins: [
|
|
211
|
+
corsOrigins: [AllowedDotTest],
|
|
212
212
|
corsMethods: ['GET'],
|
|
213
213
|
corsCredentials: true,
|
|
214
214
|
corsExposedHeaders: ['x-h1', 'x-h2']
|
|
215
215
|
}
|
|
216
216
|
const p = await request({
|
|
217
|
-
[CH.Origin]:
|
|
217
|
+
[CH.Origin]: AllowedDotTest
|
|
218
218
|
})
|
|
219
219
|
equal(p.status, 200)
|
|
220
|
-
headerIs(p, CH.AcAllowOrigin,
|
|
220
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotTest)
|
|
221
221
|
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
222
222
|
headerIs(p, CH.AcExposeHeaders, 'x-h1,x-h2')
|
|
223
223
|
})
|
package/src/server/utils/jwt.js
CHANGED
|
@@ -9,13 +9,8 @@ export function jwtCookie(cookieName, payload, path = '/') {
|
|
|
9
9
|
function jwt(payload) {
|
|
10
10
|
return [
|
|
11
11
|
'Header_Not_In_Use',
|
|
12
|
-
|
|
12
|
+
Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url'),
|
|
13
13
|
'Signature_Not_In_Use'
|
|
14
14
|
].join('.')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function toBase64Url(obj) {
|
|
18
|
-
return btoa(JSON.stringify(obj))
|
|
19
|
-
.replace('+', '-')
|
|
20
|
-
.replace('/', '_')
|
|
21
|
-
}
|
package/src/server/utils/mime.js
CHANGED
|
@@ -11,9 +11,13 @@ import { UNKNOWN_MIME_EXT } from '../ApiConstants.js'
|
|
|
11
11
|
const extToMime = {
|
|
12
12
|
'3g2': 'video/3gpp2',
|
|
13
13
|
'3gp': 'video/3gpp',
|
|
14
|
+
'3mf': 'model/3mf',
|
|
14
15
|
'7z': 'application/x-7z-compressed',
|
|
15
16
|
aac: 'audio/aac',
|
|
16
17
|
abw: 'application/x-abiword',
|
|
18
|
+
aif: 'audio/aiff',
|
|
19
|
+
aifc: 'audio/aiff',
|
|
20
|
+
aiff: 'audio/aiff',
|
|
17
21
|
apng: 'image/apng',
|
|
18
22
|
arc: 'application/x-freearc',
|
|
19
23
|
avi: 'video/x-msvideo',
|
|
@@ -28,12 +32,22 @@ const extToMime = {
|
|
|
28
32
|
csh: 'application/x-csh',
|
|
29
33
|
css: 'text/css',
|
|
30
34
|
csv: 'text/csv',
|
|
35
|
+
dae: 'model/vnd.collada+xml',
|
|
31
36
|
doc: 'application/msword',
|
|
32
37
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
38
|
+
drc: 'model/vnd.draco',
|
|
39
|
+
eml: 'message/rfc822',
|
|
33
40
|
eot: 'application/vnd.ms-fontobject',
|
|
34
41
|
epub: 'application/epub+zip',
|
|
42
|
+
exe: 'application/vnd.microsoft.portable-executable',
|
|
43
|
+
fbx: 'application/octet-stream',
|
|
44
|
+
flac: 'audio/flac',
|
|
35
45
|
gif: 'image/gif',
|
|
46
|
+
glb: 'model/gltf-binary',
|
|
47
|
+
gltf: 'model/gltf+json',
|
|
36
48
|
gz: 'application/gzip',
|
|
49
|
+
heic: 'image/heic',
|
|
50
|
+
heif: 'image/heif',
|
|
37
51
|
htm: 'text/html',
|
|
38
52
|
html: 'text/html',
|
|
39
53
|
ico: 'image/vnd.microsoft.icon',
|
|
@@ -44,13 +58,21 @@ const extToMime = {
|
|
|
44
58
|
js: 'application/javascript',
|
|
45
59
|
json: 'application/json',
|
|
46
60
|
jsonld: 'application/ld+json',
|
|
61
|
+
lz: 'application/x-lzip',
|
|
62
|
+
m4a: 'audio/mp4',
|
|
63
|
+
map: 'application/json',
|
|
64
|
+
md: 'text/markdown',
|
|
47
65
|
mid: 'audio/midi',
|
|
48
66
|
midi: 'audio/midi',
|
|
49
67
|
mjs: 'text/javascript',
|
|
68
|
+
mkv: 'video/x-matroska',
|
|
69
|
+
mov: 'video/quicktime',
|
|
50
70
|
mp3: 'audio/mpeg',
|
|
51
71
|
mp4: 'video/mp4',
|
|
52
72
|
mpeg: 'video/mpeg',
|
|
53
73
|
mpkg: 'application/vnd.apple.installer+xml',
|
|
74
|
+
mtl: 'text/plain',
|
|
75
|
+
obj: 'text/plain',
|
|
54
76
|
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
55
77
|
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
56
78
|
odt: 'application/vnd.oasis.opendocument.text',
|
|
@@ -61,19 +83,24 @@ const extToMime = {
|
|
|
61
83
|
otf: 'font/otf',
|
|
62
84
|
pdf: 'application/pdf',
|
|
63
85
|
php: 'application/x-httpd-php',
|
|
86
|
+
ply: 'application/octet-stream',
|
|
64
87
|
png: 'image/png',
|
|
65
88
|
ppt: 'application/vnd.ms-powerpoint',
|
|
66
89
|
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
67
90
|
rar: 'application/vnd.rar',
|
|
68
91
|
rtf: 'application/rtf',
|
|
69
92
|
sh: 'application/x-sh',
|
|
93
|
+
stl: 'model/stl',
|
|
70
94
|
svg: 'image/svg+xml',
|
|
71
95
|
tar: 'application/x-tar',
|
|
72
96
|
tif: 'image/tiff',
|
|
73
97
|
ts: 'video/mp2t',
|
|
74
98
|
ttf: 'font/ttf',
|
|
75
99
|
txt: 'text/plain',
|
|
100
|
+
usd: 'model/vnd.usd',
|
|
101
|
+
usdz: 'model/vnd.usdz+zip',
|
|
76
102
|
vsd: 'application/vnd.visio',
|
|
103
|
+
wasm: 'application/wasm',
|
|
77
104
|
wav: 'audio/wav',
|
|
78
105
|
weba: 'audio/webm',
|
|
79
106
|
webm: 'video/webm',
|
|
@@ -85,9 +112,11 @@ const extToMime = {
|
|
|
85
112
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
86
113
|
xml: 'application/xml',
|
|
87
114
|
xul: 'application/vnd.mozilla.xul+xml',
|
|
115
|
+
xz: 'application/x-xz',
|
|
88
116
|
yaml: 'application/yaml',
|
|
89
117
|
yml: 'application/yaml',
|
|
90
|
-
zip: 'application/zip'
|
|
118
|
+
zip: 'application/zip',
|
|
119
|
+
zst: 'application/zstd'
|
|
91
120
|
}
|
|
92
121
|
|
|
93
122
|
const mimeToExt = mapMimeToExt(extToMime)
|
|
@@ -105,8 +134,8 @@ export function mimeFor(filename) {
|
|
|
105
134
|
}
|
|
106
135
|
function extname(filename) {
|
|
107
136
|
const i = filename.lastIndexOf('.')
|
|
108
|
-
return i === -1
|
|
109
|
-
? ''
|
|
137
|
+
return i === -1
|
|
138
|
+
? ''
|
|
110
139
|
: filename.slice(i + 1)
|
|
111
140
|
}
|
|
112
141
|
|