mockaton 8.12.2 → 8.12.4
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 +66 -41
- package/index.d.ts +5 -1
- package/package.json +2 -2
- package/src/Api.js +16 -12
- package/src/MockBroker.js +7 -4
- package/src/MockDispatcher.js +1 -1
- package/src/Mockaton.test.js +1 -0
- package/src/StaticDispatcher.js +2 -2
- package/src/config.js +56 -70
- package/src/mockBrokersCollection.js +7 -7
- package/src/utils/jwt.js +2 -2
- package/src/utils/validate.js +8 -0
- package/src/utils/validate.test.js +47 -0
- package/.coverage/lcov.info +0 -2267
- package/TODO.md +0 -11
- package/dev-mockaton.js +0 -17
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|

|
|
5
5
|
|
|
6
6
|
An HTTP mock server for simulating APIs with minimal setup
|
|
7
|
-
— ideal for
|
|
7
|
+
— ideal for triggering difficult to reproduce backend states.
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
## Convention Over Code
|
|
10
|
-
|
|
11
|
-
given directory for filenames following a convention similar to the URLs.
|
|
11
|
+
Mockaton scans a given directory for filenames following a convention similar to the URLs.
|
|
12
12
|
|
|
13
13
|
For example, for <code>/<b>api/user</b>/1234</code> the filename would be:
|
|
14
14
|
<pre>
|
|
@@ -22,7 +22,7 @@ On the dashboard you can select a mock variant for a particular route, delaying
|
|
|
22
22
|
or triggering an autogenerated `500` (Internal Server Error), among other features.
|
|
23
23
|
|
|
24
24
|
Nonetheless, there’s a programmatic API, which is handy
|
|
25
|
-
for setting up tests
|
|
25
|
+
for setting up tests (see **Commander API** section).
|
|
26
26
|
|
|
27
27
|
<picture>
|
|
28
28
|
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp840x800.light.gold.png">
|
|
@@ -47,12 +47,12 @@ api/login<b>(invalid login attempt)</b>.POST.401.json
|
|
|
47
47
|
|
|
48
48
|
### Different response status code
|
|
49
49
|
For instance, you can use a `4xx` or `5xx` status code for triggering error
|
|
50
|
-
responses, or a `2xx` such as `204`
|
|
50
|
+
responses, or a `2xx` such as `204` for testing empty collections.
|
|
51
51
|
|
|
52
52
|
<pre>
|
|
53
|
-
api/videos(empty list).GET.<b>204</b>.json
|
|
54
|
-
api/videos.GET.<b>403</b>.json
|
|
55
|
-
api/videos.GET.<b>500</b>.txt
|
|
53
|
+
api/videos(empty list).GET.<b>204</b>.json # No Content
|
|
54
|
+
api/videos.GET.<b>403</b>.json # Forbidden
|
|
55
|
+
api/videos.GET.<b>500</b>.txt # Internal Server Error
|
|
56
56
|
</pre>
|
|
57
57
|
|
|
58
58
|
|
|
@@ -265,12 +265,15 @@ want a `Content-Type` header in the response.
|
|
|
265
265
|
</p>
|
|
266
266
|
</details>
|
|
267
267
|
|
|
268
|
+
<br/>
|
|
269
|
+
|
|
268
270
|
### Dynamic parameters
|
|
269
|
-
Anything within square brackets is always matched. For example, for this route
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
api/company/<b>[id]</b>/user/<b>[uid]</b>.GET.200.json
|
|
273
|
-
|
|
271
|
+
Anything within square brackets is always matched. For example, for this route:
|
|
272
|
+
<code>/api/company/<b>1234</b>/user/<b>5678</b></code>
|
|
273
|
+
|
|
274
|
+
<pre><code>api/company/<b>[id]</b>/user/<b>[uid]</b>.GET.200.json</code></pre>
|
|
275
|
+
|
|
276
|
+
<br/>
|
|
274
277
|
|
|
275
278
|
### Comments
|
|
276
279
|
Comments are anything within parentheses, including them.
|
|
@@ -283,6 +286,8 @@ api/foo.GET.200.json
|
|
|
283
286
|
|
|
284
287
|
A filename can have many comments.
|
|
285
288
|
|
|
289
|
+
<br/>
|
|
290
|
+
|
|
286
291
|
### Default mock for a route
|
|
287
292
|
You can add the comment: `(default)`.
|
|
288
293
|
Otherwise, the first file in **alphabetical order** wins.
|
|
@@ -291,9 +296,10 @@ Otherwise, the first file in **alphabetical order** wins.
|
|
|
291
296
|
api/user<b>(default)</b>.GET.200.json
|
|
292
297
|
</pre>
|
|
293
298
|
|
|
299
|
+
<br/>
|
|
294
300
|
|
|
295
301
|
### Query string params
|
|
296
|
-
The query string is ignored
|
|
302
|
+
The query string is ignored for routing purposes. In other words, it’s only used for
|
|
297
303
|
documenting the URL contract.
|
|
298
304
|
<pre>
|
|
299
305
|
api/video<b>?limit=[limit]</b>.GET.200.json
|
|
@@ -302,17 +308,18 @@ api/video<b>?limit=[limit]</b>.GET.200.json
|
|
|
302
308
|
On Windows filenames containing "?" are [not
|
|
303
309
|
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.
|
|
304
310
|
|
|
311
|
+
<br/>
|
|
305
312
|
|
|
306
313
|
### Index-like routes
|
|
307
314
|
If you have `api/foo` and `api/foo/bar`, you have two options:
|
|
308
315
|
|
|
309
|
-
**Option A
|
|
316
|
+
**Option A.** Standard naming:
|
|
310
317
|
```
|
|
311
318
|
api/foo.GET.200.json
|
|
312
319
|
api/foo/bar.GET.200.json
|
|
313
320
|
```
|
|
314
321
|
|
|
315
|
-
**Option B
|
|
322
|
+
**Option B.** Omit the URL on the filename:
|
|
316
323
|
```text
|
|
317
324
|
api/foo/.GET.200.json
|
|
318
325
|
api/foo/bar.GET.200.json
|
|
@@ -324,25 +331,39 @@ api/foo/bar.GET.200.json
|
|
|
324
331
|
### `mocksDir: string`
|
|
325
332
|
This is the only required field. The directory must exist.
|
|
326
333
|
|
|
334
|
+
### `staticDir?: string`
|
|
335
|
+
- Use Case 1: If you have a bunch of static assets you don’t want to add `.GET.200.ext`
|
|
336
|
+
- Use Case 2: For a standalone demo server. For example,
|
|
337
|
+
build your frontend bundle, and serve it from Mockaton.
|
|
338
|
+
|
|
339
|
+
Files under `config.staticDir` don’t use the filename convention, and
|
|
340
|
+
they take precedence over corresponding `GET` mocks in `config.mocksDir`.
|
|
341
|
+
For example, if you have two files for `GET /foo/bar.jpg`
|
|
342
|
+
|
|
343
|
+
<pre>
|
|
344
|
+
my-static-dir<b>/foo/bar.jpg</b>
|
|
345
|
+
my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg // Unreachable
|
|
346
|
+
</pre>
|
|
347
|
+
|
|
348
|
+
### `ignore?: RegExp`
|
|
349
|
+
Defaults to `/(\.DS_Store|~)$/`
|
|
350
|
+
|
|
351
|
+
<br/>
|
|
352
|
+
|
|
327
353
|
|
|
328
354
|
### `host?: string`
|
|
329
355
|
Defaults to `'localhost'`
|
|
330
356
|
|
|
331
|
-
|
|
332
357
|
### `port?: number`
|
|
333
358
|
Defaults to `0`, which means auto-assigned
|
|
334
359
|
|
|
335
|
-
|
|
336
|
-
### `ignore?: RegExp`
|
|
337
|
-
Defaults to `/(\.DS_Store|~)$/`
|
|
338
|
-
|
|
360
|
+
<br/>
|
|
339
361
|
|
|
340
362
|
### `delay?: number`
|
|
341
|
-
Defaults to `1200` milliseconds.
|
|
342
|
-
|
|
343
|
-
Although routes can individually be delayed with the 🕓
|
|
344
|
-
checkbox, the delay amount is globally configurable.
|
|
363
|
+
Defaults to `1200` milliseconds. Although routes can individually be delayed
|
|
364
|
+
with the 🕓 Checkbox, the amount is globally configurable with this option.
|
|
345
365
|
|
|
366
|
+
<br/>
|
|
346
367
|
|
|
347
368
|
### `proxyFallback?: string`
|
|
348
369
|
For example, `config.proxyFallback = 'http://example.com'`
|
|
@@ -384,19 +405,7 @@ Defaults to `true`. Saves the mock with the formatting output
|
|
|
384
405
|
of `JSON.stringify(data, null, ' ')` (two spaces indentation).
|
|
385
406
|
|
|
386
407
|
|
|
387
|
-
|
|
388
|
-
- Use Case 1: If you have a bunch of static assets you don’t want to add `.GET.200.ext`
|
|
389
|
-
- Use Case 2: For a standalone demo server. For example,
|
|
390
|
-
build your frontend bundle, and serve it from Mockaton.
|
|
391
|
-
|
|
392
|
-
Files under `config.staticDir` don’t use the filename convention, and
|
|
393
|
-
they take precedence over corresponding `GET` mocks in `config.mocksDir`.
|
|
394
|
-
For example, if you have two files for `GET /foo/bar.jpg`
|
|
395
|
-
|
|
396
|
-
<pre>
|
|
397
|
-
my-static-dir<b>/foo/bar.jpg</b>
|
|
398
|
-
my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg // Unreachable
|
|
399
|
-
</pre>
|
|
408
|
+
<br/>
|
|
400
409
|
|
|
401
410
|
|
|
402
411
|
### `cookies?: { [label: string]: string }`
|
|
@@ -422,6 +431,7 @@ in `config.extraHeaders`, or in function `.js` or `.ts` mock.
|
|
|
422
431
|
By the way, the `jwtCookie` helper has a hardcoded header and signature.
|
|
423
432
|
In other words, it’s useful only if you care about its payload.
|
|
424
433
|
|
|
434
|
+
<br/>
|
|
425
435
|
|
|
426
436
|
### `extraHeaders?: string[]`
|
|
427
437
|
Note: it’s a one-dimensional array. The header name goes at even indices.
|
|
@@ -434,6 +444,7 @@ config.extraHeaders = [
|
|
|
434
444
|
]
|
|
435
445
|
```
|
|
436
446
|
|
|
447
|
+
<br/>
|
|
437
448
|
|
|
438
449
|
### `extraMimes?: { [fileExt: string]: string }`
|
|
439
450
|
```js
|
|
@@ -444,6 +455,7 @@ config.extraMimes = {
|
|
|
444
455
|
Those extra media types take precedence over the built-in
|
|
445
456
|
[utils/mime.js](src/utils/mime.js), so you can override them.
|
|
446
457
|
|
|
458
|
+
<br/>
|
|
447
459
|
|
|
448
460
|
### `plugins?: [filenameTester: RegExp, plugin: Plugin][]`
|
|
449
461
|
```ts
|
|
@@ -499,6 +511,7 @@ function capitalizePlugin(filePath) {
|
|
|
499
511
|
```
|
|
500
512
|
</details>
|
|
501
513
|
|
|
514
|
+
<br/>
|
|
502
515
|
|
|
503
516
|
### `corsAllowed?: boolean`
|
|
504
517
|
Defaults to `true`. When `true`, these are the default options:
|
|
@@ -511,6 +524,7 @@ config.corsMaxAge = 0 // seconds to cache the preflight req
|
|
|
511
524
|
config.corsExposedHeaders = [] // headers you need to access in client-side JS
|
|
512
525
|
```
|
|
513
526
|
|
|
527
|
+
<br/>
|
|
514
528
|
|
|
515
529
|
### `onReady?: (dashboardUrl: string) => void`
|
|
516
530
|
By default, it will open the dashboard in your default browser on macOS and
|
|
@@ -536,10 +550,13 @@ const myMockatonAddr = 'http://localhost:2345'
|
|
|
536
550
|
const mockaton = new Commander(myMockatonAddr)
|
|
537
551
|
```
|
|
538
552
|
|
|
553
|
+
<br/>
|
|
554
|
+
|
|
539
555
|
### Select a mock file for a route
|
|
540
556
|
```js
|
|
541
557
|
await mockaton.select('api/foo.200.GET.json')
|
|
542
558
|
```
|
|
559
|
+
|
|
543
560
|
### Select all mocks that have a particular comment
|
|
544
561
|
```js
|
|
545
562
|
await mockaton.bulkSelectByComment('(demo-a)')
|
|
@@ -548,33 +565,41 @@ Parentheses are optional, so you can pass a partial match.
|
|
|
548
565
|
For example, passing `'demo-'` (without the final `a`), selects the
|
|
549
566
|
first mock in alphabetical order that matches.
|
|
550
567
|
|
|
568
|
+
<br/>
|
|
569
|
+
|
|
551
570
|
### Set route is delayed flag
|
|
552
571
|
```js
|
|
553
572
|
await mockaton.setRouteIsDelayed('GET', '/api/foo', true)
|
|
554
573
|
```
|
|
555
574
|
|
|
556
|
-
### Set route is proxied
|
|
575
|
+
### Set route is proxied flag
|
|
557
576
|
```js
|
|
558
577
|
await mockaton.setRouteIsProxied('GET', '/api/foo', true)
|
|
559
578
|
```
|
|
560
579
|
|
|
580
|
+
<br/>
|
|
581
|
+
|
|
561
582
|
### Select a cookie
|
|
562
583
|
In `config.cookies`, each key is the label used for selecting it.
|
|
563
584
|
```js
|
|
564
585
|
await mockaton.selectCookie('My Normal User')
|
|
565
586
|
```
|
|
566
587
|
|
|
567
|
-
|
|
588
|
+
<br/>
|
|
589
|
+
|
|
590
|
+
### Set fallback proxy server address
|
|
568
591
|
```js
|
|
569
592
|
await mockaton.setProxyFallback('http://example.com')
|
|
570
593
|
```
|
|
571
594
|
Pass an empty string to disable it.
|
|
572
595
|
|
|
573
|
-
### Set save proxied mocks
|
|
596
|
+
### Set save proxied responses as mocks flag
|
|
574
597
|
```js
|
|
575
598
|
await mockaton.setCollectProxied(true)
|
|
576
599
|
```
|
|
577
600
|
|
|
601
|
+
<br/>
|
|
602
|
+
|
|
578
603
|
### Reset
|
|
579
604
|
Re-initialize the collection. The selected mocks, cookies, and delays go back to
|
|
580
605
|
default, but the `proxyFallback`, `colledProxied`, and `corsAllowed` are not affected.
|
package/index.d.ts
CHANGED
|
@@ -16,13 +16,17 @@ interface Config {
|
|
|
16
16
|
|
|
17
17
|
host?: string,
|
|
18
18
|
port?: number
|
|
19
|
+
|
|
19
20
|
proxyFallback?: string
|
|
20
21
|
collectProxied?: boolean
|
|
21
22
|
formatCollectedJSON?: boolean
|
|
22
23
|
|
|
23
24
|
delay?: number
|
|
25
|
+
|
|
24
26
|
cookies?: { [label: string]: string }
|
|
27
|
+
|
|
25
28
|
extraHeaders?: string[]
|
|
29
|
+
|
|
26
30
|
extraMimes?: { [fileExt: string]: string }
|
|
27
31
|
|
|
28
32
|
plugins?: [filenameTester: RegExp, plugin: Plugin][]
|
|
@@ -46,7 +50,7 @@ export const jsToJsonPlugin: Plugin
|
|
|
46
50
|
|
|
47
51
|
// Utils
|
|
48
52
|
|
|
49
|
-
export function jwtCookie(cookieName: string, payload: any): string
|
|
53
|
+
export function jwtCookie(cookieName: string, payload: any, path?: string): string
|
|
50
54
|
|
|
51
55
|
export function parseJSON(request: IncomingMessage): Promise<any>
|
|
52
56
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mockaton",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "HTTP Mock Server",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.12.
|
|
5
|
+
"version": "8.12.4",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
package/src/Api.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { cookie } from './cookie.js'
|
|
8
|
-
import { config } from './config.js'
|
|
9
8
|
import { arEvents } from './Watcher.js'
|
|
10
9
|
import { parseJSON } from './utils/http-request.js'
|
|
11
10
|
import { listFilesRecursively } from './utils/fs.js'
|
|
12
11
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
12
|
+
import { config, isFileAllowed, ConfigValidator } from './config.js'
|
|
13
13
|
import { DF, API, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
|
|
14
14
|
import { sendOK, sendJSON, sendUnprocessableContent, sendFile } from './utils/http-response.js'
|
|
15
15
|
|
|
@@ -50,6 +50,7 @@ export const apiPatchRequests = new Map([
|
|
|
50
50
|
[API.collectProxied, setCollectProxied]
|
|
51
51
|
])
|
|
52
52
|
|
|
53
|
+
|
|
53
54
|
/* === GET === */
|
|
54
55
|
|
|
55
56
|
function serveDashboard(_, response) {
|
|
@@ -70,20 +71,18 @@ function getIsCorsAllowed(_, response) { sendJSON(response, config.corsAllowed)
|
|
|
70
71
|
function getCollectProxied(_, response) { sendJSON(response, config.collectProxied) }
|
|
71
72
|
|
|
72
73
|
function listStaticFiles(req, response) {
|
|
73
|
-
|
|
74
|
-
? listFilesRecursively(config.staticDir).filter(
|
|
75
|
-
: []
|
|
76
|
-
sendJSON(response, files)
|
|
74
|
+
sendJSON(response, config.staticDir
|
|
75
|
+
? listFilesRecursively(config.staticDir).filter(isFileAllowed)
|
|
76
|
+
: [])
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function longPollAR_Events(req, response) {
|
|
80
|
-
// e.g. tab was hidden while new mocks were added or removed
|
|
80
|
+
// needs sync e.g. when tab was hidden while new mocks were added or removed
|
|
81
81
|
const clientIsOutOfSync = parseInt(req.headers[DF.lastReceived_nAR], 10) !== arEvents.count
|
|
82
82
|
if (clientIsOutOfSync) {
|
|
83
83
|
sendJSON(response, arEvents.count)
|
|
84
84
|
return
|
|
85
85
|
}
|
|
86
|
-
|
|
87
86
|
function onAddOrRemoveMock() {
|
|
88
87
|
arEvents.unsubscribe(onAddOrRemoveMock)
|
|
89
88
|
sendJSON(response, arEvents.count)
|
|
@@ -115,7 +114,7 @@ async function selectCookie(req, response) {
|
|
|
115
114
|
|
|
116
115
|
async function selectMock(req, response) {
|
|
117
116
|
const file = await parseJSON(req)
|
|
118
|
-
const broker = mockBrokersCollection.
|
|
117
|
+
const broker = mockBrokersCollection.findBrokerByFilename(file)
|
|
119
118
|
if (!broker || !broker.hasMock(file))
|
|
120
119
|
sendUnprocessableContent(response, `Missing Mock: ${file}`)
|
|
121
120
|
else {
|
|
@@ -127,7 +126,7 @@ async function selectMock(req, response) {
|
|
|
127
126
|
async function setRouteIsDelayed(req, response) {
|
|
128
127
|
const body = await parseJSON(req)
|
|
129
128
|
const delayed = body[DF.delayed]
|
|
130
|
-
const broker = mockBrokersCollection.
|
|
129
|
+
const broker = mockBrokersCollection.findBrokerByRoute(
|
|
131
130
|
body[DF.routeMethod],
|
|
132
131
|
body[DF.routeUrlMask])
|
|
133
132
|
|
|
@@ -144,7 +143,7 @@ async function setRouteIsDelayed(req, response) {
|
|
|
144
143
|
async function setRouteIsProxied(req, response) { // TESTME
|
|
145
144
|
const body = await parseJSON(req)
|
|
146
145
|
const proxied = body[DF.proxied]
|
|
147
|
-
const broker = mockBrokersCollection.
|
|
146
|
+
const broker = mockBrokersCollection.findBrokerByRoute(
|
|
148
147
|
body[DF.routeMethod],
|
|
149
148
|
body[DF.routeUrlMask])
|
|
150
149
|
|
|
@@ -162,7 +161,7 @@ async function setRouteIsProxied(req, response) { // TESTME
|
|
|
162
161
|
|
|
163
162
|
async function updateProxyFallback(req, response) {
|
|
164
163
|
const fallback = await parseJSON(req)
|
|
165
|
-
if (
|
|
164
|
+
if (!ConfigValidator.proxyFallback(fallback)) {
|
|
166
165
|
sendUnprocessableContent(response)
|
|
167
166
|
return
|
|
168
167
|
}
|
|
@@ -173,7 +172,12 @@ async function updateProxyFallback(req, response) {
|
|
|
173
172
|
}
|
|
174
173
|
|
|
175
174
|
async function setCollectProxied(req, response) {
|
|
176
|
-
|
|
175
|
+
const collectProxied = await parseJSON(req)
|
|
176
|
+
if (!ConfigValidator.collectProxied(collectProxied)) { // TESTME
|
|
177
|
+
sendUnprocessableContent(response)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
config.collectProxied = collectProxied
|
|
177
181
|
sendOK(response)
|
|
178
182
|
}
|
|
179
183
|
|
package/src/MockBroker.js
CHANGED
|
@@ -24,7 +24,7 @@ export class MockBroker {
|
|
|
24
24
|
hasMock(file) { return this.mocks.includes(file) }
|
|
25
25
|
|
|
26
26
|
register(file) {
|
|
27
|
-
if (
|
|
27
|
+
if (this.#is500(file)) {
|
|
28
28
|
if (this.temp500IsSelected)
|
|
29
29
|
this.selectFile(file)
|
|
30
30
|
this.#deleteTemp500()
|
|
@@ -33,6 +33,10 @@ export class MockBroker {
|
|
|
33
33
|
this.#sortMocks()
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
#is500(file) {
|
|
37
|
+
return parseFilename(file).status === 500
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
#deleteTemp500() {
|
|
37
41
|
this.mocks = this.mocks.filter(file => !this.#isTemp500(file))
|
|
38
42
|
}
|
|
@@ -44,7 +48,7 @@ export class MockBroker {
|
|
|
44
48
|
#sortMocks() {
|
|
45
49
|
this.mocks.sort()
|
|
46
50
|
const defaults = this.mocks.filter(file => includesComment(file, DEFAULT_MOCK_COMMENT))
|
|
47
|
-
const temp500 = this.mocks.filter(
|
|
51
|
+
const temp500 = this.mocks.filter(this.#isTemp500)
|
|
48
52
|
this.mocks = [
|
|
49
53
|
...defaults,
|
|
50
54
|
...this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file)),
|
|
@@ -53,10 +57,9 @@ export class MockBroker {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
ensureItHas500() {
|
|
56
|
-
if (!this.#
|
|
60
|
+
if (!this.mocks.some(this.#is500))
|
|
57
61
|
this.#registerTemp500()
|
|
58
62
|
}
|
|
59
|
-
#has500() { return this.mocks.some(mock => parseFilename(mock).status === 500) }
|
|
60
63
|
#registerTemp500() {
|
|
61
64
|
const { urlMask, method } = parseFilename(this.mocks[0])
|
|
62
65
|
const file = urlMask.replace(/^\//, '') // Removes leading slash
|
package/src/MockDispatcher.js
CHANGED
|
@@ -11,7 +11,7 @@ import { sendInternalServerError, sendNotFound, sendUnprocessableContent } from
|
|
|
11
11
|
|
|
12
12
|
export async function dispatchMock(req, response) {
|
|
13
13
|
try {
|
|
14
|
-
const broker = mockBrokerCollection.
|
|
14
|
+
const broker = mockBrokerCollection.findBrokerByRoute(req.method, req.url)
|
|
15
15
|
if (!broker || broker.proxied) {
|
|
16
16
|
if (config.proxyFallback)
|
|
17
17
|
await proxy(req, response, config.delay * Boolean(broker?.delayed))
|
package/src/Mockaton.test.js
CHANGED
|
@@ -435,6 +435,7 @@ async function testAutogenerates500(url, file) {
|
|
|
435
435
|
await describe('autogenerated in-memory 500', () => {
|
|
436
436
|
it('body is empty', () => equal(body, ''))
|
|
437
437
|
it('status is: 500', () => equal(res.status, 500))
|
|
438
|
+
it('mime is empty', () => equal(res.headers.get('content-type'), ''))
|
|
438
439
|
})
|
|
439
440
|
}
|
|
440
441
|
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import fs, { readFileSync } from 'node:fs'
|
|
3
3
|
|
|
4
|
-
import { config } from './config.js'
|
|
5
4
|
import { mimeFor } from './utils/mime.js'
|
|
6
5
|
import { isDirectory, isFile } from './utils/fs.js'
|
|
6
|
+
import { config, isFileAllowed } from './config.js'
|
|
7
7
|
import { sendInternalServerError } from './utils/http-response.js'
|
|
8
8
|
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ export function isStatic(req) {
|
|
|
11
11
|
if (!config.staticDir)
|
|
12
12
|
return false
|
|
13
13
|
const f = resolvePath(req.url)
|
|
14
|
-
return f &&
|
|
14
|
+
return f && isFileAllowed(f)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export async function dispatchStatic(req, response) {
|
package/src/config.js
CHANGED
|
@@ -2,90 +2,76 @@ import { realpathSync } from 'node:fs'
|
|
|
2
2
|
import { isDirectory } from './utils/fs.js'
|
|
3
3
|
import { openInBrowser } from './utils/openInBrowser.js'
|
|
4
4
|
import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
|
|
5
|
+
import { optional, is, validate } from './utils/validate.js'
|
|
5
6
|
import { SUPPORTED_METHODS } from './utils/http-request.js'
|
|
6
7
|
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
/** @type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
formatCollectedJSON: true,
|
|
20
|
-
|
|
21
|
-
delay: 1200, // milliseconds
|
|
22
|
-
cookies: {}, // defaults to the first kv
|
|
23
|
-
extraHeaders: [],
|
|
24
|
-
extraMimes: {},
|
|
10
|
+
/** @type {{
|
|
11
|
+
* [K in keyof Config]-?: [
|
|
12
|
+
* defaultVal: Config[K],
|
|
13
|
+
* validator: (val: unknown) => boolean
|
|
14
|
+
* ]
|
|
15
|
+
* }} */
|
|
16
|
+
const schema = {
|
|
17
|
+
mocksDir: ['', isDirectory],
|
|
18
|
+
staticDir: ['', optional(isDirectory)],
|
|
19
|
+
ignore: [/(\.DS_Store|~)$/, is(RegExp)],
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
],
|
|
21
|
+
host: ['127.0.0.1', is(String)],
|
|
22
|
+
port: [0, port => Number.isInteger(port) && port >= 0 && port < 2 ** 16], // 0 means auto-assigned
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
corsHeaders: ['content-type'],
|
|
34
|
-
corsExposedHeaders: [],
|
|
35
|
-
corsCredentials: true,
|
|
36
|
-
corsMaxAge: 0,
|
|
24
|
+
proxyFallback: ['', optional(URL.canParse)], // e.g. http://localhost:9999
|
|
25
|
+
collectProxied: [false, is(Boolean)],
|
|
26
|
+
formatCollectedJSON: [true, is(Boolean)],
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
})
|
|
28
|
+
delay: [1200, ms => Number.isInteger(ms) && ms >= 0],
|
|
40
29
|
|
|
30
|
+
cookies: [{}, is(Object)], // defaults to the first kv
|
|
41
31
|
|
|
42
|
-
|
|
43
|
-
Object.assign(config, options)
|
|
44
|
-
validate(config, {
|
|
45
|
-
mocksDir: isDirectory,
|
|
46
|
-
staticDir: optional(isDirectory),
|
|
47
|
-
ignore: is(RegExp),
|
|
48
|
-
|
|
49
|
-
host: is(String),
|
|
50
|
-
port: port => Number.isInteger(port) && port >= 0 && port < 2 ** 16,
|
|
51
|
-
proxyFallback: optional(URL.canParse),
|
|
52
|
-
collectProxied: is(Boolean),
|
|
53
|
-
formatCollectedJSON: is(Boolean),
|
|
54
|
-
|
|
55
|
-
delay: ms => Number.isInteger(ms) && ms > 0,
|
|
56
|
-
cookies: is(Object),
|
|
57
|
-
extraHeaders: val => Array.isArray(val) && val.length % 2 === 0,
|
|
58
|
-
extraMimes: is(Object),
|
|
59
|
-
|
|
60
|
-
plugins: Array.isArray,
|
|
61
|
-
|
|
62
|
-
corsAllowed: is(Boolean),
|
|
63
|
-
corsOrigins: validateCorsAllowedOrigins,
|
|
64
|
-
corsMethods: validateCorsAllowedMethods,
|
|
65
|
-
corsHeaders: Array.isArray,
|
|
66
|
-
corsExposedHeaders: Array.isArray,
|
|
67
|
-
corsCredentials: is(Boolean),
|
|
68
|
-
corsMaxAge: is(Number),
|
|
69
|
-
|
|
70
|
-
onReady: is(Function)
|
|
71
|
-
})
|
|
32
|
+
extraHeaders: [[], val => Array.isArray(val) && val.length % 2 === 0],
|
|
72
33
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
34
|
+
extraMimes: [{}, is(Object)],
|
|
35
|
+
|
|
36
|
+
plugins: [
|
|
37
|
+
[
|
|
38
|
+
[/\.(js|ts)$/, jsToJsonPlugin]
|
|
39
|
+
], Array.isArray],
|
|
40
|
+
|
|
41
|
+
corsAllowed: [true, is(Boolean)],
|
|
42
|
+
corsOrigins: [['*'], validateCorsAllowedOrigins],
|
|
43
|
+
corsMethods: [SUPPORTED_METHODS, validateCorsAllowedMethods],
|
|
44
|
+
corsHeaders: [['content-type'], Array.isArray],
|
|
45
|
+
corsExposedHeaders: [[], Array.isArray],
|
|
46
|
+
corsCredentials: [true, is(Boolean)],
|
|
47
|
+
corsMaxAge: [0, is(Number)],
|
|
48
|
+
|
|
49
|
+
onReady: [await openInBrowser, is(Function)]
|
|
76
50
|
}
|
|
77
51
|
|
|
78
52
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
53
|
+
const defaults = {}
|
|
54
|
+
const validators = {}
|
|
55
|
+
for (const [k, [defaultVal, validator]] of Object.entries(schema)) {
|
|
56
|
+
defaults[k] = defaultVal
|
|
57
|
+
validators[k] = validator
|
|
83
58
|
}
|
|
84
59
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
60
|
+
/** @type {Config} */
|
|
61
|
+
export const config = Object.seal(defaults)
|
|
88
62
|
|
|
89
|
-
|
|
90
|
-
|
|
63
|
+
/** @type {Record<keyof Config, (val: unknown) => boolean>} */
|
|
64
|
+
export const ConfigValidator = Object.freeze(validators)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export const isFileAllowed = f => !config.ignore.test(f)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
export function setup(options) {
|
|
71
|
+
Object.assign(config, options)
|
|
72
|
+
validate(config, ConfigValidator)
|
|
73
|
+
|
|
74
|
+
config.mocksDir = realpathSync(config.mocksDir)
|
|
75
|
+
if (config.staticDir)
|
|
76
|
+
config.staticDir = realpathSync(config.staticDir)
|
|
91
77
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { config } from './config.js'
|
|
2
1
|
import { cookie } from './cookie.js'
|
|
3
2
|
import { MockBroker } from './MockBroker.js'
|
|
4
3
|
import { listFilesRecursively } from './utils/fs.js'
|
|
4
|
+
import { config, isFileAllowed } from './config.js'
|
|
5
5
|
import { parseFilename, filenameIsValid } from './Filename.js'
|
|
6
6
|
|
|
7
7
|
|
|
@@ -37,8 +37,8 @@ export function init() {
|
|
|
37
37
|
|
|
38
38
|
/** @returns {boolean} registered */
|
|
39
39
|
export function registerMock(file, isFromWatcher) {
|
|
40
|
-
if (
|
|
41
|
-
||
|
|
40
|
+
if (findBrokerByFilename(file)?.hasMock(file)
|
|
41
|
+
|| !isFileAllowed(file)
|
|
42
42
|
|| !filenameIsValid(file))
|
|
43
43
|
return false
|
|
44
44
|
|
|
@@ -60,7 +60,7 @@ export function registerMock(file, isFromWatcher) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function unregisterMock(file) {
|
|
63
|
-
const broker =
|
|
63
|
+
const broker = findBrokerByFilename(file)
|
|
64
64
|
if (!broker)
|
|
65
65
|
return
|
|
66
66
|
const isEmpty = broker.unregister(file)
|
|
@@ -76,20 +76,20 @@ export const getAll = () => collection
|
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
/** @returns {MockBroker | undefined} */
|
|
79
|
-
export function
|
|
79
|
+
export function findBrokerByFilename(file) {
|
|
80
80
|
const { method, urlMask } = parseFilename(file)
|
|
81
81
|
if (collection[method])
|
|
82
82
|
return collection[method][urlMask]
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* Searching
|
|
86
|
+
* Searching routes in reverse order so dynamic params (e.g.
|
|
87
87
|
* /user/[id]) don’t take precedence over exact paths (e.g.
|
|
88
88
|
* /user/name). That’s because "[]" chars are lower than alphanumeric ones.
|
|
89
89
|
* BTW, `urlMasks` always start with "/", so there’s no need to
|
|
90
90
|
* worry about the primacy of array-like keys when iterating.
|
|
91
91
|
@returns {MockBroker | undefined} */
|
|
92
|
-
export function
|
|
92
|
+
export function findBrokerByRoute(method, url) {
|
|
93
93
|
if (!collection[method])
|
|
94
94
|
return
|
|
95
95
|
const brokers = Object.values(collection[method])
|