mockaton 11.0.1 → 11.1.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/Makefile +2 -2
- package/README.md +86 -35
- package/index.d.ts +5 -2
- package/package.json +1 -1
- package/src/Api.js +19 -18
- package/src/ApiCommander.js +27 -58
- package/src/Dashboard.js +2 -4
- package/src/MockBroker.js +4 -4
- package/src/MockDispatcher.js +3 -2
- package/src/Mockaton.js +7 -4
- package/src/ProxyRelay.js +2 -2
- package/src/StaticDispatcher.js +1 -2
- package/src/Watcher.js +4 -3
- package/src/config.js +3 -1
- package/src/mockBrokersCollection.js +10 -9
- package/src/utils/http-response.js +1 -1
package/Makefile
CHANGED
|
@@ -6,7 +6,7 @@ watch:
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
test:
|
|
9
|
-
@node --test 'src/**/*.test.js'
|
|
9
|
+
@MOCKATON_WATCHER_DEBOUNCE_MS=0 node --test 'src/**/*.test.js'
|
|
10
10
|
|
|
11
11
|
test-docker:
|
|
12
12
|
@docker run --rm --interactive --tty \
|
|
@@ -16,7 +16,7 @@ test-docker:
|
|
|
16
16
|
make test
|
|
17
17
|
|
|
18
18
|
coverage:
|
|
19
|
-
@node --test --experimental-test-coverage \
|
|
19
|
+
@MOCKATON_WATCHER_DEBOUNCE_MS=0 node --test --experimental-test-coverage \
|
|
20
20
|
--test-reporter=spec --test-reporter-destination=stdout \
|
|
21
21
|
--test-reporter=lcov --test-reporter-destination=lcov.info \
|
|
22
22
|
'src/**/*.test.js'
|
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ get saved following Mockaton’s filename convention.
|
|
|
77
77
|
|
|
78
78
|
### Option 2: Fallback to Your Backend
|
|
79
79
|
<details>
|
|
80
|
-
<summary>
|
|
80
|
+
<summary>Learn more…</summary>
|
|
81
81
|
|
|
82
82
|
This option could be a bit elaborate if your backend uses third-party authentication,
|
|
83
83
|
because you’d have to manually inject cookies or `sessionStorage` tokens.
|
|
@@ -93,7 +93,12 @@ They will be saved in your `config.mocksDir` following the filename convention.
|
|
|
93
93
|
|
|
94
94
|
<br/>
|
|
95
95
|
|
|
96
|
+
<br/>
|
|
97
|
+
|
|
98
|
+
|
|
96
99
|
## Motivation
|
|
100
|
+
<details>
|
|
101
|
+
<summary>Motivation…</summary>
|
|
97
102
|
|
|
98
103
|
**No API state should be too hard to test.**
|
|
99
104
|
With Mockaton, developers can achieve correctness and speed.
|
|
@@ -116,15 +121,41 @@ With Mockaton, developers can achieve correctness and speed.
|
|
|
116
121
|
- checking out long-lived branches
|
|
117
122
|
- bisecting bugs
|
|
118
123
|
|
|
124
|
+
</details>
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
## Use Cases
|
|
129
|
+
<details>
|
|
130
|
+
<summary>Use Cases…</summary>
|
|
131
|
+
|
|
132
|
+
### Testing Backend or Frontend
|
|
133
|
+
- Empty responses
|
|
134
|
+
- Errors such as _Bad Request_ and _Internal Server Error_
|
|
135
|
+
- Mocking third-party APIs
|
|
136
|
+
- Polled resources (for triggering their different states)
|
|
137
|
+
- alerts
|
|
138
|
+
- notifications
|
|
139
|
+
- slow to build resources
|
|
140
|
+
|
|
141
|
+
### Testing Frontend
|
|
142
|
+
- Spinners by delaying responses
|
|
143
|
+
- Setting up UI tests
|
|
144
|
+
|
|
145
|
+
### Demoing complex backend states
|
|
146
|
+
Sometimes, the ideal flow you need is too difficult to reproduce from the actual backend.
|
|
147
|
+
For this, you can **Bulk Select** mocks by comments to simulate the complete states
|
|
148
|
+
you want. For example, by adding `(demo-part1)`, `(demo-part2)` to the filenames.
|
|
149
|
+
|
|
150
|
+
Similarly, you can deploy a **Standalone Demo Server** by compiling the frontend app and
|
|
151
|
+
putting its built assets in `config.staticDir`. And simulate the flow by Bulk Selecting mocks.
|
|
152
|
+
The [aot-fetch-demo repo](https://github.com/ericfortis/aot-fetch-demo) has a working example.
|
|
153
|
+
|
|
154
|
+
</details>
|
|
119
155
|
|
|
120
156
|
<br/>
|
|
121
157
|
|
|
122
|
-
|
|
123
|
-
- Zero dependencies (no runtime and no build packages).
|
|
124
|
-
- Does not write to disk. Except when you select ✅ **Save Mocks** for scraping mocks from a backend.
|
|
125
|
-
- Does not initiate network connections (no logs, no telemetry).
|
|
126
|
-
- Does not hijack your HTTP client.
|
|
127
|
-
- Auditable. Organized and small — under 4 KLoC (half is UI and tests).
|
|
158
|
+
|
|
128
159
|
|
|
129
160
|
<br/>
|
|
130
161
|
|
|
@@ -147,6 +178,16 @@ npx mockaton --port 2345
|
|
|
147
178
|
curl localhost:2345/api/foo
|
|
148
179
|
```
|
|
149
180
|
|
|
181
|
+
5. Optionally, use a `mockaton.config.js`
|
|
182
|
+
```js
|
|
183
|
+
import { defineConfig } from 'mockaton'
|
|
184
|
+
|
|
185
|
+
export default defineConfig({
|
|
186
|
+
port: 2345,
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
|
|
150
191
|
### Alternative Installations
|
|
151
192
|
<details>
|
|
152
193
|
<summary>With NPM (package.json)…</summary>
|
|
@@ -185,6 +226,9 @@ ln -s `realpath mockaton/src/cli.js` ~/bin/mockaton # some dir in your $PATH
|
|
|
185
226
|
## CLI Options
|
|
186
227
|
The CLI options override their counterparts in `mockaton.config.js`
|
|
187
228
|
|
|
229
|
+
<details>
|
|
230
|
+
<summary>CLI Options…</summary>
|
|
231
|
+
|
|
188
232
|
```txt
|
|
189
233
|
-c, --config <file> (default: ./mockaton.config.js)
|
|
190
234
|
|
|
@@ -200,10 +244,17 @@ The CLI options override their counterparts in `mockaton.config.js`
|
|
|
200
244
|
-h, --help Show this help
|
|
201
245
|
-v, --version Show version
|
|
202
246
|
```
|
|
247
|
+
</details>
|
|
203
248
|
|
|
204
249
|
|
|
205
250
|
## mockaton.config.js (Optional)
|
|
251
|
+
Mockaton looks for a file `mockaton.config.js` in its current working directory.
|
|
252
|
+
|
|
253
|
+
<details>
|
|
254
|
+
<summary>Defaults Overview… </summary>
|
|
255
|
+
|
|
206
256
|
As an overview, these are the defaults:
|
|
257
|
+
|
|
207
258
|
```js
|
|
208
259
|
import {
|
|
209
260
|
defineConfig,
|
|
@@ -247,11 +298,14 @@ export default defineConfig({
|
|
|
247
298
|
],
|
|
248
299
|
|
|
249
300
|
onReady: await openInBrowser,
|
|
301
|
+
watcherEnabled: true
|
|
250
302
|
})
|
|
251
303
|
```
|
|
252
304
|
|
|
305
|
+
</details>
|
|
306
|
+
|
|
253
307
|
<details>
|
|
254
|
-
<summary><b>Config Documentation
|
|
308
|
+
<summary><b>Config Documentation…</b></summary>
|
|
255
309
|
|
|
256
310
|
### `mocksDir?: string`
|
|
257
311
|
Defaults to `'mockaton-mocks'`.
|
|
@@ -495,11 +549,18 @@ Defaults to `'normal'`.
|
|
|
495
549
|
- `normal`: info, mock access, warnings, and errors
|
|
496
550
|
- `verbose`: normal + API access
|
|
497
551
|
|
|
552
|
+
<br/>
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
### `watcherEnabled?: boolean`
|
|
556
|
+
Defaults to `true`. When `true`, newly added mocks get registered,
|
|
557
|
+
or unregistered when deleting them.
|
|
558
|
+
|
|
498
559
|
</details>
|
|
499
560
|
|
|
500
561
|
|
|
501
562
|
<details>
|
|
502
|
-
<summary>Programmatic Launch (Optional)
|
|
563
|
+
<summary>Programmatic Launch (Optional)…</summary>
|
|
503
564
|
|
|
504
565
|
|
|
505
566
|
```js
|
|
@@ -536,32 +597,6 @@ permutations for out-of-stock, new-arrival, and discontinued.
|
|
|
536
597
|
<br/>
|
|
537
598
|
|
|
538
599
|
|
|
539
|
-
## Use Cases
|
|
540
|
-
### Testing Backend or Frontend
|
|
541
|
-
- Empty responses
|
|
542
|
-
- Errors such as _Bad Request_ and _Internal Server Error_
|
|
543
|
-
- Mocking third-party APIs
|
|
544
|
-
- Polled resources (for triggering their different states)
|
|
545
|
-
- alerts
|
|
546
|
-
- notifications
|
|
547
|
-
- slow to build resources
|
|
548
|
-
|
|
549
|
-
### Testing Frontend
|
|
550
|
-
- Spinners by delaying responses
|
|
551
|
-
- Setting up UI tests
|
|
552
|
-
|
|
553
|
-
### Demoing complex backend states
|
|
554
|
-
Sometimes, the ideal flow you need is too difficult to reproduce from the actual backend.
|
|
555
|
-
For this, you can **Bulk Select** mocks by comments to simulate the complete states
|
|
556
|
-
you want. For example, by adding `(demo-part1)`, `(demo-part2)` to the filenames.
|
|
557
|
-
|
|
558
|
-
Similarly, you can deploy a **Standalone Demo Server** by compiling the frontend app and
|
|
559
|
-
putting its built assets in `config.staticDir`. And simulate the flow by Bulk Selecting mocks.
|
|
560
|
-
The [aot-fetch-demo repo](https://github.com/ericfortis/aot-fetch-demo) has a working example.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
<br/>
|
|
564
|
-
|
|
565
600
|
|
|
566
601
|
## You can write JSON mocks in JavaScript or TypeScript
|
|
567
602
|
For example, `api/foo.GET.200.js`
|
|
@@ -841,6 +876,16 @@ await mockaton.reset()
|
|
|
841
876
|
</details>
|
|
842
877
|
|
|
843
878
|
|
|
879
|
+
<br/>
|
|
880
|
+
|
|
881
|
+
## Privacy and Security
|
|
882
|
+
- Zero dependencies (no runtime and no build packages).
|
|
883
|
+
- Does not write to disk. Except when you select ✅ **Save Mocks** for scraping mocks from a backend.
|
|
884
|
+
- Does not initiate network connections (no logs, no telemetry).
|
|
885
|
+
- Does not hijack your HTTP client.
|
|
886
|
+
- Auditable. Organized and small — under 4 KLoC (half is UI and tests).
|
|
887
|
+
|
|
888
|
+
|
|
844
889
|
<br/>
|
|
845
890
|
|
|
846
891
|
## Alternatives worth learning as well
|
|
@@ -862,6 +907,12 @@ hijack the HTTP client in Node.js and browsers.
|
|
|
862
907
|
- [Fetch Mock](https://github.com/wheresrhys/fetch-mock)
|
|
863
908
|
- [Mentoss](https://github.com/humanwhocodes/mentoss) Has a server side too
|
|
864
909
|
|
|
910
|
+
### Server Side
|
|
911
|
+
|
|
912
|
+
- [Wire Mock](https://github.com/wiremock/wiremock)
|
|
913
|
+
- [Mock](https://github.com/dhuan/mock)
|
|
914
|
+
- [Swagger](https://swagger.io/)
|
|
915
|
+
|
|
865
916
|
<br/>
|
|
866
917
|
|
|
867
918
|
---
|
package/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ type Plugin = (
|
|
|
5
5
|
request: IncomingMessage,
|
|
6
6
|
response: OutgoingMessage
|
|
7
7
|
) => Promise<{
|
|
8
|
-
mime: string
|
|
8
|
+
mime: string
|
|
9
9
|
body: string | Uint8Array
|
|
10
10
|
}>
|
|
11
11
|
|
|
@@ -30,7 +30,7 @@ interface Config {
|
|
|
30
30
|
extraHeaders?: string[]
|
|
31
31
|
extraMimes?: { [fileExt: string]: string }
|
|
32
32
|
|
|
33
|
-
corsAllowed?: boolean
|
|
33
|
+
corsAllowed?: boolean
|
|
34
34
|
corsOrigins?: string[]
|
|
35
35
|
corsMethods?: string[]
|
|
36
36
|
corsHeaders?: string[]
|
|
@@ -42,10 +42,13 @@ interface Config {
|
|
|
42
42
|
plugins?: [filenameTester: RegExp, plugin: Plugin][]
|
|
43
43
|
|
|
44
44
|
onReady?: (address: string) => void
|
|
45
|
+
|
|
46
|
+
watcherEnabled?: boolean
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
export function Mockaton(options: Partial<Config>): Promise<Server | undefined>
|
|
51
|
+
|
|
49
52
|
export function defineConfig(options: Partial<Config>): Partial<Config>
|
|
50
53
|
|
|
51
54
|
export const jsToJsonPlugin: Plugin
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -12,7 +12,7 @@ import * as staticCollection from './staticCollection.js'
|
|
|
12
12
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
13
13
|
import { config, ConfigValidator } from './config.js'
|
|
14
14
|
import { DashboardHtml, CSP } from './DashboardHtml.js'
|
|
15
|
-
import { sendOK, sendJSON,
|
|
15
|
+
import { sendOK, sendJSON, sendUnprocessable, sendFile, sendHTML } from './utils/http-response.js'
|
|
16
16
|
import { API, LONG_POLL_SERVER_TIMEOUT, HEADER_SYNC_VERSION } from './ApiConstants.js'
|
|
17
17
|
|
|
18
18
|
|
|
@@ -81,7 +81,8 @@ function getState(_, response) {
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
function longPollClientSyncVersion(req, response) {
|
|
84
|
-
|
|
84
|
+
const clientVersion = req.headers[HEADER_SYNC_VERSION]
|
|
85
|
+
if (clientVersion !== undefined && uiSyncVersion.version !== Number(clientVersion)) {
|
|
85
86
|
// e.g., tab was hidden while new mocks were added or removed
|
|
86
87
|
sendJSON(response, uiSyncVersion.version)
|
|
87
88
|
return
|
|
@@ -115,7 +116,7 @@ async function selectCookie(req, response) {
|
|
|
115
116
|
|
|
116
117
|
const error = cookie.setCurrent(cookieKey)
|
|
117
118
|
if (error)
|
|
118
|
-
|
|
119
|
+
sendUnprocessable(response, error?.message || error)
|
|
119
120
|
else
|
|
120
121
|
sendJSON(response, cookie.list())
|
|
121
122
|
}
|
|
@@ -126,7 +127,7 @@ async function selectMock(req, response) {
|
|
|
126
127
|
|
|
127
128
|
const broker = mockBrokersCollection.brokerByFilename(file)
|
|
128
129
|
if (!broker || !broker.hasMock(file))
|
|
129
|
-
|
|
130
|
+
sendUnprocessable(response, `Missing Mock: ${file}`)
|
|
130
131
|
else {
|
|
131
132
|
broker.selectFile(file)
|
|
132
133
|
sendJSON(response, broker)
|
|
@@ -139,7 +140,7 @@ async function toggle500(req, response) {
|
|
|
139
140
|
|
|
140
141
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
141
142
|
if (!broker)
|
|
142
|
-
|
|
143
|
+
sendUnprocessable(response, `Route does not exist: ${method} ${urlMask}`)
|
|
143
144
|
else {
|
|
144
145
|
broker.toggle500()
|
|
145
146
|
sendJSON(response, broker)
|
|
@@ -152,9 +153,9 @@ async function setRouteIsDelayed(req, response) {
|
|
|
152
153
|
|
|
153
154
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
154
155
|
if (!broker)
|
|
155
|
-
|
|
156
|
+
sendUnprocessable(response, `Route does not exist: ${method} ${urlMask}`)
|
|
156
157
|
else if (typeof delayed !== 'boolean')
|
|
157
|
-
|
|
158
|
+
sendUnprocessable(response, `Expected boolean for "delayed"`)
|
|
158
159
|
else {
|
|
159
160
|
broker.setDelayed(delayed)
|
|
160
161
|
sendJSON(response, broker)
|
|
@@ -167,11 +168,11 @@ async function setRouteIsProxied(req, response) {
|
|
|
167
168
|
|
|
168
169
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
169
170
|
if (!broker)
|
|
170
|
-
|
|
171
|
+
sendUnprocessable(response, `Route does not exist: ${method} ${urlMask}`)
|
|
171
172
|
else if (typeof proxied !== 'boolean')
|
|
172
|
-
|
|
173
|
+
sendUnprocessable(response, `Expected boolean for "proxied"`)
|
|
173
174
|
else if (proxied && !config.proxyFallback)
|
|
174
|
-
|
|
175
|
+
sendUnprocessable(response, `There’s no proxy fallback`)
|
|
175
176
|
else {
|
|
176
177
|
broker.setProxied(proxied)
|
|
177
178
|
sendJSON(response, broker)
|
|
@@ -183,7 +184,7 @@ async function updateProxyFallback(req, response) {
|
|
|
183
184
|
const fallback = await parseJSON(req)
|
|
184
185
|
|
|
185
186
|
if (!ConfigValidator.proxyFallback(fallback))
|
|
186
|
-
|
|
187
|
+
sendUnprocessable(response, `Invalid Proxy Fallback URL`)
|
|
187
188
|
else {
|
|
188
189
|
config.proxyFallback = fallback
|
|
189
190
|
sendOK(response)
|
|
@@ -195,7 +196,7 @@ async function setCollectProxied(req, response) {
|
|
|
195
196
|
const collectProxied = await parseJSON(req)
|
|
196
197
|
|
|
197
198
|
if (!ConfigValidator.collectProxied(collectProxied))
|
|
198
|
-
|
|
199
|
+
sendUnprocessable(response, `Expected a boolean for "collectProxied"`)
|
|
199
200
|
else {
|
|
200
201
|
config.collectProxied = collectProxied
|
|
201
202
|
sendOK(response)
|
|
@@ -215,7 +216,7 @@ async function setCorsAllowed(req, response) {
|
|
|
215
216
|
const corsAllowed = await parseJSON(req)
|
|
216
217
|
|
|
217
218
|
if (!ConfigValidator.corsAllowed(corsAllowed))
|
|
218
|
-
|
|
219
|
+
sendUnprocessable(response, `Expected boolean for "corsAllowed"`)
|
|
219
220
|
else {
|
|
220
221
|
config.corsAllowed = corsAllowed
|
|
221
222
|
sendOK(response)
|
|
@@ -227,7 +228,7 @@ async function setGlobalDelay(req, response) {
|
|
|
227
228
|
const delay = await parseJSON(req)
|
|
228
229
|
|
|
229
230
|
if (!ConfigValidator.delay(delay))
|
|
230
|
-
|
|
231
|
+
sendUnprocessable(response, `Expected non-negative integer for "delay"`)
|
|
231
232
|
else {
|
|
232
233
|
config.delay = delay
|
|
233
234
|
sendOK(response)
|
|
@@ -241,9 +242,9 @@ async function setStaticRouteStatusCode(req, response) {
|
|
|
241
242
|
|
|
242
243
|
const broker = staticCollection.brokerByRoute(urlMask)
|
|
243
244
|
if (!broker)
|
|
244
|
-
|
|
245
|
+
sendUnprocessable(response, `Static route does not exist: ${urlMask}`)
|
|
245
246
|
else if (!(status === 200 || status === 404))
|
|
246
|
-
|
|
247
|
+
sendUnprocessable(response, `Expected 200 or 404 status code`)
|
|
247
248
|
else {
|
|
248
249
|
broker.setStatus(status)
|
|
249
250
|
sendOK(response)
|
|
@@ -256,9 +257,9 @@ async function setStaticRouteIsDelayed(req, response) {
|
|
|
256
257
|
|
|
257
258
|
const broker = staticCollection.brokerByRoute(urlMask)
|
|
258
259
|
if (!broker)
|
|
259
|
-
|
|
260
|
+
sendUnprocessable(response, `Static route does not exist: ${urlMask}`)
|
|
260
261
|
else if (typeof delayed !== 'boolean')
|
|
261
|
-
|
|
262
|
+
sendUnprocessable(response, `Expected boolean for "delayed"`)
|
|
262
263
|
else {
|
|
263
264
|
broker.setDelayed(delayed)
|
|
264
265
|
sendOK(response)
|
package/src/ApiCommander.js
CHANGED
|
@@ -9,80 +9,49 @@ export class Commander {
|
|
|
9
9
|
this.#addr = addr
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
#patch = (api, body) =>
|
|
13
|
-
fetch(this.#addr + api, {
|
|
14
|
-
method: 'PATCH',
|
|
15
|
-
body: JSON.stringify(body)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
12
|
/** @returns {JsonPromise<State>} */
|
|
19
13
|
getState = () =>
|
|
20
14
|
fetch(this.#addr + API.state)
|
|
21
15
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
16
|
+
/**
|
|
17
|
+
* @param {number?} currSyncVer - On mismatch, it responds immediately. Otherwise, long polls.
|
|
18
|
+
* @param {AbortSignal} abortSignal
|
|
19
|
+
* @returns {JsonPromise<number>}
|
|
20
|
+
*/
|
|
21
|
+
getSyncVersion = (currSyncVer = undefined, abortSignal = undefined) =>
|
|
24
22
|
fetch(this.#addr + API.syncVersion, {
|
|
25
23
|
signal: AbortSignal.any([
|
|
26
24
|
abortSignal,
|
|
27
25
|
AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
|
|
28
26
|
].filter(Boolean)),
|
|
29
|
-
headers:
|
|
30
|
-
[HEADER_SYNC_VERSION]: currSyncVer
|
|
31
|
-
|
|
27
|
+
headers: currSyncVer !== undefined
|
|
28
|
+
? { [HEADER_SYNC_VERSION]: currSyncVer }
|
|
29
|
+
: {}
|
|
32
30
|
})
|
|
33
31
|
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
return this.#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return this.#patch(API.globalDelay, delay)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
bulkSelectByComment(comment) {
|
|
44
|
-
return this.#patch(API.bulkSelect, comment)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
selectCookie(cookieKey) {
|
|
48
|
-
return this.#patch(API.cookies, cookieKey)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
setProxyFallback(proxyAddr) {
|
|
52
|
-
return this.#patch(API.fallback, proxyAddr)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
setCollectProxied(shouldCollect) {
|
|
56
|
-
return this.#patch(API.collectProxied, shouldCollect)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
setCorsAllowed(value) {
|
|
60
|
-
return this.#patch(API.cors, value)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
select(file) {
|
|
65
|
-
return this.#patch(API.select, file)
|
|
33
|
+
#patch(api, body) {
|
|
34
|
+
return fetch(this.#addr + api, {
|
|
35
|
+
method: 'PATCH',
|
|
36
|
+
body: JSON.stringify(body)
|
|
37
|
+
})
|
|
66
38
|
}
|
|
67
39
|
|
|
68
|
-
|
|
69
|
-
return this.#patch(API.toggle500, [method, urlMask])
|
|
70
|
-
}
|
|
40
|
+
reset = () => this.#patch(API.reset)
|
|
71
41
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
42
|
+
selectCookie = label => this.#patch(API.cookies, label)
|
|
43
|
+
setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
|
|
44
|
+
setCorsAllowed = value => this.#patch(API.cors, value)
|
|
45
|
+
setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
|
|
46
|
+
setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
|
|
75
47
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
48
|
+
select = file => this.#patch(API.select, file)
|
|
49
|
+
bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
|
|
80
50
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
51
|
+
toggle500 = (method, urlMask) => this.#patch(API.toggle500, [method, urlMask])
|
|
52
|
+
setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
|
|
53
|
+
setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
|
|
84
54
|
|
|
85
|
-
setStaticRouteStatus(urlMask, status)
|
|
86
|
-
|
|
87
|
-
}
|
|
55
|
+
setStaticRouteStatus = (urlMask, status) => this.#patch(API.staticStatus, [urlMask, status])
|
|
56
|
+
setStaticRouteIsDelayed = (urlMask, delayed) => this.#patch(API.delayStatic, [urlMask, delayed])
|
|
88
57
|
}
|
package/src/Dashboard.js
CHANGED
|
@@ -734,7 +734,7 @@ function SettingsIcon() {
|
|
|
734
734
|
* The version increments when a mock file is added, removed, or renamed.
|
|
735
735
|
*/
|
|
736
736
|
function initRealTimeUpdates() {
|
|
737
|
-
let oldVersion =
|
|
737
|
+
let oldVersion = undefined // undefined waits until next event or timeout
|
|
738
738
|
let controller = new AbortController()
|
|
739
739
|
|
|
740
740
|
longPoll()
|
|
@@ -755,11 +755,9 @@ function initRealTimeUpdates() {
|
|
|
755
755
|
ErrorToast.close()
|
|
756
756
|
|
|
757
757
|
const version = await response.json()
|
|
758
|
-
const skipUpdate = oldVersion === -1
|
|
759
758
|
if (oldVersion !== version) { // because it could be < or >
|
|
760
759
|
oldVersion = version
|
|
761
|
-
|
|
762
|
-
store.fetchState()
|
|
760
|
+
store.fetchState()
|
|
763
761
|
}
|
|
764
762
|
longPoll()
|
|
765
763
|
}
|
package/src/MockBroker.js
CHANGED
|
@@ -2,8 +2,8 @@ import { includesComment, extractComments, parseFilename } from './Filename.js'
|
|
|
2
2
|
import { DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* MockBroker is a state for a particular route. It knows the available mock
|
|
5
|
+
/**
|
|
6
|
+
* MockBroker is a state for a particular route. It knows the available mock
|
|
7
7
|
* files that can be served for the route, the currently selected file, etc.
|
|
8
8
|
*/
|
|
9
9
|
export class MockBroker {
|
|
@@ -27,7 +27,7 @@ export class MockBroker {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
register(file) {
|
|
30
|
-
if (this.auto500 && this.#is500(file))
|
|
30
|
+
if (this.auto500 && this.#is500(file))
|
|
31
31
|
this.selectFile(file)
|
|
32
32
|
this.mocks.push(file)
|
|
33
33
|
this.#sortMocks()
|
|
@@ -114,7 +114,7 @@ class UrlMatcher {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
#disregardVariables(str) { // Stars out all parts that are in square brackets
|
|
117
|
-
return str.replace(/\[.*?]/g, '[^/]
|
|
117
|
+
return str.replace(/\[.*?]/g, '[^/]+')
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// Appending a '/' so URLs ending with variables don't match
|
package/src/MockDispatcher.js
CHANGED
|
@@ -14,11 +14,11 @@ import { sendInternalServerError, sendMockNotFound } from './utils/http-response
|
|
|
14
14
|
export async function dispatchMock(req, response) {
|
|
15
15
|
try {
|
|
16
16
|
const isHead = req.method === 'HEAD'
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
let broker = mockBrokerCollection.brokerByRoute(req.method, req.url)
|
|
19
19
|
if (!broker && isHead)
|
|
20
20
|
broker = mockBrokerCollection.brokerByRoute('GET', req.url)
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
if (config.proxyFallback && (!broker || broker.proxied)) {
|
|
23
23
|
await proxy(req, response, broker?.delayed ? calcDelay() : 0)
|
|
24
24
|
return
|
|
@@ -42,6 +42,7 @@ export async function dispatchMock(req, response) {
|
|
|
42
42
|
logger.accessMock(req.url, broker.file)
|
|
43
43
|
response.setHeader('Content-Type', mime)
|
|
44
44
|
response.setHeader('Content-Length', length(body))
|
|
45
|
+
|
|
45
46
|
setTimeout(() => response.end(isHead ? null : body),
|
|
46
47
|
Number(broker.delayed && calcDelay()))
|
|
47
48
|
}
|
package/src/Mockaton.js
CHANGED
|
@@ -11,7 +11,7 @@ import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
|
11
11
|
import { watchMocksDir, watchStaticDir } from './Watcher.js'
|
|
12
12
|
import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
13
13
|
import { BodyReaderError, hasControlChars } from './utils/http-request.js'
|
|
14
|
-
import { sendNoContent, sendInternalServerError,
|
|
14
|
+
import { sendNoContent, sendInternalServerError, sendUnprocessable, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
export function Mockaton(options) {
|
|
@@ -20,8 +20,11 @@ export function Mockaton(options) {
|
|
|
20
20
|
|
|
21
21
|
mockBrokerCollection.init()
|
|
22
22
|
staticCollection.init()
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
|
|
24
|
+
if (options.watcherEnabled) {
|
|
25
|
+
watchMocksDir()
|
|
26
|
+
watchStaticDir()
|
|
27
|
+
}
|
|
25
28
|
|
|
26
29
|
const server = createServer(onRequest)
|
|
27
30
|
server.on('error', reject)
|
|
@@ -75,7 +78,7 @@ async function onRequest(req, response) {
|
|
|
75
78
|
}
|
|
76
79
|
catch (error) {
|
|
77
80
|
if (error instanceof BodyReaderError)
|
|
78
|
-
|
|
81
|
+
sendUnprocessable(response, `${error.name}: ${error.message}`)
|
|
79
82
|
else
|
|
80
83
|
sendInternalServerError(response, error)
|
|
81
84
|
}
|
package/src/ProxyRelay.js
CHANGED
|
@@ -6,7 +6,7 @@ import { extFor } from './utils/mime.js'
|
|
|
6
6
|
import { write, isFile } from './utils/fs.js'
|
|
7
7
|
import { makeMockFilename } from './Filename.js'
|
|
8
8
|
import { readBody, BodyReaderError } from './utils/http-request.js'
|
|
9
|
-
import {
|
|
9
|
+
import { sendUnprocessable, sendBadGateway } from './utils/http-response.js'
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
export async function proxy(req, response, delay) {
|
|
@@ -22,7 +22,7 @@ export async function proxy(req, response, delay) {
|
|
|
22
22
|
}
|
|
23
23
|
catch (error) { // TESTME
|
|
24
24
|
if (error instanceof BodyReaderError)
|
|
25
|
-
|
|
25
|
+
sendUnprocessable(response, error.name)
|
|
26
26
|
else
|
|
27
27
|
sendBadGateway(response, error)
|
|
28
28
|
return
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
|
|
4
|
+
import { isFile } from './utils/fs.js'
|
|
4
5
|
import { logger } from './utils/logger.js'
|
|
5
6
|
import { mimeFor } from './utils/mime.js'
|
|
6
7
|
import { brokerByRoute } from './staticCollection.js'
|
|
7
8
|
import { config, calcDelay } from './config.js'
|
|
8
9
|
import { sendMockNotFound, sendPartialContent } from './utils/http-response.js'
|
|
9
|
-
import { execFileSync } from 'node:child_process'
|
|
10
|
-
import { isFile } from './utils/fs.js'
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
export async function dispatchStatic(req, response) {
|
package/src/Watcher.js
CHANGED
|
@@ -15,9 +15,10 @@ import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
|
15
15
|
* and also renames, which are two events (delete + add).
|
|
16
16
|
*/
|
|
17
17
|
export const uiSyncVersion = new class extends EventEmitter {
|
|
18
|
+
delay = Number(process.env.MOCKATON_WATCHER_DEBOUNCE_MS ?? 80)
|
|
18
19
|
version = 0
|
|
19
20
|
|
|
20
|
-
increment = this.#debounce(() => {
|
|
21
|
+
increment = /** @type {function} */ this.#debounce(() => {
|
|
21
22
|
this.version++
|
|
22
23
|
super.emit('ARR')
|
|
23
24
|
})
|
|
@@ -33,7 +34,7 @@ export const uiSyncVersion = new class extends EventEmitter {
|
|
|
33
34
|
let timer
|
|
34
35
|
return () => {
|
|
35
36
|
clearTimeout(timer)
|
|
36
|
-
timer = setTimeout(fn,
|
|
37
|
+
timer = setTimeout(fn, this.delay)
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -64,7 +65,7 @@ export function watchStaticDir() {
|
|
|
64
65
|
const dir = config.staticDir
|
|
65
66
|
if (!dir)
|
|
66
67
|
return
|
|
67
|
-
|
|
68
|
+
|
|
68
69
|
watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
69
70
|
if (!file)
|
|
70
71
|
return
|
package/src/config.js
CHANGED
|
@@ -41,20 +41,22 @@ export function init() {
|
|
|
41
41
|
/** @returns {boolean} registered */
|
|
42
42
|
export function registerMock(file, isFromWatcher = false) {
|
|
43
43
|
if (brokerByFilename(file)?.hasMock(file)
|
|
44
|
-
|| !isFileAllowed(basename(file))
|
|
44
|
+
|| !isFileAllowed(basename(file))
|
|
45
45
|
|| !filenameIsValid(file))
|
|
46
46
|
return false
|
|
47
47
|
|
|
48
48
|
const { method, urlMask } = parseFilename(file)
|
|
49
49
|
collection[method] ??= {}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
let broker = collection[method][urlMask]
|
|
52
|
+
|
|
53
|
+
if (!broker)
|
|
54
|
+
broker = collection[method][urlMask] = new MockBroker(file)
|
|
53
55
|
else
|
|
54
|
-
|
|
56
|
+
broker.register(file)
|
|
55
57
|
|
|
56
|
-
if (isFromWatcher && !
|
|
57
|
-
|
|
58
|
+
if (isFromWatcher && !broker.file)
|
|
59
|
+
broker.selectDefaultFile()
|
|
58
60
|
|
|
59
61
|
return true
|
|
60
62
|
}
|
|
@@ -83,8 +85,7 @@ export function unregisterMock(file) {
|
|
|
83
85
|
/** @returns {MockBroker | undefined} */
|
|
84
86
|
export function brokerByFilename(file) {
|
|
85
87
|
const { method, urlMask } = parseFilename(file)
|
|
86
|
-
|
|
87
|
-
return collection[method][urlMask]
|
|
88
|
+
return collection[method]?.[urlMask]
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
@@ -113,7 +114,7 @@ export function extractAllComments() {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
export function setMocksMatchingComment(comment) {
|
|
116
|
-
forEachBroker(b =>
|
|
117
|
+
forEachBroker(b =>
|
|
117
118
|
b.setByMatchingComment(comment))
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -53,7 +53,7 @@ export function sendTooLongURI(response) {
|
|
|
53
53
|
response.end()
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function
|
|
56
|
+
export function sendUnprocessable(response, error) {
|
|
57
57
|
logger.access(response, error)
|
|
58
58
|
response.statusCode = 422
|
|
59
59
|
response.end(error)
|