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

|
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.3",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
@@ -18,15 +18,15 @@
|
|
|
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",
|
|
30
|
-
"puppeteer": "24.
|
|
30
|
+
"puppeteer": "24.18.0"
|
|
31
31
|
}
|
|
32
32
|
}
|
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.css
CHANGED
|
@@ -165,6 +165,7 @@ header {
|
|
|
165
165
|
border: 0;
|
|
166
166
|
border-right: 3px solid transparent;
|
|
167
167
|
margin-top: 4px;
|
|
168
|
+
outline: 1px solid var(--colorSecondaryActionBorder);
|
|
168
169
|
color: var(--colorText);
|
|
169
170
|
font-size: 11px;
|
|
170
171
|
background-color: var(--colorComboBoxHeaderBackground);
|
|
@@ -252,7 +253,7 @@ header {
|
|
|
252
253
|
main {
|
|
253
254
|
display: grid;
|
|
254
255
|
min-height: 0;
|
|
255
|
-
grid-template-columns: min
|
|
256
|
+
grid-template-columns: minmax(min-content, max-content) 1fr;
|
|
256
257
|
|
|
257
258
|
@media (max-width: 1160px) {
|
|
258
259
|
grid-template-columns: min(620px) 1fr;
|
|
@@ -322,13 +323,11 @@ table {
|
|
|
322
323
|
.MockSelector {
|
|
323
324
|
width: 100%;
|
|
324
325
|
height: 26px;
|
|
325
|
-
padding-right:
|
|
326
|
-
padding-left:
|
|
327
|
-
text-align: right;
|
|
328
|
-
direction: rtl;
|
|
326
|
+
padding-right: 8px;
|
|
327
|
+
padding-left: 8px;
|
|
329
328
|
text-overflow: ellipsis;
|
|
330
329
|
font-size: 12px;
|
|
331
|
-
background-position:
|
|
330
|
+
background-position: calc(100% - 4px) center;
|
|
332
331
|
|
|
333
332
|
&.nonDefault {
|
|
334
333
|
font-weight: bold;
|
|
@@ -519,6 +518,7 @@ table {
|
|
|
519
518
|
|
|
520
519
|
.ProgressBar {
|
|
521
520
|
position: relative;
|
|
521
|
+
top: -4px;
|
|
522
522
|
width: 100%;
|
|
523
523
|
height: 2px;
|
|
524
524
|
background: var(--colorComboBoxHeaderBackground);
|
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
|
})
|