mockaton 8.11.4 → 8.11.6
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 +36 -37
- package/package.json +1 -1
- package/src/ApiConstants.js +2 -0
- package/src/Dashboard.css +26 -27
- package/src/Dashboard.js +9 -4
- package/src/ProxyRelay.js +19 -8
- package/src/utils/http-response.js +8 -0
package/README.md
CHANGED
|
@@ -3,26 +3,35 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
HTTP mock server for developing and testing frontends.
|
|
7
|
-
|
|
8
|
-
## Convention over Code
|
|
9
6
|
|
|
10
7
|
With Mockaton you don’t need to write code for wiring mocks. Instead, it scans a
|
|
11
|
-
given directory for
|
|
8
|
+
given directory for filenames following a convention similar to the URLs.
|
|
12
9
|
|
|
13
|
-
For example, for <code>/<b>api/user</b>/1234</code> the
|
|
10
|
+
For example, for <code>/<b>api/user</b>/1234</code> the filename would be:
|
|
14
11
|
<pre>
|
|
15
12
|
<code>my-mocks-dir/<b>api/user</b>/[user-id].GET.200.json</code>
|
|
16
13
|
</pre>
|
|
17
14
|
|
|
18
|
-
## Multiple Mock Variants
|
|
19
|
-
You can have different mocks for a particular route by adding
|
|
20
|
-
comments and/or using different status codes.
|
|
21
15
|
|
|
16
|
+
On the dashboard you can select a mock variant for a particular route, delaying responses,
|
|
17
|
+
and triggering an autogenerated `500` (Internal Server Error), among other features.
|
|
18
|
+
|
|
19
|
+
Nonetheless, there’s a programmatic API, which is handy
|
|
20
|
+
for setting up tests. See **Commander API** section.
|
|
21
|
+
|
|
22
|
+
<picture>
|
|
23
|
+
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp840x800.light.gold.png">
|
|
24
|
+
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp840x800.dark.gold.png">
|
|
25
|
+
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp840x800.light.gold.png">
|
|
26
|
+
</picture>
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Multiple Mock Variants
|
|
30
|
+
Each route can have different mocks. There’s two options for doing that:
|
|
22
31
|
|
|
23
|
-
### Adding comments
|
|
24
|
-
Comments are anything within parentheses,
|
|
25
|
-
|
|
32
|
+
### Adding comments to the filename
|
|
33
|
+
Comments are anything within parentheses,
|
|
34
|
+
including them. A filename can have many comments.
|
|
26
35
|
|
|
27
36
|
<pre>
|
|
28
37
|
api/login<b>(locked out user)</b>.POST.423.json
|
|
@@ -31,18 +40,19 @@ api/login<b>(invalid login attempt)</b>.POST.401.json
|
|
|
31
40
|
|
|
32
41
|
### Different response status code
|
|
33
42
|
For instance, using a `4xx` or `5xx` status code for triggering error
|
|
34
|
-
responses. Or `2xx` such as `204` (No Content) for testing collections
|
|
43
|
+
responses. Or a `2xx` such as `204` (No Content) for testing empty collections.
|
|
35
44
|
|
|
36
45
|
<pre>
|
|
37
|
-
api/videos.GET.<b>204</b>.json
|
|
46
|
+
api/videos(empty list).GET.<b>204</b>.json
|
|
38
47
|
api/videos.GET.<b>403</b>.json
|
|
39
|
-
api/videos.GET.<b>500</b>.
|
|
48
|
+
api/videos.GET.<b>500</b>.txt
|
|
40
49
|
</pre>
|
|
41
50
|
|
|
42
51
|
|
|
52
|
+
|
|
43
53
|
## Fallback to your Backend
|
|
44
|
-
Mockaton can
|
|
45
|
-
mocks for, or
|
|
54
|
+
No need to mock everything. Mockaton can request from your backend the routes
|
|
55
|
+
you don’t have mocks for, or routes that have the ☁️ **Cloud Checkbox** checked.
|
|
46
56
|
|
|
47
57
|
|
|
48
58
|
### Scrapping Mocks from your Backend
|
|
@@ -51,23 +61,11 @@ They will be saved on your `config.mocksDir` following the filename convention.
|
|
|
51
61
|
|
|
52
62
|
|
|
53
63
|
|
|
54
|
-
## Dashboard
|
|
55
|
-
On the dashboard you can select a mock variant for a particular route, delaying responses,
|
|
56
|
-
and triggering an autogenerated `500` (Internal Server Error), among other features.
|
|
57
|
-
|
|
58
|
-
Nonetheless, there’s a programmatic API see
|
|
59
|
-
**Commander API** below, which is handy setting up tests.
|
|
60
|
-
|
|
61
|
-
<picture>
|
|
62
|
-
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp830x800.light.gold.png">
|
|
63
|
-
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp830x800.dark.gold.png">
|
|
64
|
-
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp830x800.light.gold.png">
|
|
65
|
-
</picture>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
64
|
## Basic Usage
|
|
65
|
+
Mockaton is a Node.js program with no build or runtime NPM dependencies.
|
|
66
|
+
|
|
70
67
|
`tsx` is only needed if you want to write mocks in TypeScript.
|
|
68
|
+
|
|
71
69
|
```sh
|
|
72
70
|
npm install mockaton tsx --save-dev
|
|
73
71
|
```
|
|
@@ -91,7 +89,7 @@ node --import=tsx my-mockaton.js
|
|
|
91
89
|
|
|
92
90
|
## Demo App (Vite)
|
|
93
91
|
|
|
94
|
-
This is a minimal React + Vite + Mockaton app. It’s
|
|
92
|
+
This is a minimal React + Vite + Mockaton app. It’s a list of
|
|
95
93
|
colors, which contains all of their possible states. For example,
|
|
96
94
|
permutations for out-of-stock, new-arrival, and discontinued.
|
|
97
95
|
|
|
@@ -282,7 +280,7 @@ documenting the URL contract.
|
|
|
282
280
|
api/video<b>?limit=[limit]</b>.GET.200.json
|
|
283
281
|
</pre>
|
|
284
282
|
|
|
285
|
-
|
|
283
|
+
On Windows filenames containing "?" are [not
|
|
286
284
|
permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query string it’s ignored anyway.
|
|
287
285
|
|
|
288
286
|
|
|
@@ -369,10 +367,11 @@ Mockaton’s predefined list. For that, you can add it to <code>config.extraMime
|
|
|
369
367
|
Files under `config.staticDir` don’t use the filename convention.
|
|
370
368
|
They take precedence over the `GET` mocks in `config.mocksDir`.
|
|
371
369
|
For example, if you have two files for `GET /foo/bar.jpg`
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
my-
|
|
375
|
-
|
|
370
|
+
|
|
371
|
+
<pre>
|
|
372
|
+
my-static-dir<b>/foo/bar.jpg</b>
|
|
373
|
+
my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg // Unreacheable
|
|
374
|
+
</pre>
|
|
376
375
|
|
|
377
376
|
|
|
378
377
|
### `cookies?: { [label: string]: string }`
|
package/package.json
CHANGED
package/src/ApiConstants.js
CHANGED
package/src/Dashboard.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.15), 0 1px 1px 0 rgba(0, 0, 0, 0.15), 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
|
3
|
-
--radius:
|
|
3
|
+
--radius: 6px;
|
|
4
4
|
--radiusSmall: 4px;
|
|
5
5
|
}
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
--colorSecondaryAction: #555;
|
|
18
18
|
--colorDisabledMockSelector: #444;
|
|
19
19
|
--colorHover: #dfefff;
|
|
20
|
-
--colorLabel: #
|
|
20
|
+
--colorLabel: #444;
|
|
21
21
|
--colorLightRed: #ffe4ee;
|
|
22
22
|
--colorRed: #da0f00;
|
|
23
23
|
--colorText: #000;
|
|
@@ -89,7 +89,7 @@ select {
|
|
|
89
89
|
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888888'><path d='M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z'/></svg>") no-repeat;
|
|
90
90
|
background-color: var(--colorComboBoxBackground);
|
|
91
91
|
background-size: 16px;
|
|
92
|
-
background-position:
|
|
92
|
+
background-position: 100% center;
|
|
93
93
|
|
|
94
94
|
&:enabled {
|
|
95
95
|
box-shadow: var(--boxShadow1);
|
|
@@ -103,6 +103,10 @@ select {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
.red {
|
|
107
|
+
color: var(--colorRed);
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
.Header {
|
|
107
111
|
position: fixed;
|
|
108
112
|
z-index: 100;
|
|
@@ -115,12 +119,12 @@ select {
|
|
|
115
119
|
border-bottom: 1px solid rgba(127, 127, 127, 0.1);
|
|
116
120
|
background: var(--colorHeaderBackground);
|
|
117
121
|
box-shadow: var(--boxShadow1);
|
|
118
|
-
gap:
|
|
122
|
+
gap: 10px;
|
|
119
123
|
|
|
120
124
|
img {
|
|
121
125
|
width: 130px;
|
|
122
126
|
align-self: center;
|
|
123
|
-
margin-right:
|
|
127
|
+
margin-right: 22px;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
.Field {
|
|
@@ -129,7 +133,6 @@ select {
|
|
|
129
133
|
span {
|
|
130
134
|
display: flex;
|
|
131
135
|
align-items: center;
|
|
132
|
-
margin-left: 6px;
|
|
133
136
|
color: var(--colorLabel);
|
|
134
137
|
font-size: 11px;
|
|
135
138
|
gap: 4px;
|
|
@@ -149,9 +152,9 @@ select {
|
|
|
149
152
|
select {
|
|
150
153
|
width: 100%;
|
|
151
154
|
height: 28px;
|
|
152
|
-
padding: 4px
|
|
155
|
+
padding: 4px 8px;
|
|
153
156
|
border-right: 3px solid transparent;
|
|
154
|
-
margin-top:
|
|
157
|
+
margin-top: 4px;
|
|
155
158
|
color: var(--colorText);
|
|
156
159
|
font-size: 11px;
|
|
157
160
|
box-shadow: var(--boxShadow1);
|
|
@@ -164,7 +167,7 @@ select {
|
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
&.GlobalDelayField {
|
|
167
|
-
width:
|
|
170
|
+
width: 84px;
|
|
168
171
|
|
|
169
172
|
input[type=number] {
|
|
170
173
|
padding-right: 0;
|
|
@@ -181,7 +184,7 @@ select {
|
|
|
181
184
|
|
|
182
185
|
&.FallbackBackend {
|
|
183
186
|
position: relative;
|
|
184
|
-
width:
|
|
187
|
+
width: 210px;
|
|
185
188
|
|
|
186
189
|
.SaveProxiedCheckbox {
|
|
187
190
|
position: absolute;
|
|
@@ -194,10 +197,6 @@ select {
|
|
|
194
197
|
font-size: 11px;
|
|
195
198
|
gap: 4px;
|
|
196
199
|
|
|
197
|
-
span {
|
|
198
|
-
margin-left: 0;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
200
|
input:disabled + span {
|
|
202
201
|
opacity: 0.8;
|
|
203
202
|
}
|
|
@@ -243,7 +242,7 @@ select {
|
|
|
243
242
|
}
|
|
244
243
|
|
|
245
244
|
tr {
|
|
246
|
-
border-top:
|
|
245
|
+
border-top: 2px solid transparent;
|
|
247
246
|
}
|
|
248
247
|
}
|
|
249
248
|
|
|
@@ -282,10 +281,10 @@ select {
|
|
|
282
281
|
|
|
283
282
|
.PreviewLink {
|
|
284
283
|
position: relative;
|
|
285
|
-
left: -
|
|
284
|
+
left: -6px;
|
|
286
285
|
display: inline-block;
|
|
287
|
-
width:
|
|
288
|
-
padding: 8px
|
|
286
|
+
width: 272px;
|
|
287
|
+
padding: 8px 6px;
|
|
289
288
|
border-radius: var(--radius);
|
|
290
289
|
color: var(--colorAccent);
|
|
291
290
|
text-decoration: none;
|
|
@@ -302,14 +301,14 @@ select {
|
|
|
302
301
|
.MockSelector {
|
|
303
302
|
width: 260px;
|
|
304
303
|
height: 30px;
|
|
305
|
-
padding-right:
|
|
306
|
-
padding-left:
|
|
304
|
+
padding-right: 5px;
|
|
305
|
+
padding-left: 16px;
|
|
307
306
|
border: 0;
|
|
308
307
|
text-align: right;
|
|
309
308
|
direction: rtl;
|
|
310
309
|
text-overflow: ellipsis;
|
|
311
310
|
font-size: 12px;
|
|
312
|
-
background-position:
|
|
311
|
+
background-position: 2px center;
|
|
313
312
|
|
|
314
313
|
&.nonDefault {
|
|
315
314
|
font-weight: bold;
|
|
@@ -319,7 +318,7 @@ select {
|
|
|
319
318
|
background: var(--color4xxBackground);
|
|
320
319
|
}
|
|
321
320
|
&:disabled {
|
|
322
|
-
padding-right:
|
|
321
|
+
padding-right: 4px;
|
|
323
322
|
appearance: none;
|
|
324
323
|
background: transparent;
|
|
325
324
|
cursor: default;
|
|
@@ -368,8 +367,8 @@ select {
|
|
|
368
367
|
}
|
|
369
368
|
|
|
370
369
|
> svg {
|
|
371
|
-
width:
|
|
372
|
-
height:
|
|
370
|
+
width: 18px;
|
|
371
|
+
height: 18px;
|
|
373
372
|
stroke-width: 2.5px;
|
|
374
373
|
border-radius: 50%;
|
|
375
374
|
background: var(--colorSecondaryButtonBackground);
|
|
@@ -377,7 +376,7 @@ select {
|
|
|
377
376
|
}
|
|
378
377
|
|
|
379
378
|
.ProxyToggler {
|
|
380
|
-
padding: 1px
|
|
379
|
+
padding: 1px 3px;
|
|
381
380
|
background: var(--colorSecondaryButtonBackground);
|
|
382
381
|
border-radius: var(--radiusSmall);
|
|
383
382
|
|
|
@@ -425,7 +424,7 @@ select {
|
|
|
425
424
|
|
|
426
425
|
.InternalServerErrorToggler {
|
|
427
426
|
display: flex;
|
|
428
|
-
margin-left:
|
|
427
|
+
margin-left: 8px;
|
|
429
428
|
cursor: pointer;
|
|
430
429
|
|
|
431
430
|
> input {
|
|
@@ -445,7 +444,7 @@ select {
|
|
|
445
444
|
}
|
|
446
445
|
|
|
447
446
|
> span {
|
|
448
|
-
padding:
|
|
447
|
+
padding: 4px;
|
|
449
448
|
font-size: 10px;
|
|
450
449
|
font-weight: bold;
|
|
451
450
|
color: var(--colorSecondaryAction);
|
package/src/Dashboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_500_COMMENT } from './ApiConstants.js'
|
|
1
|
+
import { DEFAULT_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
|
|
2
2
|
import { parseFilename } from './Filename.js'
|
|
3
3
|
import { Commander } from './Commander.js'
|
|
4
4
|
|
|
@@ -20,6 +20,7 @@ const Strings = {
|
|
|
20
20
|
delay_ms: 'Delay (ms)',
|
|
21
21
|
empty_response_body: '/* Empty Response Body */',
|
|
22
22
|
fallback_server: 'Fallback Backend',
|
|
23
|
+
fallback_server_error: '⛔ Fallback Backend Error',
|
|
23
24
|
fallback_server_placeholder: 'Type Server Address',
|
|
24
25
|
got: 'Got',
|
|
25
26
|
internal_server_error: 'Internal Server Error',
|
|
@@ -51,6 +52,7 @@ const CSS = {
|
|
|
51
52
|
SaveProxiedCheckbox: 'SaveProxiedCheckbox',
|
|
52
53
|
StaticFilesList: 'StaticFilesList',
|
|
53
54
|
|
|
55
|
+
red: 'red',
|
|
54
56
|
empty: 'empty',
|
|
55
57
|
chosen: 'chosen',
|
|
56
58
|
status4xx: 'status4xx',
|
|
@@ -425,10 +427,12 @@ function PayloadViewerTitle({ file, status, statusText }) {
|
|
|
425
427
|
r('abbr', { title: statusText }, status),
|
|
426
428
|
'.' + ext))
|
|
427
429
|
}
|
|
428
|
-
function PayloadViewerTitleWhenProxied({ mime, status, statusText }) {
|
|
430
|
+
function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad }) {
|
|
429
431
|
return (
|
|
430
432
|
r('span', null,
|
|
431
|
-
|
|
433
|
+
gatewayIsBad
|
|
434
|
+
? r('span', { className: CSS.red }, Strings.fallback_server_error + ' ')
|
|
435
|
+
: r('span', null, Strings.got + ' '),
|
|
432
436
|
r('abbr', { title: statusText }, status),
|
|
433
437
|
' ' + mime))
|
|
434
438
|
}
|
|
@@ -452,7 +456,8 @@ async function updatePayloadViewer(method, urlMask, response) {
|
|
|
452
456
|
payloadViewerTitleRef.current.replaceChildren(PayloadViewerTitleWhenProxied({
|
|
453
457
|
status: response.status,
|
|
454
458
|
statusText: response.statusText,
|
|
455
|
-
mime
|
|
459
|
+
mime,
|
|
460
|
+
gatewayIsBad: response.headers.get(HEADER_FOR_502)
|
|
456
461
|
}))
|
|
457
462
|
else
|
|
458
463
|
payloadViewerTitleRef.current.replaceChildren(PayloadViewerTitle({
|
package/src/ProxyRelay.js
CHANGED
|
@@ -3,19 +3,30 @@ import { randomUUID } from 'node:crypto'
|
|
|
3
3
|
|
|
4
4
|
import { config } from './config.js'
|
|
5
5
|
import { extFor } from './utils/mime.js'
|
|
6
|
-
import { readBody } from './utils/http-request.js'
|
|
7
6
|
import { write, isFile } from './utils/fs.js'
|
|
8
7
|
import { makeMockFilename } from './Filename.js'
|
|
8
|
+
import { readBody, BodyReaderError } from './utils/http-request.js'
|
|
9
|
+
import { sendUnprocessableContent, sendBadGateway } from './utils/http-response.js'
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
export async function proxy(req, response, delay) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
:
|
|
18
|
-
|
|
13
|
+
let proxyResponse
|
|
14
|
+
try {
|
|
15
|
+
proxyResponse = await fetch(config.proxyFallback + req.url, {
|
|
16
|
+
method: req.method,
|
|
17
|
+
headers: req.headers,
|
|
18
|
+
body: req.method === 'GET' || req.method === 'HEAD'
|
|
19
|
+
? undefined
|
|
20
|
+
: await readBody(req)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
catch (error) { // TESTME
|
|
24
|
+
if (error instanceof BodyReaderError)
|
|
25
|
+
sendUnprocessableContent(response, error.name)
|
|
26
|
+
else
|
|
27
|
+
sendBadGateway(response, error)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
const headers = Object.fromEntries(proxyResponse.headers)
|
|
21
32
|
headers['set-cookie'] = proxyResponse.headers.getSetCookie() // parses multiple into an array
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { mimeFor } from './mime.js'
|
|
3
|
+
import { HEADER_FOR_502 } from '../ApiConstants.js'
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export function sendOK(response) {
|
|
@@ -42,3 +43,10 @@ export function sendInternalServerError(response, error) {
|
|
|
42
43
|
response.statusCode = 500
|
|
43
44
|
response.end()
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
export function sendBadGateway(response, error) {
|
|
48
|
+
console.error('Fallback Proxy Error:', error.cause.message)
|
|
49
|
+
response.statusCode = 502
|
|
50
|
+
response.setHeader(HEADER_FOR_502, 1)
|
|
51
|
+
response.end()
|
|
52
|
+
}
|