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 CHANGED
@@ -3,26 +3,35 @@
3
3
  ![NPM Version](https://img.shields.io/npm/v/mockaton)
4
4
  ![NPM Version](https://img.shields.io/npm/l/mockaton)
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 mock filenames following a convention similar to the URLs.
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 mock filename would be:
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&nbsp;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 in filenames
24
- Comments are anything within parentheses, including them. They
25
- are ignored for URL purposes, so you can add as many as you want.
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 without records.
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>.empty
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 fallback to your real backend on routes you don’t have
45
- mocks for, or on routes that have the ☁️ **Cloud Checkbox** checked.
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&nbsp;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 mainly a list of
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
- Speaking of which, on Windows filenames containing "?" are [not
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
- my-static-dir/foo/bar.jpg
374
- my-mocks-dir/foo/bar.jpg.GET.200.jpg // Unreacheable
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
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "A deterministic server-side for developing and testing frontend clients",
4
4
  "type": "module",
5
- "version": "8.11.4",
5
+ "version": "8.11.6",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -30,3 +30,5 @@ export const DEFAULT_MOCK_COMMENT = '(default)'
30
30
  export const EXT_FOR_UNKNOWN_MIME = 'unknown'
31
31
 
32
32
  export const LONG_POLL_SERVER_TIMEOUT = 8_000
33
+
34
+ export const HEADER_FOR_502 = 'mockaton502'
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: 28px;
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: #555;
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: 98% center;
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: 6px;
122
+ gap: 10px;
119
123
 
120
124
  img {
121
125
  width: 130px;
122
126
  align-self: center;
123
- margin-right: 20px;
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 12px;
155
+ padding: 4px 8px;
153
156
  border-right: 3px solid transparent;
154
- margin-top: 3px;
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: 88px;
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: 212px;
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: 1px solid transparent;
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: -10px;
284
+ left: -6px;
286
285
  display: inline-block;
287
- width: 282px;
288
- padding: 8px 11px;
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: 11px;
306
- padding-left: 17px;
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: 5px center;
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: 10px;
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: 20px;
372
- height: 20px;
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 4px;
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: 6px;
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: 5px 4px;
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
- Strings.got + ' ',
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
- const proxyResponse = await fetch(config.proxyFallback + req.url, {
13
- method: req.method,
14
- headers: req.headers,
15
- body: req.method === 'GET' || req.method === 'HEAD'
16
- ? undefined
17
- : await readBody(req)
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
+ }