mockaton 12.6.0 → 12.7.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/index.d.ts +1 -0
- package/index.js +2 -1
- package/package.json +1 -1
- package/src/client/ApiCommander.js +3 -0
- package/src/client/ApiConstants.js +4 -1
- package/src/client/IndexHtml.js +32 -0
- package/src/client/app-store.js +64 -64
- package/src/client/app.css +53 -24
- package/src/client/app.js +40 -8
- package/src/client/dom-utils.js +1 -2
- package/src/client/payload-viewer.js +5 -3
- package/src/server/Api.js +20 -3
- package/src/server/MockDispatcher.js +2 -18
- package/src/server/MockDispatcherPlugins.js +22 -0
- package/src/server/Mockaton.test.config.js +17 -0
- package/src/server/Mockaton.test.js +109 -84
- package/src/server/Watcher.js +19 -2
- package/src/server/WatcherDevClient.js +1 -1
- package/src/server/cli.js +3 -0
- package/src/server/cli.test.js +39 -0
- package/src/server/config.js +2 -1
- package/src/server/utils/mime.js +5 -8
- package/src/server/utils/mime.test.js +1 -8
- package/src/client/indexHtml.js +0 -31
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { Commander } from './src/client/ApiCommander.js'
|
|
2
|
+
export { API } from './src/client/ApiConstants.js'
|
|
2
3
|
|
|
3
4
|
export { Mockaton } from './src/server/Mockaton.js'
|
|
4
5
|
export { jwtCookie } from './src/server/utils/jwt.js'
|
|
5
|
-
export { jsToJsonPlugin } from './src/server/
|
|
6
|
+
export { jsToJsonPlugin, echoFilePlugin } from './src/server/MockDispatcherPlugins.js'
|
|
6
7
|
export { parseJSON, BodyReaderError } from './src/server/utils/HttpIncomingMessage.js'
|
|
7
8
|
export { parseSplats, parseQueryParams } from './src/server/utils/UrlParsers.js'
|
|
8
9
|
|
package/package.json
CHANGED
|
@@ -30,6 +30,9 @@ export class Commander {
|
|
|
30
30
|
/** @returns {Promise<Response>} */
|
|
31
31
|
setCorsAllowed = value => this.#patch(API.cors, value)
|
|
32
32
|
|
|
33
|
+
/** @returns {Promise<Response>} */
|
|
34
|
+
setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
|
|
35
|
+
|
|
33
36
|
/** @returns {Promise<Response>} */
|
|
34
37
|
setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
|
|
35
38
|
|
|
@@ -23,11 +23,14 @@ export const API = {
|
|
|
23
23
|
throws: MOUNT + '/throws',
|
|
24
24
|
toggle500: MOUNT + '/toggle500',
|
|
25
25
|
watchHotReload: MOUNT + '/watch-hot-reload',
|
|
26
|
+
watchMocks: MOUNT + '/watch-mocks',
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export const HEADER_502 = 'Mockaton502'
|
|
29
30
|
export const HEADER_SYNC_VERSION = 'sync_version'
|
|
30
31
|
|
|
31
32
|
export const DEFAULT_MOCK_COMMENT = '(default)'
|
|
32
|
-
export const UNKNOWN_MIME_EXT = 'unknown'
|
|
33
33
|
export const LONG_POLL_SERVER_TIMEOUT = 8_000
|
|
34
|
+
|
|
35
|
+
export const EXT_UNKNOWN_MIME = 'unknown'
|
|
36
|
+
export const EXT_EMPTY = 'empty'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { API } from './ApiConstants.js'
|
|
2
|
+
|
|
3
|
+
export const CSP = [
|
|
4
|
+
`default-src 'self'`,
|
|
5
|
+
`img-src data: blob: 'self'`
|
|
6
|
+
].join(';')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// language=html
|
|
10
|
+
export const IndexHtml = (hotReloadEnabled, version) =>
|
|
11
|
+
`
|
|
12
|
+
<!DOCTYPE html>
|
|
13
|
+
<html lang="en-US">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="UTF-8">
|
|
16
|
+
<base href="${API.dashboard}/">
|
|
17
|
+
|
|
18
|
+
<script type="module" src="app.js"></script>
|
|
19
|
+
<link rel="preload" href="${API.state}" as="fetch" crossorigin>
|
|
20
|
+
|
|
21
|
+
<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">
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
23
|
+
<meta name="description" content="HTTP Mock Server">
|
|
24
|
+
<title>Mockaton v${version}</title>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
${hotReloadEnabled
|
|
28
|
+
? '<script type="module" src="watcherDev.js"></script>'
|
|
29
|
+
: ''}
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
`.trim()
|
package/src/client/app-store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Commander } from './ApiCommander.js'
|
|
2
2
|
import { parseFilename, extractComments } from './Filename.js'
|
|
3
|
+
import { EXT_UNKNOWN_MIME, EXT_EMPTY } from './ApiConstants.js'
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export const t = translation => translation[0]
|
|
@@ -25,84 +26,85 @@ export const store = {
|
|
|
25
26
|
return Boolean(store.proxyFallback)
|
|
26
27
|
},
|
|
27
28
|
|
|
29
|
+
groupByMethod: initPreference('groupByMethod'),
|
|
30
|
+
toggleGroupByMethod() {
|
|
31
|
+
store.groupByMethod = !store.groupByMethod
|
|
32
|
+
togglePreference('groupByMethod', store.groupByMethod)
|
|
33
|
+
store.render()
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
chosenLink: { method: '', urlMask: '' },
|
|
37
|
+
setChosenLink(method, urlMask) {
|
|
38
|
+
store.chosenLink = { method, urlMask }
|
|
39
|
+
},
|
|
40
|
+
get hasChosenLink() {
|
|
41
|
+
return store.chosenLink.method && store.chosenLink.urlMask
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
|
|
28
45
|
getSyncVersion: api.getSyncVersion,
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
_request(action, onSuccess) {
|
|
31
48
|
Promise.try(async () => {
|
|
32
49
|
const response = await action()
|
|
33
|
-
if (
|
|
34
|
-
|
|
50
|
+
if (response.ok) return response
|
|
51
|
+
throw response
|
|
35
52
|
})
|
|
36
53
|
.then(onSuccess)
|
|
37
54
|
.catch(store.onError)
|
|
38
55
|
},
|
|
39
56
|
|
|
40
|
-
|
|
41
|
-
store.
|
|
57
|
+
fetchState() {
|
|
58
|
+
store._request(api.getState, async response => {
|
|
42
59
|
Object.assign(store, await response.json())
|
|
43
|
-
|
|
60
|
+
|
|
44
61
|
if (store.showProxyField === null) // isFirstCall
|
|
45
62
|
store.showProxyField = Boolean(store.proxyFallback)
|
|
46
|
-
|
|
63
|
+
|
|
47
64
|
store.render()
|
|
48
65
|
})
|
|
49
66
|
},
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
store.groupByMethod = !store.groupByMethod
|
|
54
|
-
togglePreference('groupByMethod', store.groupByMethod)
|
|
55
|
-
store.render()
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
chosenLink: { method: '', urlMask: '' },
|
|
60
|
-
setChosenLink(method, urlMask) {
|
|
61
|
-
store.chosenLink = { method, urlMask }
|
|
62
|
-
},
|
|
63
|
-
get hasChosenLink() {
|
|
64
|
-
return store.chosenLink.method && store.chosenLink.urlMask
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
async reset() {
|
|
68
|
-
store._action(api.reset, () => {
|
|
68
|
+
reset() {
|
|
69
|
+
store._request(api.reset, () => {
|
|
69
70
|
store.setChosenLink('', '')
|
|
70
71
|
store.fetchState()
|
|
71
72
|
})
|
|
72
73
|
},
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
store.
|
|
76
|
-
store.fetchState)
|
|
75
|
+
bulkSelectByComment(value) {
|
|
76
|
+
store._request(() => api.bulkSelectByComment(value), () => {
|
|
77
|
+
store.fetchState()
|
|
78
|
+
})
|
|
77
79
|
},
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
store.
|
|
81
|
+
setGlobalDelay(value) {
|
|
82
|
+
store._request(() => api.setGlobalDelay(value), () => {
|
|
81
83
|
store.delay = value
|
|
82
84
|
})
|
|
83
85
|
},
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
store.
|
|
87
|
+
setGlobalDelayJitter(value) {
|
|
88
|
+
store._request(() => api.setGlobalDelayJitter(value), () => {
|
|
87
89
|
store.delayJitter = value
|
|
88
90
|
})
|
|
89
91
|
},
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
store.
|
|
93
|
+
selectCookie(name) {
|
|
94
|
+
store._request(() => api.selectCookie(name), async response => {
|
|
93
95
|
store.cookies = await response.json()
|
|
94
96
|
})
|
|
95
97
|
},
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
store.
|
|
99
|
+
setProxyFallback(value) {
|
|
100
|
+
store._request(() => api.setProxyFallback(value), () => {
|
|
99
101
|
store.proxyFallback = value
|
|
100
102
|
store.render()
|
|
101
103
|
})
|
|
102
104
|
},
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
store.
|
|
106
|
+
setCollectProxied(checked) {
|
|
107
|
+
store._request(() => api.setCollectProxied(checked), () => {
|
|
106
108
|
store.collectProxied = checked
|
|
107
109
|
})
|
|
108
110
|
},
|
|
@@ -166,8 +168,8 @@ export const store = {
|
|
|
166
168
|
store.renderRow(method, urlMask)
|
|
167
169
|
},
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
store.
|
|
171
|
+
selectFile(file) {
|
|
172
|
+
store._request(() => api.select(file), async response => {
|
|
171
173
|
const { method, urlMask } = parseFilename(file)
|
|
172
174
|
store.setBroker(await response.json())
|
|
173
175
|
store.setChosenLink(method, urlMask)
|
|
@@ -175,36 +177,36 @@ export const store = {
|
|
|
175
177
|
})
|
|
176
178
|
},
|
|
177
179
|
|
|
178
|
-
|
|
179
|
-
store.
|
|
180
|
+
toggle500(method, urlMask) {
|
|
181
|
+
store._request(() => api.toggle500(method, urlMask), async response => {
|
|
180
182
|
store.setBroker(await response.json())
|
|
181
183
|
store.setChosenLink(method, urlMask)
|
|
182
184
|
store.renderRow(method, urlMask)
|
|
183
185
|
})
|
|
184
186
|
},
|
|
185
187
|
|
|
186
|
-
|
|
187
|
-
store.
|
|
188
|
+
setProxied(method, urlMask, checked) {
|
|
189
|
+
store._request(() => api.setRouteIsProxied(method, urlMask, checked), async response => {
|
|
188
190
|
store.setBroker(await response.json())
|
|
189
191
|
store.setChosenLink(method, urlMask)
|
|
190
192
|
store.renderRow(method, urlMask)
|
|
191
193
|
})
|
|
192
194
|
},
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
store.
|
|
196
|
+
setDelayed(method, urlMask, checked) {
|
|
197
|
+
store._request(() => api.setRouteIsDelayed(method, urlMask, checked), async response => {
|
|
196
198
|
store.setBroker(await response.json())
|
|
197
199
|
})
|
|
198
200
|
},
|
|
199
201
|
|
|
200
|
-
|
|
201
|
-
store.
|
|
202
|
+
setDelayedStatic(route, checked) {
|
|
203
|
+
store._request(() => api.setStaticRouteIsDelayed(route, checked), () => {
|
|
202
204
|
store.staticBrokers[route].delayed = checked
|
|
203
205
|
})
|
|
204
206
|
},
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
store.
|
|
208
|
+
setStaticRouteStatus(route, status) {
|
|
209
|
+
store._request(() => api.setStaticRouteStatus(route, status), () => {
|
|
208
210
|
store.staticBrokers[route].status = status
|
|
209
211
|
})
|
|
210
212
|
}
|
|
@@ -250,26 +252,24 @@ function togglePreference(param, nextVal) {
|
|
|
250
252
|
* @param {string[]} paths - sorted
|
|
251
253
|
*/
|
|
252
254
|
export function dittoSplitPaths(paths) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const prev = pathsInParts[i - 1]
|
|
258
|
-
const curr = pathsInParts[i]
|
|
255
|
+
const pParts = paths.map(p => p.split('/').filter(Boolean))
|
|
256
|
+
return paths.map((p, i) => {
|
|
257
|
+
if (i === 0)
|
|
258
|
+
return ['', p]
|
|
259
259
|
|
|
260
|
+
const prev = pParts[i - 1]
|
|
261
|
+
const curr = pParts[i]
|
|
260
262
|
const min = Math.min(curr.length, prev.length)
|
|
261
263
|
let j = 0
|
|
262
264
|
while (j < min && curr[j] === prev[j])
|
|
263
265
|
j++
|
|
264
266
|
|
|
265
267
|
if (!j) // no common dirs
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
return result
|
|
268
|
+
return ['', p]
|
|
269
|
+
|
|
270
|
+
const ditto = '/' + curr.slice(0, j).join('/') + '/'
|
|
271
|
+
return [ditto, p.slice(ditto.length)]
|
|
272
|
+
})
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
|
|
@@ -343,7 +343,7 @@ export class BrokerRowModel {
|
|
|
343
343
|
const { status, ext } = parseFilename(file)
|
|
344
344
|
return [
|
|
345
345
|
status,
|
|
346
|
-
ext ===
|
|
346
|
+
ext === EXT_EMPTY || ext === EXT_UNKNOWN_MIME ? '' : ext,
|
|
347
347
|
extractComments(file).join(' ')
|
|
348
348
|
].filter(Boolean).join(' ')
|
|
349
349
|
}
|
package/src/client/app.css
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
color-scheme: light dark;
|
|
3
3
|
--colorBackground: light-dark(#fff, #181818);
|
|
4
|
-
--
|
|
4
|
+
--colorInputBackground: light-dark(#fff, #222);
|
|
5
5
|
--colorSecondaryButtonBackground: light-dark(#fcfcfc, #2c2c2c);
|
|
6
6
|
--colorHeaderBackground: light-dark(#f2f2f3, #141414);
|
|
7
7
|
--colorComboBoxBackground: light-dark(#eee, #2a2a2a);
|
|
8
|
-
--colorBorder: light-dark(#e0e0e0, #
|
|
8
|
+
--colorBorder: light-dark(#e0e0e0, #323232);
|
|
9
|
+
--colorBorderActive: light-dark(#c8c8c8, #3c3c3c);
|
|
9
10
|
--colorSecondaryAction: light-dark(#666, #aaa);
|
|
10
11
|
--colorLabel: light-dark(#555, #aaa);
|
|
11
12
|
--colorDisabledMockSelector: light-dark(#444, #a9b9b9);
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
--colorPurple: light-dark(#9b71e8, #ae81ff);
|
|
18
19
|
--colorGreen: light-dark(#388e3c, #a6e22e);
|
|
19
20
|
--color4xxBackground: light-dark(#ffedd1, #68554a);
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
accent-color: var(--colorAccent);
|
|
22
23
|
--radius: 16px;
|
|
23
24
|
}
|
|
@@ -115,10 +116,6 @@ header {
|
|
|
115
116
|
opacity: 1;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
@media (max-width: 740px) {
|
|
119
|
-
display: none;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
119
|
svg {
|
|
123
120
|
width: 120px;
|
|
124
121
|
pointer-events: none;
|
|
@@ -126,6 +123,20 @@ header {
|
|
|
126
123
|
}
|
|
127
124
|
}
|
|
128
125
|
|
|
126
|
+
@media (max-width: 570px) {
|
|
127
|
+
.Logo {
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&:has(.FallbackBackend) {
|
|
133
|
+
@media (max-width: 740px) {
|
|
134
|
+
.Logo {
|
|
135
|
+
display: none;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
> div {
|
|
130
141
|
display: flex;
|
|
131
142
|
width: 100%;
|
|
@@ -133,9 +144,11 @@ header {
|
|
|
133
144
|
align-items: flex-end;
|
|
134
145
|
gap: 16px 12px;
|
|
135
146
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
&:has(.FallbackBackend) {
|
|
148
|
+
@media (max-width: 600px) {
|
|
149
|
+
.HelpLink {
|
|
150
|
+
margin-left: unset;
|
|
151
|
+
}
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
}
|
|
@@ -209,7 +222,7 @@ header {
|
|
|
209
222
|
margin-top: 2px;
|
|
210
223
|
color: var(--colorText);
|
|
211
224
|
font-size: 11px;
|
|
212
|
-
background-color: var(--
|
|
225
|
+
background-color: var(--colorInputBackground);
|
|
213
226
|
border-radius: var(--radius);
|
|
214
227
|
}
|
|
215
228
|
|
|
@@ -301,14 +314,14 @@ main {
|
|
|
301
314
|
}
|
|
302
315
|
|
|
303
316
|
.leftSide {
|
|
304
|
-
width: 100% !important;
|
|
317
|
+
width: 100% !important; /* because it's resizable in js */
|
|
305
318
|
height: 50%;
|
|
306
319
|
border-right: 0;
|
|
307
320
|
}
|
|
308
321
|
}
|
|
309
322
|
|
|
310
323
|
.leftSide {
|
|
311
|
-
width: 50%;
|
|
324
|
+
width: 50%;
|
|
312
325
|
border-top: 1px solid var(--colorBorder);
|
|
313
326
|
border-right: 1px solid var(--colorBorder);
|
|
314
327
|
}
|
|
@@ -323,14 +336,31 @@ main {
|
|
|
323
336
|
.Resizer {
|
|
324
337
|
position: absolute;
|
|
325
338
|
top: 0;
|
|
326
|
-
left:
|
|
339
|
+
left: -1px;
|
|
327
340
|
width: 8px;
|
|
328
341
|
height: 100%;
|
|
329
|
-
border-left:
|
|
342
|
+
border-left: 1px solid transparent;
|
|
330
343
|
cursor: col-resize;
|
|
331
344
|
|
|
345
|
+
&:hover,
|
|
332
346
|
&:active {
|
|
333
|
-
border-color: var(--
|
|
347
|
+
border-color: var(--colorBorderActive);
|
|
348
|
+
}
|
|
349
|
+
&::before,
|
|
350
|
+
&::after {
|
|
351
|
+
position: absolute;
|
|
352
|
+
left: -2px;
|
|
353
|
+
content: '';
|
|
354
|
+
}
|
|
355
|
+
&::before {
|
|
356
|
+
top: calc(50% + 6px);
|
|
357
|
+
height: 22px;
|
|
358
|
+
border-left: 3px solid var(--colorBackground);
|
|
359
|
+
}
|
|
360
|
+
&::after {
|
|
361
|
+
top: calc(50% + 10px);
|
|
362
|
+
height: 14px;
|
|
363
|
+
border-left: 3px dotted var(--colorBorder);
|
|
334
364
|
}
|
|
335
365
|
}
|
|
336
366
|
}
|
|
@@ -364,7 +394,7 @@ main {
|
|
|
364
394
|
text-align-last: center;
|
|
365
395
|
color: var(--colorText);
|
|
366
396
|
font-size: 11px;
|
|
367
|
-
background-color: var(--
|
|
397
|
+
background-color: var(--colorInputBackground);
|
|
368
398
|
border-radius: var(--radius);
|
|
369
399
|
}
|
|
370
400
|
}
|
|
@@ -622,7 +652,7 @@ main {
|
|
|
622
652
|
left: -16px;
|
|
623
653
|
width: calc(100% + 32px);
|
|
624
654
|
height: 2px;
|
|
625
|
-
background: var(--
|
|
655
|
+
background: var(--colorInputBackground);
|
|
626
656
|
|
|
627
657
|
> div {
|
|
628
658
|
position: absolute;
|
|
@@ -651,6 +681,7 @@ main {
|
|
|
651
681
|
bottom: 12px;
|
|
652
682
|
left: 12px;
|
|
653
683
|
padding: 12px 16px;
|
|
684
|
+
padding-right: 42px;
|
|
654
685
|
cursor: pointer;
|
|
655
686
|
background: var(--colorRed);
|
|
656
687
|
color: white;
|
|
@@ -659,18 +690,16 @@ main {
|
|
|
659
690
|
transform: translateY(20px);
|
|
660
691
|
animation: _kfToastIn 240ms forwards;
|
|
661
692
|
|
|
662
|
-
|
|
693
|
+
&::after {
|
|
663
694
|
position: absolute;
|
|
664
695
|
top: 0;
|
|
665
|
-
|
|
666
|
-
width:
|
|
667
|
-
height:
|
|
696
|
+
right: 16px;
|
|
697
|
+
width: 12px;
|
|
698
|
+
height: 12px;
|
|
668
699
|
text-align: center;
|
|
669
700
|
content: '×';
|
|
670
701
|
font-size: 28px;
|
|
671
702
|
line-height: 34px;
|
|
672
|
-
border-radius: var(--radius);
|
|
673
|
-
background: rgba(0, 0, 0, 0.5);
|
|
674
703
|
}
|
|
675
704
|
}
|
|
676
705
|
@keyframes _kfToastIn {
|
package/src/client/app.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createElement as r,
|
|
3
3
|
createSvgElement as s,
|
|
4
|
-
t, classNames, restoreFocus, Fragment,
|
|
4
|
+
t, classNames, restoreFocus, Fragment, defineClassNames
|
|
5
5
|
} from './dom-utils.js'
|
|
6
6
|
import { store } from './app-store.js'
|
|
7
7
|
import { PayloadViewer, previewMock } from './payload-viewer.js'
|
|
8
8
|
|
|
9
9
|
import CSS from './app.css' with { type: 'css' }
|
|
10
|
-
|
|
10
|
+
document.adoptedStyleSheets.push(CSS)
|
|
11
|
+
defineClassNames(CSS)
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
store.onError = onError
|
|
@@ -480,13 +481,38 @@ function DelayToggler({ checked, commit, optClassName }) {
|
|
|
480
481
|
function ClickDragToggler({ checked, commit, className, title, body }) {
|
|
481
482
|
function onPointerEnter(event) {
|
|
482
483
|
if (event.buttons === 1)
|
|
483
|
-
onPointerDown.call(this)
|
|
484
|
+
onPointerDown.call(this, event)
|
|
484
485
|
}
|
|
485
|
-
|
|
486
|
+
|
|
487
|
+
function onPointerDown(event) {
|
|
488
|
+
if (event.altKey) {
|
|
489
|
+
onExclusiveClick.call(this)
|
|
490
|
+
return
|
|
491
|
+
}
|
|
486
492
|
this.checked = !this.checked
|
|
487
493
|
this.focus()
|
|
488
494
|
commit(this.checked)
|
|
489
495
|
}
|
|
496
|
+
|
|
497
|
+
function onExclusiveClick() {
|
|
498
|
+
const selector = selectorForColumnOf(this)
|
|
499
|
+
if (!selector)
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
// Uncheck all other in the column.
|
|
503
|
+
for (const elem of leftSideRef.elem.querySelectorAll(selector))
|
|
504
|
+
if (elem !== this && elem.checked && !elem.disabled) {
|
|
505
|
+
elem.checked = false
|
|
506
|
+
elem.dispatchEvent(new Event('change'))
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!this.checked) {
|
|
510
|
+
this.checked = true
|
|
511
|
+
this.dispatchEvent(new Event('change'))
|
|
512
|
+
}
|
|
513
|
+
this.focus()
|
|
514
|
+
}
|
|
515
|
+
|
|
490
516
|
function onClick(event) {
|
|
491
517
|
if (event.pointerType === 'mouse')
|
|
492
518
|
event.preventDefault()
|
|
@@ -666,18 +692,24 @@ function initRealTimeUpdates() {
|
|
|
666
692
|
}
|
|
667
693
|
}
|
|
668
694
|
|
|
695
|
+
function selectorForColumnOf(elem) {
|
|
696
|
+
return columnSelectors().find(s => elem?.matches(s))
|
|
697
|
+
}
|
|
669
698
|
|
|
670
|
-
function
|
|
671
|
-
|
|
699
|
+
function columnSelectors() {
|
|
700
|
+
return [
|
|
672
701
|
`.${CSS.TableRow} .${CSS.ProxyToggler} input`,
|
|
673
702
|
`.${CSS.TableRow} .${CSS.DelayToggler} input`,
|
|
674
703
|
`.${CSS.TableRow} .${CSS.StatusCodeToggler} input`,
|
|
675
704
|
`.${CSS.TableRow} .${CSS.PreviewLink}`,
|
|
676
705
|
// No .MockSelector because down/up arrows have native behavior on them
|
|
677
706
|
]
|
|
707
|
+
}
|
|
678
708
|
|
|
709
|
+
|
|
710
|
+
function initKeyboardNavigation() {
|
|
679
711
|
const rowSelectors = [
|
|
680
|
-
...columnSelectors,
|
|
712
|
+
...columnSelectors(),
|
|
681
713
|
`.${CSS.TableRow} .${CSS.MockSelector}:enabled`,
|
|
682
714
|
]
|
|
683
715
|
|
|
@@ -686,7 +718,7 @@ function initKeyboardNavigation() {
|
|
|
686
718
|
case 'ArrowDown':
|
|
687
719
|
case 'ArrowUp': {
|
|
688
720
|
const pivot = document.activeElement
|
|
689
|
-
const sel =
|
|
721
|
+
const sel = selectorForColumnOf(pivot)
|
|
690
722
|
if (sel) {
|
|
691
723
|
const offset = key === 'ArrowDown' ? +1 : -1
|
|
692
724
|
const siblings = leftSideRef.elem.querySelectorAll(sel)
|
package/src/client/dom-utils.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createElement as r,
|
|
3
|
-
t, classNames,
|
|
3
|
+
t, classNames, defineClassNames
|
|
4
4
|
} from './dom-utils.js'
|
|
5
5
|
import { parseFilename } from './Filename.js'
|
|
6
6
|
import { HEADER_502 } from './ApiConstants.js'
|
|
7
7
|
import { store } from './app-store.js'
|
|
8
8
|
|
|
9
9
|
import CSS from './app.css' with { type: 'css' }
|
|
10
|
-
|
|
10
|
+
defineClassNames(CSS)
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
const payloadViewerTitleRef = {}
|
|
@@ -84,7 +84,9 @@ export async function previewMock() {
|
|
|
84
84
|
if (proxied || file)
|
|
85
85
|
await updatePayloadViewer(proxied, file, response)
|
|
86
86
|
}
|
|
87
|
-
catch {
|
|
87
|
+
catch (error) {
|
|
88
|
+
clearTimeout(spinnerTimer)
|
|
89
|
+
store.onError(error)
|
|
88
90
|
payloadViewerCodeRef.elem.replaceChildren()
|
|
89
91
|
}
|
|
90
92
|
}
|
package/src/server/Api.js
CHANGED
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
DASHBOARD_ASSETS,
|
|
11
11
|
CLIENT_DIR
|
|
12
12
|
} from './WatcherDevClient.js'
|
|
13
|
-
import { longPollClientSyncVersion } from './Watcher.js'
|
|
13
|
+
import { longPollClientSyncVersion, startWatchers, stopWatchers } from './Watcher.js'
|
|
14
14
|
|
|
15
15
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
16
16
|
|
|
17
17
|
import { API } from '../client/ApiConstants.js'
|
|
18
|
-
import { IndexHtml, CSP } from '../client/
|
|
18
|
+
import { IndexHtml, CSP } from '../client/IndexHtml.js'
|
|
19
19
|
|
|
20
20
|
import { cookie } from './cookie.js'
|
|
21
21
|
import { config, ConfigValidator } from './config.js'
|
|
@@ -54,7 +54,9 @@ export const apiPatchReqs = new Map([
|
|
|
54
54
|
[API.toggle500, toggleRoute500],
|
|
55
55
|
|
|
56
56
|
[API.delayStatic, setStaticRouteIsDelayed],
|
|
57
|
-
[API.staticStatus, setStaticRouteStatusCode]
|
|
57
|
+
[API.staticStatus, setStaticRouteStatusCode],
|
|
58
|
+
|
|
59
|
+
[API.watchMocks, setWatchMocks]
|
|
58
60
|
])
|
|
59
61
|
|
|
60
62
|
|
|
@@ -109,6 +111,21 @@ async function setCorsAllowed(req, response) {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
|
|
114
|
+
async function setWatchMocks(req, response) {
|
|
115
|
+
const enabled = await req.json()
|
|
116
|
+
|
|
117
|
+
if (typeof enabled !== 'boolean')
|
|
118
|
+
response.unprocessable(`Expected boolean for "watchMocks"`)
|
|
119
|
+
else {
|
|
120
|
+
if (enabled)
|
|
121
|
+
startWatchers()
|
|
122
|
+
else
|
|
123
|
+
stopWatchers()
|
|
124
|
+
response.ok()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
112
129
|
async function setGlobalDelay(req, response) {
|
|
113
130
|
const delay = await req.json()
|
|
114
131
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
3
|
-
import { pathToFileURL } from 'node:url'
|
|
4
2
|
|
|
5
3
|
import { logger } from './utils/logger.js'
|
|
6
|
-
import { mimeFor } from './utils/mime.js'
|
|
7
4
|
|
|
8
5
|
import { proxy } from './ProxyRelay.js'
|
|
9
6
|
import { cookie } from './cookie.js'
|
|
7
|
+
import { echoFilePlugin } from './MockDispatcherPlugins.js'
|
|
10
8
|
import { brokerByRoute } from './mockBrokersCollection.js'
|
|
11
9
|
import { config, calcDelay } from './config.js'
|
|
12
10
|
|
|
@@ -61,21 +59,7 @@ async function applyPlugins(filePath, req, response) {
|
|
|
61
59
|
for (const [regex, plugin] of config.plugins)
|
|
62
60
|
if (regex.test(filePath))
|
|
63
61
|
return await plugin(filePath, req, response)
|
|
64
|
-
return
|
|
65
|
-
mime: mimeFor(filePath),
|
|
66
|
-
body: readFileSync(filePath)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function jsToJsonPlugin(filePath, req, response) {
|
|
71
|
-
const jsExport = (await import(pathToFileURL(filePath))).default
|
|
72
|
-
const body = typeof jsExport === 'function'
|
|
73
|
-
? await jsExport(req, response)
|
|
74
|
-
: JSON.stringify(jsExport, null, 2)
|
|
75
|
-
return {
|
|
76
|
-
mime: response.getHeader('Content-Type') || mimeFor('.json'), // jsFunc are allowed to set it
|
|
77
|
-
body
|
|
78
|
-
}
|
|
62
|
+
return echoFilePlugin(filePath)
|
|
79
63
|
}
|
|
80
64
|
|
|
81
65
|
function length(body) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
|
|
4
|
+
import { mimeFor } from './utils/mime.js'
|
|
5
|
+
|
|
6
|
+
export function echoFilePlugin(filePath) {
|
|
7
|
+
return {
|
|
8
|
+
mime: mimeFor(filePath),
|
|
9
|
+
body: readFileSync(filePath)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function jsToJsonPlugin(filePath, req, response) {
|
|
14
|
+
const jsExport = (await import(pathToFileURL(filePath))).default
|
|
15
|
+
const body = typeof jsExport === 'function'
|
|
16
|
+
? await jsExport(req, response)
|
|
17
|
+
: JSON.stringify(jsExport, null, 2)
|
|
18
|
+
return {
|
|
19
|
+
mime: response.getHeader('Content-Type') || mimeFor('.json'), // js functions are allowed to set it
|
|
20
|
+
body
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jwtCookie } from '../../index.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
cookies: {
|
|
5
|
+
userA: 'CookieA',
|
|
6
|
+
userB: jwtCookie('CookieB', { email: 'john@example.test' }),
|
|
7
|
+
},
|
|
8
|
+
extraHeaders: ['custom_header_name', 'custom_header_val'],
|
|
9
|
+
extraMimes: {
|
|
10
|
+
['custom_extension']: 'custom_mime'
|
|
11
|
+
},
|
|
12
|
+
logLevel: 'verbose',
|
|
13
|
+
corsOrigins: ['https://example.test'],
|
|
14
|
+
corsExposedHeaders: ['Content-Encoding'],
|
|
15
|
+
watcherEnabled: false, // But we enable it at run-time
|
|
16
|
+
watcherDebounceMs: 0
|
|
17
|
+
}
|
|
@@ -1,64 +1,57 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
2
3
|
import { tmpdir } from 'node:os'
|
|
3
4
|
import { promisify } from 'node:util'
|
|
4
5
|
import { createServer } from 'node:http'
|
|
6
|
+
import { mkdtempSync } from 'node:fs'
|
|
5
7
|
import { randomUUID } from 'node:crypto'
|
|
6
8
|
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
7
9
|
import { describe, test, before, beforeEach, after } from 'node:test'
|
|
8
|
-
|
|
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
|
-
|
|
14
|
-
import { logger } from './utils/logger.js'
|
|
15
12
|
import { mimeFor } from './utils/mime.js'
|
|
16
|
-
import { readBody } from './utils/HttpIncomingMessage.js'
|
|
17
|
-
import { CorsHeader } from './utils/http-cors.js'
|
|
18
|
-
|
|
19
|
-
import { Mockaton } from './Mockaton.js'
|
|
20
|
-
import { watchMocksDir, watchStaticDir } from './Watcher.js'
|
|
21
|
-
|
|
22
|
-
import { Commander } from '../client/ApiCommander.js'
|
|
23
13
|
import { parseFilename } from '../client/Filename.js'
|
|
14
|
+
import { API, Commander } from '../../index.js'
|
|
15
|
+
|
|
16
|
+
import CONFIG from './Mockaton.test.config.js'
|
|
24
17
|
|
|
25
18
|
|
|
26
19
|
const mocksDir = mkdtempSync(join(tmpdir(), 'mocks'))
|
|
27
20
|
const staticDir = mkdtempSync(join(tmpdir(), 'static'))
|
|
28
21
|
|
|
22
|
+
const stdout = []
|
|
23
|
+
const stderr = []
|
|
24
|
+
const proc = spawn(join(import.meta.dirname, 'cli.js'), [
|
|
25
|
+
'--config', join(import.meta.dirname, 'Mockaton.test.config.js'),
|
|
26
|
+
'--mocks-dir', mocksDir,
|
|
27
|
+
'--static-dir', staticDir,
|
|
28
|
+
'--no-open'
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
proc.stdout.on('data', data => { stdout.push(data.toString()) })
|
|
32
|
+
proc.stderr.on('data', data => { stderr.push(data.toString()) })
|
|
33
|
+
|
|
34
|
+
const serverAddr = await new Promise((resolve, reject) => {
|
|
35
|
+
proc.stdout.once('data', () => {
|
|
36
|
+
const addr = stdout[0].match(/Listening::(http:[^\n]+)/)[1]
|
|
37
|
+
resolve(addr)
|
|
38
|
+
})
|
|
39
|
+
proc.on('error', reject)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
after(() => proc.kill('SIGUSR2'))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const readFromMocksDir = f => readFile(join(mocksDir, f), 'utf8')
|
|
46
|
+
|
|
29
47
|
const makeDirInMocks = dir => mkdir(join(mocksDir, dir), { recursive: true })
|
|
30
48
|
const makeDirInStaticMocks = dir => mkdir(join(staticDir, dir), { recursive: true })
|
|
31
49
|
|
|
32
50
|
const renameInMocksDir = (src, target) => rename(join(mocksDir, src), join(mocksDir, target))
|
|
33
51
|
const renameInStaticMocksDir = (src, target) => rename(join(staticDir, src), join(staticDir, target))
|
|
34
52
|
|
|
35
|
-
const readFromMocksDir = f => readFile(join(mocksDir, f), 'utf8')
|
|
36
|
-
|
|
37
53
|
|
|
38
|
-
const
|
|
39
|
-
const CUSTOM_EXT = 'custom_extension'
|
|
40
|
-
const CUSTOM_MIME = 'custom_mime'
|
|
41
|
-
const CUSTOM_HEADER_NAME = 'custom_header_name'
|
|
42
|
-
const CUSTOM_HEADER_VAL = 'custom_header_val'
|
|
43
|
-
const ALLOWED_ORIGIN = 'https://example.test'
|
|
44
|
-
|
|
45
|
-
const server = await Mockaton({
|
|
46
|
-
mocksDir,
|
|
47
|
-
staticDir,
|
|
48
|
-
onReady() {},
|
|
49
|
-
cookies: COOKIES,
|
|
50
|
-
extraHeaders: [CUSTOM_HEADER_NAME, CUSTOM_HEADER_VAL],
|
|
51
|
-
extraMimes: { [CUSTOM_EXT]: CUSTOM_MIME },
|
|
52
|
-
logLevel: 'verbose',
|
|
53
|
-
corsOrigins: [ALLOWED_ORIGIN],
|
|
54
|
-
corsExposedHeaders: ['Content-Encoding'],
|
|
55
|
-
watcherEnabled: false, // But we enable it at run-time
|
|
56
|
-
watcherDebounceMs: 0
|
|
57
|
-
})
|
|
58
|
-
after(() => server?.close())
|
|
59
|
-
|
|
60
|
-
const api = new Commander(
|
|
61
|
-
`http://${server.address().address}:${server.address().port}`)
|
|
54
|
+
const api = new Commander(serverAddr)
|
|
62
55
|
|
|
63
56
|
/** @returns {Promise<State>} */
|
|
64
57
|
async function fetchState() {
|
|
@@ -172,14 +165,7 @@ describe('Rejects malicious URLs', () => {
|
|
|
172
165
|
|
|
173
166
|
|
|
174
167
|
describe('Warnings', () => {
|
|
175
|
-
|
|
176
|
-
const spy = t.mock.method(logger, method)
|
|
177
|
-
spy.mock.mockImplementation(() => null)
|
|
178
|
-
return spy.mock
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
test('rejects invalid filenames', async t => {
|
|
182
|
-
const spy = spyLogger(t, 'warn')
|
|
168
|
+
test('rejects invalid filenames', async () => {
|
|
183
169
|
const fx0 = new Fixture('bar.GET._INVALID_STATUS_.json')
|
|
184
170
|
const fx1 = new Fixture('foo._INVALID_METHOD_.202.json')
|
|
185
171
|
const fx2 = new Fixture('missing-method-and-status.json')
|
|
@@ -188,30 +174,29 @@ describe('Warnings', () => {
|
|
|
188
174
|
await fx2.write()
|
|
189
175
|
await api.reset()
|
|
190
176
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
177
|
+
const log = stderr.join('')
|
|
178
|
+
match(log, /Invalid HTTP Response Status: "NaN"/)
|
|
179
|
+
match(log, /Unrecognized HTTP Method: "_INVALID_METHOD_"/)
|
|
180
|
+
match(log, /Invalid Filename Convention/)
|
|
194
181
|
|
|
195
182
|
await fx0.unlink()
|
|
196
183
|
await fx1.unlink()
|
|
197
184
|
await fx2.unlink()
|
|
198
185
|
})
|
|
199
186
|
|
|
200
|
-
test('body parser rejects invalid JSON in API requests', async
|
|
201
|
-
const spy = spyLogger(t, 'access')
|
|
187
|
+
test('body parser rejects invalid JSON in API requests', async () => {
|
|
202
188
|
const r = await request(API.cookies, {
|
|
203
189
|
method: 'PATCH',
|
|
204
190
|
body: '[invalid_json]'
|
|
205
191
|
})
|
|
206
192
|
equal(r.status, 422)
|
|
207
|
-
|
|
193
|
+
match(stdout.at(-1), /BodyReaderError: Could not parse/)
|
|
208
194
|
})
|
|
209
195
|
|
|
210
|
-
test('returns 500 when a handler throws', async
|
|
211
|
-
const spy = spyLogger(t, 'error')
|
|
196
|
+
test('returns 500 when a handler throws', async () => {
|
|
212
197
|
const r = await request(API.throws)
|
|
213
198
|
equal(r.status, 500)
|
|
214
|
-
|
|
199
|
+
match(stderr.at(-1), /Test500/)
|
|
215
200
|
})
|
|
216
201
|
})
|
|
217
202
|
|
|
@@ -239,13 +224,13 @@ describe('CORS', () => {
|
|
|
239
224
|
const r = await request('/does-not-matter', {
|
|
240
225
|
method: 'OPTIONS',
|
|
241
226
|
headers: {
|
|
242
|
-
[
|
|
243
|
-
|
|
227
|
+
'origin': CONFIG.corsOrigins[0],
|
|
228
|
+
'access-control-request-method': 'GET'
|
|
244
229
|
}
|
|
245
230
|
})
|
|
246
231
|
equal(r.status, 204)
|
|
247
|
-
equal(r.headers.get(
|
|
248
|
-
equal(r.headers.get(
|
|
232
|
+
equal(r.headers.get('access-control-allow-origin'), CONFIG.corsOrigins[0])
|
|
233
|
+
equal(r.headers.get('access-control-allow-methods'), 'GET')
|
|
249
234
|
})
|
|
250
235
|
|
|
251
236
|
test('responds', async () => {
|
|
@@ -253,12 +238,12 @@ describe('CORS', () => {
|
|
|
253
238
|
await fx.sync()
|
|
254
239
|
const r = await fx.request({
|
|
255
240
|
headers: {
|
|
256
|
-
[
|
|
241
|
+
'origin': CONFIG.corsOrigins[0]
|
|
257
242
|
}
|
|
258
243
|
})
|
|
259
244
|
equal(r.status, 200)
|
|
260
|
-
equal(r.headers.get(
|
|
261
|
-
equal(r.headers.get(
|
|
245
|
+
equal(r.headers.get('access-control-allow-origin'), CONFIG.corsOrigins[0])
|
|
246
|
+
equal(r.headers.get('access-control-expose-headers'), 'Content-Encoding')
|
|
262
247
|
await fx.unlink()
|
|
263
248
|
})
|
|
264
249
|
})
|
|
@@ -274,6 +259,11 @@ describe('Dashboard', () => {
|
|
|
274
259
|
const r = await request(API.dashboard + '?foo=bar')
|
|
275
260
|
match(await r.text(), new RegExp('<!DOCTYPE html>'))
|
|
276
261
|
})
|
|
262
|
+
|
|
263
|
+
test('serves assets', async () => {
|
|
264
|
+
const r = await request(API.dashboard + '/app.css')
|
|
265
|
+
match(await r.text(), new RegExp(':root {'))
|
|
266
|
+
})
|
|
277
267
|
})
|
|
278
268
|
|
|
279
269
|
|
|
@@ -293,7 +283,7 @@ describe('Cookie', () => {
|
|
|
293
283
|
const fx = new Fixture('update-cookie.GET.200.json')
|
|
294
284
|
await fx.sync()
|
|
295
285
|
const resA = await fx.request()
|
|
296
|
-
equal(resA.headers.get('set-cookie'),
|
|
286
|
+
equal(resA.headers.get('set-cookie'), CONFIG.cookies.userA)
|
|
297
287
|
|
|
298
288
|
const response = await api.selectCookie('userB')
|
|
299
289
|
deepEqual(await response.json(), [
|
|
@@ -302,7 +292,7 @@ describe('Cookie', () => {
|
|
|
302
292
|
])
|
|
303
293
|
|
|
304
294
|
const resB = await fx.request()
|
|
305
|
-
equal(resB.headers.get('set-cookie'),
|
|
295
|
+
equal(resB.headers.get('set-cookie'), CONFIG.cookies.userB)
|
|
306
296
|
await fx.unlink()
|
|
307
297
|
})
|
|
308
298
|
})
|
|
@@ -378,6 +368,7 @@ describe('Proxy Fallback', () => {
|
|
|
378
368
|
describe('Fallback', () => {
|
|
379
369
|
let fallbackServer
|
|
380
370
|
const CUSTOM_COOKIES = ['cookieX=x', 'cookieY=y']
|
|
371
|
+
const BODY_PAYLOAD = 'text_req_body'
|
|
381
372
|
before(async () => {
|
|
382
373
|
fallbackServer = createServer(async (req, response) => {
|
|
383
374
|
response.writeHead(423, {
|
|
@@ -385,7 +376,7 @@ describe('Proxy Fallback', () => {
|
|
|
385
376
|
'content-type': mimeFor('.txt'),
|
|
386
377
|
'set-cookie': CUSTOM_COOKIES
|
|
387
378
|
})
|
|
388
|
-
response.end(
|
|
379
|
+
response.end(BODY_PAYLOAD)
|
|
389
380
|
})
|
|
390
381
|
await promisify(fallbackServer.listen).bind(fallbackServer, 0, '127.0.0.1')()
|
|
391
382
|
await api.setProxyFallback(`http://localhost:${fallbackServer.address().port}`)
|
|
@@ -395,19 +386,14 @@ describe('Proxy Fallback', () => {
|
|
|
395
386
|
after(() => fallbackServer.close())
|
|
396
387
|
|
|
397
388
|
test('Relays to fallback server and saves the mock', async () => {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
const r = await request(`/non-existing-mock/${randomUUID()}`, {
|
|
401
|
-
method: 'POST',
|
|
402
|
-
body: reqBodyPayload
|
|
403
|
-
})
|
|
389
|
+
const r = await request(`/non-existing-mock/${randomUUID()}`, { method: 'POST' })
|
|
404
390
|
equal(r.status, 423)
|
|
405
391
|
equal(r.headers.get('custom_header'), 'my_custom_header')
|
|
406
392
|
equal(r.headers.get('set-cookie'), CUSTOM_COOKIES.join(', '))
|
|
407
|
-
equal(await r.text(),
|
|
393
|
+
equal(await r.text(), BODY_PAYLOAD)
|
|
408
394
|
|
|
409
395
|
const savedBody = await readFromMocksDir('non-existing-mock/[id].POST.423.txt')
|
|
410
|
-
equal(savedBody,
|
|
396
|
+
equal(savedBody, BODY_PAYLOAD)
|
|
411
397
|
})
|
|
412
398
|
})
|
|
413
399
|
|
|
@@ -623,7 +609,7 @@ describe('Dynamic Function Mocks', () => {
|
|
|
623
609
|
equal(r.status, 200)
|
|
624
610
|
equal(r.headers.get('content-length'), '1')
|
|
625
611
|
equal(r.headers.get('content-type'), mimeFor('.json'))
|
|
626
|
-
equal(r.headers.get('set-cookie'),
|
|
612
|
+
equal(r.headers.get('set-cookie'), CONFIG.cookies.userA)
|
|
627
613
|
equal(await r.text(), 'A')
|
|
628
614
|
await fx.unlink()
|
|
629
615
|
})
|
|
@@ -835,10 +821,12 @@ describe('MIME', () => {
|
|
|
835
821
|
})
|
|
836
822
|
|
|
837
823
|
test('derives content-type from custom mime', async () => {
|
|
838
|
-
const
|
|
824
|
+
const ext = Object.keys(CONFIG.extraMimes)[0]
|
|
825
|
+
const mime = Object.values(CONFIG.extraMimes)[0]
|
|
826
|
+
const fx = new Fixture(`tmp.GET.200.${ext}`)
|
|
839
827
|
await fx.sync()
|
|
840
828
|
const r = await fx.request()
|
|
841
|
-
equal(r.headers.get('content-type'),
|
|
829
|
+
equal(r.headers.get('content-type'), mime)
|
|
842
830
|
await fx.unlink()
|
|
843
831
|
})
|
|
844
832
|
})
|
|
@@ -852,9 +840,8 @@ describe('Headers', () => {
|
|
|
852
840
|
})
|
|
853
841
|
|
|
854
842
|
test('custom headers are included', async () => {
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
equal(val, CUSTOM_HEADER_VAL)
|
|
843
|
+
const { headers } = await api.getState()
|
|
844
|
+
equal(headers.get(CONFIG.extraHeaders[0]), CONFIG.extraHeaders[1])
|
|
858
845
|
})
|
|
859
846
|
})
|
|
860
847
|
|
|
@@ -1054,13 +1041,35 @@ test('head for get. returns the headers without body only for GETs requested as
|
|
|
1054
1041
|
})
|
|
1055
1042
|
|
|
1056
1043
|
|
|
1057
|
-
describe('
|
|
1058
|
-
|
|
1044
|
+
describe('Watch mocks API toggler', () => {
|
|
1045
|
+
test('422 for non boolean', async () => {
|
|
1046
|
+
const r = await api.setWatchMocks('not-a-boolean')
|
|
1047
|
+
equal(r.status, 422)
|
|
1048
|
+
equal(await r.text(), 'Expected boolean for "watchMocks"')
|
|
1049
|
+
})
|
|
1059
1050
|
|
|
1051
|
+
test('200', async () => {
|
|
1052
|
+
equal((await api.setWatchMocks(true)).status, 200)
|
|
1053
|
+
equal((await api.setWatchMocks(false)).status, 200)
|
|
1054
|
+
})
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
describe('Registering Mocks', () => {
|
|
1060
1059
|
const fxA = new Fixture('register(default).GET.200.json')
|
|
1061
1060
|
const fxB = new Fixture('register(alt).GET.200.json')
|
|
1062
1061
|
|
|
1062
|
+
test('when watcher is off, newly added mocks do not get registered', async () => {
|
|
1063
|
+
await api.setWatchMocks(false)
|
|
1064
|
+
const fx = new Fixture('non-auto-registered-file.GET.200.json')
|
|
1065
|
+
await fx.write()
|
|
1066
|
+
await sleep()
|
|
1067
|
+
equal(await fx.fetchBroker(), undefined)
|
|
1068
|
+
await fx.unlink()
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1063
1071
|
test('register', async () => {
|
|
1072
|
+
await api.setWatchMocks(true)
|
|
1064
1073
|
await fxA.register()
|
|
1065
1074
|
await fxB.register()
|
|
1066
1075
|
const b = await fxA.fetchBroker()
|
|
@@ -1141,11 +1150,19 @@ describe('Registering Mocks', () => {
|
|
|
1141
1150
|
|
|
1142
1151
|
|
|
1143
1152
|
describe('Registering Static Mocks', () => {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1153
|
+
test('when watcher is off, newly added mocks do not get registered', async () => {
|
|
1154
|
+
await api.setWatchMocks(false)
|
|
1155
|
+
const fx = new FixtureStatic('non-auto-registered-file.txt')
|
|
1156
|
+
await fx.write()
|
|
1157
|
+
await sleep()
|
|
1158
|
+
const { staticBrokers } = await fetchState()
|
|
1159
|
+
equal(staticBrokers['/' + fx.file], undefined)
|
|
1160
|
+
await fx.unlink()
|
|
1161
|
+
})
|
|
1147
1162
|
|
|
1163
|
+
const fx = new FixtureStatic('static-register.txt', 'static-body')
|
|
1148
1164
|
test('registers static', async () => {
|
|
1165
|
+
await api.setWatchMocks(true)
|
|
1149
1166
|
await fx.register()
|
|
1150
1167
|
const { staticBrokers } = await fetchState()
|
|
1151
1168
|
deepEqual(staticBrokers, {
|
|
@@ -1155,6 +1172,9 @@ describe('Registering Static Mocks', () => {
|
|
|
1155
1172
|
delayed: false
|
|
1156
1173
|
}
|
|
1157
1174
|
})
|
|
1175
|
+
const response = await fx.request()
|
|
1176
|
+
equal(response.status, 200)
|
|
1177
|
+
equal(await response.text(), fx.body)
|
|
1158
1178
|
})
|
|
1159
1179
|
|
|
1160
1180
|
test('unregisters static', async () => {
|
|
@@ -1203,3 +1223,8 @@ describe('Registering Static Mocks', () => {
|
|
|
1203
1223
|
})
|
|
1204
1224
|
})
|
|
1205
1225
|
})
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
async function sleep(ms = 100) {
|
|
1229
|
+
await new Promise(resolve => setTimeout(resolve, ms))
|
|
1230
|
+
}
|
package/src/server/Watcher.js
CHANGED
|
@@ -14,6 +14,10 @@ import * as staticCollection from './staticCollection.js'
|
|
|
14
14
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
let mocksWatcher = null
|
|
18
|
+
let staticWatcher = null
|
|
19
|
+
|
|
20
|
+
|
|
17
21
|
/**
|
|
18
22
|
* ARR Event = Add, Remove, or Rename Mock
|
|
19
23
|
*
|
|
@@ -47,7 +51,7 @@ const uiSyncVersion = new class extends EventEmitter {
|
|
|
47
51
|
|
|
48
52
|
export function watchMocksDir() {
|
|
49
53
|
const dir = config.mocksDir
|
|
50
|
-
watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
54
|
+
mocksWatcher = mocksWatcher || watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
51
55
|
if (!file)
|
|
52
56
|
return
|
|
53
57
|
|
|
@@ -73,7 +77,7 @@ export function watchStaticDir() {
|
|
|
73
77
|
if (!dir)
|
|
74
78
|
return
|
|
75
79
|
|
|
76
|
-
watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
80
|
+
staticWatcher = staticWatcher || watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
77
81
|
if (!file)
|
|
78
82
|
return
|
|
79
83
|
|
|
@@ -113,3 +117,16 @@ export function longPollClientSyncVersion(req, response) {
|
|
|
113
117
|
})
|
|
114
118
|
uiSyncVersion.subscribe(onARR)
|
|
115
119
|
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
export function startWatchers() {
|
|
123
|
+
watchMocksDir()
|
|
124
|
+
watchStaticDir()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function stopWatchers() {
|
|
128
|
+
mocksWatcher?.close()
|
|
129
|
+
staticWatcher?.close()
|
|
130
|
+
mocksWatcher = null
|
|
131
|
+
staticWatcher = null
|
|
132
|
+
}
|
|
@@ -17,7 +17,7 @@ const devClientWatcher = new class extends EventEmitter {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
// Although `client/
|
|
20
|
+
// Although `client/IndexHtml.js` is watched, it returns a stale version.
|
|
21
21
|
// i.e., it would need to be a dynamic import + cache busting.
|
|
22
22
|
export function watchDevSPA() {
|
|
23
23
|
watch(CLIENT_DIR, (_, file) => {
|
package/src/server/cli.js
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { equal } from 'node:assert/strict'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { describe, test } from 'node:test'
|
|
5
|
+
|
|
6
|
+
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
7
|
+
|
|
8
|
+
const cli = (...args) => spawnSync(join(import.meta.dirname, 'cli.js'), args, {
|
|
9
|
+
encoding: 'utf8'
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('CLI', () => {
|
|
13
|
+
test('--invalid-flag', () => {
|
|
14
|
+
const { stderr, status } = cli('--invalid-flag')
|
|
15
|
+
equal(stderr.trim(), `Unknown option '--invalid-flag'`)
|
|
16
|
+
equal(status, 1)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('invalid config file', () => {
|
|
20
|
+
const { stderr, status } = cli('--config', 'non-existing-file.js')
|
|
21
|
+
equal(stderr.trim(), `Invalid config file: non-existing-file.js`)
|
|
22
|
+
equal(status, 1)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('-v outputs version from package.json', () => {
|
|
26
|
+
const { stdout, status } = cli('-v')
|
|
27
|
+
equal(stdout.trim(), pkgJSON.version)
|
|
28
|
+
equal(status, 0)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('-h outputs usage message', () => {
|
|
32
|
+
const { stdout, status } = cli('-h')
|
|
33
|
+
equal(stdout.split('\n')[0], 'Usage: mockaton [options]')
|
|
34
|
+
equal(status, 0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Mockaton.test.js tests the remaining cli branch
|
|
38
|
+
})
|
|
39
|
+
|
package/src/server/config.js
CHANGED
|
@@ -7,7 +7,8 @@ import { openInBrowser } from './utils/openInBrowser.js'
|
|
|
7
7
|
import { optional, is, validate } from './utils/validate.js'
|
|
8
8
|
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
/** @type {{
|
package/src/server/utils/mime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { MIMEType } from 'node:util'
|
|
1
2
|
import { config } from '../config.js'
|
|
2
|
-
import {
|
|
3
|
+
import { EXT_UNKNOWN_MIME, EXT_EMPTY } from '../../client/ApiConstants.js'
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
// Generated with:
|
|
@@ -143,15 +144,11 @@ function extname(filename) {
|
|
|
143
144
|
export function extFor(mime) {
|
|
144
145
|
return mime
|
|
145
146
|
? findExt(mime)
|
|
146
|
-
:
|
|
147
|
+
: EXT_EMPTY
|
|
147
148
|
}
|
|
148
149
|
function findExt(rawMime) {
|
|
149
|
-
const m =
|
|
150
|
+
const m = new MIMEType(rawMime).essence
|
|
150
151
|
const extraMimeToExt = mapMimeToExt(config.extraMimes)
|
|
151
|
-
return extraMimeToExt[m] || mimeToExt[m] ||
|
|
152
|
+
return extraMimeToExt[m] || mimeToExt[m] || EXT_UNKNOWN_MIME
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
export function parseMime(mime) {
|
|
155
|
-
return mime.split(';')[0].toLowerCase()
|
|
156
|
-
// RFC 9110 §8.3.1
|
|
157
|
-
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import { equal } from 'node:assert/strict'
|
|
3
|
-
import {
|
|
3
|
+
import { extFor, mimeFor } from './mime.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
test('parseMime', () => [
|
|
7
|
-
'text/html',
|
|
8
|
-
'TEXT/html',
|
|
9
|
-
'text/html; charset=utf-8'
|
|
10
|
-
].map(input =>
|
|
11
|
-
equal(parseMime(input), 'text/html')))
|
|
12
|
-
|
|
13
6
|
test('extFor', () => [
|
|
14
7
|
'text/html',
|
|
15
8
|
'Text/html',
|
package/src/client/indexHtml.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { API } from './ApiConstants.js'
|
|
2
|
-
|
|
3
|
-
export const CSP = [
|
|
4
|
-
`default-src 'self'`,
|
|
5
|
-
`img-src data: blob: 'self'`
|
|
6
|
-
].join(';')
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// language=html
|
|
10
|
-
export const IndexHtml = (hotReloadEnabled, version) => `
|
|
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>
|
|
19
|
-
|
|
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>
|
|
31
|
-
`
|