mockaton 12.2.1 → 12.2.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 +3 -21
- package/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/client/ApiCommander.js +1 -1
- package/src/client/ApiConstants.js +3 -2
- package/src/client/Filename.js +45 -1
- package/src/client/app-store.js +2 -2
- package/src/client/app.js +100 -105
- package/src/client/dom-utils.js +8 -13
- package/src/client/indexHtml.js +19 -20
- package/src/client/styles.css +11 -9
- package/src/server/Api.js +7 -6
- package/src/server/MockBroker.js +2 -2
- package/src/server/Mockaton.js +2 -1
- package/src/server/Mockaton.test.js +3 -2
- package/src/server/ProxyRelay.js +2 -1
- package/src/server/StaticDispatcher.js +1 -1
- package/src/server/Watcher.js +1 -1
- package/src/server/WatcherDevClient.js +3 -2
- package/src/server/mockBrokersCollection.js +1 -1
- package/src/server/utils/HttpIncomingMessage.js +2 -2
- package/src/server/utils/HttpServerResponse.js +1 -1
- package/src/server/utils/mime.js +1 -1
- package/src/server/ApiConstants.js +0 -32
- package/src/server/Filename.js +0 -65
package/README.md
CHANGED
|
@@ -8,8 +8,7 @@ for testing difficult to reproduce backend states.
|
|
|
8
8
|
[](https://codecov.io/github/ericfortis/mockaton)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
## [Documentation ↗](https://mockaton.com)
|
|
12
|
-
## [Changelog ↗](https://mockaton.com/changelog)
|
|
11
|
+
## [Documentation ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog)
|
|
13
12
|
|
|
14
13
|
## Overview
|
|
15
14
|
With Mockaton, you don’t need to write code for wiring up your
|
|
@@ -22,7 +21,6 @@ For example, for [/api/company/123](#), the filename could be:
|
|
|
22
21
|
<code>my-mocks-dir/<b>api/company</b>/[id].GET.200.json</code>
|
|
23
22
|
</pre>
|
|
24
23
|
|
|
25
|
-
<br/>
|
|
26
24
|
|
|
27
25
|
## Dashboard
|
|
28
26
|
|
|
@@ -32,23 +30,13 @@ For example, for [/api/company/123](#), the filename could be:
|
|
|
32
30
|
<img alt="Mockaton Dashboard" src="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.dark.gold.png">
|
|
33
31
|
</picture>
|
|
34
32
|
|
|
35
|
-
On the dashboard you can:
|
|
36
|
-
- Select a mock variant for a particular route
|
|
37
|
-
- 🕓 Delay responses
|
|
38
|
-
- Trigger an autogenerated `500` error
|
|
39
|
-
- …and cycle it off (for testing retries)
|
|
40
|
-
|
|
41
|
-
Nonetheless, there’s a [Control API ↗](https://mockaton.com/api), which is handy for
|
|
42
|
-
setting up tests.
|
|
43
|
-
|
|
44
33
|
<br/>
|
|
45
34
|
|
|
46
35
|
|
|
47
36
|
## Quick Start (Docker)
|
|
48
37
|
This will spin up Mockaton with the sample directories
|
|
49
|
-
included in this repo mounted on the container
|
|
50
|
-
|
|
51
|
-
_[mockaton-mocks/](./mockaton-mocks) and [mockaton-static-mocks/](./mockaton-static-mocks)_
|
|
38
|
+
included in this repo mounted on the container ([mockaton-mocks/](./mockaton-mocks)
|
|
39
|
+
and [mockaton-static-mocks/](./mockaton-static-mocks))
|
|
52
40
|
|
|
53
41
|
```sh
|
|
54
42
|
git clone https://github.com/ericfortis/mockaton.git --depth 1
|
|
@@ -57,13 +45,7 @@ make docker
|
|
|
57
45
|
```
|
|
58
46
|
Dashboard: http://localhost:2020/mockaton
|
|
59
47
|
|
|
60
|
-
|
|
61
48
|
Test it:
|
|
62
49
|
```shell
|
|
63
50
|
curl localhost:2020/api/user
|
|
64
51
|
```
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## License
|
|
69
|
-
MIT
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@ export class Commander {
|
|
|
23
23
|
|
|
24
24
|
/** @returns {Promise<Response>} */
|
|
25
25
|
setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
/** @returns {Promise<Response>} */
|
|
28
28
|
setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
|
|
29
29
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
/** @
|
|
1
|
+
/** # @SharedWithServer */
|
|
2
2
|
|
|
3
3
|
const MOUNT = '/mockaton'
|
|
4
|
+
|
|
4
5
|
export const API = {
|
|
5
6
|
dashboard: MOUNT,
|
|
6
|
-
|
|
7
|
+
|
|
7
8
|
bulkSelect: MOUNT + '/bulk-select-by-comment',
|
|
8
9
|
collectProxied: MOUNT + '/collect-proxied',
|
|
9
10
|
cookies: MOUNT + '/cookies',
|
package/src/client/Filename.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/** # @SharedWithServer */
|
|
2
|
+
|
|
3
|
+
const METHODS = [ // @KeepSync node:http.METHODS
|
|
4
|
+
'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE',
|
|
5
|
+
'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE',
|
|
6
|
+
'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS',
|
|
7
|
+
'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT',
|
|
8
|
+
'QUERY', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE',
|
|
9
|
+
'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE'
|
|
10
|
+
]
|
|
2
11
|
|
|
3
12
|
const reComments = /\(.*?\)/g // Anything within parentheses
|
|
4
13
|
|
|
@@ -6,6 +15,25 @@ export function extractComments(file) {
|
|
|
6
15
|
return Array.from(file.matchAll(reComments), ([c]) => c)
|
|
7
16
|
}
|
|
8
17
|
|
|
18
|
+
export function includesComment(file, search) {
|
|
19
|
+
return extractComments(file).some(c => c.includes(search))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export function validateFilename(file) {
|
|
24
|
+
const tokens = file.replace(reComments, '').split('.')
|
|
25
|
+
if (tokens.length < 4)
|
|
26
|
+
return 'Invalid Filename Convention'
|
|
27
|
+
|
|
28
|
+
const { status, method } = parseFilename(file)
|
|
29
|
+
if (!responseStatusIsValid(status))
|
|
30
|
+
return `Invalid HTTP Response Status: "${status}"`
|
|
31
|
+
|
|
32
|
+
if (!METHODS.includes(method))
|
|
33
|
+
return `Unrecognized HTTP Method: "${method}"`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
9
37
|
export function parseFilename(file) {
|
|
10
38
|
const tokens = file.replace(reComments, '').split('.')
|
|
11
39
|
return {
|
|
@@ -22,3 +50,19 @@ function removeTrailingSlash(url = '') {
|
|
|
22
50
|
.replace('/?', '?')
|
|
23
51
|
.replace('/#', '#')
|
|
24
52
|
}
|
|
53
|
+
|
|
54
|
+
function responseStatusIsValid(status) {
|
|
55
|
+
return Number.isInteger(status)
|
|
56
|
+
&& status >= 100
|
|
57
|
+
&& status <= 599
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function makeMockFilename(url, method, status, ext) {
|
|
61
|
+
const urlMask = replaceIds(removeTrailingSlash(url))
|
|
62
|
+
return [urlMask, method, status, ext].join('.')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const reUuidV4 = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi
|
|
66
|
+
function replaceIds(filename) {
|
|
67
|
+
return filename.replaceAll(reUuidV4, '[id]')
|
|
68
|
+
}
|
package/src/client/app-store.js
CHANGED
|
@@ -82,7 +82,7 @@ export const store = {
|
|
|
82
82
|
}
|
|
83
83
|
catch (error) { store.onError(error) }
|
|
84
84
|
},
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
async setGlobalDelayJitter(value) {
|
|
87
87
|
try {
|
|
88
88
|
const response = await api.setGlobalDelayJitter(value)
|
|
@@ -160,7 +160,7 @@ export const store = {
|
|
|
160
160
|
r.setUrlMaskDittoed(store._dittoCache.get(r.key))
|
|
161
161
|
return r
|
|
162
162
|
},
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
staticBrokersAsRows() {
|
|
165
165
|
const rows = Object.values(store.staticBrokers)
|
|
166
166
|
.map(b => new StaticBrokerRowModel(b))
|
package/src/client/app.js
CHANGED
|
@@ -11,18 +11,13 @@ import { HEADER_502 } from './ApiConstants.js'
|
|
|
11
11
|
import CSS from './styles.css' with { type: 'css' }
|
|
12
12
|
adoptCSS(CSS)
|
|
13
13
|
|
|
14
|
-
const FocusGroup = {
|
|
15
|
-
ProxyToggler: 0,
|
|
16
|
-
DelayToggler: 1,
|
|
17
|
-
StatusToggler: 2,
|
|
18
|
-
PreviewLink: 3
|
|
19
|
-
}
|
|
20
14
|
|
|
21
15
|
const t = translation => translation[0]
|
|
22
16
|
|
|
23
17
|
store.onError = onError
|
|
24
18
|
store.render = render
|
|
25
19
|
store.renderRow = renderRow
|
|
20
|
+
|
|
26
21
|
store.fetchState()
|
|
27
22
|
initRealTimeUpdates()
|
|
28
23
|
initKeyboardNavigation()
|
|
@@ -64,7 +59,8 @@ function Header() {
|
|
|
64
59
|
r('header', null,
|
|
65
60
|
r('a', {
|
|
66
61
|
className: CSS.Logo,
|
|
67
|
-
href: 'https://mockaton.com'
|
|
62
|
+
href: 'https://mockaton.com',
|
|
63
|
+
alt: t`Documentation`
|
|
68
64
|
},
|
|
69
65
|
Logo()),
|
|
70
66
|
r('div', null,
|
|
@@ -95,7 +91,6 @@ function GlobalDelayField() {
|
|
|
95
91
|
r('label', className(CSS.Field, CSS.GlobalDelayField),
|
|
96
92
|
r('span', null, t`Delay (ms)`),
|
|
97
93
|
r('input', {
|
|
98
|
-
name: 'delay',
|
|
99
94
|
type: 'number',
|
|
100
95
|
min: 0,
|
|
101
96
|
step: 100,
|
|
@@ -125,7 +120,6 @@ function GlobalDelayJitterField() {
|
|
|
125
120
|
r('label', className(CSS.Field, CSS.GlobalDelayJitterField),
|
|
126
121
|
r('span', null, t`Max Jitter %`),
|
|
127
122
|
r('input', {
|
|
128
|
-
name: 'delay-jitter',
|
|
129
123
|
type: 'number',
|
|
130
124
|
min: 0,
|
|
131
125
|
max: 300,
|
|
@@ -148,7 +142,9 @@ function CookieSelector() {
|
|
|
148
142
|
r('select', {
|
|
149
143
|
autocomplete: 'off',
|
|
150
144
|
disabled,
|
|
151
|
-
title: disabled
|
|
145
|
+
title: disabled
|
|
146
|
+
? t`No cookies specified in config.cookies`
|
|
147
|
+
: undefined,
|
|
152
148
|
onChange() { store.selectCookie(this.value) }
|
|
153
149
|
}, list.map(([value, selected]) =>
|
|
154
150
|
r('option', { value, selected }, value)))))
|
|
@@ -169,9 +165,8 @@ function ProxyFallbackField() {
|
|
|
169
165
|
r('label', null,
|
|
170
166
|
r('span', null, t`Fallback`),
|
|
171
167
|
r('input', {
|
|
172
|
-
name: 'fallback',
|
|
173
168
|
type: 'url',
|
|
174
|
-
|
|
169
|
+
name: 'fallback',
|
|
175
170
|
placeholder: t`Type backend address`,
|
|
176
171
|
value: store.proxyFallback,
|
|
177
172
|
onChange
|
|
@@ -183,7 +178,6 @@ function SaveProxiedCheckbox(ref) {
|
|
|
183
178
|
return (
|
|
184
179
|
r('label', className(CSS.SaveProxiedCheckbox),
|
|
185
180
|
r('input', {
|
|
186
|
-
name: 'save-proxied',
|
|
187
181
|
ref,
|
|
188
182
|
type: 'checkbox',
|
|
189
183
|
disabled: !store.canProxy,
|
|
@@ -204,13 +198,13 @@ function ResetButton() {
|
|
|
204
198
|
|
|
205
199
|
|
|
206
200
|
function HelpLink() {
|
|
207
|
-
return
|
|
201
|
+
return (
|
|
202
|
+
r('a', {
|
|
208
203
|
target: '_blank',
|
|
209
204
|
href: 'https://mockaton.com',
|
|
210
205
|
title: t`Documentation`,
|
|
211
206
|
className: CSS.HelpLink
|
|
212
|
-
},
|
|
213
|
-
HelpIcon())
|
|
207
|
+
}, HelpIcon()))
|
|
214
208
|
}
|
|
215
209
|
|
|
216
210
|
|
|
@@ -232,7 +226,9 @@ function BulkSelector() {
|
|
|
232
226
|
r('select', {
|
|
233
227
|
autocomplete: 'off',
|
|
234
228
|
disabled,
|
|
235
|
-
title: disabled
|
|
229
|
+
title: disabled
|
|
230
|
+
? t`No mock files have comments which are anything within parentheses on the filename.`
|
|
231
|
+
: undefined,
|
|
236
232
|
onChange
|
|
237
233
|
},
|
|
238
234
|
r('option', { value: firstOption }, firstOption),
|
|
@@ -246,7 +242,6 @@ function GroupByMethod() {
|
|
|
246
242
|
return (
|
|
247
243
|
r('label', className(CSS.GroupByMethod),
|
|
248
244
|
r('input', {
|
|
249
|
-
name: 'group-by-method',
|
|
250
245
|
type: 'checkbox',
|
|
251
246
|
checked: store.groupByMethod,
|
|
252
247
|
onChange: store.toggleGroupByMethod
|
|
@@ -284,11 +279,20 @@ function Row(row, i) {
|
|
|
284
279
|
},
|
|
285
280
|
store.canProxy && ProxyToggler(method, urlMask, row.proxied),
|
|
286
281
|
|
|
287
|
-
|
|
282
|
+
DelayToggler({
|
|
283
|
+
checked: row.delayed,
|
|
284
|
+
commit(checked) { store.setDelayed(method, urlMask, checked) },
|
|
285
|
+
}),
|
|
288
286
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
StatusCodeToggler({
|
|
288
|
+
title: t`Internal Server Error`,
|
|
289
|
+
label: t`500`,
|
|
290
|
+
disabled: row.opts.length === 1 && row.status === 500,
|
|
291
|
+
checked: !row.proxied && row.status === 500,
|
|
292
|
+
onChange() {
|
|
293
|
+
store.toggle500(method, urlMask)
|
|
294
|
+
}
|
|
295
|
+
}),
|
|
292
296
|
|
|
293
297
|
!store.groupByMethod && r('span', className(CSS.Method), method),
|
|
294
298
|
|
|
@@ -327,7 +331,6 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
|
|
|
327
331
|
...className(CSS.PreviewLink, isChosen && CSS.chosen),
|
|
328
332
|
href: urlMask,
|
|
329
333
|
autofocus,
|
|
330
|
-
'data-focus-group': FocusGroup.PreviewLink,
|
|
331
334
|
onClick
|
|
332
335
|
}, ditto
|
|
333
336
|
? [r('span', className(CSS.dittoDir), ditto), tail]
|
|
@@ -339,8 +342,15 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
|
|
|
339
342
|
function MockSelector(row) {
|
|
340
343
|
return (
|
|
341
344
|
r('select', {
|
|
342
|
-
onChange() {
|
|
343
|
-
|
|
345
|
+
onChange() {
|
|
346
|
+
store.selectFile(this.value)
|
|
347
|
+
},
|
|
348
|
+
onKeyDown(event) {
|
|
349
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft')
|
|
350
|
+
event.preventDefault()
|
|
351
|
+
// Because in Firefox they change the select.option, and
|
|
352
|
+
// we use those keys for spreadsheet-like navigation.
|
|
353
|
+
},
|
|
344
354
|
'aria-label': t`Mock Selector`,
|
|
345
355
|
disabled: row.opts.length < 2,
|
|
346
356
|
...className(
|
|
@@ -352,30 +362,6 @@ function MockSelector(row) {
|
|
|
352
362
|
}
|
|
353
363
|
|
|
354
364
|
|
|
355
|
-
function DelayRouteToggler(method, urlMask, checked) {
|
|
356
|
-
return ClickDragToggler({
|
|
357
|
-
checked,
|
|
358
|
-
commit(checked) { store.setDelayed(method, urlMask, checked) },
|
|
359
|
-
focusGroup: FocusGroup.DelayToggler
|
|
360
|
-
})
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function InternalServerErrorToggler(method, urlMask, checked, disabled) {
|
|
364
|
-
return (
|
|
365
|
-
r('label', {
|
|
366
|
-
className: CSS.InternalServerErrorToggler,
|
|
367
|
-
title: t`Internal Server Error`
|
|
368
|
-
},
|
|
369
|
-
r('input', {
|
|
370
|
-
type: 'checkbox',
|
|
371
|
-
disabled,
|
|
372
|
-
checked,
|
|
373
|
-
onChange() { store.toggle500(method, urlMask) },
|
|
374
|
-
'data-focus-group': FocusGroup.StatusToggler
|
|
375
|
-
}),
|
|
376
|
-
r('span', className(CSS.checkboxBody), t`500`)))
|
|
377
|
-
}
|
|
378
|
-
|
|
379
365
|
function ProxyToggler(method, urlMask, checked) {
|
|
380
366
|
return (
|
|
381
367
|
r('label', {
|
|
@@ -386,7 +372,6 @@ function ProxyToggler(method, urlMask, checked) {
|
|
|
386
372
|
type: 'checkbox',
|
|
387
373
|
checked,
|
|
388
374
|
onChange() { store.setProxied(method, urlMask, this.checked) },
|
|
389
|
-
'data-focus-group': FocusGroup.ProxyToggler
|
|
390
375
|
}),
|
|
391
376
|
CloudIcon()))
|
|
392
377
|
}
|
|
@@ -404,7 +389,9 @@ function StaticFilesList() {
|
|
|
404
389
|
className(CSS.TableHeading,
|
|
405
390
|
store.canProxy && CSS.canProxy,
|
|
406
391
|
!store.groupByMethod && CSS.nonGroupedByMethod),
|
|
407
|
-
store.groupByMethod
|
|
392
|
+
store.groupByMethod
|
|
393
|
+
? t`Static GET`
|
|
394
|
+
: t`Static`),
|
|
408
395
|
rows.map(StaticRow))
|
|
409
396
|
}
|
|
410
397
|
|
|
@@ -418,9 +405,25 @@ function StaticRow(row) {
|
|
|
418
405
|
...className(CSS.TableRow,
|
|
419
406
|
render.count > 1 && row.isNew && CSS.animIn)
|
|
420
407
|
},
|
|
421
|
-
DelayStaticRouteToggler(row.urlMask, row.delayed),
|
|
422
408
|
|
|
423
|
-
|
|
409
|
+
DelayToggler({
|
|
410
|
+
optClassName: store.canProxy && CSS.canProxy,
|
|
411
|
+
checked: row.delayed,
|
|
412
|
+
commit(checked) {
|
|
413
|
+
store.setDelayedStatic(row.urlMask, checked)
|
|
414
|
+
}
|
|
415
|
+
}),
|
|
416
|
+
|
|
417
|
+
StatusCodeToggler({
|
|
418
|
+
title: t`Not Found`,
|
|
419
|
+
label: t`404`,
|
|
420
|
+
checked: row.status === 404,
|
|
421
|
+
onChange() {
|
|
422
|
+
store.setStaticRouteStatus(row.urlMask, this.checked
|
|
423
|
+
? 404
|
|
424
|
+
: 200)
|
|
425
|
+
}
|
|
426
|
+
}),
|
|
424
427
|
|
|
425
428
|
!groupByMethod && r('span', className(CSS.Method), 'GET'),
|
|
426
429
|
|
|
@@ -428,42 +431,27 @@ function StaticRow(row) {
|
|
|
428
431
|
href: row.urlMask,
|
|
429
432
|
target: '_blank',
|
|
430
433
|
className: CSS.PreviewLink,
|
|
431
|
-
'data-focus-group': FocusGroup.PreviewLink
|
|
432
434
|
}, ditto
|
|
433
435
|
? [r('span', className(CSS.dittoDir), ditto), tail]
|
|
434
436
|
: tail)))
|
|
435
437
|
}
|
|
436
438
|
|
|
437
|
-
function
|
|
438
|
-
return ClickDragToggler({
|
|
439
|
-
optClassName: store.canProxy && CSS.canProxy,
|
|
440
|
-
checked,
|
|
441
|
-
focusGroup: FocusGroup.DelayToggler,
|
|
442
|
-
commit(checked) {
|
|
443
|
-
store.setDelayedStatic(route, checked)
|
|
444
|
-
}
|
|
445
|
-
})
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function NotFoundToggler(route, checked) {
|
|
439
|
+
function StatusCodeToggler({ title, label, onChange, checked }) {
|
|
449
440
|
return (
|
|
450
441
|
r('label', {
|
|
451
|
-
|
|
452
|
-
|
|
442
|
+
title,
|
|
443
|
+
className: CSS.StatusCodeToggler
|
|
453
444
|
},
|
|
454
445
|
r('input', {
|
|
455
446
|
type: 'checkbox',
|
|
456
447
|
checked,
|
|
457
|
-
|
|
458
|
-
onChange() {
|
|
459
|
-
store.setStaticRouteStatus(route, this.checked ? 404 : 200)
|
|
460
|
-
}
|
|
448
|
+
onChange
|
|
461
449
|
}),
|
|
462
|
-
r('span', className(CSS.checkboxBody),
|
|
450
|
+
r('span', className(CSS.checkboxBody), label)))
|
|
463
451
|
}
|
|
464
452
|
|
|
465
453
|
|
|
466
|
-
function
|
|
454
|
+
function DelayToggler({ checked, commit, optClassName }) {
|
|
467
455
|
function onPointerEnter(event) {
|
|
468
456
|
if (event.buttons === 1)
|
|
469
457
|
onPointerDown.call(this)
|
|
@@ -487,7 +475,6 @@ function ClickDragToggler({ checked, commit, focusGroup, optClassName }) {
|
|
|
487
475
|
},
|
|
488
476
|
r('input', {
|
|
489
477
|
type: 'checkbox',
|
|
490
|
-
'data-focus-group': focusGroup,
|
|
491
478
|
checked,
|
|
492
479
|
onPointerEnter,
|
|
493
480
|
onPointerDown,
|
|
@@ -557,9 +544,10 @@ function PayloadViewer() {
|
|
|
557
544
|
}
|
|
558
545
|
|
|
559
546
|
function RightToolbar() {
|
|
560
|
-
return
|
|
561
|
-
r('
|
|
562
|
-
|
|
547
|
+
return (
|
|
548
|
+
r('div', className(CSS.SubToolbar),
|
|
549
|
+
r('h2', { ref: payloadViewerTitleRef },
|
|
550
|
+
!store.hasChosenLink && t`Preview`)))
|
|
563
551
|
}
|
|
564
552
|
|
|
565
553
|
|
|
@@ -589,7 +577,11 @@ const SPINNER_DELAY = 80
|
|
|
589
577
|
function PayloadViewerProgressBar() {
|
|
590
578
|
return (
|
|
591
579
|
r('div', className(CSS.ProgressBar),
|
|
592
|
-
r('div', {
|
|
580
|
+
r('div', {
|
|
581
|
+
style: {
|
|
582
|
+
animationDuration: store.delay - SPINNER_DELAY + 'ms'
|
|
583
|
+
}
|
|
584
|
+
})))
|
|
593
585
|
}
|
|
594
586
|
|
|
595
587
|
async function previewMock() {
|
|
@@ -765,45 +757,48 @@ function initRealTimeUpdates() {
|
|
|
765
757
|
|
|
766
758
|
|
|
767
759
|
function initKeyboardNavigation() {
|
|
768
|
-
|
|
760
|
+
const columnSelectors = [
|
|
761
|
+
`.${CSS.TableRow} .${CSS.ProxyToggler} input`,
|
|
762
|
+
`.${CSS.TableRow} .${CSS.DelayToggler} input`,
|
|
763
|
+
`.${CSS.TableRow} .${CSS.StatusCodeToggler} input`,
|
|
764
|
+
`.${CSS.TableRow} .${CSS.PreviewLink}`,
|
|
765
|
+
// No .MockSelector because down/up arrows have native behavior on them
|
|
766
|
+
]
|
|
769
767
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
768
|
+
const rowSelectors = [
|
|
769
|
+
...columnSelectors,
|
|
770
|
+
`.${CSS.TableRow} .${CSS.MockSelector}:enabled`,
|
|
771
|
+
]
|
|
772
|
+
|
|
773
|
+
addEventListener('keydown', function ({ key }) {
|
|
774
|
+
switch (key) {
|
|
773
775
|
case 'ArrowDown':
|
|
774
776
|
case 'ArrowUp': {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
777
|
+
const pivot = document.activeElement
|
|
778
|
+
const sel = columnSelectors.find(s => pivot?.matches(s))
|
|
779
|
+
if (sel) {
|
|
780
|
+
const offset = key === 'ArrowDown' ? +1 : -1
|
|
781
|
+
const siblings = leftSideRef.elem.querySelectorAll(sel)
|
|
782
|
+
circularAdjacent(offset, siblings, pivot).focus()
|
|
779
783
|
}
|
|
780
784
|
break
|
|
781
785
|
}
|
|
782
786
|
case 'ArrowRight':
|
|
783
787
|
case 'ArrowLeft': {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
788
|
+
const pivot = document.activeElement
|
|
789
|
+
const sel = rowSelectors.find(s => pivot?.matches(s))
|
|
790
|
+
if (sel) {
|
|
791
|
+
const offset = key === 'ArrowRight' ? +1 : -1
|
|
792
|
+
const siblings = pivot.closest(`.${CSS.TableRow}`).querySelectorAll(rowSelectors.join(','))
|
|
793
|
+
circularAdjacent(offset, siblings, pivot).focus()
|
|
787
794
|
}
|
|
788
795
|
break
|
|
789
796
|
}
|
|
790
797
|
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function rowFocusable(el, step) {
|
|
794
|
-
const row = el.closest(`.${CSS.TableRow}`)
|
|
795
|
-
if (row) {
|
|
796
|
-
const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
|
|
797
|
-
return circularAdjacent(step, focusables, el)
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function allInFocusGroup(focusGroup) {
|
|
802
|
-
return Array.from(leftSideRef.elem.querySelectorAll(
|
|
803
|
-
`.${CSS.TableRow} [data-focus-group="${focusGroup}"]:is(input, a)`))
|
|
804
|
-
}
|
|
798
|
+
})
|
|
805
799
|
|
|
806
|
-
function circularAdjacent(step
|
|
800
|
+
function circularAdjacent(step, siblings, pivot) {
|
|
801
|
+
const arr = Array.from(siblings)
|
|
807
802
|
return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
|
|
808
803
|
}
|
|
809
804
|
}
|
package/src/client/dom-utils.js
CHANGED
|
@@ -6,12 +6,14 @@ export function className(...args) {
|
|
|
6
6
|
|
|
7
7
|
export function createElement(tag, props, ...children) {
|
|
8
8
|
const elem = document.createElement(tag)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
if (props)
|
|
10
|
+
for (const [k, v] of Object.entries(props))
|
|
11
|
+
if (v === undefined) continue
|
|
12
|
+
else if (k === 'ref') v.elem = elem
|
|
13
|
+
else if (k === 'style') Object.assign(elem.style, v)
|
|
14
|
+
else if (k.startsWith('on')) elem.addEventListener(k.slice(2).toLowerCase(), ...[v].flat())
|
|
15
|
+
else if (k in elem) elem[k] = v
|
|
16
|
+
else elem.setAttribute(k, v)
|
|
15
17
|
elem.append(...children.flat().filter(Boolean))
|
|
16
18
|
return elem
|
|
17
19
|
}
|
|
@@ -66,13 +68,6 @@ function selectorFor(elem) {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
// Minimal implementation of CSS Modules in the browser
|
|
71
|
-
// TODO think about avoiding clashes when using multiple files. e.g.:
|
|
72
|
-
// - should the user pass a prefix?, or
|
|
73
|
-
// - should the ensure there's a unique top-level classname on each file
|
|
74
|
-
// TODO ignore rules in comments?
|
|
75
|
-
|
|
76
71
|
export function adoptCSS(sheet) {
|
|
77
72
|
document.adoptedStyleSheets.push(sheet)
|
|
78
73
|
Object.assign(sheet, extractClassNames(sheet))
|
package/src/client/indexHtml.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { API } from './ApiConstants.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
export const CSP = [
|
|
5
4
|
`default-src 'self'`,
|
|
6
5
|
`img-src data: blob: 'self'`
|
|
@@ -9,24 +8,24 @@ export const CSP = [
|
|
|
9
8
|
|
|
10
9
|
// language=html
|
|
11
10
|
export const IndexHtml = (hotReloadEnabled, version) => `
|
|
12
|
-
<!DOCTYPE html>
|
|
13
|
-
<html lang="en-US">
|
|
14
|
-
<head>
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
<!DOCTYPE html>
|
|
12
|
+
<html lang="en-US">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="UTF-8">
|
|
15
|
+
<base href="${API.dashboard}/">
|
|
16
|
+
|
|
17
|
+
<script type="module" src="app.js"></script>
|
|
18
|
+
<link rel="preload" href="${API.state}" as="fetch" crossorigin>
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
: ''}
|
|
30
|
-
</body>
|
|
31
|
-
</html>
|
|
20
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
22
|
+
<meta name="description" content="HTTP Mock Server">
|
|
23
|
+
<title>Mockaton v${version}</title>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
${hotReloadEnabled
|
|
27
|
+
? '<script type="module" src="watcherDev.js"></script>'
|
|
28
|
+
: ''}
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
32
31
|
`
|
package/src/client/styles.css
CHANGED
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
:root {
|
|
8
8
|
color-scheme: light;
|
|
9
9
|
--color4xxBackground: #ffedd1;
|
|
10
|
-
--colorAccent: #
|
|
10
|
+
--colorAccent: #0059dd;
|
|
11
11
|
--colorBackground: #fff;
|
|
12
12
|
--colorComboBoxHeaderBackground: #fff;
|
|
13
13
|
--colorComboBoxBackground: #eee;
|
|
14
|
-
--colorHeaderBackground: #
|
|
14
|
+
--colorHeaderBackground: #f2f2f3;
|
|
15
15
|
--colorSecondaryButtonBackground: #fcfcfc;
|
|
16
16
|
--colorSecondaryActionBorder: #e0e0e0;
|
|
17
17
|
--colorSecondaryAction: #666;
|
|
18
18
|
--colorDisabledMockSelector: #444;
|
|
19
|
-
--colorHover: rgba(119, 193, 255, 0.
|
|
19
|
+
--colorHover: rgba(119, 193, 255, 0.4);
|
|
20
20
|
--colorLabel: #555;
|
|
21
21
|
--colorLightRed: #ffe4ee;
|
|
22
22
|
--colorRed: #da0f00;
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
--color4xxBackground: #68554a;
|
|
33
33
|
--colorAccent: #2495ff;
|
|
34
34
|
--colorBackground: #181818;
|
|
35
|
-
--colorHeaderBackground: #
|
|
35
|
+
--colorHeaderBackground: #141414;
|
|
36
36
|
--colorComboBoxBackground: #2a2a2a;
|
|
37
|
-
--colorSecondaryButtonBackground: #
|
|
37
|
+
--colorSecondaryButtonBackground: #2c2c2c;
|
|
38
38
|
--colorSecondaryActionBorder: #333;
|
|
39
39
|
--colorSecondaryAction: #aaa;
|
|
40
40
|
--colorComboBoxHeaderBackground: #222;
|
|
@@ -72,6 +72,7 @@ body {
|
|
|
72
72
|
font-family: inherit;
|
|
73
73
|
font-size: 100%;
|
|
74
74
|
scrollbar-width: thin;
|
|
75
|
+
corner-shape: squircle
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
a:focus-visible,
|
|
@@ -86,7 +87,6 @@ a,
|
|
|
86
87
|
select,
|
|
87
88
|
button,
|
|
88
89
|
input[type=checkbox] {
|
|
89
|
-
outline: 0;
|
|
90
90
|
cursor: pointer;
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -198,6 +198,7 @@ header {
|
|
|
198
198
|
flex-shrink: 0;
|
|
199
199
|
align-self: end;
|
|
200
200
|
margin-left: auto;
|
|
201
|
+
border-radius: 50%;
|
|
201
202
|
fill: var(--colorSecondaryAction);
|
|
202
203
|
|
|
203
204
|
&:hover {
|
|
@@ -457,6 +458,7 @@ main {
|
|
|
457
458
|
min-width: 140px;
|
|
458
459
|
flex-grow: 1;
|
|
459
460
|
padding: 6px 8px;
|
|
461
|
+
margin-right: -2px;
|
|
460
462
|
margin-left: 4px;
|
|
461
463
|
border-radius: var(--radius);
|
|
462
464
|
word-break: break-word;
|
|
@@ -476,7 +478,7 @@ main {
|
|
|
476
478
|
|
|
477
479
|
|
|
478
480
|
.MockSelector {
|
|
479
|
-
min-width:
|
|
481
|
+
min-width: 58px;
|
|
480
482
|
height: 24px;
|
|
481
483
|
padding-right: 20px;
|
|
482
484
|
padding-left: 8px;
|
|
@@ -493,6 +495,7 @@ main {
|
|
|
493
495
|
}
|
|
494
496
|
&:disabled {
|
|
495
497
|
padding-right: 4px;
|
|
498
|
+
padding-left: 0;
|
|
496
499
|
border-color: transparent;
|
|
497
500
|
appearance: none;
|
|
498
501
|
background: transparent;
|
|
@@ -604,8 +607,7 @@ main {
|
|
|
604
607
|
}
|
|
605
608
|
}
|
|
606
609
|
|
|
607
|
-
.
|
|
608
|
-
.NotFoundToggler {
|
|
610
|
+
.StatusCodeToggler {
|
|
609
611
|
display: flex;
|
|
610
612
|
margin-right: 10px;
|
|
611
613
|
margin-left: 8px;
|
package/src/server/Api.js
CHANGED
|
@@ -13,9 +13,10 @@ import {
|
|
|
13
13
|
import { longPollClientSyncVersion } from './Watcher.js'
|
|
14
14
|
|
|
15
15
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
16
|
+
|
|
17
|
+
import { API } from '../client/ApiConstants.js'
|
|
16
18
|
import { IndexHtml, CSP } from '../client/indexHtml.js'
|
|
17
19
|
|
|
18
|
-
import { API } from './ApiConstants.js'
|
|
19
20
|
import { cookie } from './cookie.js'
|
|
20
21
|
import { config, ConfigValidator } from './config.js'
|
|
21
22
|
|
|
@@ -29,7 +30,7 @@ export const apiGetReqs = new Map([
|
|
|
29
30
|
|
|
30
31
|
[API.state, getState],
|
|
31
32
|
[API.syncVersion, longPollClientSyncVersion],
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
[API.watchHotReload, longPollDevClientHotReload],
|
|
34
35
|
[API.throws, () => { throw new Error('Test500') }]
|
|
35
36
|
])
|
|
@@ -41,17 +42,17 @@ export const apiPatchReqs = new Map([
|
|
|
41
42
|
[API.cookies, selectCookie],
|
|
42
43
|
[API.globalDelay, setGlobalDelay],
|
|
43
44
|
[API.globalDelayJitter, setGlobalDelayJitter],
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
[API.fallback, setProxyFallback],
|
|
46
47
|
[API.collectProxied, setCollectProxied],
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
[API.bulkSelect, bulkUpdateBrokersByCommentTag],
|
|
49
50
|
|
|
50
51
|
[API.delay, setRouteIsDelayed],
|
|
51
52
|
[API.select, selectMock],
|
|
52
53
|
[API.proxied, setRouteIsProxied],
|
|
53
54
|
[API.toggle500, toggleRoute500],
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
[API.delayStatic, setStaticRouteIsDelayed],
|
|
56
57
|
[API.staticStatus, setStaticRouteStatusCode]
|
|
57
58
|
])
|
|
@@ -220,7 +221,7 @@ async function setRouteIsProxied(req, response) {
|
|
|
220
221
|
|
|
221
222
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
222
223
|
if (!broker)
|
|
223
|
-
response.unprocessable(
|
|
224
|
+
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
224
225
|
else if (typeof proxied !== 'boolean')
|
|
225
226
|
response.unprocessable(`Expected boolean for "proxied"`)
|
|
226
227
|
else if (proxied && !config.proxyFallback)
|
package/src/server/MockBroker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_MOCK_COMMENT } from '
|
|
2
|
-
import { includesComment, extractComments
|
|
1
|
+
import { DEFAULT_MOCK_COMMENT } from '../client/ApiConstants.js'
|
|
2
|
+
import { parseFilename, includesComment, extractComments } from '../client/Filename.js'
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
/**
|
package/src/server/Mockaton.js
CHANGED
|
@@ -8,7 +8,8 @@ import { IncomingMessage } from './utils/HttpIncomingMessage.js'
|
|
|
8
8
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
9
9
|
import { BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
|
|
10
10
|
|
|
11
|
-
import { API } from '
|
|
11
|
+
import { API } from '../client/ApiConstants.js'
|
|
12
|
+
|
|
12
13
|
import { config, setup } from './config.js'
|
|
13
14
|
import { apiPatchReqs, apiGetReqs } from './Api.js'
|
|
14
15
|
|
|
@@ -9,17 +9,18 @@ import { describe, test, before, beforeEach, after } from 'node:test'
|
|
|
9
9
|
import { mkdtempSync } from 'node:fs'
|
|
10
10
|
import { writeFile, unlink, mkdir, readFile, rename } from 'node:fs/promises'
|
|
11
11
|
|
|
12
|
+
import { API } from '../client/ApiConstants.js'
|
|
13
|
+
|
|
12
14
|
import { logger } from './utils/logger.js'
|
|
13
15
|
import { mimeFor } from './utils/mime.js'
|
|
14
16
|
import { readBody } from './utils/HttpIncomingMessage.js'
|
|
15
17
|
import { CorsHeader } from './utils/http-cors.js'
|
|
16
18
|
|
|
17
|
-
import { API } from './ApiConstants.js'
|
|
18
19
|
import { Mockaton } from './Mockaton.js'
|
|
19
|
-
import { parseFilename } from './Filename.js'
|
|
20
20
|
import { watchMocksDir, watchStaticDir } from './Watcher.js'
|
|
21
21
|
|
|
22
22
|
import { Commander } from '../client/ApiCommander.js'
|
|
23
|
+
import { parseFilename } from '../client/Filename.js'
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
const mocksDir = mkdtempSync(join(tmpdir(), 'mocks'))
|
package/src/server/ProxyRelay.js
CHANGED
|
@@ -6,7 +6,8 @@ import { write, isFile } from './utils/fs.js'
|
|
|
6
6
|
import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
|
|
7
7
|
|
|
8
8
|
import { config } from './config.js'
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
import { makeMockFilename } from '../client/Filename.js'
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
export async function proxy(req, response, delay) {
|
package/src/server/Watcher.js
CHANGED
|
@@ -5,7 +5,7 @@ import { EventEmitter } from 'node:events'
|
|
|
5
5
|
import {
|
|
6
6
|
HEADER_SYNC_VERSION,
|
|
7
7
|
LONG_POLL_SERVER_TIMEOUT
|
|
8
|
-
} from '
|
|
8
|
+
} from '../client/ApiConstants.js'
|
|
9
9
|
|
|
10
10
|
import { config } from './config.js'
|
|
11
11
|
import { isFile, isDirectory } from './utils/fs.js'
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { EventEmitter } from 'node:events'
|
|
3
3
|
import { watch, readdirSync } from 'node:fs'
|
|
4
|
+
|
|
4
5
|
import { config } from './config.js'
|
|
5
|
-
import { LONG_POLL_SERVER_TIMEOUT } from '
|
|
6
|
+
import { LONG_POLL_SERVER_TIMEOUT } from '../client/ApiConstants.js'
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
export const CLIENT_DIR = join(import.meta.dirname, '../client')
|
|
@@ -31,7 +32,7 @@ export function longPollDevClientHotReload(req, response) {
|
|
|
31
32
|
response.notFound()
|
|
32
33
|
return
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
function onDevChange(file) {
|
|
36
37
|
devClientWatcher.unsubscribe(onDevChange)
|
|
37
38
|
response.json(file)
|
|
@@ -6,7 +6,7 @@ import { listFilesRecursively } from './utils/fs.js'
|
|
|
6
6
|
import { cookie } from './cookie.js'
|
|
7
7
|
import { MockBroker } from './MockBroker.js'
|
|
8
8
|
import { config, isFileAllowed } from './config.js'
|
|
9
|
-
import { parseFilename, validateFilename } from '
|
|
9
|
+
import { parseFilename, validateFilename } from '../client/Filename.js'
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -69,7 +69,7 @@ export function hasControlChars(url) {
|
|
|
69
69
|
|
|
70
70
|
export function decode(url) {
|
|
71
71
|
const candidate = decodeURIComponent(url)
|
|
72
|
-
return candidate === decodeURIComponent(candidate)
|
|
73
|
-
? candidate
|
|
72
|
+
return candidate === decodeURIComponent(candidate)
|
|
73
|
+
? candidate
|
|
74
74
|
: '' // reject multiple encodings
|
|
75
75
|
}
|
|
@@ -3,7 +3,7 @@ import fs, { readFileSync } from 'node:fs'
|
|
|
3
3
|
|
|
4
4
|
import { logger } from './logger.js'
|
|
5
5
|
import { mimeFor } from './mime.js'
|
|
6
|
-
import { HEADER_502 } from '
|
|
6
|
+
import { HEADER_502 } from '../../client/ApiConstants.js'
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
export class ServerResponse extends http.ServerResponse {
|
package/src/server/utils/mime.js
CHANGED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// @KeepSync src/client/ApiConstants.js
|
|
2
|
-
|
|
3
|
-
const MOUNT = '/mockaton'
|
|
4
|
-
export const API = {
|
|
5
|
-
dashboard: MOUNT,
|
|
6
|
-
|
|
7
|
-
bulkSelect: MOUNT + '/bulk-select-by-comment',
|
|
8
|
-
collectProxied: MOUNT + '/collect-proxied',
|
|
9
|
-
cookies: MOUNT + '/cookies',
|
|
10
|
-
cors: MOUNT + '/cors',
|
|
11
|
-
delay: MOUNT + '/delay',
|
|
12
|
-
delayStatic: MOUNT + '/delay-static',
|
|
13
|
-
fallback: MOUNT + '/fallback',
|
|
14
|
-
globalDelay: MOUNT + '/global-delay',
|
|
15
|
-
globalDelayJitter: MOUNT + '/global-delay-jitter',
|
|
16
|
-
proxied: MOUNT + '/proxied',
|
|
17
|
-
reset: MOUNT + '/reset',
|
|
18
|
-
select: MOUNT + '/select',
|
|
19
|
-
state: MOUNT + '/state',
|
|
20
|
-
staticStatus: MOUNT + '/static-status',
|
|
21
|
-
syncVersion: MOUNT + '/sync-version',
|
|
22
|
-
throws: MOUNT + '/throws',
|
|
23
|
-
toggle500: MOUNT + '/toggle500',
|
|
24
|
-
watchHotReload: MOUNT + '/watch-hot-reload',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const HEADER_502 = 'Mockaton502'
|
|
28
|
-
export const HEADER_SYNC_VERSION = 'sync_version'
|
|
29
|
-
|
|
30
|
-
export const DEFAULT_MOCK_COMMENT = '(default)'
|
|
31
|
-
export const UNKNOWN_MIME_EXT = 'unknown'
|
|
32
|
-
export const LONG_POLL_SERVER_TIMEOUT = 8_000
|
package/src/server/Filename.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/** @KeepSync src/client/Filename.js */
|
|
2
|
-
|
|
3
|
-
import { METHODS } from 'node:http'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const reComments = /\(.*?\)/g // Anything within parentheses
|
|
7
|
-
|
|
8
|
-
export function extractComments(file) {
|
|
9
|
-
return Array.from(file.matchAll(reComments), ([c]) => c)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function includesComment(file, search) {
|
|
13
|
-
return extractComments(file).some(c => c.includes(search))
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export function validateFilename(file) {
|
|
18
|
-
const tokens = file.replace(reComments, '').split('.')
|
|
19
|
-
if (tokens.length < 4)
|
|
20
|
-
return 'Invalid Filename Convention'
|
|
21
|
-
|
|
22
|
-
const { status, method } = parseFilename(file)
|
|
23
|
-
if (!responseStatusIsValid(status))
|
|
24
|
-
return `Invalid HTTP Response Status: "${status}"`
|
|
25
|
-
|
|
26
|
-
if (!METHODS.includes(method))
|
|
27
|
-
return `Unrecognized HTTP Method: "${method}"`
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export function parseFilename(file) {
|
|
32
|
-
const tokens = file.replace(reComments, '').split('.')
|
|
33
|
-
return {
|
|
34
|
-
ext: tokens.pop(),
|
|
35
|
-
status: Number(tokens.pop()),
|
|
36
|
-
method: tokens.pop(),
|
|
37
|
-
urlMask: '/' + removeTrailingSlash(tokens.join('.'))
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function removeTrailingSlash(url = '') {
|
|
42
|
-
return url
|
|
43
|
-
.replace(/\/$/, '')
|
|
44
|
-
.replace('/?', '?')
|
|
45
|
-
.replace('/#', '#')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function responseStatusIsValid(status) {
|
|
49
|
-
return Number.isInteger(status)
|
|
50
|
-
&& status >= 100
|
|
51
|
-
&& status <= 599
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
export function makeMockFilename(url, method, status, ext) {
|
|
56
|
-
const urlMask = replaceIds(removeTrailingSlash(url))
|
|
57
|
-
return [urlMask, method, status, ext].join('.')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const reUuidV4 = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi
|
|
61
|
-
function replaceIds(filename) {
|
|
62
|
-
return filename.replaceAll(reUuidV4, '[id]')
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|