mockaton 8.20.1 → 8.20.2
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 +72 -41
- package/index.d.ts +22 -24
- package/package.json +3 -3
- package/src/Api.js +28 -18
- package/src/Commander.js +52 -36
- package/src/Dashboard.js +199 -138
- package/src/Mockaton.js +1 -1
- package/src/mockBrokersCollection.js +3 -2
- package/src/staticCollection.js +2 -2
- package/src/utils/http-request.js +9 -3
package/README.md
CHANGED
|
@@ -5,21 +5,39 @@
|
|
|
5
5
|
[](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
|
|
6
6
|
[](https://github.com/ericfortis/mockaton/actions/workflows/github-code-scanning/codeql)
|
|
7
7
|
|
|
8
|
-
An HTTP mock server for simulating APIs with minimal setup
|
|
9
|
-
|
|
8
|
+
An HTTP mock server for simulating APIs with minimal setup — ideal for testing
|
|
9
|
+
difficult to reproduce states. For example, triggering an error on a third-party API.
|
|
10
|
+
Similarly, if you are a frontend developer, triggering it on your project’s backend.
|
|
10
11
|
|
|
12
|
+
<br/>
|
|
13
|
+
|
|
14
|
+
## Motivation
|
|
15
|
+
|
|
16
|
+
**No API state should be too hard to test.**
|
|
17
|
+
With Mockaton, developers can achieve correctness without sacrificing speed.
|
|
18
|
+
|
|
19
|
+
### Correctness
|
|
20
|
+
- Enables testing of complex or rare scenarios that would otherwise be skipped.
|
|
21
|
+
- Allows for deterministic, comprehensive, and consistent state.
|
|
22
|
+
|
|
23
|
+
### Speed
|
|
24
|
+
- Prevents progress from being blocked by waiting for APIs.
|
|
25
|
+
- Avoids spinning up and updating hefty backends when developing UIs.
|
|
26
|
+
|
|
27
|
+
<br/>
|
|
11
28
|
|
|
12
29
|
## Overview
|
|
13
30
|
With Mockaton, you don’t need to write code for wiring up your
|
|
14
31
|
mocks. Instead, a given directory is scanned for filenames
|
|
15
|
-
following a convention similar to the URLs.
|
|
32
|
+
following a convention similar to the URLs.
|
|
16
33
|
|
|
17
|
-
For example, for [/api/user/123](#), the
|
|
34
|
+
For example, for [/api/user/123](#), the filename could be:
|
|
18
35
|
|
|
19
36
|
<pre>
|
|
20
37
|
<code>my-mocks-dir/<b>api/user</b>/[user-id].GET.200.json</code>
|
|
21
38
|
</pre>
|
|
22
39
|
|
|
40
|
+
<br/>
|
|
23
41
|
|
|
24
42
|
## Dashboard
|
|
25
43
|
|
|
@@ -62,30 +80,34 @@ api/videos.GET.<b>500</b>.txt # Internal Server Error
|
|
|
62
80
|
|
|
63
81
|
<br/>
|
|
64
82
|
|
|
65
|
-
## Scraping
|
|
83
|
+
## Scraping Mocks from your Backend
|
|
66
84
|
|
|
67
85
|
### Option 1: Browser Extension
|
|
68
|
-
This [browser extension](https://github.com/ericfortis/download-http-requests-browser-ext)
|
|
69
|
-
lets you download all the HTTP responses, and they
|
|
86
|
+
This companion [browser-devtools extension](https://github.com/ericfortis/download-http-requests-browser-ext)
|
|
87
|
+
lets you download all the HTTP responses at once, and they
|
|
70
88
|
get saved following Mockaton’s filename convention.
|
|
71
89
|
|
|
72
90
|
### Option 2: Fallback to Your Backend
|
|
73
|
-
|
|
91
|
+
<details>
|
|
92
|
+
<summary>Details</summary>
|
|
93
|
+
|
|
94
|
+
This option could be a bit elaborate if your backend uses third-party auth,
|
|
74
95
|
because you’d have to manually inject cookies or `sessionStorage` tokens.
|
|
75
96
|
|
|
76
97
|
On the other hand, proxying to your backend is straightforward if your backend
|
|
77
|
-
handles the session cookie, or if you can develop without auth.
|
|
98
|
+
handles the session cookie, or if you can develop without auth.
|
|
78
99
|
|
|
79
100
|
Either way you can forward requests to your backend for routes you don’t have
|
|
80
101
|
mocks for, or routes that have the ☁️ **Cloud Checkbox** checked. In addition, by
|
|
81
102
|
checking ✅ **Save Mocks**, you can collect the responses that hit your backend.
|
|
82
103
|
They will be saved in your `config.mocksDir` following the filename convention.
|
|
104
|
+
</details>
|
|
83
105
|
|
|
84
106
|
|
|
85
107
|
<br/>
|
|
86
108
|
|
|
87
109
|
|
|
88
|
-
## Basic Usage
|
|
110
|
+
## Basic Usage
|
|
89
111
|
```sh
|
|
90
112
|
npm install mockaton --save-dev
|
|
91
113
|
```
|
|
@@ -95,23 +117,25 @@ Create a `my-mockaton.js` file
|
|
|
95
117
|
import { resolve } from 'node:path'
|
|
96
118
|
import { Mockaton } from 'mockaton'
|
|
97
119
|
|
|
98
|
-
// See the Config section for more options
|
|
99
120
|
Mockaton({
|
|
100
121
|
mocksDir: resolve('my-mocks-dir'), // must exist
|
|
101
122
|
port: 2345
|
|
102
|
-
})
|
|
123
|
+
}) // The Config section below documents more options
|
|
103
124
|
```
|
|
104
125
|
|
|
105
126
|
```sh
|
|
106
127
|
node my-mockaton.js
|
|
107
128
|
```
|
|
108
129
|
|
|
109
|
-
|
|
130
|
+
<details>
|
|
131
|
+
<summary>About TypeScript in Node < 23.6</summary>
|
|
110
132
|
If you want to write mocks in TypeScript in a version older than Node 23.6:
|
|
133
|
+
|
|
111
134
|
```shell
|
|
112
135
|
npm install tsx
|
|
113
136
|
node --import=tsx my-mockaton.js
|
|
114
137
|
```
|
|
138
|
+
</details>
|
|
115
139
|
|
|
116
140
|
|
|
117
141
|
<br/>
|
|
@@ -128,11 +152,13 @@ npm run start # in another terminal
|
|
|
128
152
|
```
|
|
129
153
|
|
|
130
154
|
The demo app has a list of colors containing all of their possible states. For example,
|
|
131
|
-
permutations for out-of-stock, new-arrival, and discontinued.
|
|
155
|
+
permutations for out-of-stock, new-arrival, and discontinued.
|
|
132
156
|
|
|
133
157
|
<img src="./demo-app-vite/pixaton-tests/pic-for-readme.vp740x880.light.gold.png" alt="Mockaton Demo App Screenshot" width="740" />
|
|
134
158
|
|
|
135
159
|
<br/>
|
|
160
|
+
<br/>
|
|
161
|
+
|
|
136
162
|
|
|
137
163
|
## Use Cases
|
|
138
164
|
### Testing Backend or Frontend
|
|
@@ -163,21 +189,12 @@ putting its built assets in `config.staticDir`. And simulate the flow by Bulk Se
|
|
|
163
189
|
The [aot-fetch-demo repo](https://github.com/ericfortis/aot-fetch-demo) has a working example.
|
|
164
190
|
|
|
165
191
|
|
|
166
|
-
<br/>
|
|
167
|
-
|
|
168
|
-
## Motivation
|
|
169
|
-
- Avoids spinning up and updating hefty backends when developing UIs.
|
|
170
|
-
- Allows for a deterministic, comprehensive, and consistent backend state. For example, having
|
|
171
|
-
a collection with all the possible state variants helps for spotting inadvertent bugs.
|
|
172
|
-
- Sometimes frontend progress is blocked by waiting for backend APIs.
|
|
173
|
-
|
|
174
192
|
<br/>
|
|
175
193
|
|
|
176
194
|
|
|
177
195
|
## You can write JSON mocks in JavaScript or TypeScript
|
|
178
196
|
For example, `api/foo.GET.200.js`
|
|
179
197
|
|
|
180
|
-
|
|
181
198
|
**Option A:** An Object, Array, or String is sent as JSON.
|
|
182
199
|
|
|
183
200
|
```js
|
|
@@ -189,7 +206,7 @@ export default { foo: 'bar' }
|
|
|
189
206
|
Return a `string | Buffer | Uint8Array`, but don’t call `response.end()`
|
|
190
207
|
|
|
191
208
|
```js
|
|
192
|
-
export default (request, response) =>
|
|
209
|
+
export default (request, response) =>
|
|
193
210
|
JSON.stringify({ foo: 'bar' })
|
|
194
211
|
```
|
|
195
212
|
|
|
@@ -206,6 +223,7 @@ you want to concatenate newly added colors.
|
|
|
206
223
|
```js
|
|
207
224
|
import { parseJSON } from 'mockaton'
|
|
208
225
|
|
|
226
|
+
|
|
209
227
|
export default async function insertColor(request, response) {
|
|
210
228
|
const color = await parseJSON(request)
|
|
211
229
|
globalThis.newColorsDatabase ??= []
|
|
@@ -223,6 +241,7 @@ export default async function insertColor(request, response) {
|
|
|
223
241
|
```js
|
|
224
242
|
import colorsFixture from './colors.json' with { type: 'json' }
|
|
225
243
|
|
|
244
|
+
|
|
226
245
|
export default function listColors() {
|
|
227
246
|
return JSON.stringify([
|
|
228
247
|
...colorsFixture,
|
|
@@ -305,7 +324,7 @@ A filename can have many comments.
|
|
|
305
324
|
<br/>
|
|
306
325
|
|
|
307
326
|
### Default mock for a route
|
|
308
|
-
You can add the comment: `(default)`.
|
|
327
|
+
You can add the comment: `(default)`.
|
|
309
328
|
Otherwise, the first file in **alphabetical order** wins.
|
|
310
329
|
|
|
311
330
|
<pre>
|
|
@@ -322,7 +341,8 @@ api/video<b>?limit=[limit]</b>.GET.200.json
|
|
|
322
341
|
</pre>
|
|
323
342
|
|
|
324
343
|
On Windows, filenames containing "?" are [not
|
|
325
|
-
permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query
|
|
344
|
+
permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query
|
|
345
|
+
string it’s ignored anyway.
|
|
326
346
|
|
|
327
347
|
<br/>
|
|
328
348
|
|
|
@@ -353,9 +373,9 @@ it’s convenient for serving 200 GET requests without having to add the filenam
|
|
|
353
373
|
extension convention. For example, for using Mockaton as a standalone demo server,
|
|
354
374
|
as explained above in the _Use Cases_ section.
|
|
355
375
|
|
|
356
|
-
Files under `config.staticDir`
|
|
357
|
-
|
|
358
|
-
|
|
376
|
+
Files under `config.staticDir` take precedence over corresponding
|
|
377
|
+
`GET` mocks in `config.mocksDir` (regardless of status code).
|
|
378
|
+
For example, if you have two files for `GET /foo/bar.jpg`:
|
|
359
379
|
<pre>
|
|
360
380
|
my-static-dir<b>/foo/bar.jpg</b> <span style="color:green"> // Wins</span>
|
|
361
381
|
my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg <span style="color:red"> // Unreachable</span>
|
|
@@ -365,7 +385,9 @@ my-static-dir<b>/foo/bar.jpg</b> <span style="color:green"> // Wins</span>
|
|
|
365
385
|
<br/>
|
|
366
386
|
|
|
367
387
|
### `ignore?: RegExp`
|
|
368
|
-
Defaults to `/(\.DS_Store|~)
|
|
388
|
+
Defaults to `/(\.DS_Store|~)$/`. The regex rule is
|
|
389
|
+
tested against the basename (filename without directory path).
|
|
390
|
+
|
|
369
391
|
|
|
370
392
|
<br/>
|
|
371
393
|
|
|
@@ -378,13 +400,13 @@ Defaults to `0`, which means auto-assigned
|
|
|
378
400
|
|
|
379
401
|
<br/>
|
|
380
402
|
|
|
381
|
-
### `delay?: number`
|
|
403
|
+
### `delay?: number`
|
|
382
404
|
Defaults to `1200` milliseconds. Although routes can individually be delayed
|
|
383
405
|
with the 🕓 Checkbox, the amount is globally configurable with this option.
|
|
384
406
|
|
|
385
407
|
### `delayJitter?: number`
|
|
386
408
|
Defaults to `0`. Range: `[0.0, 3.0]`. Maximum percentage of the delay to add.
|
|
387
|
-
For example, `0.5` will add at most `600ms` to the default delay.
|
|
409
|
+
For example, `0.5` will add at most `600ms` to the default delay.
|
|
388
410
|
|
|
389
411
|
<br/>
|
|
390
412
|
|
|
@@ -396,7 +418,7 @@ or that you manually picked with the ☁️ **Cloud Checkbox**.
|
|
|
396
418
|
|
|
397
419
|
### `collectProxied?: boolean`
|
|
398
420
|
Defaults to `false`. With this flag you can save mocks that hit
|
|
399
|
-
your proxy fallback to `config.mocksDir`. If the URL has v4 UUIDs,
|
|
421
|
+
your proxy fallback to `config.mocksDir`. If the URL has v4 UUIDs,
|
|
400
422
|
the filename will have `[id]` in their place. For example:
|
|
401
423
|
|
|
402
424
|
<pre>
|
|
@@ -423,7 +445,7 @@ the predefined list. For that, you can add it to <code>config.extraMimes</code>
|
|
|
423
445
|
|
|
424
446
|
|
|
425
447
|
### `formatCollectedJSON?: boolean`
|
|
426
|
-
Defaults to `true`. Saves the mock with two spaces indentation —
|
|
448
|
+
Defaults to `true`. Saves the mock with two spaces indentation —
|
|
427
449
|
the formatting output of `JSON.stringify(data, null, ' ')`
|
|
428
450
|
|
|
429
451
|
|
|
@@ -435,6 +457,7 @@ the formatting output of `JSON.stringify(data, null, ' ')`
|
|
|
435
457
|
```js
|
|
436
458
|
import { jwtCookie } from 'mockaton'
|
|
437
459
|
|
|
460
|
+
|
|
438
461
|
config.cookies = {
|
|
439
462
|
'My Admin User': 'my-cookie=1;Path=/;SameSite=strict',
|
|
440
463
|
'My Normal User': 'my-cookie=0;Path=/;SameSite=strict',
|
|
@@ -449,7 +472,7 @@ The selected cookie, which is the first one by default, is sent in every respons
|
|
|
449
472
|
`Set-Cookie` header (as long as its value is not an empty string). The object key is just
|
|
450
473
|
a label for UI display purposes, and also for selecting a cookie via the Commander API.
|
|
451
474
|
|
|
452
|
-
If you need to send more than one cookie, you can inject them globally
|
|
475
|
+
If you need to send more than one cookie, you can inject them globally
|
|
453
476
|
in `config.extraHeaders`, or individually in a function `.js` or `.ts` mock.
|
|
454
477
|
|
|
455
478
|
By the way, the `jwtCookie` helper has a hardcoded header and signature.
|
|
@@ -509,12 +532,13 @@ import { parse } from 'yaml'
|
|
|
509
532
|
import { readFileSync } from 'node:js'
|
|
510
533
|
import { jsToJsonPlugin } from 'mockaton'
|
|
511
534
|
|
|
535
|
+
|
|
512
536
|
config.plugins = [
|
|
513
|
-
|
|
537
|
+
|
|
514
538
|
// Although `jsToJsonPlugin` is set by default, you need to include it if you need it.
|
|
515
539
|
// IOW, your plugins array overwrites the default list. This way you can remove it.
|
|
516
|
-
[/\.(js|ts)$/, jsToJsonPlugin],
|
|
517
|
-
|
|
540
|
+
[/\.(js|ts)$/, jsToJsonPlugin],
|
|
541
|
+
|
|
518
542
|
[/\.yml$/, yamlToJsonPlugin],
|
|
519
543
|
[/foo\.GET\.200\.txt$/, capitalizePlugin], // e.g. GET /api/foo would be capitalized
|
|
520
544
|
]
|
|
@@ -575,6 +599,7 @@ All of its methods return their `fetch` response promise.
|
|
|
575
599
|
```js
|
|
576
600
|
import { Commander } from 'mockaton'
|
|
577
601
|
|
|
602
|
+
|
|
578
603
|
const myMockatonAddr = 'http://localhost:2345'
|
|
579
604
|
const mockaton = new Commander(myMockatonAddr)
|
|
580
605
|
```
|
|
@@ -649,11 +674,17 @@ example, if you are polling, and you want to test the state change.
|
|
|
649
674
|
- Chrome DevTools allows for [overriding responses](https://developer.chrome.com/docs/devtools/overrides)
|
|
650
675
|
- Reverse Proxies such as [Burp](https://portswigger.net/burp) are also handy for overriding responses
|
|
651
676
|
|
|
652
|
-
### Client
|
|
677
|
+
### Client Side
|
|
653
678
|
In contrast to Mockaton, which is an HTTP Server, these programs
|
|
654
|
-
|
|
679
|
+
hijack the client (e.g., `fetch`) in Node.js and browsers.
|
|
655
680
|
|
|
656
|
-
- [
|
|
681
|
+
- [Mock Server Worker (MSW)](https://mswjs.io)
|
|
657
682
|
- [Nock](https://github.com/nock/nock)
|
|
658
683
|
- [Fetch Mock](https://github.com/wheresrhys/fetch-mock)
|
|
659
684
|
- [Mentoss](https://github.com/humanwhocodes/mentoss) Has a server side too
|
|
685
|
+
|
|
686
|
+
<br/>
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+

|
package/index.d.ts
CHANGED
|
@@ -55,33 +55,31 @@ export function jwtCookie(cookieName: string, payload: any, path?: string): stri
|
|
|
55
55
|
|
|
56
56
|
export function parseJSON(request: IncomingMessage): Promise<any>
|
|
57
57
|
|
|
58
|
+
export type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
|
|
58
59
|
|
|
59
|
-
export class Commander {
|
|
60
|
-
constructor(addr: string)
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
// API
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
listComments(): Promise<Response>
|
|
78
|
-
|
|
79
|
-
setProxyFallback(proxyAddr: string): Promise<Response>
|
|
80
|
-
|
|
81
|
-
reset(): Promise<Response>
|
|
63
|
+
export type ClientMockBroker = {
|
|
64
|
+
mocks: string[]
|
|
65
|
+
currentMock: {
|
|
66
|
+
file: string
|
|
67
|
+
delayed: boolean
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export type ClientBrokersByMethod = {
|
|
71
|
+
[method: string]: {
|
|
72
|
+
[urlMask: string]: ClientMockBroker
|
|
73
|
+
}
|
|
74
|
+
}
|
|
82
75
|
|
|
76
|
+
export type ClientStaticBroker = {
|
|
77
|
+
route: string
|
|
78
|
+
delayed: boolean
|
|
79
|
+
status: number
|
|
80
|
+
}
|
|
81
|
+
export type ClientStaticBrokers = {
|
|
82
|
+
[route: string]: ClientStaticBroker
|
|
83
|
+
}
|
|
83
84
|
|
|
84
|
-
getCorsAllowed(): Promise<Response>
|
|
85
85
|
|
|
86
|
-
setCorsAllowed(value: boolean): Promise<Response>
|
|
87
|
-
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "mockaton",
|
|
3
3
|
"description": "HTTP Mock Server",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.20.
|
|
5
|
+
"version": "8.20.2",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "node --test \"src/**/*.test.js\"",
|
|
20
20
|
"coverage": "node --test --test-reporter=lcov --test-reporter-destination=.coverage/lcov.info --experimental-test-coverage \"src/**/*.test.js\"",
|
|
21
|
-
"start": "node dev-mockaton.js",
|
|
21
|
+
"start": "node --watch dev-mockaton.js",
|
|
22
22
|
"pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none \"pixaton-tests/**/*.test.js\"",
|
|
23
23
|
"outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"open": "^10
|
|
26
|
+
"open": "^10"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"pixaton": "1.1.2",
|
package/src/Api.js
CHANGED
|
@@ -101,7 +101,7 @@ function longPollClientSyncVersion(req, response) {
|
|
|
101
101
|
|
|
102
102
|
function reinitialize(_, response) {
|
|
103
103
|
mockBrokersCollection.init()
|
|
104
|
-
staticCollection.init()
|
|
104
|
+
staticCollection.init()
|
|
105
105
|
sendOK(response)
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -131,17 +131,17 @@ async function setRouteIsDelayed(req, response) {
|
|
|
131
131
|
body[DF.routeMethod],
|
|
132
132
|
body[DF.routeUrlMask])
|
|
133
133
|
|
|
134
|
-
if (!broker)
|
|
134
|
+
if (!broker)
|
|
135
135
|
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
|
|
136
136
|
else if (typeof delayed !== 'boolean')
|
|
137
|
-
sendUnprocessableContent(response, `Expected
|
|
137
|
+
sendUnprocessableContent(response, `Expected boolean for "delayed"`)
|
|
138
138
|
else {
|
|
139
139
|
broker.setDelayed(delayed)
|
|
140
140
|
sendOK(response)
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
async function setRouteIsProxied(req, response) {
|
|
144
|
+
async function setRouteIsProxied(req, response) {
|
|
145
145
|
const body = await parseJSON(req)
|
|
146
146
|
const proxied = body[DF.proxied]
|
|
147
147
|
const broker = mockBrokersCollection.brokerByRoute(
|
|
@@ -151,7 +151,7 @@ async function setRouteIsProxied(req, response) { // TESTME
|
|
|
151
151
|
if (!broker)
|
|
152
152
|
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
|
|
153
153
|
else if (typeof proxied !== 'boolean')
|
|
154
|
-
sendUnprocessableContent(response, `Expected
|
|
154
|
+
sendUnprocessableContent(response, `Expected boolean for "proxied"`)
|
|
155
155
|
else if (proxied && !config.proxyFallback)
|
|
156
156
|
sendUnprocessableContent(response, `There’s no proxy fallback`)
|
|
157
157
|
else {
|
|
@@ -163,10 +163,10 @@ async function setRouteIsProxied(req, response) { // TESTME
|
|
|
163
163
|
async function updateProxyFallback(req, response) {
|
|
164
164
|
const fallback = await parseJSON(req)
|
|
165
165
|
if (!ConfigValidator.proxyFallback(fallback)) {
|
|
166
|
-
sendUnprocessableContent(response)
|
|
166
|
+
sendUnprocessableContent(response, `Invalid Proxy Fallback URL`)
|
|
167
167
|
return
|
|
168
168
|
}
|
|
169
|
-
if (!fallback)
|
|
169
|
+
if (!fallback)
|
|
170
170
|
mockBrokersCollection.ensureAllRoutesHaveSelectedMock()
|
|
171
171
|
config.proxyFallback = fallback
|
|
172
172
|
sendOK(response)
|
|
@@ -174,8 +174,8 @@ async function updateProxyFallback(req, response) {
|
|
|
174
174
|
|
|
175
175
|
async function setCollectProxied(req, response) {
|
|
176
176
|
const collectProxied = await parseJSON(req)
|
|
177
|
-
if (!ConfigValidator.collectProxied(collectProxied)) {
|
|
178
|
-
sendUnprocessableContent(response)
|
|
177
|
+
if (!ConfigValidator.collectProxied(collectProxied)) {
|
|
178
|
+
sendUnprocessableContent(response, `Expected a boolean for "collectProxied"`)
|
|
179
179
|
return
|
|
180
180
|
}
|
|
181
181
|
config.collectProxied = collectProxied
|
|
@@ -188,12 +188,22 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
async function setCorsAllowed(req, response) {
|
|
191
|
-
|
|
191
|
+
const corsAllowed = await parseJSON(req)
|
|
192
|
+
if (!ConfigValidator.corsAllowed(corsAllowed)) {
|
|
193
|
+
sendUnprocessableContent(response, `Expected boolean for "corsAllowed"`)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
config.corsAllowed = corsAllowed
|
|
192
197
|
sendOK(response)
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
async function setGlobalDelay(req, response) {
|
|
196
|
-
|
|
200
|
+
async function setGlobalDelay(req, response) {
|
|
201
|
+
const delay = await parseJSON(req)
|
|
202
|
+
if (!ConfigValidator.delay(delay)) {
|
|
203
|
+
sendUnprocessableContent(response, `Expected non-negative integer for "delay"`)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
config.delay = delay
|
|
197
207
|
sendOK(response)
|
|
198
208
|
}
|
|
199
209
|
|
|
@@ -203,10 +213,10 @@ async function setStaticRouteStatusCode(req, response) {
|
|
|
203
213
|
const status = Number(body[DF.statusCode])
|
|
204
214
|
const broker = staticCollection.brokerByRoute(body[DF.routeUrlMask])
|
|
205
215
|
|
|
206
|
-
if (!broker)
|
|
207
|
-
sendUnprocessableContent(response, `
|
|
216
|
+
if (!broker)
|
|
217
|
+
sendUnprocessableContent(response, `Static route does not exist: ${body[DF.routeUrlMask]}`)
|
|
208
218
|
else if (!(status === 200 || status === 404))
|
|
209
|
-
sendUnprocessableContent(response, `Expected
|
|
219
|
+
sendUnprocessableContent(response, `Expected 200 or 404 status code`)
|
|
210
220
|
else {
|
|
211
221
|
broker.setStatus(status)
|
|
212
222
|
sendOK(response)
|
|
@@ -219,10 +229,10 @@ async function setStaticRouteIsDelayed(req, response) {
|
|
|
219
229
|
const delayed = body[DF.delayed]
|
|
220
230
|
const broker = staticCollection.brokerByRoute(body[DF.routeUrlMask])
|
|
221
231
|
|
|
222
|
-
if (!broker)
|
|
223
|
-
sendUnprocessableContent(response, `
|
|
232
|
+
if (!broker)
|
|
233
|
+
sendUnprocessableContent(response, `Static route does not exist: ${body[DF.routeUrlMask]}`)
|
|
224
234
|
else if (typeof delayed !== 'boolean')
|
|
225
|
-
sendUnprocessableContent(response, `Expected
|
|
235
|
+
sendUnprocessableContent(response, `Expected boolean for "delayed"`)
|
|
226
236
|
else {
|
|
227
237
|
broker.setDelayed(delayed)
|
|
228
238
|
sendOK(response)
|
package/src/Commander.js
CHANGED
|
@@ -18,13 +18,65 @@ export class Commander {
|
|
|
18
18
|
})
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** @type {JsonPromise<ClientBrokersByMethod>} */
|
|
21
22
|
listMocks() {
|
|
22
23
|
return this.#get(API.mocks)
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** @type {JsonPromise<ClientStaticBrokers>} */
|
|
27
|
+
listStaticFiles() {
|
|
28
|
+
return this.#get(API.static)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @type {JsonPromise<[label:string, selected:boolean][]>} */
|
|
32
|
+
listCookies() {
|
|
33
|
+
return this.#get(API.cookies)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @type {JsonPromise<string[]>} */
|
|
37
|
+
listComments() {
|
|
38
|
+
return this.#get(API.comments)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @type {JsonPromise<string>} */
|
|
42
|
+
getProxyFallback() {
|
|
43
|
+
return this.#get(API.fallback)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @type {JsonPromise<boolean>} */
|
|
47
|
+
getCollectProxied() {
|
|
48
|
+
return this.#get(API.collectProxied)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @type {JsonPromise<boolean>} */
|
|
52
|
+
getCorsAllowed() {
|
|
53
|
+
return this.#get(API.cors)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @type {JsonPromise<number>} */
|
|
57
|
+
getGlobalDelay() {
|
|
58
|
+
return this.#get(API.globalDelay)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @type {JsonPromise<number>} */
|
|
62
|
+
getSyncVersion(currentSyncVersion, abortSignal) {
|
|
63
|
+
return fetch(API.syncVersion, {
|
|
64
|
+
signal: AbortSignal.any([abortSignal, AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)]),
|
|
65
|
+
headers: {
|
|
66
|
+
[DF.syncVersion]: currentSyncVersion
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
reset() {
|
|
73
|
+
return this.#patch(API.reset)
|
|
74
|
+
}
|
|
75
|
+
|
|
25
76
|
select(file) {
|
|
26
77
|
return this.#patch(API.select, file)
|
|
27
78
|
}
|
|
79
|
+
|
|
28
80
|
bulkSelectByComment(comment) {
|
|
29
81
|
return this.#patch(API.bulkSelect, comment)
|
|
30
82
|
}
|
|
@@ -59,59 +111,23 @@ export class Commander {
|
|
|
59
111
|
})
|
|
60
112
|
}
|
|
61
113
|
|
|
62
|
-
listCookies() {
|
|
63
|
-
return this.#get(API.cookies)
|
|
64
|
-
}
|
|
65
114
|
selectCookie(cookieKey) {
|
|
66
115
|
return this.#patch(API.cookies, cookieKey)
|
|
67
116
|
}
|
|
68
117
|
|
|
69
|
-
listComments() {
|
|
70
|
-
return this.#get(API.comments)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
getProxyFallback() {
|
|
74
|
-
return this.#get(API.fallback)
|
|
75
|
-
}
|
|
76
118
|
setProxyFallback(proxyAddr) {
|
|
77
119
|
return this.#patch(API.fallback, proxyAddr)
|
|
78
120
|
}
|
|
79
121
|
|
|
80
|
-
getCollectProxied() {
|
|
81
|
-
return this.#get(API.collectProxied)
|
|
82
|
-
}
|
|
83
122
|
setCollectProxied(shouldCollect) {
|
|
84
123
|
return this.#patch(API.collectProxied, shouldCollect)
|
|
85
124
|
}
|
|
86
125
|
|
|
87
|
-
getCorsAllowed() {
|
|
88
|
-
return this.#get(API.cors)
|
|
89
|
-
}
|
|
90
126
|
setCorsAllowed(value) {
|
|
91
127
|
return this.#patch(API.cors, value)
|
|
92
128
|
}
|
|
93
129
|
|
|
94
|
-
getGlobalDelay() {
|
|
95
|
-
return this.#get(API.globalDelay)
|
|
96
|
-
}
|
|
97
130
|
setGlobalDelay(delay) {
|
|
98
131
|
return this.#patch(API.globalDelay, delay)
|
|
99
132
|
}
|
|
100
|
-
|
|
101
|
-
listStaticFiles() {
|
|
102
|
-
return this.#get(API.static)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
reset() {
|
|
106
|
-
return this.#patch(API.reset)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
getSyncVersion(currentSyncVersion, abortSignal) {
|
|
110
|
-
return fetch(API.syncVersion, {
|
|
111
|
-
signal: AbortSignal.any([abortSignal, AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)]),
|
|
112
|
-
headers: {
|
|
113
|
-
[DF.syncVersion]: currentSyncVersion
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
133
|
}
|
package/src/Dashboard.js
CHANGED
|
@@ -38,83 +38,117 @@ const Strings = {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const CSS = {
|
|
41
|
-
BulkSelector:
|
|
42
|
-
DelayToggler:
|
|
43
|
-
ErrorToast:
|
|
44
|
-
FallbackBackend:
|
|
45
|
-
Field:
|
|
46
|
-
GlobalDelayField:
|
|
47
|
-
Help:
|
|
48
|
-
InternalServerErrorToggler:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
status4xx:
|
|
41
|
+
BulkSelector: null,
|
|
42
|
+
DelayToggler: null,
|
|
43
|
+
ErrorToast: null,
|
|
44
|
+
FallbackBackend: null,
|
|
45
|
+
Field: null,
|
|
46
|
+
GlobalDelayField: null,
|
|
47
|
+
Help: null,
|
|
48
|
+
InternalServerErrorToggler: null,
|
|
49
|
+
MockList: null,
|
|
50
|
+
MockSelector: null,
|
|
51
|
+
NotFoundToggler: null,
|
|
52
|
+
PayloadViewer: null,
|
|
53
|
+
PreviewLink: null,
|
|
54
|
+
ProgressBar: null,
|
|
55
|
+
ProxyToggler: null,
|
|
56
|
+
ResetButton: null,
|
|
57
|
+
SaveProxiedCheckbox: null,
|
|
58
|
+
StaticFilesList: null,
|
|
59
|
+
|
|
60
|
+
chosen: null,
|
|
61
|
+
dittoDir: null,
|
|
62
|
+
empty: null,
|
|
63
|
+
leftSide: null,
|
|
64
|
+
nonDefault: null,
|
|
65
|
+
red: null,
|
|
66
|
+
rightSide: null,
|
|
67
|
+
status4xx: null
|
|
68
|
+
}
|
|
69
|
+
for (const k of Object.keys(CSS))
|
|
70
|
+
CSS[k] = k
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
const state = {
|
|
74
|
+
/** @type {ClientBrokersByMethod} */
|
|
75
|
+
brokersByMethod: {},
|
|
76
|
+
|
|
77
|
+
/** @type {ClientStaticBrokers} */
|
|
78
|
+
staticBrokers: {},
|
|
79
|
+
|
|
80
|
+
/** @type {[label:string, selected:boolean][]} */
|
|
81
|
+
cookies: [],
|
|
82
|
+
|
|
83
|
+
/** @type {string[]} */
|
|
84
|
+
comments: [],
|
|
85
|
+
|
|
86
|
+
delay: 0,
|
|
87
|
+
|
|
88
|
+
collectProxied: false,
|
|
89
|
+
|
|
90
|
+
fallbackAddress: '',
|
|
91
|
+
|
|
92
|
+
get canProxy() {
|
|
93
|
+
return Boolean(this.fallbackAddress)
|
|
94
|
+
}
|
|
68
95
|
}
|
|
69
96
|
|
|
70
|
-
const r = createElement
|
|
71
|
-
const s = createSvgElement
|
|
72
|
-
|
|
73
97
|
const mockaton = new Commander(window.location.origin)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
init()
|
|
98
|
+
updateState()
|
|
77
99
|
initLongPoll()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return Promise.all([
|
|
100
|
+
function updateState() {
|
|
101
|
+
Promise.all([
|
|
81
102
|
mockaton.listMocks(),
|
|
103
|
+
mockaton.listStaticFiles(),
|
|
82
104
|
mockaton.listCookies(),
|
|
83
105
|
mockaton.listComments(),
|
|
84
106
|
mockaton.getGlobalDelay(),
|
|
85
107
|
mockaton.getCollectProxied(),
|
|
86
|
-
mockaton.getProxyFallback()
|
|
87
|
-
mockaton.listStaticFiles()
|
|
108
|
+
mockaton.getProxyFallback()
|
|
88
109
|
].map(api => api.then(response => response.ok && response.json())))
|
|
89
|
-
.then(data =>
|
|
110
|
+
.then(data => {
|
|
111
|
+
state.brokersByMethod = data[0]
|
|
112
|
+
state.staticBrokers = data[1]
|
|
113
|
+
state.cookies = data[2]
|
|
114
|
+
state.comments = data[3]
|
|
115
|
+
state.delay = data[4]
|
|
116
|
+
state.collectProxied = data[5]
|
|
117
|
+
state.fallbackAddress = data[6]
|
|
118
|
+
document.body.replaceChildren(...App())
|
|
119
|
+
})
|
|
90
120
|
.catch(onError)
|
|
91
121
|
}
|
|
92
122
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
const r = createElement
|
|
124
|
+
const s = createSvgElement
|
|
125
|
+
|
|
126
|
+
function App() {
|
|
96
127
|
return [
|
|
97
|
-
r(Header
|
|
128
|
+
r(Header),
|
|
98
129
|
r('main', null,
|
|
99
|
-
r('div',
|
|
100
|
-
r(MockList
|
|
101
|
-
r(StaticFilesList
|
|
102
|
-
r('div',
|
|
103
|
-
r(PayloadViewer)))
|
|
130
|
+
r('div', className(CSS.leftSide),
|
|
131
|
+
r(MockList),
|
|
132
|
+
r(StaticFilesList)),
|
|
133
|
+
r('div', className(CSS.rightSide),
|
|
134
|
+
r(PayloadViewer)))
|
|
135
|
+
]
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
function Header({ cookies, comments, delay, fallbackAddress, collectProxied }) {
|
|
139
|
+
function Header() {
|
|
110
140
|
return (
|
|
111
141
|
r('header', null,
|
|
112
|
-
r(
|
|
142
|
+
r('img', {
|
|
143
|
+
alt: Strings.title,
|
|
144
|
+
src: 'mockaton/logo.svg',
|
|
145
|
+
width: 160
|
|
146
|
+
}),
|
|
113
147
|
r('div', null,
|
|
114
|
-
r(CookieSelector
|
|
115
|
-
r(BulkSelector
|
|
116
|
-
r(GlobalDelayField
|
|
117
|
-
r(ProxyFallbackField
|
|
148
|
+
r(CookieSelector),
|
|
149
|
+
r(BulkSelector),
|
|
150
|
+
r(GlobalDelayField),
|
|
151
|
+
r(ProxyFallbackField),
|
|
118
152
|
r(ResetButton)),
|
|
119
153
|
r('a', {
|
|
120
154
|
className: CSS.Help,
|
|
@@ -124,22 +158,17 @@ function Header({ cookies, comments, delay, fallbackAddress, collectProxied }) {
|
|
|
124
158
|
}, r(HelpIcon))))
|
|
125
159
|
}
|
|
126
160
|
|
|
127
|
-
function Logo() {
|
|
128
|
-
return (
|
|
129
|
-
r('img', {
|
|
130
|
-
alt: Strings.title,
|
|
131
|
-
src: 'mockaton/logo.svg',
|
|
132
|
-
width: 160
|
|
133
|
-
}))
|
|
134
|
-
}
|
|
135
161
|
|
|
136
|
-
function CookieSelector(
|
|
162
|
+
function CookieSelector() {
|
|
163
|
+
const { cookies } = state
|
|
137
164
|
function onChange() {
|
|
138
|
-
mockaton.selectCookie(this.value)
|
|
165
|
+
mockaton.selectCookie(this.value)
|
|
166
|
+
.then(parseError)
|
|
167
|
+
.catch(onError)
|
|
139
168
|
}
|
|
140
169
|
const disabled = cookies.length <= 1
|
|
141
170
|
return (
|
|
142
|
-
r('label',
|
|
171
|
+
r('label', className(CSS.Field),
|
|
143
172
|
r('span', null, Strings.cookie),
|
|
144
173
|
r('select', {
|
|
145
174
|
autocomplete: 'off',
|
|
@@ -150,7 +179,9 @@ function CookieSelector({ cookies }) {
|
|
|
150
179
|
r('option', { value, selected }, value)))))
|
|
151
180
|
}
|
|
152
181
|
|
|
153
|
-
|
|
182
|
+
|
|
183
|
+
function BulkSelector() {
|
|
184
|
+
const { comments } = state
|
|
154
185
|
// UX wise this should be a menu instead of this `select`.
|
|
155
186
|
// But this way is easier to implement, with a few hacks.
|
|
156
187
|
const firstOption = Strings.pick_comment
|
|
@@ -158,7 +189,8 @@ function BulkSelector({ comments }) {
|
|
|
158
189
|
const value = this.value
|
|
159
190
|
this.value = firstOption // Hack
|
|
160
191
|
mockaton.bulkSelectByComment(value)
|
|
161
|
-
.then(
|
|
192
|
+
.then(parseError)
|
|
193
|
+
.then(updateState)
|
|
162
194
|
.catch(onError)
|
|
163
195
|
}
|
|
164
196
|
const disabled = !comments.length
|
|
@@ -166,7 +198,7 @@ function BulkSelector({ comments }) {
|
|
|
166
198
|
? []
|
|
167
199
|
: [firstOption].concat(comments)
|
|
168
200
|
return (
|
|
169
|
-
r('label',
|
|
201
|
+
r('label', className(CSS.Field),
|
|
170
202
|
r('span', null, Strings.bulk_select),
|
|
171
203
|
r('select', {
|
|
172
204
|
className: CSS.BulkSelector,
|
|
@@ -179,13 +211,17 @@ function BulkSelector({ comments }) {
|
|
|
179
211
|
r('option', { value }, value)))))
|
|
180
212
|
}
|
|
181
213
|
|
|
182
|
-
|
|
214
|
+
|
|
215
|
+
function GlobalDelayField() {
|
|
216
|
+
const { delay } = state
|
|
183
217
|
function onChange() {
|
|
184
|
-
|
|
185
|
-
mockaton.setGlobalDelay(
|
|
218
|
+
state.delay = this.valueAsNumber
|
|
219
|
+
mockaton.setGlobalDelay(state.delay)
|
|
220
|
+
.then(parseError)
|
|
221
|
+
.catch(onError)
|
|
186
222
|
}
|
|
187
223
|
return (
|
|
188
|
-
r('label',
|
|
224
|
+
r('label', className(CSS.Field, CSS.GlobalDelayField),
|
|
189
225
|
r('span', null, r(TimerIcon), Strings.delay_ms),
|
|
190
226
|
r('input', {
|
|
191
227
|
type: 'number',
|
|
@@ -197,7 +233,9 @@ function GlobalDelayField({ delay }) {
|
|
|
197
233
|
})))
|
|
198
234
|
}
|
|
199
235
|
|
|
200
|
-
|
|
236
|
+
|
|
237
|
+
function ProxyFallbackField() {
|
|
238
|
+
const { fallbackAddress, collectProxied } = state
|
|
201
239
|
function onChange() {
|
|
202
240
|
const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
|
|
203
241
|
saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
|
|
@@ -206,11 +244,12 @@ function ProxyFallbackField({ fallbackAddress, collectProxied }) {
|
|
|
206
244
|
this.reportValidity()
|
|
207
245
|
else
|
|
208
246
|
mockaton.setProxyFallback(this.value.trim())
|
|
209
|
-
.then(
|
|
247
|
+
.then(parseError)
|
|
248
|
+
.then(updateState)
|
|
210
249
|
.catch(onError)
|
|
211
250
|
}
|
|
212
251
|
return (
|
|
213
|
-
r('div',
|
|
252
|
+
r('div', className(CSS.Field, CSS.FallbackBackend),
|
|
214
253
|
r('label', null,
|
|
215
254
|
r('span', null, r(CloudIcon), Strings.fallback_server),
|
|
216
255
|
r('input', {
|
|
@@ -226,12 +265,15 @@ function ProxyFallbackField({ fallbackAddress, collectProxied }) {
|
|
|
226
265
|
})))
|
|
227
266
|
}
|
|
228
267
|
|
|
229
|
-
function SaveProxiedCheckbox({ disabled
|
|
268
|
+
function SaveProxiedCheckbox({ disabled }) {
|
|
269
|
+
const { collectProxied } = state
|
|
230
270
|
function onChange() {
|
|
231
|
-
mockaton.setCollectProxied(this.checked)
|
|
271
|
+
mockaton.setCollectProxied(this.checked)
|
|
272
|
+
.then(parseError)
|
|
273
|
+
.catch(onError)
|
|
232
274
|
}
|
|
233
275
|
return (
|
|
234
|
-
r('label',
|
|
276
|
+
r('label', className(CSS.SaveProxiedCheckbox),
|
|
235
277
|
r('input', {
|
|
236
278
|
type: 'checkbox',
|
|
237
279
|
disabled,
|
|
@@ -241,34 +283,39 @@ function SaveProxiedCheckbox({ disabled, collectProxied }) {
|
|
|
241
283
|
r('span', null, Strings.save_proxied)))
|
|
242
284
|
}
|
|
243
285
|
|
|
286
|
+
|
|
244
287
|
function ResetButton() {
|
|
288
|
+
function onClick() {
|
|
289
|
+
mockaton.reset()
|
|
290
|
+
.then(parseError)
|
|
291
|
+
.then(updateState)
|
|
292
|
+
.catch(onError)
|
|
293
|
+
}
|
|
245
294
|
return (
|
|
246
295
|
r('button', {
|
|
247
296
|
className: CSS.ResetButton,
|
|
248
|
-
onClick
|
|
249
|
-
mockaton.reset()
|
|
250
|
-
.then(init)
|
|
251
|
-
.catch(onError)
|
|
252
|
-
}
|
|
297
|
+
onClick
|
|
253
298
|
}, Strings.reset))
|
|
254
299
|
}
|
|
255
300
|
|
|
256
301
|
|
|
302
|
+
|
|
257
303
|
/** # MockList */
|
|
258
304
|
|
|
259
|
-
function MockList(
|
|
260
|
-
const
|
|
261
|
-
if (!
|
|
305
|
+
function MockList() {
|
|
306
|
+
const { brokersByMethod } = state
|
|
307
|
+
if (!Object.keys(brokersByMethod).length)
|
|
262
308
|
return (
|
|
263
|
-
r('div',
|
|
309
|
+
r('div', className(CSS.empty),
|
|
264
310
|
Strings.no_mocks_found))
|
|
265
311
|
return (
|
|
266
312
|
r('div', null,
|
|
267
313
|
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
268
|
-
r(SectionByMethod, { method, brokers
|
|
314
|
+
r(SectionByMethod, { method, brokers })))))
|
|
269
315
|
}
|
|
270
316
|
|
|
271
|
-
function SectionByMethod({ method, brokers
|
|
317
|
+
function SectionByMethod({ method, brokers }) {
|
|
318
|
+
const canProxy = state.canProxy
|
|
272
319
|
const brokersSorted = Object.entries(brokers)
|
|
273
320
|
.filter(([, broker]) => broker.mocks.length > 1) // >1 because of autogen500
|
|
274
321
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
@@ -309,16 +356,17 @@ function PreviewLink({ method, urlMask, urlMaskDittoed }) {
|
|
|
309
356
|
href: urlMask,
|
|
310
357
|
onClick
|
|
311
358
|
}, ditto
|
|
312
|
-
? [r('span',
|
|
359
|
+
? [r('span', className(CSS.dittoDir), ditto), tail]
|
|
313
360
|
: tail))
|
|
314
361
|
}
|
|
315
362
|
|
|
316
|
-
/** @param {{ broker:
|
|
363
|
+
/** @param {{ broker: ClientMockBroker }} props */
|
|
317
364
|
function MockSelector({ broker }) {
|
|
318
365
|
function onChange() {
|
|
319
366
|
const { urlMask, method } = parseFilename(this.value)
|
|
320
367
|
mockaton.select(this.value)
|
|
321
|
-
.then(
|
|
368
|
+
.then(parseError)
|
|
369
|
+
.then(updateState)
|
|
322
370
|
.then(() => linkFor(method, urlMask)?.click())
|
|
323
371
|
.catch(onError)
|
|
324
372
|
}
|
|
@@ -339,7 +387,7 @@ function MockSelector({ broker }) {
|
|
|
339
387
|
autocomplete: 'off',
|
|
340
388
|
'data-qaid': urlMask,
|
|
341
389
|
disabled: files.length <= 1,
|
|
342
|
-
className
|
|
390
|
+
...className(
|
|
343
391
|
CSS.MockSelector,
|
|
344
392
|
selected !== files[0] && CSS.nonDefault,
|
|
345
393
|
status >= 400 && status < 500 && CSS.status4xx)
|
|
@@ -350,11 +398,13 @@ function MockSelector({ broker }) {
|
|
|
350
398
|
}, file))))
|
|
351
399
|
}
|
|
352
400
|
|
|
353
|
-
/** @param {{ broker:
|
|
401
|
+
/** @param {{ broker: ClientMockBroker }} props */
|
|
354
402
|
function DelayRouteToggler({ broker }) {
|
|
355
403
|
function onChange() {
|
|
356
404
|
const { method, urlMask } = parseFilename(broker.mocks[0])
|
|
357
|
-
mockaton.setRouteIsDelayed(method, urlMask, this.checked)
|
|
405
|
+
mockaton.setRouteIsDelayed(method, urlMask, this.checked)
|
|
406
|
+
.then(parseError)
|
|
407
|
+
.catch(onError)
|
|
358
408
|
}
|
|
359
409
|
return (
|
|
360
410
|
r('label', {
|
|
@@ -369,7 +419,7 @@ function DelayRouteToggler({ broker }) {
|
|
|
369
419
|
TimerIcon()))
|
|
370
420
|
}
|
|
371
421
|
|
|
372
|
-
/** @param {{ broker:
|
|
422
|
+
/** @param {{ broker: ClientMockBroker }} props */
|
|
373
423
|
function InternalServerErrorToggler({ broker }) {
|
|
374
424
|
function onChange() {
|
|
375
425
|
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
@@ -377,7 +427,8 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
377
427
|
this.checked
|
|
378
428
|
? broker.mocks.find(f => parseFilename(f).status === 500)
|
|
379
429
|
: broker.mocks[0])
|
|
380
|
-
.then(
|
|
430
|
+
.then(parseError)
|
|
431
|
+
.then(updateState)
|
|
381
432
|
.then(() => linkFor(method, urlMask)?.click())
|
|
382
433
|
.catch(onError)
|
|
383
434
|
}
|
|
@@ -395,12 +446,13 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
395
446
|
r('span', null, '500')))
|
|
396
447
|
}
|
|
397
448
|
|
|
398
|
-
/** @param {{ broker:
|
|
449
|
+
/** @param {{ broker: ClientMockBroker }} props */
|
|
399
450
|
function ProxyToggler({ broker }) {
|
|
400
451
|
function onChange() {
|
|
401
452
|
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
402
453
|
mockaton.setRouteIsProxied(method, urlMask, this.checked)
|
|
403
|
-
.then(
|
|
454
|
+
.then(parseError)
|
|
455
|
+
.then(updateState)
|
|
404
456
|
.then(() => linkFor(method, urlMask)?.click())
|
|
405
457
|
.catch(onError)
|
|
406
458
|
}
|
|
@@ -418,24 +470,25 @@ function ProxyToggler({ broker }) {
|
|
|
418
470
|
}
|
|
419
471
|
|
|
420
472
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
473
|
+
|
|
474
|
+
/** # StaticFilesList */
|
|
475
|
+
|
|
476
|
+
function StaticFilesList() {
|
|
477
|
+
const { staticBrokers } = state
|
|
478
|
+
const canProxy = state.canProxy
|
|
479
|
+
if (!Object.keys(staticBrokers).length)
|
|
427
480
|
return null
|
|
428
|
-
const dp = dittoSplitPaths(Object.keys(
|
|
429
|
-
? [r('span',
|
|
481
|
+
const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
|
|
482
|
+
? [r('span', className(CSS.dittoDir), ditto), tail]
|
|
430
483
|
: tail)
|
|
431
484
|
return (
|
|
432
|
-
r('table',
|
|
485
|
+
r('table', className(CSS.StaticFilesList),
|
|
433
486
|
r('thead', null,
|
|
434
487
|
r('tr', null,
|
|
435
488
|
r('th', { colspan: 2 + Number(canProxy) }),
|
|
436
489
|
r('th', null, Strings.static_get))),
|
|
437
490
|
r('tbody', null,
|
|
438
|
-
Object.values(
|
|
491
|
+
Object.values(staticBrokers).map((broker, i) =>
|
|
439
492
|
r('tr', null,
|
|
440
493
|
canProxy && r('td', null, r(ProxyStaticToggler, {})),
|
|
441
494
|
r('td', null, r(DelayStaticRouteToggler, { broker })),
|
|
@@ -444,11 +497,11 @@ function StaticFilesList({ brokers, canProxy }) {
|
|
|
444
497
|
)))))
|
|
445
498
|
}
|
|
446
499
|
|
|
447
|
-
|
|
448
|
-
/** @param {{ broker: StaticBroker }} props */
|
|
500
|
+
/** @param {{ broker: ClientStaticBroker }} props */
|
|
449
501
|
function DelayStaticRouteToggler({ broker }) {
|
|
450
502
|
function onChange() {
|
|
451
503
|
mockaton.setStaticRouteIsDelayed(broker.route, this.checked)
|
|
504
|
+
.then(parseError)
|
|
452
505
|
.catch(onError)
|
|
453
506
|
}
|
|
454
507
|
return (
|
|
@@ -464,10 +517,11 @@ function DelayStaticRouteToggler({ broker }) {
|
|
|
464
517
|
TimerIcon()))
|
|
465
518
|
}
|
|
466
519
|
|
|
467
|
-
/** @param {{ broker:
|
|
520
|
+
/** @param {{ broker: ClientStaticBroker }} props */
|
|
468
521
|
function NotFoundToggler({ broker }) {
|
|
469
522
|
function onChange() {
|
|
470
523
|
mockaton.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
|
|
524
|
+
.then(parseError)
|
|
471
525
|
.catch(onError)
|
|
472
526
|
}
|
|
473
527
|
return (
|
|
@@ -500,6 +554,8 @@ function ProxyStaticToggler({}) { // TODO
|
|
|
500
554
|
r(CloudIcon)))
|
|
501
555
|
}
|
|
502
556
|
|
|
557
|
+
|
|
558
|
+
|
|
503
559
|
/** # Payload Preview */
|
|
504
560
|
|
|
505
561
|
const payloadViewerTitleRef = useRef()
|
|
@@ -507,17 +563,16 @@ const payloadViewerRef = useRef()
|
|
|
507
563
|
|
|
508
564
|
function PayloadViewer() {
|
|
509
565
|
return (
|
|
510
|
-
r('div',
|
|
566
|
+
r('div', className(CSS.PayloadViewer),
|
|
511
567
|
r('h2', { ref: payloadViewerTitleRef }, Strings.preview),
|
|
512
568
|
r('pre', null,
|
|
513
569
|
r('code', { ref: payloadViewerRef }, Strings.click_link_to_preview))))
|
|
514
570
|
}
|
|
515
571
|
|
|
516
|
-
|
|
517
572
|
function PayloadViewerProgressBar() {
|
|
518
573
|
return (
|
|
519
|
-
r('div',
|
|
520
|
-
r('div', { style: { animationDuration:
|
|
574
|
+
r('div', className(CSS.ProgressBar),
|
|
575
|
+
r('div', { style: { animationDuration: state.delay + 'ms' } })))
|
|
521
576
|
}
|
|
522
577
|
|
|
523
578
|
function PayloadViewerTitle({ file, status, statusText }) {
|
|
@@ -532,7 +587,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
|
|
|
532
587
|
return (
|
|
533
588
|
r('span', null,
|
|
534
589
|
gatewayIsBad
|
|
535
|
-
? r('span',
|
|
590
|
+
? r('span', className(CSS.red), Strings.fallback_server_error + ' ')
|
|
536
591
|
: r('span', null, Strings.got + ' '),
|
|
537
592
|
r('abbr', { title: statusText }, status),
|
|
538
593
|
' ' + mime))
|
|
@@ -610,9 +665,19 @@ function mockSelectorFor(method, urlMask) {
|
|
|
610
665
|
|
|
611
666
|
/** # Misc */
|
|
612
667
|
|
|
668
|
+
async function parseError(response) {
|
|
669
|
+
if (response.ok)
|
|
670
|
+
return
|
|
671
|
+
if (response.status === 422)
|
|
672
|
+
throw await response.text()
|
|
673
|
+
throw response.statusText
|
|
674
|
+
}
|
|
675
|
+
|
|
613
676
|
function onError(error) {
|
|
614
677
|
if (error?.message === 'Failed to fetch')
|
|
615
678
|
showErrorToast('Looks like the Mockaton server is not running')
|
|
679
|
+
else
|
|
680
|
+
showErrorToast(error || 'Unexpected Error')
|
|
616
681
|
console.error(error)
|
|
617
682
|
}
|
|
618
683
|
|
|
@@ -673,7 +738,7 @@ async function poll() {
|
|
|
673
738
|
const syncVersion = await response.json()
|
|
674
739
|
if (poll.oldSyncVersion !== syncVersion) { // because it could be < or >
|
|
675
740
|
poll.oldSyncVersion = syncVersion
|
|
676
|
-
await
|
|
741
|
+
await updateState()
|
|
677
742
|
}
|
|
678
743
|
poll()
|
|
679
744
|
}
|
|
@@ -689,30 +754,27 @@ async function poll() {
|
|
|
689
754
|
|
|
690
755
|
/** # Utils */
|
|
691
756
|
|
|
692
|
-
function
|
|
693
|
-
return args.filter(Boolean).join(' ')
|
|
757
|
+
function className(...args) {
|
|
758
|
+
return { className: args.filter(Boolean).join(' ') }
|
|
694
759
|
}
|
|
695
760
|
|
|
696
761
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
function createElement(elem, props = null, ...children) {
|
|
762
|
+
function createElement(elem, props, ...children) {
|
|
700
763
|
if (typeof elem === 'function')
|
|
701
764
|
return elem(props)
|
|
702
765
|
|
|
703
766
|
const node = document.createElement(elem)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
node.setAttribute(key, value)
|
|
767
|
+
for (const [key, value] of Object.entries(props || {}))
|
|
768
|
+
if (key === 'ref')
|
|
769
|
+
value.current = node
|
|
770
|
+
else if (key.startsWith('on'))
|
|
771
|
+
node.addEventListener(key.replace(/^on/, '').toLowerCase(), value)
|
|
772
|
+
else if (key === 'style')
|
|
773
|
+
Object.assign(node.style, value)
|
|
774
|
+
else if (key in node)
|
|
775
|
+
node[key] = value
|
|
776
|
+
else
|
|
777
|
+
node.setAttribute(key, value)
|
|
716
778
|
node.append(...children.flat().filter(Boolean))
|
|
717
779
|
return node
|
|
718
780
|
}
|
|
@@ -730,7 +792,6 @@ function useRef() {
|
|
|
730
792
|
}
|
|
731
793
|
|
|
732
794
|
|
|
733
|
-
|
|
734
795
|
/**
|
|
735
796
|
* Think of this as a way of printing a directory tree in which
|
|
736
797
|
* the repeated folder paths are kept but styled differently.
|
package/src/Mockaton.js
CHANGED
|
@@ -63,7 +63,7 @@ async function onRequest(req, response) {
|
|
|
63
63
|
}
|
|
64
64
|
catch (error) {
|
|
65
65
|
if (error instanceof BodyReaderError)
|
|
66
|
-
sendUnprocessableContent(response, error.name)
|
|
66
|
+
sendUnprocessableContent(response, `${error.name}: ${error.message}`)
|
|
67
67
|
else
|
|
68
68
|
sendInternalServerError(response, error)
|
|
69
69
|
}
|
package/src/staticCollection.js
CHANGED
|
@@ -3,7 +3,7 @@ import { listFilesRecursively } from './utils/fs.js'
|
|
|
3
3
|
import { config, isFileAllowed } from './config.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class StaticBroker {
|
|
6
|
+
export class StaticBroker {
|
|
7
7
|
constructor(route) {
|
|
8
8
|
this.route = route
|
|
9
9
|
this.delayed = false
|
|
@@ -14,7 +14,7 @@ class StaticBroker {
|
|
|
14
14
|
setStatus(value) { this.status = value }
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** @type {{ [route:
|
|
17
|
+
/** @type {{ [route:string]: StaticBroker }} */
|
|
18
18
|
let collection = {}
|
|
19
19
|
|
|
20
20
|
export const all = () => collection
|
|
@@ -4,7 +4,13 @@ import { METHODS } from 'node:http'
|
|
|
4
4
|
export const SUPPORTED_METHODS = METHODS
|
|
5
5
|
export const methodIsSupported = method => SUPPORTED_METHODS.includes(method)
|
|
6
6
|
|
|
7
|
-
export class BodyReaderError extends Error {
|
|
7
|
+
export class BodyReaderError extends Error {
|
|
8
|
+
name = 'BodyReaderError'
|
|
9
|
+
constructor(msg) {
|
|
10
|
+
super()
|
|
11
|
+
this.message = msg
|
|
12
|
+
}
|
|
13
|
+
}
|
|
8
14
|
|
|
9
15
|
export const parseJSON = req => readBody(req, JSON.parse)
|
|
10
16
|
|
|
@@ -31,13 +37,13 @@ export function readBody(req, parser = a => a) {
|
|
|
31
37
|
req.removeListener('end', onEnd)
|
|
32
38
|
req.removeListener('error', onEnd)
|
|
33
39
|
if (lengthSoFar !== expectedLength)
|
|
34
|
-
reject(new BodyReaderError())
|
|
40
|
+
reject(new BodyReaderError('Length mismatch'))
|
|
35
41
|
else
|
|
36
42
|
try {
|
|
37
43
|
resolve(parser(Buffer.concat(body).toString()))
|
|
38
44
|
}
|
|
39
45
|
catch (_) {
|
|
40
|
-
reject(new BodyReaderError())
|
|
46
|
+
reject(new BodyReaderError('Could not parse'))
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
})
|