mockaton 13.5.0 → 13.6.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 +60 -43
- package/index.d.ts +14 -14
- package/package.json +1 -1
- package/src/client/ApiCommander.js +1 -2
- package/src/client/Filename.js +9 -10
- package/src/client/app.js +10 -9
- package/src/client/dir/dittoSplitPaths.js +3 -3
- package/src/client/dir/groupByFolder.test.js +24 -68
- package/src/server/Api.js +8 -5
- package/src/server/MockBroker.js +16 -15
- package/src/server/Mockaton.test.js +3 -1
- package/src/server/cli.js +10 -7
- package/src/server/utils/HttpServerResponse.js +2 -2
- package/www/src/assets/openapi.json +3 -3
package/README.md
CHANGED
|
@@ -1,34 +1,12 @@
|
|
|
1
|
-
# Mockaton
|
|
2
|
-
|
|
3
|
-
An HTTP mock server for simulating APIs with minimal setup — ideal
|
|
4
|
-
for testing difficult to reproduce backend states.
|
|
5
|
-
|
|
6
1
|

|
|
7
2
|
[](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
|
|
8
3
|
[](https://codecov.io/github/ericfortis/mockaton)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
5
|
|
|
6
|
+
## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog)
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## TL;DR
|
|
14
|
-
```shell
|
|
15
|
-
npx mockaton my-mocks-dir
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
It’s like `servedir`, but supports dynamic segments in filenames. For example:
|
|
19
|
-
|
|
20
|
-
**Route**: [/api/company/123](#) <br/>
|
|
21
|
-
**File**: my-mocks-dir/api/company/[id].GET.200.json
|
|
22
|
-
|
|
23
|
-
Statics assets don’t need that extension:
|
|
24
|
-
|
|
25
|
-
**Route**: [/media/avatar.png](#) <br/>
|
|
26
|
-
**File**: my-mocks-dir/media/avatar.png
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## Dashboard
|
|
30
|
-
Besides the dashboard, there’s a programmatic [Control API](https://mockaton.com/api).
|
|
31
|
-
Also, there’s a [Browser Extension](https://mockaton.com/scraping) for scraping responses from your backend.
|
|
8
|
+
Mockaton is an HTTP mock server for simulating APIs, designed
|
|
9
|
+
for testing difficult to reproduce backend states with minimal setup.
|
|
32
10
|
|
|
33
11
|
<picture>
|
|
34
12
|
<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">
|
|
@@ -36,52 +14,91 @@ Also, there’s a [Browser Extension](https://mockaton.com/scraping) for scrapin
|
|
|
36
14
|
<img alt="Mockaton Dashboard" src="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.dark.gold.png">
|
|
37
15
|
</picture>
|
|
38
16
|
|
|
39
|
-
<br/>
|
|
40
17
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
included in this repo mounted on the container. Mentioned dir is: [mockaton-mocks/](./mockaton-mocks).
|
|
18
|
+
## Demo (Docker)
|
|
19
|
+
This will spin up Mockaton with the [sample directory](./mockaton-mocks)
|
|
20
|
+
included in this repo mounted on the container.
|
|
45
21
|
|
|
46
22
|
```sh
|
|
47
23
|
git clone https://github.com/ericfortis/mockaton.git --depth 1
|
|
48
24
|
cd mockaton
|
|
49
25
|
make docker
|
|
50
26
|
```
|
|
51
|
-
Dashboard: [localhost:2020/mockaton](http://localhost:2020/mockaton)
|
|
52
|
-
|
|
53
27
|
Test it:
|
|
54
|
-
```
|
|
28
|
+
```sh
|
|
55
29
|
curl localhost:2020/api/user
|
|
56
30
|
```
|
|
31
|
+
Dashboard: [localhost:2020/mockaton](http://localhost:2020/mockaton)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Basic Usage
|
|
35
|
+
```sh
|
|
36
|
+
npx mockaton my-mocks-dir/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Mockaton will serve the files on the given directory. It’s a file-system
|
|
40
|
+
based router, so filenames can have dynamic parameters and comments.
|
|
41
|
+
Also, each route can have different mock file variants.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
| Route | Filename | Description |
|
|
45
|
+
| -----| -----| ---|
|
|
46
|
+
| /api/company/123 | api/company/[id].GET.200.json | `[id]` is a dynamic parameter |
|
|
47
|
+
| /media/avatar.png | media/avatar.png | Statics assets don’t need the above extension |
|
|
48
|
+
| /api/login | api/login(invalid attempt).POST.401.json | Anything within parenthesis is a **comment**, they are ignored when routing |
|
|
49
|
+
| /api/login | api/login(default).GET.200.json | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
|
|
50
|
+
| /api/login | api/login(locked out user).POST.423.ts | TypeScript or JavaScript mocks are sent as JSON by default |
|
|
51
|
+
|
|
57
52
|
|
|
53
|
+
## Docs
|
|
54
|
+
- How to **configure** Mockaton? See [CLI and mockaton.config.js](https://mockaton.com/config) docs.
|
|
55
|
+
- How to **control** Mockaton? Besides the dashboard, there’s a [Programmatic API](https://mockaton.com/api).
|
|
56
|
+
- How to **add plugins**? You can write [Plugins](https://mockaton.com/plugins) for customizing responses.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## How to scrape your backend APIs?
|
|
60
|
+
Mockaton has a [Browser Extension](https://mockaton.com/scraping) that lets
|
|
61
|
+
you download in bulk all your API responses following Mockaton’s filename convention.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## How to create mocks?
|
|
65
|
+
|
|
66
|
+
Write to your mocks directory. Alternatively, there’s an API [PATCH /mockaton/write-mock](https://mockaton.com/api).
|
|
67
|
+
```sh
|
|
68
|
+
mkdir -p my-mocks-dir/api
|
|
69
|
+
echo '{ "name": "John" }' > my-mocks-dir/api/user.GET.200.json
|
|
70
|
+
sleep 0.1 # Wait for the watcher to register it
|
|
71
|
+
```
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
### Example A: JSON
|
|
74
|
+
- **Route:** /api/company/123
|
|
75
|
+
- **Filename:** api/company/[id].GET.200.json
|
|
61
76
|
|
|
62
|
-
<code>my_mocks_dir/<b>api/company/[id]</b>.GET.200.json</code>
|
|
63
77
|
```json
|
|
64
78
|
{
|
|
65
79
|
"name": "Acme, Inc."
|
|
66
80
|
}
|
|
67
81
|
```
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
### Example B: TypeScript or JavaScript
|
|
84
|
+
Exporting an Object, Array, or String is sent as JSON.
|
|
70
85
|
|
|
71
|
-
|
|
86
|
+
- **Route:** /api/company/abc
|
|
87
|
+
- **Filename:** api/company/[id].GET.200.ts
|
|
72
88
|
|
|
73
|
-
<code>my_mocks_dir/<b>api/company/[id]</b>.GET.200.ts</code>
|
|
74
89
|
```ts
|
|
75
90
|
export default {
|
|
76
91
|
name: 'Acme, Inc.'
|
|
77
92
|
}
|
|
78
93
|
```
|
|
79
94
|
|
|
80
|
-
|
|
95
|
+
### Example C: [Function Mocks](https://mockaton.com/function-mocks)
|
|
96
|
+
With a function mock you can do pretty much anything you could do with a normal backend handler.</p>
|
|
97
|
+
For example, you can handle complex logic, URL parsing, saving toa database, etc.
|
|
81
98
|
|
|
82
|
-
|
|
99
|
+
- **Route:** /api/company/abc/user/999
|
|
100
|
+
- **Filename:** api/company/[companyId]/user/[userId].GET.200.ts
|
|
83
101
|
|
|
84
|
-
<code>my_mocks_dir/<b>api/company/[companyId]/user/[userId]</b>.GET.200.ts</code>
|
|
85
102
|
```ts
|
|
86
103
|
import { IncomingMessage, OutgoingMessage } from 'node:http'
|
|
87
104
|
import { parseSegments } from 'mockaton'
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Server, IncomingMessage, OutgoingMessage } from 'node:http'
|
|
2
2
|
|
|
3
|
-
export type Plugin = (
|
|
3
|
+
export declare type Plugin = (
|
|
4
4
|
filePath: string,
|
|
5
5
|
request: IncomingMessage,
|
|
6
6
|
response: OutgoingMessage
|
|
@@ -9,7 +9,7 @@ export type Plugin = (
|
|
|
9
9
|
body: string | Uint8Array
|
|
10
10
|
}>
|
|
11
11
|
|
|
12
|
-
export interface Config {
|
|
12
|
+
export declare interface Config {
|
|
13
13
|
mocksDir?: string
|
|
14
14
|
ignore?: RegExp
|
|
15
15
|
watcherEnabled?: boolean
|
|
@@ -49,28 +49,28 @@ export interface Config {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
export function Mockaton(options: Partial<Config>): Promise<Server | undefined>
|
|
52
|
+
export declare function Mockaton(options: Partial<Config>): Promise<Server | undefined>
|
|
53
53
|
|
|
54
|
-
export function defineConfig(options: Partial<Config>): Partial<Config>
|
|
54
|
+
export declare function defineConfig(options: Partial<Config>): Partial<Config>
|
|
55
55
|
|
|
56
|
-
export const jsToJsonPlugin: Plugin
|
|
57
|
-
export const echoFilePlugin: Plugin
|
|
56
|
+
export declare const jsToJsonPlugin: Plugin
|
|
57
|
+
export declare const echoFilePlugin: Plugin
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
// Utils
|
|
61
61
|
|
|
62
|
-
export function jwtCookie(cookieName: string, payload: any, path?: string): string
|
|
62
|
+
export declare function jwtCookie(cookieName: string, payload: any, path?: string): string
|
|
63
63
|
|
|
64
|
-
export function parseJSON(request: IncomingMessage): Promise<any>
|
|
65
|
-
export function parseSegments(reqUrl: string, filename: string): Record<string, string>
|
|
66
|
-
export function parseQueryParams(reqUrl: string): URLSearchParams
|
|
64
|
+
export declare function parseJSON(request: IncomingMessage): Promise<any>
|
|
65
|
+
export declare function parseSegments(reqUrl: string, filename: string): Record<string, string>
|
|
66
|
+
export declare function parseQueryParams(reqUrl: string): URLSearchParams
|
|
67
67
|
|
|
68
|
-
export type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
|
|
68
|
+
export declare type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
// API
|
|
72
72
|
|
|
73
|
-
export type ClientMockBroker = {
|
|
73
|
+
export declare type ClientMockBroker = {
|
|
74
74
|
mocks: string[]
|
|
75
75
|
file: string
|
|
76
76
|
status: number
|
|
@@ -79,13 +79,13 @@ export type ClientMockBroker = {
|
|
|
79
79
|
delayed: boolean
|
|
80
80
|
proxied: boolean
|
|
81
81
|
}
|
|
82
|
-
export type ClientBrokersByMethod = {
|
|
82
|
+
export declare type ClientBrokersByMethod = {
|
|
83
83
|
[method: string]: {
|
|
84
84
|
[urlMask: string]: ClientMockBroker
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
export interface State {
|
|
88
|
+
export declare interface State {
|
|
89
89
|
brokersByMethod: ClientBrokersByMethod
|
|
90
90
|
|
|
91
91
|
cookies: [label: string, selected: boolean][]
|
package/package.json
CHANGED
package/src/client/Filename.js
CHANGED
|
@@ -25,23 +25,22 @@ export function parseFilename(file) {
|
|
|
25
25
|
const followsConvention = tokens.length > 3
|
|
26
26
|
&& responseStatusIsValid(Number(tokens.at(-2)))
|
|
27
27
|
&& METHODS.includes(tokens.at(-3))
|
|
28
|
-
const isStatic = !followsConvention
|
|
29
28
|
|
|
30
|
-
return
|
|
29
|
+
return followsConvention
|
|
31
30
|
? {
|
|
32
|
-
isStatic,
|
|
33
|
-
ext: tokens.pop() || '',
|
|
34
|
-
status: 200,
|
|
35
|
-
method: 'GET',
|
|
36
|
-
urlMask: '/' + file
|
|
37
|
-
}
|
|
38
|
-
: {
|
|
39
|
-
isStatic,
|
|
31
|
+
isStatic: false,
|
|
40
32
|
ext: tokens.pop(),
|
|
41
33
|
status: Number(tokens.pop()),
|
|
42
34
|
method: tokens.pop(),
|
|
43
35
|
urlMask: '/' + removeTrailingSlash(tokens.join('.'))
|
|
44
36
|
}
|
|
37
|
+
: {
|
|
38
|
+
isStatic: true,
|
|
39
|
+
ext: tokens.pop() || '',
|
|
40
|
+
status: 200,
|
|
41
|
+
method: 'GET',
|
|
42
|
+
urlMask: '/' + file
|
|
43
|
+
}
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
export function removeTrailingSlash(url = '') {
|
package/src/client/app.js
CHANGED
|
@@ -38,15 +38,16 @@ function App() {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function LeftSide() {
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
return (
|
|
42
|
+
r('div', {
|
|
43
|
+
ref: LeftSide.ref,
|
|
44
|
+
style: { width: LeftSide.ref.width },
|
|
45
|
+
className: CSS.leftSide
|
|
46
|
+
},
|
|
47
|
+
r('div', { className: CSS.SubToolbar },
|
|
48
|
+
GroupByMethod(),
|
|
49
|
+
BulkSelector()),
|
|
50
|
+
r('div', { className: CSS.Table }, MockList())))
|
|
50
51
|
}
|
|
51
52
|
LeftSide.ref = { width: undefined }
|
|
52
53
|
LeftSide.$ = selector => LeftSide.ref.elem.querySelector(selector)
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* @param {string[]} paths - sorted
|
|
5
5
|
*/
|
|
6
6
|
export function dittoSplitPaths(paths) {
|
|
7
|
-
const
|
|
7
|
+
const segments = paths.map(p => p.split('/').filter(Boolean))
|
|
8
8
|
return paths.map((p, i) => {
|
|
9
9
|
if (i === 0)
|
|
10
10
|
return ['', p]
|
|
11
11
|
|
|
12
|
-
const prev =
|
|
13
|
-
const curr =
|
|
12
|
+
const prev = segments[i - 1]
|
|
13
|
+
const curr = segments[i]
|
|
14
14
|
const min = Math.min(curr.length, prev.length)
|
|
15
15
|
let j = 0
|
|
16
16
|
while (j < min && curr[j] === prev[j])
|
|
@@ -2,80 +2,36 @@ import { test } from 'node:test'
|
|
|
2
2
|
import { deepEqual } from 'node:assert/strict'
|
|
3
3
|
import { groupByFolder } from './groupByFolder.js'
|
|
4
4
|
|
|
5
|
+
const PartialBrokerRowModel = (method, urlMask, ...children) =>
|
|
6
|
+
({ method, urlMask, children })
|
|
5
7
|
|
|
6
8
|
test('groupByFolder', () => {
|
|
7
9
|
const input = [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
PartialBrokerRowModel('GET', '/api/user'),
|
|
11
|
+
PartialBrokerRowModel('GET', '/api/user/avatar'),
|
|
12
|
+
PartialBrokerRowModel('GET', '/api/video/[id]'),
|
|
13
|
+
PartialBrokerRowModel('GET', '/index.html'),
|
|
14
|
+
PartialBrokerRowModel('GET', '/media/file-a.txt'),
|
|
15
|
+
PartialBrokerRowModel('GET', '/media/file-b.txt'),
|
|
16
|
+
PartialBrokerRowModel('GET', '/media/sub/file-aa.txt'),
|
|
17
|
+
PartialBrokerRowModel('GET', '/media/sub/file-bb.txt'),
|
|
18
|
+
PartialBrokerRowModel('POST', '/api/user'),
|
|
19
|
+
PartialBrokerRowModel('POST', '/api/user/avatar/foo'),
|
|
20
|
+
PartialBrokerRowModel('PATCH', '/api/user')
|
|
19
21
|
]
|
|
20
22
|
|
|
21
|
-
|
|
22
23
|
const expected = [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
children: []
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
}, {
|
|
38
|
-
urlMask: '/api/user',
|
|
39
|
-
method: 'POST',
|
|
40
|
-
children: []
|
|
41
|
-
}, {
|
|
42
|
-
urlMask: '/api/user',
|
|
43
|
-
method: 'PATCH',
|
|
44
|
-
children: []
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
urlMask: '/api/video/[id]',
|
|
50
|
-
method: 'GET',
|
|
51
|
-
children: []
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
urlMask: '/index.html',
|
|
55
|
-
method: 'GET',
|
|
56
|
-
children: []
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
urlMask: '/media/file-a.txt',
|
|
60
|
-
method: 'GET',
|
|
61
|
-
children: [
|
|
62
|
-
{
|
|
63
|
-
urlMask: '/media/file-b.txt',
|
|
64
|
-
method: 'GET',
|
|
65
|
-
children: []
|
|
66
|
-
}, {
|
|
67
|
-
urlMask: '/media/sub/file-aa.txt',
|
|
68
|
-
method: 'GET',
|
|
69
|
-
children: [
|
|
70
|
-
{
|
|
71
|
-
urlMask: '/media/sub/file-bb.txt',
|
|
72
|
-
method: 'GET',
|
|
73
|
-
children: []
|
|
74
|
-
}
|
|
75
|
-
]
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
}
|
|
24
|
+
PartialBrokerRowModel('GET', '/api/user',
|
|
25
|
+
PartialBrokerRowModel('GET', '/api/user/avatar',
|
|
26
|
+
PartialBrokerRowModel('POST', '/api/user/avatar/foo')),
|
|
27
|
+
PartialBrokerRowModel('POST', '/api/user'),
|
|
28
|
+
PartialBrokerRowModel('PATCH', '/api/user')),
|
|
29
|
+
PartialBrokerRowModel('GET', '/api/video/[id]'),
|
|
30
|
+
PartialBrokerRowModel('GET', '/index.html'),
|
|
31
|
+
PartialBrokerRowModel('GET', '/media/file-a.txt',
|
|
32
|
+
PartialBrokerRowModel('GET', '/media/file-b.txt'),
|
|
33
|
+
PartialBrokerRowModel('GET', '/media/sub/file-aa.txt',
|
|
34
|
+
PartialBrokerRowModel('GET', '/media/sub/file-bb.txt')))
|
|
79
35
|
]
|
|
80
36
|
|
|
81
37
|
deepEqual(groupByFolder(input), expected)
|
package/src/server/Api.js
CHANGED
|
@@ -97,6 +97,7 @@ function onDevWatch(req, response) {
|
|
|
97
97
|
else
|
|
98
98
|
response.notFound()
|
|
99
99
|
}
|
|
100
|
+
|
|
100
101
|
/** # PATCH */
|
|
101
102
|
|
|
102
103
|
function reset(_, response) {
|
|
@@ -257,13 +258,15 @@ async function setRouteIsProxied(req, response) {
|
|
|
257
258
|
|
|
258
259
|
async function writeMock(req, response) {
|
|
259
260
|
if (config.readOnly)
|
|
260
|
-
return response.forbidden()
|
|
261
|
+
return response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly, or --no-read-only (CLI)')
|
|
261
262
|
|
|
262
263
|
const [file, content] = await req.json()
|
|
264
|
+
if (typeof file !== 'string')
|
|
265
|
+
return response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
|
|
266
|
+
|
|
263
267
|
const path = await resolveIn(config.mocksDir, file)
|
|
264
|
-
|
|
265
268
|
if (!path)
|
|
266
|
-
return response.forbidden()
|
|
269
|
+
return response.forbidden('Filename path resolves outside config.mocksDir')
|
|
267
270
|
|
|
268
271
|
await write(path, content)
|
|
269
272
|
|
|
@@ -277,13 +280,13 @@ async function writeMock(req, response) {
|
|
|
277
280
|
|
|
278
281
|
async function deleteMock(req, response) {
|
|
279
282
|
if (config.readOnly)
|
|
280
|
-
return response.forbidden()
|
|
283
|
+
return response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly, or --no-read-only (CLI)')
|
|
281
284
|
|
|
282
285
|
const file = await req.json()
|
|
283
286
|
const path = await resolveIn(config.mocksDir, file)
|
|
284
287
|
|
|
285
288
|
if (!path)
|
|
286
|
-
return response.forbidden()
|
|
289
|
+
return response.forbidden('Filename path resolves outside config.mocksDir')
|
|
287
290
|
|
|
288
291
|
if (!isFile(path))
|
|
289
292
|
return response.unprocessable(`Missing Mock: ${file}`)
|
package/src/server/MockBroker.js
CHANGED
|
@@ -7,26 +7,19 @@ import { parseFilename, includesComment, extractComments, removeQueryStringAndFr
|
|
|
7
7
|
* files that can be served for the route, the currently selected file, etc.
|
|
8
8
|
*/
|
|
9
9
|
export class MockBroker {
|
|
10
|
+
file = '' // selected mock filename
|
|
11
|
+
mocks = [] // filenames
|
|
12
|
+
isStatic = false // doesn’t follow filename convention
|
|
13
|
+
delayed = false
|
|
14
|
+
proxied = false
|
|
15
|
+
status = -1
|
|
16
|
+
autoStatus = 0
|
|
17
|
+
|
|
10
18
|
constructor(file) {
|
|
11
|
-
this.file = '' // selected mock filename
|
|
12
|
-
this.mocks = [] // filenames
|
|
13
|
-
this.status = -1
|
|
14
|
-
this.isStatic = false // doesn’t follow filename convention
|
|
15
|
-
this.delayed = false
|
|
16
|
-
this.proxied = false
|
|
17
|
-
this.autoStatus = 0
|
|
18
19
|
this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
|
|
19
20
|
this.register(file)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
#isStatus = (file, status) => parseFilename(file).status === status
|
|
23
|
-
|
|
24
|
-
#sortMocks() {
|
|
25
|
-
this.mocks.sort()
|
|
26
|
-
const defaults = this.mocks.filter(f => includesComment(f, DEFAULT_MOCK_COMMENT))
|
|
27
|
-
this.mocks = Array.from(new Set(defaults).union(new Set(this.mocks)))
|
|
28
|
-
}
|
|
29
|
-
|
|
30
23
|
register(file) {
|
|
31
24
|
if (this.autoStatus && this.#isStatus(file, this.autoStatus))
|
|
32
25
|
this.selectFile(file)
|
|
@@ -42,6 +35,14 @@ export class MockBroker {
|
|
|
42
35
|
return brokerIsEmpty
|
|
43
36
|
}
|
|
44
37
|
|
|
38
|
+
#isStatus = (file, status) => parseFilename(file).status === status
|
|
39
|
+
|
|
40
|
+
#sortMocks() {
|
|
41
|
+
this.mocks.sort()
|
|
42
|
+
const defaults = this.mocks.filter(f => includesComment(f, DEFAULT_MOCK_COMMENT))
|
|
43
|
+
this.mocks = Array.from(new Set(defaults).union(new Set(this.mocks)))
|
|
44
|
+
}
|
|
45
|
+
|
|
45
46
|
hasMock = file => this.mocks.includes(file)
|
|
46
47
|
|
|
47
48
|
selectFile(filename) {
|
|
@@ -986,12 +986,14 @@ test('head for get. returns the headers without body only for GETs requested as
|
|
|
986
986
|
|
|
987
987
|
|
|
988
988
|
describe('Write and Delete Mock', () => {
|
|
989
|
-
test('
|
|
989
|
+
test('rejects filenames resolving outside mocksDir', async () => {
|
|
990
990
|
const r = await api.writeMock('../outside.txt', '')
|
|
991
991
|
equal(r.status, 403)
|
|
992
|
+
match(await r.text(), /Filename path resolves outside config.mocksDir/)
|
|
992
993
|
|
|
993
994
|
const r2 = await api.deleteMock('../outside.txt')
|
|
994
995
|
equal(r2.status, 403)
|
|
996
|
+
match(await r2.text(), /Filename path resolves outside config.mocksDir/)
|
|
995
997
|
})
|
|
996
998
|
|
|
997
999
|
test('write and delete (with watcher)', async () => {
|
package/src/server/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ try {
|
|
|
22
22
|
|
|
23
23
|
quiet: { short: 'q', type: 'boolean' },
|
|
24
24
|
'no-open': { short: 'n', type: 'boolean' },
|
|
25
|
+
'no-read-only': { type: 'boolean' },
|
|
25
26
|
|
|
26
27
|
help: { short: 'h', type: 'boolean' },
|
|
27
28
|
version: { short: 'v', type: 'boolean' }
|
|
@@ -48,16 +49,17 @@ else if (args.help)
|
|
|
48
49
|
Usage: mockaton [mocks-dir] [options]
|
|
49
50
|
|
|
50
51
|
Options:
|
|
51
|
-
-c, --config <file>
|
|
52
|
+
-c, --config <file> (default: ./mockaton.config.js)
|
|
52
53
|
|
|
53
|
-
-H, --host <host>
|
|
54
|
-
-p, --port <port>
|
|
54
|
+
-H, --host <host> (default: 127.0.0.1)
|
|
55
|
+
-p, --port <port> (default: 0) which means auto-assigned
|
|
55
56
|
|
|
56
|
-
-q, --quiet
|
|
57
|
-
--no-open
|
|
57
|
+
-q, --quiet Show errors only
|
|
58
|
+
--no-open Don't open dashboard in a browser
|
|
59
|
+
--no-read-only Allow writing and deleting mocks via API
|
|
58
60
|
|
|
59
|
-
-h, --help
|
|
60
|
-
-v, --version
|
|
61
|
+
-h, --help
|
|
62
|
+
-v, --version
|
|
61
63
|
|
|
62
64
|
Notes:
|
|
63
65
|
* mockaton.config.js supports more options, see: https://mockaton.com/config
|
|
@@ -81,6 +83,7 @@ else {
|
|
|
81
83
|
|
|
82
84
|
if (args.quiet) opts.logLevel = 'quiet'
|
|
83
85
|
if (args['no-open']) opts.onReady = () => {}
|
|
86
|
+
if (args['no-read-only']) opts.readOnly = false
|
|
84
87
|
|
|
85
88
|
try {
|
|
86
89
|
await Mockaton(opts)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"paths": {
|
|
13
13
|
"/mockaton/openapi": {
|
|
14
14
|
"get": {
|
|
15
|
-
"summary": "Get
|
|
15
|
+
"summary": "Get OpenAPI spec",
|
|
16
16
|
"x-js-client-example": "await mockaton.getOpenAPI()",
|
|
17
17
|
"responses": {
|
|
18
18
|
"200": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"/mockaton/write-mock": {
|
|
52
52
|
"patch": {
|
|
53
53
|
"summary": "Write a mock file",
|
|
54
|
-
"description": "Writes a mock file to the mocks directory. Requires `config.readOnly = false`",
|
|
54
|
+
"description": "Writes a mock file to the mocks directory. Requires `config.readOnly = false` and `config.watcherEnabled = true`.",
|
|
55
55
|
"x-js-client-example": "await mockaton.writeMock('api/user/friends.GET.200.json', '{ \"friends\": [] }')",
|
|
56
56
|
"requestBody": {
|
|
57
57
|
"required": true,
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
},
|
|
78
78
|
"responses": {
|
|
79
79
|
"200": {
|
|
80
|
-
"description": "OK"
|
|
80
|
+
"description": "OK. The mock has been written and registered successfully."
|
|
81
81
|
},
|
|
82
82
|
"403": {
|
|
83
83
|
"description": "Forbidden (read-only mode or outside mocksDir)"
|