mockaton 5.0.3 → 6.1.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 +9 -19
- package/Tests.js +0 -1
- package/index.d.ts +0 -1
- package/package.json +1 -1
- package/sample-mocks/api/user.GET.200.json +1 -0
- package/src/Config.js +0 -2
- package/src/Dashboard.css +4 -4
- package/src/Dashboard.js +2 -12
- package/src/MockBroker.js +7 -15
- package/src/MockDispatcher.js +1 -7
- package/src/Route.js +16 -37
- package/src/mockBrokersCollection.js +2 -1
- package/src/utils/mime.js +52 -8
- package/sample-mocks/api/user/.GET.200.json +0 -1
- package/sample-mocks/api/user/edit-name.PATCH.md +0 -9
package/README.md
CHANGED
|
@@ -48,10 +48,9 @@ interface Config {
|
|
|
48
48
|
host?: string, // defaults to 'localhost'
|
|
49
49
|
port?: number // defaults to 0, which means auto-assigned
|
|
50
50
|
delay?: number // defaults to 1200 (ms)
|
|
51
|
-
onReady?: (dashboardUrl: string) => void // defaults to
|
|
51
|
+
onReady?: (dashboardUrl: string) => void // defaults to trying to open macOS default browser. pass a noop to prevent opening the dashboard
|
|
52
52
|
cookies?: object
|
|
53
53
|
proxyFallback?: string // e.g. http://localhost:9999 Target for relaying routes without mocks
|
|
54
|
-
allowedExt?: RegExp // /\.(md|json|txt|js)$/ Just for excluding temporary editor files (e.g. JetBrains appends a ~)
|
|
55
54
|
extraHeaders?: []
|
|
56
55
|
}
|
|
57
56
|
```
|
|
@@ -118,8 +117,6 @@ The delay is globally configurable via `Config.delay = 1200` (milliseconds).
|
|
|
118
117
|
|
|
119
118
|
The **file extension** can be anything, but `.md` is reserved for documentation.
|
|
120
119
|
|
|
121
|
-
The `Config.allowedExt` regex defaults to: `/\.(md|json|txt|js)$/`
|
|
122
|
-
|
|
123
120
|
|
|
124
121
|
### Dynamic Parameters
|
|
125
122
|
Anything within square brackets. For example:
|
|
@@ -149,18 +146,20 @@ permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file)
|
|
|
149
146
|
but since that’s part of the query string, it’s ignored anyway.
|
|
150
147
|
|
|
151
148
|
|
|
152
|
-
|
|
153
149
|
### Default (index-like) route
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
**Option A.** Place it outside the directory:
|
|
151
|
+
```
|
|
152
|
+
api/foo/
|
|
153
|
+
api/foo.GET.200.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Option B.** Omit the filename:
|
|
157
157
|
```text
|
|
158
158
|
api/foo/.GET.200.json
|
|
159
|
-
api/foo/?bar=[bar].GET.200.json
|
|
160
|
-
api/foo/(my comment).GET.200.json
|
|
161
159
|
```
|
|
162
160
|
|
|
163
161
|
|
|
162
|
+
|
|
164
163
|
## `Config.cookies`
|
|
165
164
|
The selected cookie is sent in every response in the `Set-Cookie` header.
|
|
166
165
|
|
|
@@ -197,15 +196,6 @@ Config.extraHeaders = [
|
|
|
197
196
|
]
|
|
198
197
|
```
|
|
199
198
|
|
|
200
|
-
## Documenting Contracts (.md)
|
|
201
|
-
This is handy for documenting request payload parameters. The
|
|
202
|
-
dashboard prints the Markdown document as plain text (I know, I know).
|
|
203
|
-
|
|
204
|
-
```text
|
|
205
|
-
api/foo/[user-id].POST.md
|
|
206
|
-
api/foo/[user-id].POST.201.json
|
|
207
|
-
```
|
|
208
|
-
|
|
209
199
|
---
|
|
210
200
|
|
|
211
201
|
## API
|
package/Tests.js
CHANGED
|
@@ -354,7 +354,6 @@ async function testInvalidFilenamesAreIgnored() {
|
|
|
354
354
|
const consoleErrorSpy = t.mock.method(console, 'error')
|
|
355
355
|
consoleErrorSpy.mock.mockImplementation(() => {}) // so they don’t render in the test report
|
|
356
356
|
|
|
357
|
-
// An extension is needed for testing because of `Config.allowedExt`
|
|
358
357
|
write('api/_INVALID_FILENAME_CONVENTION_.json', '')
|
|
359
358
|
write('api/bad-filename.GET._INVALID_STATUS_.json', '')
|
|
360
359
|
write('api/bad-filename._INVALID_METHOD_.200.json', '')
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"This mock is for /api/user"
|
package/src/Config.js
CHANGED
|
@@ -12,7 +12,6 @@ export const Config = {
|
|
|
12
12
|
cookies: {}, // defaults to the first kv
|
|
13
13
|
onReady: openInBrowser,
|
|
14
14
|
proxyFallback: '', // e.g. http://localhost:9999
|
|
15
|
-
allowedExt: /\.(md|json|txt|js)$/, // Just for excluding temporary editor files (e.g. JetBrains appends a ~)
|
|
16
15
|
extraHeaders: []
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -27,7 +26,6 @@ export function setup(options) {
|
|
|
27
26
|
cookies: is(Object),
|
|
28
27
|
onReady: is(Function),
|
|
29
28
|
proxyFallback: optional(URL.canParse),
|
|
30
|
-
allowedExt: is(RegExp),
|
|
31
29
|
extraHeaders: Array.isArray
|
|
32
30
|
})
|
|
33
31
|
}
|
package/src/Dashboard.css
CHANGED
|
@@ -107,6 +107,10 @@ menu {
|
|
|
107
107
|
top: 0;
|
|
108
108
|
width: 50%;
|
|
109
109
|
margin-left: 16px;
|
|
110
|
+
|
|
111
|
+
h2 {
|
|
112
|
+
padding-top: 20px;
|
|
113
|
+
}
|
|
110
114
|
|
|
111
115
|
pre {
|
|
112
116
|
tab-size: 2;
|
|
@@ -119,10 +123,6 @@ menu {
|
|
|
119
123
|
background: var(--colorLightGrey);
|
|
120
124
|
font-family: monospace;
|
|
121
125
|
}
|
|
122
|
-
|
|
123
|
-
&.Documentation {
|
|
124
|
-
margin-bottom: 12px;
|
|
125
|
-
}
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
package/src/Dashboard.js
CHANGED
|
@@ -19,7 +19,6 @@ const Strings = {
|
|
|
19
19
|
const CSS = {
|
|
20
20
|
DelayToggler: 'DelayToggler',
|
|
21
21
|
InternalServerErrorToggler: 'InternalServerErrorToggler',
|
|
22
|
-
Documentation: 'Documentation',
|
|
23
22
|
MockSelector: 'MockSelector',
|
|
24
23
|
PayloadViewer: 'PayloadViewer',
|
|
25
24
|
PreviewLink: 'PreviewLink',
|
|
@@ -30,7 +29,6 @@ const CSS = {
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
const r = createElement
|
|
33
|
-
const refDocumentation = useRef()
|
|
34
32
|
const refPayloadViewer = useRef()
|
|
35
33
|
const refPayloadFile = useRef()
|
|
36
34
|
|
|
@@ -64,7 +62,6 @@ function DevPanel(brokersByMethod, cookies, comments) {
|
|
|
64
62
|
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
65
63
|
r(SectionByMethod, { method, brokers }))),
|
|
66
64
|
r('div', { className: CSS.PayloadViewer },
|
|
67
|
-
r('pre', { ref: refDocumentation, className: CSS.Documentation }),
|
|
68
65
|
r('h2', { ref: refPayloadFile }, Strings.mock),
|
|
69
66
|
r('pre', { ref: refPayloadViewer }, Strings.click_link_to_preview)))))
|
|
70
67
|
}
|
|
@@ -134,14 +131,14 @@ function SectionByMethod({ method, brokers }) {
|
|
|
134
131
|
.filter(([, broker]) => broker.mocks.length > 1) // Excludes Markdown only routes (>1 because of the autogen500)
|
|
135
132
|
.map(([urlMask, broker]) =>
|
|
136
133
|
r('tr', null,
|
|
137
|
-
r('td', null, r(PreviewLink, { method, urlMask
|
|
134
|
+
r('td', null, r(PreviewLink, { method, urlMask })),
|
|
138
135
|
r('td', null, r(MockSelector, { broker })),
|
|
139
136
|
r('td', null, r(DelayToggler, { broker })),
|
|
140
137
|
r('td', null, r(InternalServerErrorToggler, { broker }))
|
|
141
138
|
))))
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
function PreviewLink({ method, urlMask
|
|
141
|
+
function PreviewLink({ method, urlMask }) {
|
|
145
142
|
return (
|
|
146
143
|
r('a', {
|
|
147
144
|
className: CSS.PreviewLink,
|
|
@@ -150,13 +147,6 @@ function PreviewLink({ method, urlMask, documentation }) {
|
|
|
150
147
|
async onClick(event) {
|
|
151
148
|
event.preventDefault()
|
|
152
149
|
try {
|
|
153
|
-
if (documentation) {
|
|
154
|
-
const r = await fetch(documentation)
|
|
155
|
-
refDocumentation.current.innerText = await r.text()
|
|
156
|
-
}
|
|
157
|
-
else
|
|
158
|
-
refDocumentation.current.innerText = ''
|
|
159
|
-
|
|
160
150
|
const spinner = setTimeout(() => refPayloadViewer.current.innerText = Strings.fetching, 180)
|
|
161
151
|
const res = await fetch(this.href, {
|
|
162
152
|
method: this.getAttribute('data-method')
|
package/src/MockBroker.js
CHANGED
|
@@ -5,19 +5,15 @@ import { isDirectory } from './utils/fs.js'
|
|
|
5
5
|
import { DEFAULT_500_COMMENT } from './ApiConstants.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
// MockBroker is a state for a particular route. It knows the available
|
|
9
|
-
//
|
|
10
|
-
// file, and its delay. Also, knows if the route has documentation (md)
|
|
8
|
+
// MockBroker is a state for a particular route. It knows the available mock files
|
|
9
|
+
// that can be served for the route, the currently selected file, and its delay.
|
|
11
10
|
export class MockBroker {
|
|
12
11
|
#route
|
|
13
12
|
|
|
14
13
|
constructor(file) {
|
|
15
14
|
this.#route = new Route(file)
|
|
16
15
|
this.method = this.#route.method
|
|
17
|
-
|
|
18
|
-
this.documentation = '' // .md
|
|
19
|
-
|
|
20
|
-
this.mocks = [] // *.json,txt,js
|
|
16
|
+
this.mocks = []
|
|
21
17
|
this.currentMock = {
|
|
22
18
|
file: '',
|
|
23
19
|
delay: 0
|
|
@@ -27,13 +23,9 @@ export class MockBroker {
|
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
register(file) {
|
|
30
|
-
if (
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
if (!this.mocks.length)
|
|
34
|
-
this.currentMock.file = file // The first mock file option for a particular route becomes the default
|
|
35
|
-
this.mocks.push(file)
|
|
36
|
-
}
|
|
26
|
+
if (!this.mocks.length)
|
|
27
|
+
this.currentMock.file = file // The first mock file option for a particular route becomes the default
|
|
28
|
+
this.mocks.push(file)
|
|
37
29
|
}
|
|
38
30
|
|
|
39
31
|
urlMaskMatches(url) { return this.#route.urlMaskMatches(url) }
|
|
@@ -77,7 +69,7 @@ export class MockBroker {
|
|
|
77
69
|
}
|
|
78
70
|
|
|
79
71
|
#registerTemp500() {
|
|
80
|
-
const { urlMask, method } = Route.parseFilename(this.mocks[0]
|
|
72
|
+
const { urlMask, method } = Route.parseFilename(this.mocks[0])
|
|
81
73
|
let mask = urlMask
|
|
82
74
|
if (isDirectory(join(Config.mocksDir, urlMask)))
|
|
83
75
|
mask = urlMask + '/'
|
package/src/MockDispatcher.js
CHANGED
|
@@ -7,16 +7,10 @@ import { Config } from './Config.js'
|
|
|
7
7
|
import { mimeFor } from './utils/mime.js'
|
|
8
8
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
9
9
|
import { JsonBodyParserError } from './utils/http-request.js'
|
|
10
|
-
import { sendInternalServerError, sendNotFound,
|
|
10
|
+
import { sendInternalServerError, sendNotFound, sendBadRequest } from './utils/http-response.js'
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
export async function dispatchMock(req, response) {
|
|
14
|
-
/* Serve Documentation */
|
|
15
|
-
if (req.method === 'GET' && req.url.endsWith('.md')) {
|
|
16
|
-
sendFile(response, join(Config.mocksDir, decodeURIComponent(req.url)))
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
|
|
20
14
|
const broker = mockBrokerCollection.getBrokerForUrl(req.method, req.url)
|
|
21
15
|
if (!broker) {
|
|
22
16
|
if (Config.proxyFallback)
|
package/src/Route.js
CHANGED
|
@@ -44,43 +44,22 @@ export class Route {
|
|
|
44
44
|
|
|
45
45
|
static parseFilename(file) {
|
|
46
46
|
const tokens = file.replace(Route.reComments, '').split('.')
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
method,
|
|
64
|
-
status: 200
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parseMock(tokens) {
|
|
69
|
-
if (tokens.length < 4)
|
|
70
|
-
return { error: 'Invalid Filename Convention' }
|
|
71
|
-
|
|
72
|
-
const status = Number(tokens.at(-2))
|
|
73
|
-
if (!responseStatusIsValid(status))
|
|
74
|
-
return { error: `Invalid HTTP Response Status: "${status}"` }
|
|
75
|
-
|
|
76
|
-
const method = tokens.at(-3)
|
|
77
|
-
if (!httpMethods.includes(method))
|
|
78
|
-
return { error: `Unrecognized HTTP Method: "${method}"` }
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
urlMask: '/' + removeTrailingSlash(tokens.at(-4)),
|
|
82
|
-
method,
|
|
83
|
-
status
|
|
47
|
+
if (tokens.length < 4)
|
|
48
|
+
return { error: 'Invalid Filename Convention' }
|
|
49
|
+
|
|
50
|
+
const status = Number(tokens.at(-2))
|
|
51
|
+
if (!responseStatusIsValid(status))
|
|
52
|
+
return { error: `Invalid HTTP Response Status: "${status}"` }
|
|
53
|
+
|
|
54
|
+
const method = tokens.at(-3)
|
|
55
|
+
if (!httpMethods.includes(method))
|
|
56
|
+
return { error: `Unrecognized HTTP Method: "${method}"` }
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
urlMask: '/' + removeTrailingSlash(tokens.at(-4)),
|
|
60
|
+
method,
|
|
61
|
+
status
|
|
62
|
+
}
|
|
84
63
|
}
|
|
85
64
|
}
|
|
86
65
|
|
|
@@ -5,6 +5,7 @@ import { Route } from './Route.js'
|
|
|
5
5
|
import { Config } from './Config.js'
|
|
6
6
|
import { cookie } from './cookie.js'
|
|
7
7
|
import { isFile } from './utils/fs.js'
|
|
8
|
+
import { mimeFor } from './utils/mime.js'
|
|
8
9
|
import { MockBroker } from './MockBroker.js'
|
|
9
10
|
|
|
10
11
|
|
|
@@ -25,7 +26,7 @@ export function init() {
|
|
|
25
26
|
cookie.init(Config.cookies)
|
|
26
27
|
|
|
27
28
|
const files = readDir(Config.mocksDir, { recursive: true })
|
|
28
|
-
.filter(f =>
|
|
29
|
+
.filter(f => isFile(join(Config.mocksDir, f)) && mimeFor(f))
|
|
29
30
|
.sort()
|
|
30
31
|
|
|
31
32
|
for (const file of files) {
|
package/src/utils/mime.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
|
2
|
+
// m = {};
|
|
3
|
+
// for (const row of tbody.children)
|
|
4
|
+
// m[row.children[0].querySelector('code').innerText] = row.children[2].querySelector('code').innerText
|
|
5
|
+
|
|
2
6
|
const mimes = {
|
|
3
|
-
|
|
7
|
+
'3g2': 'video/3gpp2',
|
|
8
|
+
'3gp': 'video/3gpp',
|
|
9
|
+
'7z': 'application/x-7z-compressed',
|
|
10
|
+
aac: 'audio/aac',
|
|
11
|
+
abw: 'application/x-abiword',
|
|
4
12
|
apng: 'image/apng',
|
|
13
|
+
arc: 'application/x-freearc',
|
|
14
|
+
avi: 'video/x-msvideo',
|
|
5
15
|
avif: 'image/avif',
|
|
16
|
+
azw: 'application/vnd.amazon.ebook',
|
|
17
|
+
bin: 'application/octet-stream',
|
|
18
|
+
bmp: 'image/bmp',
|
|
19
|
+
bz2: 'application/x-bzip2',
|
|
20
|
+
bz: 'application/x-bzip',
|
|
21
|
+
cda: 'application/x-cdf',
|
|
22
|
+
cjs: 'text/javascript',
|
|
23
|
+
csh: 'application/x-csh',
|
|
6
24
|
css: 'text/css',
|
|
25
|
+
csv: 'text/csv',
|
|
7
26
|
doc: 'application/msword',
|
|
8
27
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
9
28
|
eot: 'application/vnd.ms-fontobject',
|
|
@@ -14,34 +33,59 @@ const mimes = {
|
|
|
14
33
|
html: 'text/html',
|
|
15
34
|
ico: 'image/vnd.microsoft.icon',
|
|
16
35
|
ics: 'text/calendar',
|
|
36
|
+
jar: 'application/java-archive',
|
|
17
37
|
jpeg: 'image/jpeg',
|
|
18
38
|
jpg: 'image/jpeg',
|
|
19
|
-
js: '
|
|
39
|
+
js: 'text/javascript',
|
|
20
40
|
json: 'application/json',
|
|
21
41
|
jsonld: 'application/ld+json',
|
|
22
|
-
|
|
42
|
+
mid: 'audio/midi',
|
|
23
43
|
mjs: 'text/javascript',
|
|
24
44
|
mp3: 'audio/mpeg',
|
|
25
45
|
mp4: 'video/mp4',
|
|
26
|
-
|
|
46
|
+
mpeg: 'video/mpeg',
|
|
47
|
+
mpkg: 'application/vnd.apple.installer+xml',
|
|
48
|
+
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
49
|
+
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
50
|
+
odt: 'application/vnd.oasis.opendocument.text',
|
|
51
|
+
oga: 'audio/ogg',
|
|
52
|
+
ogv: 'video/ogg',
|
|
53
|
+
ogx: 'application/ogg',
|
|
54
|
+
opus: 'audio/ogg',
|
|
55
|
+
otf: 'font/otf',
|
|
27
56
|
pdf: 'application/pdf',
|
|
57
|
+
php: 'application/x-httpd-php',
|
|
28
58
|
png: 'image/png',
|
|
59
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
60
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
61
|
+
rar: 'application/vnd.rar',
|
|
62
|
+
rtf: 'application/rtf',
|
|
63
|
+
sh: 'application/x-sh',
|
|
64
|
+
svg: 'image/svg+xml',
|
|
65
|
+
tar: 'application/x-tar',
|
|
66
|
+
tif: 'image/tiff',
|
|
67
|
+
ts: 'video/mp2t',
|
|
29
68
|
ttf: 'font/ttf',
|
|
30
|
-
txt: 'plain
|
|
69
|
+
txt: 'text/plain',
|
|
70
|
+
vsd: 'application/vnd.visio',
|
|
31
71
|
wav: 'audio/wav',
|
|
32
72
|
weba: 'audio/webm',
|
|
33
73
|
webm: 'video/webm',
|
|
34
74
|
webp: 'image/webp',
|
|
35
75
|
woff2: 'font/woff2',
|
|
36
76
|
woff: 'font/woff',
|
|
77
|
+
xhtml: 'application/xhtml+xml',
|
|
78
|
+
xls: 'application/vnd.ms-excel',
|
|
79
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
37
80
|
xml: 'application/xml',
|
|
81
|
+
xul: 'application/vnd.mozilla.xul+xml',
|
|
38
82
|
zip: 'application/zip'
|
|
39
83
|
}
|
|
40
84
|
|
|
41
85
|
export function mimeFor(filename) {
|
|
42
|
-
const ext = filename.replace(/.*\./, '')
|
|
86
|
+
const ext = filename.replace(/.*\./, '').toLowerCase()
|
|
43
87
|
const mime = mimes[ext] || ''
|
|
44
88
|
if (!mime)
|
|
45
|
-
console.
|
|
89
|
+
console.info(`Missing MIME for ${filename}`)
|
|
46
90
|
return mime
|
|
47
|
-
}
|
|
91
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"This is an index-like route (i.e. /api/user). This file has no name, only the extension convention is needed for the index path."
|