mockaton 13.11.3 → 13.12.0
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 +42 -27
- package/package.json +1 -1
- package/skills/mockaton/SKILL.md +21 -19
- package/src/client/ApiCommander.js +20 -19
- package/src/client/{app.js → App.js} +6 -6
- package/src/client/{app-header.js → Header.js} +4 -4
- package/src/client/IndexHtml.js +1 -1
- package/src/client/{app-mock-list.js → MockList.js} +7 -8
- package/src/client/{app-payload-viewer.js → PayloadViewer.js} +3 -3
- package/src/client/{app-store.test.js → store.test.js} +1 -1
- package/src/server/Api.js +22 -20
- package/src/server/MockBroker.js +2 -8
- package/src/server/Mockaton.js +7 -9
- package/src/server/Mockaton.test.js +1 -1
- package/src/server/ProxyRelay.js +2 -2
- package/src/server/stores/brokers.js +1 -4
- package/src/server/stores/config.js +2 -2
- package/src/server/utils/HttpIncomingMessage.js +17 -16
- package/src/server/utils/logger.js +7 -1
- package/src/server/utils/mime.js +2 -0
- /package/src/client/{app.css → App.css} +0 -0
- /package/src/client/{app-header.css → Header.css} +0 -0
- /package/src/client/{app-mock-list.css → MockList.css} +0 -0
- /package/src/client/{app-payload-viewer.css → PayloadViewer.css} +0 -0
- /package/src/client/{graphics.js → icons.js} +0 -0
- /package/src/client/{app-store.js → store.js} +0 -0
package/README.md
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
<!-- SKILLS_IGNORE_BEGIN -->
|
|
2
|
-
<img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
|
|
3
2
|
|
|
4
3
|

|
|
5
4
|
[](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
|
|
6
5
|
[](https://codecov.io/github/ericfortis/mockaton)
|
|
7
6
|
|
|
7
|
+
<img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
|
|
8
|
+
|
|
9
|
+
No API state should be too difficult to test.
|
|
10
|
+
|
|
8
11
|
## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog) | [Skills](skills/mockaton/SKILL.md)
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
Simulate API states that are normally ignored. For example,
|
|
15
|
+
click the 500 button to trigger an error on an endpoint. Then
|
|
16
|
+
unclick it to test your retry logic. Similarly, click the clock
|
|
17
|
+
button 🕓 to delay a response so you can test spinners —
|
|
18
|
+
you‘ve seen them in production at the top left, or restarting
|
|
19
|
+
their animation midway.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Dashboard
|
|
23
|
+
Besides the dashboard UI, there’s [an API](https://mockaton.com/api)
|
|
24
|
+
you can use to set up tests.
|
|
12
25
|
|
|
13
26
|
<picture>
|
|
14
27
|
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.light.gold.png">
|
|
@@ -35,47 +48,44 @@ curl localhost:2020/api/user
|
|
|
35
48
|
|
|
36
49
|
|
|
37
50
|
# Installation ([more options ↗](https://mockaton.com/installation))
|
|
38
|
-
Mockaton has no dependencies.
|
|
39
|
-
|
|
40
51
|
```sh
|
|
41
52
|
npm install -g mockaton
|
|
42
53
|
```
|
|
54
|
+
<i>Mockaton is a Node.js app with no dependencies.</i>
|
|
55
|
+
|
|
56
|
+
<!-- SKILLS_IGNORE_BEGIN -->
|
|
57
|
+
## Skills
|
|
58
|
+
```sh
|
|
59
|
+
npx skills add ericfortis/mockaton
|
|
60
|
+
```
|
|
61
|
+
<!-- SKILLS_IGNORE_END -->
|
|
62
|
+
|
|
63
|
+
|
|
43
64
|
|
|
44
65
|
## Basic Usage
|
|
45
66
|
```sh
|
|
46
|
-
mockaton --port 2020 my-mocks-dir
|
|
67
|
+
mockaton --port 2020 my-mocks-dir
|
|
47
68
|
```
|
|
48
69
|
|
|
49
|
-
Mockaton will serve the files on the given directory. It's a file-system based router
|
|
50
|
-
filenames can have dynamic parameters.
|
|
51
|
-
|
|
52
|
-
Similarly, each route can have different response status
|
|
70
|
+
Mockaton will serve the files on the given directory. It's a file-system based router in which
|
|
71
|
+
filenames can have dynamic parameters and comments. For paraments use square brackets `[]`,
|
|
72
|
+
and for comments use parentheses `()`. Comments are handy because this way each route
|
|
73
|
+
can have different mock file variants. Similarly, each route can have different response status
|
|
74
|
+
code variants.
|
|
53
75
|
|
|
54
76
|
|
|
55
77
|
| Route | Filename | Description |
|
|
56
78
|
| -----| -----| ---|
|
|
57
|
-
| /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
|
|
58
|
-
| /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
|
|
59
|
-
| /api/login | api/login(invalid attempt).POST.401.ts |
|
|
60
|
-
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
|
|
61
|
-
| /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## Docs
|
|
65
|
-
- [Configuration: CLI and mockaton.config.js](https://mockaton.com/config).
|
|
66
|
-
- [Programmatic API](https://mockaton.com/api), in which
|
|
67
|
-
you can delay a route, select a different mock file, such as a 500 error, among other options.
|
|
79
|
+
| /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
|
|
80
|
+
| /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
|
|
81
|
+
| /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. |
|
|
82
|
+
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins. |
|
|
83
|
+
| /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
|
|
68
84
|
|
|
69
85
|
<!-- SKILLS_IGNORE_BEGIN -->
|
|
70
86
|
## How to scrape your backend APIs?
|
|
71
87
|
Mockaton has a [Browser Extension](https://mockaton.com/scraping) that lets
|
|
72
88
|
you download in bulk all your API responses following Mockaton's filename convention.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
## Skills
|
|
76
|
-
```sh
|
|
77
|
-
npx skills add ericfortis/mockaton
|
|
78
|
-
```
|
|
79
89
|
<!-- SKILLS_IGNORE_END -->
|
|
80
90
|
|
|
81
91
|
|
|
@@ -135,3 +145,8 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
|
|
|
135
145
|
})
|
|
136
146
|
}
|
|
137
147
|
```
|
|
148
|
+
|
|
149
|
+
## Docs
|
|
150
|
+
- [Configuration](https://mockaton.com/config): CLI and mockaton.config.js
|
|
151
|
+
- [API](https://mockaton.com/api): Programatically, you can delay a route, select a different mock file, etc.
|
|
152
|
+
|
package/package.json
CHANGED
package/skills/mockaton/SKILL.md
CHANGED
|
@@ -4,36 +4,34 @@ description: Generates and serves mock HTTP APIs. Use when creating, editing, or
|
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
# Installation ([more options ↗](https://mockaton.com/installation))
|
|
7
|
-
Mockaton has no dependencies.
|
|
8
|
-
|
|
9
7
|
```sh
|
|
10
8
|
npm install -g mockaton
|
|
11
9
|
```
|
|
10
|
+
<i>Mockaton is a Node.js app with no dependencies.</i>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
## Basic Usage
|
|
14
17
|
```sh
|
|
15
|
-
mockaton --port 2020 my-mocks-dir
|
|
18
|
+
mockaton --port 2020 my-mocks-dir
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
Mockaton will serve the files on the given directory. It's a file-system based router
|
|
19
|
-
filenames can have dynamic parameters.
|
|
20
|
-
|
|
21
|
-
Similarly, each route can have different response status
|
|
21
|
+
Mockaton will serve the files on the given directory. It's a file-system based router in which
|
|
22
|
+
filenames can have dynamic parameters and comments. For paraments use square brackets `[]`,
|
|
23
|
+
and for comments use parentheses `()`. Comments are handy because this way each route
|
|
24
|
+
can have different mock file variants. Similarly, each route can have different response status
|
|
25
|
+
code variants.
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
| Route | Filename | Description |
|
|
25
29
|
| -----| -----| ---|
|
|
26
|
-
| /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
|
|
27
|
-
| /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
|
|
28
|
-
| /api/login | api/login(invalid attempt).POST.401.ts |
|
|
29
|
-
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
|
|
30
|
-
| /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Docs
|
|
34
|
-
- [Configuration: CLI and mockaton.config.js](https://mockaton.com/config).
|
|
35
|
-
- [Programmatic API](https://mockaton.com/api), in which
|
|
36
|
-
you can delay a route, select a different mock file, such as a 500 error, among other options.
|
|
30
|
+
| /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
|
|
31
|
+
| /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
|
|
32
|
+
| /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. |
|
|
33
|
+
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins. |
|
|
34
|
+
| /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
|
|
@@ -93,4 +91,8 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
|
|
|
93
91
|
name: 'Acme, Inc.'
|
|
94
92
|
})
|
|
95
93
|
}
|
|
96
|
-
```
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Docs
|
|
97
|
+
- [Configuration](https://mockaton.com/config): CLI and mockaton.config.js
|
|
98
|
+
- [API](https://mockaton.com/api): Programatically, you can delay a route, select a different mock file, etc.
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { API } from './ApiConstants.js'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Client for controlling Mockaton via its HTTP API
|
|
6
|
+
* @see https://mockaton.com/api
|
|
7
|
+
* @example const mockaton = new Commander('http://localhost:2020')
|
|
8
|
+
*/
|
|
5
9
|
export class Commander {
|
|
6
|
-
/** @param {string}
|
|
7
|
-
constructor(
|
|
8
|
-
this.addr =
|
|
10
|
+
/** @param {string} mockatonServerAddr */
|
|
11
|
+
constructor(mockatonServerAddr) {
|
|
12
|
+
this.addr = mockatonServerAddr
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
/** @returns {Promise<Response>} */
|
|
@@ -16,38 +20,35 @@ export class Commander {
|
|
|
16
20
|
|
|
17
21
|
reset = () => this.#patch(API.reset)
|
|
18
22
|
|
|
19
|
-
setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
|
|
20
|
-
setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
|
|
21
|
-
setCorsAllowed = value => this.#patch(API.cors, value)
|
|
22
|
-
setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
|
|
23
|
-
setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
|
|
24
|
-
setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
|
|
25
|
-
|
|
26
|
-
/** @returns {JsonPromise<State.cookies>} */
|
|
27
|
-
selectCookie = label => this.#patch(API.cookies, label)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
|
|
31
23
|
|
|
32
24
|
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
33
25
|
select = file => this.#patch(API.select, file)
|
|
34
26
|
|
|
27
|
+
bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
|
|
35
28
|
|
|
36
29
|
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
37
30
|
toggleStatus = (method, urlMask, status) => this.#patch(API.toggleStatus, [method, urlMask, status])
|
|
38
31
|
|
|
32
|
+
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
33
|
+
setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
|
|
39
34
|
|
|
40
35
|
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
41
36
|
setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
|
|
42
37
|
|
|
43
|
-
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
44
|
-
setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
|
|
45
38
|
|
|
39
|
+
/** @returns {JsonPromise<State.cookies>} */
|
|
40
|
+
selectCookie = label => this.#patch(API.cookies, label)
|
|
41
|
+
|
|
42
|
+
setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
|
|
43
|
+
setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
|
|
44
|
+
setCorsAllowed = value => this.#patch(API.cors, value)
|
|
45
|
+
setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
|
|
46
|
+
setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
|
|
47
|
+
setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
|
|
46
48
|
|
|
47
49
|
writeMock = (file, content) => this.#patch(API.writeMock, [file, content])
|
|
48
50
|
deleteMock = file => this.#patch(API.deleteMock, file)
|
|
49
51
|
|
|
50
|
-
|
|
51
52
|
/** @returns {JsonPromise<State>} */
|
|
52
53
|
getState = () => fetch(this.addr + API.state)
|
|
53
54
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createElement as r, t, restoreFocus, Fragment } from './utils/dom.js'
|
|
2
2
|
|
|
3
|
-
import { store } from './
|
|
3
|
+
import { store } from './store.js'
|
|
4
4
|
import { API } from './ApiConstants.js'
|
|
5
|
-
import { Header } from './
|
|
5
|
+
import { Header } from './Header.js'
|
|
6
6
|
import { adoptSheet } from './utils/css.js'
|
|
7
|
-
import { PayloadViewer, previewMock } from './
|
|
8
|
-
import { MockList, initKeyboardNavigation, renderRow } from './
|
|
7
|
+
import { PayloadViewer, previewMock } from './PayloadViewer.js'
|
|
8
|
+
import { MockList, initKeyboardNavigation, renderRow } from './MockList.js'
|
|
9
9
|
|
|
10
|
-
import CSS from './
|
|
11
|
-
adoptSheet(CSS, './
|
|
10
|
+
import CSS from './App.css' with { type: 'css' }
|
|
11
|
+
adoptSheet(CSS, './App.css')
|
|
12
12
|
|
|
13
13
|
store.onError = onError
|
|
14
14
|
store.render = render
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createElement as r, t } from './utils/dom.js'
|
|
2
|
-
import { Logo, HelpIcon } from './
|
|
2
|
+
import { Logo, HelpIcon } from './icons.js'
|
|
3
3
|
import { adoptSheet } from './utils/css.js'
|
|
4
|
-
import { store } from './
|
|
4
|
+
import { store } from './store.js'
|
|
5
5
|
|
|
6
|
-
import CSS from './
|
|
7
|
-
adoptSheet(CSS, './
|
|
6
|
+
import CSS from './Header.css' with { type: 'css' }
|
|
7
|
+
adoptSheet(CSS, './Header.css')
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
export function Header() {
|
package/src/client/IndexHtml.js
CHANGED
|
@@ -13,7 +13,7 @@ export const IndexHtml = (hotReloadEnabled, version) => `
|
|
|
13
13
|
<meta charset="UTF-8">
|
|
14
14
|
<base href="${API.root}/">
|
|
15
15
|
|
|
16
|
-
<script type="module" src="
|
|
16
|
+
<script type="module" src="App.js"></script>
|
|
17
17
|
<link rel="preload" href="${API.state}" as="fetch" crossorigin>
|
|
18
18
|
|
|
19
19
|
<link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createElement as r, t, Fragment } from './utils/dom.js'
|
|
2
2
|
|
|
3
|
-
import { store } from './
|
|
4
|
-
import { previewMock } from './
|
|
3
|
+
import { store } from './store.js'
|
|
4
|
+
import { previewMock } from './PayloadViewer.js'
|
|
5
5
|
import { classNames, adoptSheet } from './utils/css.js'
|
|
6
|
-
import { TimerIcon, CloudIcon, ChevronDownIcon } from './
|
|
6
|
+
import { TimerIcon, CloudIcon, ChevronDownIcon } from './icons.js'
|
|
7
7
|
|
|
8
|
-
import CSS from './
|
|
9
|
-
adoptSheet(CSS, './
|
|
8
|
+
import CSS from './MockList.css' with { type: 'css' }
|
|
9
|
+
adoptSheet(CSS, './MockList.css')
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
export function MockList() {
|
|
@@ -76,9 +76,8 @@ function TableContent() {
|
|
|
76
76
|
|
|
77
77
|
if (store.groupByMethod)
|
|
78
78
|
return Object.keys(store.brokersByMethod).map(method => Fragment(
|
|
79
|
-
r('div', {
|
|
80
|
-
|
|
81
|
-
}, method),
|
|
79
|
+
r('div', { className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy) },
|
|
80
|
+
method),
|
|
82
81
|
FolderGroups(store.folderGroupsByMethod(method))))
|
|
83
82
|
|
|
84
83
|
return FolderGroups(store.folderGroupsByMethod('*'))
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createElement as r, t } from './utils/dom.js'
|
|
2
2
|
import { parseFilename } from './Filename.js'
|
|
3
|
-
import { store } from './
|
|
3
|
+
import { store } from './store.js'
|
|
4
4
|
import { adoptSheet } from './utils/css.js'
|
|
5
5
|
|
|
6
|
-
import CSS from './
|
|
7
|
-
adoptSheet(CSS, './
|
|
6
|
+
import CSS from './PayloadViewer.css' with { type: 'css' }
|
|
7
|
+
adoptSheet(CSS, './PayloadViewer.css')
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
const titleRef = {}
|
package/src/server/Api.js
CHANGED
|
@@ -18,7 +18,7 @@ import { IndexHtml, CSP } from '../client/IndexHtml.js'
|
|
|
18
18
|
import { cookie } from './stores/cookies.js'
|
|
19
19
|
import { config, ConfigValidator, reinitConfig } from './stores/config.js'
|
|
20
20
|
import * as brokers from './stores/brokers.js'
|
|
21
|
-
import * as Watcher
|
|
21
|
+
import * as Watcher from './stores/Watcher.js'
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
|
|
@@ -269,19 +269,21 @@ async function setRouteIsProxied(req, response) {
|
|
|
269
269
|
|
|
270
270
|
|
|
271
271
|
async function writeMock(req, response) {
|
|
272
|
-
if (config.readOnly)
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
if (config.readOnly) {
|
|
273
|
+
response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly')
|
|
274
|
+
return
|
|
275
|
+
}
|
|
275
276
|
const [file, content] = await req.json()
|
|
276
|
-
if (typeof file !== 'string')
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
if (typeof file !== 'string') {
|
|
278
|
+
response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
|
|
279
|
+
return
|
|
280
|
+
}
|
|
279
281
|
const path = await resolveIn(config.mocksDir, file)
|
|
280
|
-
if (!path)
|
|
281
|
-
|
|
282
|
-
|
|
282
|
+
if (!path) {
|
|
283
|
+
response.forbidden('Filename path resolves outside config.mocksDir')
|
|
284
|
+
return
|
|
285
|
+
}
|
|
283
286
|
await write(path, content)
|
|
284
|
-
|
|
285
287
|
if (!config.watcherEnabled) {
|
|
286
288
|
brokers.registerMock(file, true)
|
|
287
289
|
Watcher.emitChange()
|
|
@@ -291,19 +293,19 @@ async function writeMock(req, response) {
|
|
|
291
293
|
|
|
292
294
|
|
|
293
295
|
async function deleteMock(req, response) {
|
|
294
|
-
if (config.readOnly)
|
|
295
|
-
|
|
296
|
-
|
|
296
|
+
if (config.readOnly) {
|
|
297
|
+
response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly')
|
|
298
|
+
return
|
|
299
|
+
}
|
|
297
300
|
const file = await req.json()
|
|
298
301
|
const path = await resolveIn(config.mocksDir, file)
|
|
299
302
|
|
|
300
303
|
if (!path)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
await rm(path)
|
|
304
|
+
response.forbidden('Filename path resolves outside config.mocksDir')
|
|
305
|
+
else if (!isFile(path))
|
|
306
|
+
response.unprocessable(`Missing Mock: ${file}`)
|
|
307
|
+
else
|
|
308
|
+
await rm(path)
|
|
307
309
|
|
|
308
310
|
if (!config.watcherEnabled) {
|
|
309
311
|
brokers.unregisterMock(file)
|
package/src/server/MockBroker.js
CHANGED
|
@@ -108,21 +108,15 @@ class UrlMatcher {
|
|
|
108
108
|
this.#urlRegex = this.#buildUrlRegex(file)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
#escapeRegex(str) {
|
|
112
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
113
|
-
}
|
|
114
|
-
|
|
115
111
|
#buildUrlRegex(file) {
|
|
116
112
|
let { urlMask } = parseFilename(file)
|
|
117
113
|
urlMask = removeQueryStringAndFragment(urlMask)
|
|
118
|
-
urlMask =
|
|
114
|
+
urlMask = RegExp.escape(urlMask)
|
|
119
115
|
urlMask = this.#disregardVariables(urlMask)
|
|
120
116
|
return new RegExp('^' + urlMask + '/*$')
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
#disregardVariables(
|
|
124
|
-
return str.replace(/\\\[.*?\\]/g, '[^/]+')
|
|
125
|
-
}
|
|
119
|
+
#disregardVariables = str => str.replace(/\\\[.*?\\]/g, '[^/]+')
|
|
126
120
|
|
|
127
121
|
// Appending a '/' so URLs ending with variables don't match
|
|
128
122
|
// URLs that have a path after that variable. For example,
|
package/src/server/Mockaton.js
CHANGED
|
@@ -6,16 +6,16 @@ import pkgJSON from '../../package.json' with { type: 'json' }
|
|
|
6
6
|
import { logger } from './utils/logger.js'
|
|
7
7
|
import { ServerResponse } from './utils/HttpServerResponse.js'
|
|
8
8
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
9
|
-
import { IncomingMessage
|
|
9
|
+
import { IncomingMessage } from './utils/HttpIncomingMessage.js'
|
|
10
10
|
import { watchDevSPA } from './utils/WatcherDevClient.js'
|
|
11
11
|
|
|
12
12
|
import { API } from '../client/ApiConstants.js'
|
|
13
|
-
|
|
13
|
+
import { dispatchMock } from './MockDispatcher.js'
|
|
14
14
|
import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
|
|
15
|
+
|
|
16
|
+
import * as brokers from './stores/brokers.js'
|
|
15
17
|
import { cookie } from './stores/cookies.js'
|
|
16
18
|
import { config, initConfig } from './stores/config.js'
|
|
17
|
-
import * as brokers from './stores/brokers.js'
|
|
18
|
-
import { dispatchMock } from './MockDispatcher.js'
|
|
19
19
|
import { watchMocksDir } from './stores/Watcher.js'
|
|
20
20
|
|
|
21
21
|
|
|
@@ -60,13 +60,11 @@ async function onRequest(req, response) {
|
|
|
60
60
|
logger.verbose('APP', response)
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (url.length > 2048) {
|
|
63
|
+
if (req.url?.length > 2048) {
|
|
66
64
|
response.uriTooLong()
|
|
67
65
|
return
|
|
68
66
|
}
|
|
69
|
-
if (
|
|
67
|
+
if (req.urlHasControlChars()) {
|
|
70
68
|
response.badRequest()
|
|
71
69
|
return
|
|
72
70
|
}
|
|
@@ -83,7 +81,7 @@ async function onRequest(req, response) {
|
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
catch (error) {
|
|
86
|
-
if (error instanceof BodyReaderError)
|
|
84
|
+
if (error instanceof IncomingMessage.BodyReaderError)
|
|
87
85
|
response.unprocessable(`${error.name}: ${error.message}`)
|
|
88
86
|
else {
|
|
89
87
|
logger.error(500, req.url, error?.message || error, error?.stack || '')
|
package/src/server/ProxyRelay.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
3
3
|
|
|
4
4
|
import { extFor } from './utils/mime.js'
|
|
5
5
|
import { write, isFile, resolveIn } from './utils/fs.js'
|
|
6
|
-
import {
|
|
6
|
+
import { IncomingMessage } from './utils/HttpIncomingMessage.js'
|
|
7
7
|
import { logger } from './utils/logger.js'
|
|
8
8
|
|
|
9
9
|
import { makeMockFilename } from '../client/Filename.js'
|
|
@@ -24,7 +24,7 @@ export async function proxy(req, response, delay) {
|
|
|
24
24
|
})
|
|
25
25
|
}
|
|
26
26
|
catch (error) { // TESTME
|
|
27
|
-
if (error instanceof BodyReaderError)
|
|
27
|
+
if (error instanceof IncomingMessage.BodyReaderError)
|
|
28
28
|
response.unprocessable(error.name)
|
|
29
29
|
else {
|
|
30
30
|
response.badGateway()
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { basename } from 'node:path'
|
|
2
|
-
|
|
3
1
|
import { config, isFileAllowed } from './config.js'
|
|
4
2
|
import { MockBroker } from '../MockBroker.js'
|
|
5
3
|
import { listFilesRecursively } from '../utils/fs.js'
|
|
@@ -35,8 +33,7 @@ export function init() {
|
|
|
35
33
|
|
|
36
34
|
/** @returns {boolean} registered */
|
|
37
35
|
export function registerMock(file, isFromWatcher = false) {
|
|
38
|
-
if (brokerByFilename(file)?.hasMock(file) ||
|
|
39
|
-
!isFileAllowed(basename(file)))
|
|
36
|
+
if (brokerByFilename(file)?.hasMock(file) || !isFileAllowed(file))
|
|
40
37
|
return false
|
|
41
38
|
|
|
42
39
|
const { method, urlMask } = parseFilename(file)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
1
|
+
import { resolve, basename } from 'node:path'
|
|
2
2
|
import { lstatSync } from 'node:fs'
|
|
3
3
|
import { METHODS } from 'node:http'
|
|
4
4
|
|
|
@@ -89,7 +89,7 @@ export function reinitConfig() {
|
|
|
89
89
|
initConfig(originalOpts)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export const isFileAllowed = f => !config.ignore.test(f)
|
|
92
|
+
export const isFileAllowed = f => !config.ignore.test(basename(f))
|
|
93
93
|
|
|
94
94
|
export const calcDelay = () => config.delayJitter
|
|
95
95
|
? config.delay * (1 + Math.random() * config.delayJitter)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import http, { METHODS } from 'node:http'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
export const reControlAndDelChars = /[\x00-\x1f\x7f]/
|
|
5
|
+
|
|
4
6
|
export const methodIsSupported = method => METHODS.includes(method)
|
|
5
7
|
|
|
6
8
|
export function removeQueryStringAndFragment(url = '') {
|
|
@@ -16,6 +18,8 @@ export class BodyReaderError extends Error {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export class IncomingMessage extends http.IncomingMessage {
|
|
21
|
+
static BodyReaderError = BodyReaderError
|
|
22
|
+
|
|
19
23
|
json() {
|
|
20
24
|
return this.body(JSON.parse)
|
|
21
25
|
}
|
|
@@ -29,18 +33,28 @@ export class IncomingMessage extends http.IncomingMessage {
|
|
|
29
33
|
for await (const chunk of this) {
|
|
30
34
|
lengthSoFar += chunk.length
|
|
31
35
|
if (lengthSoFar > MAX_BODY_SIZE)
|
|
32
|
-
throw new BodyReaderError(`Body too large. Max is ${MAX_BODY_SIZE} bytes`)
|
|
36
|
+
throw new IncomingMessage.BodyReaderError(`Body too large. Max is ${MAX_BODY_SIZE} bytes`)
|
|
33
37
|
chunks.push(chunk)
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
if (lengthSoFar !== expectedLength)
|
|
37
|
-
throw new BodyReaderError('Length mismatch')
|
|
41
|
+
throw new IncomingMessage.BodyReaderError('Length mismatch')
|
|
38
42
|
|
|
39
43
|
try {
|
|
40
44
|
return parser(Buffer.concat(chunks).toString())
|
|
41
45
|
}
|
|
42
46
|
catch (_) {
|
|
43
|
-
throw new BodyReaderError('Could not parse')
|
|
47
|
+
throw new IncomingMessage.BodyReaderError('Could not parse')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
urlHasControlChars() {
|
|
52
|
+
try {
|
|
53
|
+
const decoded = decode(this.url || '')
|
|
54
|
+
return !decoded || reControlAndDelChars.test(decoded)
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return true
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
}
|
|
@@ -49,19 +63,6 @@ export const parseJSON = req =>
|
|
|
49
63
|
IncomingMessage.prototype.body.call(req, JSON.parse)
|
|
50
64
|
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
export const reControlAndDelChars = /[\x00-\x1f\x7f]/
|
|
54
|
-
|
|
55
|
-
export function hasControlChars(url) {
|
|
56
|
-
try {
|
|
57
|
-
const decoded = decode(url)
|
|
58
|
-
return !decoded || reControlAndDelChars.test(decoded)
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
66
|
export function decode(url) {
|
|
66
67
|
const candidate = decodeURIComponent(url)
|
|
67
68
|
return candidate === decodeURIComponent(candidate)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { styleText } from 'node:util'
|
|
1
2
|
import { decode, reControlAndDelChars } from './HttpIncomingMessage.js'
|
|
2
3
|
|
|
3
4
|
|
|
@@ -39,7 +40,12 @@ export const logger = new class {
|
|
|
39
40
|
#msg(...msg) {
|
|
40
41
|
if (!msg.at(-1))
|
|
41
42
|
msg.pop()
|
|
42
|
-
|
|
43
|
+
const parts = msg.map(this.#sanitize)
|
|
44
|
+
parts[1] = styleText(['bold', 'green'], parts[1])
|
|
45
|
+
return [
|
|
46
|
+
styleText('yellow', new Date().toISOString()),
|
|
47
|
+
...parts
|
|
48
|
+
].join(styleText('dim', '::'))
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
#sanitize(url) {
|
package/src/server/utils/mime.js
CHANGED
|
@@ -57,6 +57,7 @@ const extToMime = {
|
|
|
57
57
|
js: 'application/javascript',
|
|
58
58
|
json: 'application/json',
|
|
59
59
|
jsonld: 'application/ld+json',
|
|
60
|
+
jxl: 'image/jxl',
|
|
60
61
|
lz: 'application/x-lzip',
|
|
61
62
|
m4a: 'audio/mp4',
|
|
62
63
|
md: 'text/markdown',
|
|
@@ -91,6 +92,7 @@ const extToMime = {
|
|
|
91
92
|
stl: 'model/stl',
|
|
92
93
|
svg: 'image/svg+xml',
|
|
93
94
|
tar: 'application/x-tar',
|
|
95
|
+
tga: 'image/x-targa',
|
|
94
96
|
tif: 'image/tiff',
|
|
95
97
|
ts: 'video/mp2t',
|
|
96
98
|
ttf: 'font/ttf',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|