mockaton 13.6.0 → 13.6.2
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 +10 -9
- package/package.json +5 -3
- package/src/client/app-header.js +1 -0
- package/src/server/Api.js +16 -17
- package/src/server/MockBroker.js +1 -1
- package/src/server/Mockaton.test.config.js +1 -1
- package/src/server/Mockaton.test.js +5 -5
- package/src/server/ProxyRelay.js +10 -3
- package/src/server/cli.test.js +1 -1
- package/src/server/config.js +14 -15
- package/src/server/utils/http-cors.js +9 -3
- package/src/server/utils/validate.js +37 -4
- package/src/server/utils/validate.test.js +8 -27
- package/www/src/assets/SKILLS.md +84 -0
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
<!-- SKILLS_IGNORE_BEGIN -->
|
|
1
2
|

|
|
2
3
|
[](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
|
|
3
4
|
[](https://codecov.io/github/ericfortis/mockaton)
|
|
4
5
|
[](https://opensource.org/licenses/MIT)
|
|
5
6
|
|
|
6
|
-
## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog)
|
|
7
|
+
## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog) | [Skills ↗](https://mockaton.com/assets/SKILLS.md)
|
|
7
8
|
|
|
8
9
|
Mockaton is an HTTP mock server for simulating APIs, designed
|
|
9
10
|
for testing difficult to reproduce backend states with minimal setup.
|
|
@@ -29,14 +30,14 @@ Test it:
|
|
|
29
30
|
curl localhost:2020/api/user
|
|
30
31
|
```
|
|
31
32
|
Dashboard: [localhost:2020/mockaton](http://localhost:2020/mockaton)
|
|
32
|
-
|
|
33
|
+
<!-- SKILLS_IGNORE_END -->
|
|
33
34
|
|
|
34
35
|
## Basic Usage
|
|
35
36
|
```sh
|
|
36
37
|
npx mockaton my-mocks-dir/
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
Mockaton will serve the files on the given directory. It
|
|
40
|
+
Mockaton will serve the files on the given directory. It's a file-system
|
|
40
41
|
based router, so filenames can have dynamic parameters and comments.
|
|
41
42
|
Also, each route can have different mock file variants.
|
|
42
43
|
|
|
@@ -44,7 +45,7 @@ Also, each route can have different mock file variants.
|
|
|
44
45
|
| Route | Filename | Description |
|
|
45
46
|
| -----| -----| ---|
|
|
46
47
|
| /api/company/123 | api/company/[id].GET.200.json | `[id]` is a dynamic parameter |
|
|
47
|
-
| /media/avatar.png | media/avatar.png | Statics assets don
|
|
48
|
+
| /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
|
|
48
49
|
| /api/login | api/login(invalid attempt).POST.401.json | Anything within parenthesis is a **comment**, they are ignored when routing |
|
|
49
50
|
| /api/login | api/login(default).GET.200.json | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
|
|
50
51
|
| /api/login | api/login(locked out user).POST.423.ts | TypeScript or JavaScript mocks are sent as JSON by default |
|
|
@@ -52,18 +53,18 @@ Also, each route can have different mock file variants.
|
|
|
52
53
|
|
|
53
54
|
## Docs
|
|
54
55
|
- How to **configure** Mockaton? See [CLI and mockaton.config.js](https://mockaton.com/config) docs.
|
|
55
|
-
- How to **control** Mockaton? Besides the dashboard, there
|
|
56
|
+
- How to **control** Mockaton? Besides the dashboard, there's a [Programmatic API](https://mockaton.com/api).
|
|
56
57
|
- How to **add plugins**? You can write [Plugins](https://mockaton.com/plugins) for customizing responses.
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
<!-- SKILLS_IGNORE_BEGIN -->
|
|
59
60
|
## How to scrape your backend APIs?
|
|
60
61
|
Mockaton has a [Browser Extension](https://mockaton.com/scraping) that lets
|
|
61
|
-
you download in bulk all your API responses following Mockaton
|
|
62
|
-
|
|
62
|
+
you download in bulk all your API responses following Mockaton's filename convention.
|
|
63
|
+
<!-- SKILLS_IGNORE_END -->
|
|
63
64
|
|
|
64
65
|
## How to create mocks?
|
|
65
66
|
|
|
66
|
-
Write to your mocks directory. Alternatively, there
|
|
67
|
+
Write to your mocks directory. Alternatively, there's an API [PATCH /mockaton/write-mock](https://mockaton.com/api).
|
|
67
68
|
```sh
|
|
68
69
|
mkdir -p my-mocks-dir/api
|
|
69
70
|
echo '{ "name": "John" }' > my-mocks-dir/api/user.GET.200.json
|
package/package.json
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
"name": "mockaton",
|
|
3
3
|
"description": "HTTP Mock Server",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "13.6.
|
|
5
|
+
"version": "13.6.2",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"import": "./index.js",
|
|
9
9
|
"types": "./index.d.ts"
|
|
10
10
|
},
|
|
11
|
-
"./openapi.json": "./www/src/assets/openapi.json"
|
|
11
|
+
"./openapi.json": "./www/src/assets/openapi.json",
|
|
12
|
+
"./SKILLS.md": "./www/src/assets/SKILLS.md"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"src",
|
|
15
16
|
"index.js",
|
|
16
17
|
"index.d.ts",
|
|
17
|
-
"www/src/assets/openapi.json"
|
|
18
|
+
"www/src/assets/openapi.json",
|
|
19
|
+
"www/src/assets/SKILLS.md"
|
|
18
20
|
],
|
|
19
21
|
"license": "MIT",
|
|
20
22
|
"homepage": "https://mockaton.com",
|
package/src/client/app-header.js
CHANGED
package/src/server/Api.js
CHANGED
|
@@ -110,9 +110,9 @@ function reset(_, response) {
|
|
|
110
110
|
|
|
111
111
|
async function setCorsAllowed(req, response) {
|
|
112
112
|
const corsAllowed = await req.json()
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
response.unprocessable(
|
|
113
|
+
const err = ConfigValidator.corsAllowed(corsAllowed)
|
|
114
|
+
if (err)
|
|
115
|
+
response.unprocessable(err)
|
|
116
116
|
else {
|
|
117
117
|
config.corsAllowed = corsAllowed
|
|
118
118
|
response.ok()
|
|
@@ -123,9 +123,9 @@ async function setCorsAllowed(req, response) {
|
|
|
123
123
|
|
|
124
124
|
async function setGlobalDelay(req, response) {
|
|
125
125
|
const delay = await req.json()
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
response.unprocessable(
|
|
126
|
+
const err = ConfigValidator.delay(delay)
|
|
127
|
+
if (err)
|
|
128
|
+
response.unprocessable(err)
|
|
129
129
|
else {
|
|
130
130
|
config.delay = delay
|
|
131
131
|
response.ok()
|
|
@@ -135,9 +135,9 @@ async function setGlobalDelay(req, response) {
|
|
|
135
135
|
|
|
136
136
|
async function setGlobalDelayJitter(req, response) {
|
|
137
137
|
const jitter = await req.json()
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
response.unprocessable(
|
|
138
|
+
const err = ConfigValidator.delayJitter(jitter)
|
|
139
|
+
if (err)
|
|
140
|
+
response.unprocessable(err)
|
|
141
141
|
else {
|
|
142
142
|
config.delayJitter = jitter
|
|
143
143
|
response.ok()
|
|
@@ -148,7 +148,6 @@ async function setGlobalDelayJitter(req, response) {
|
|
|
148
148
|
|
|
149
149
|
async function selectCookie(req, response) {
|
|
150
150
|
const cookieKey = await req.json()
|
|
151
|
-
|
|
152
151
|
const error = cookie.setCurrent(cookieKey)
|
|
153
152
|
if (error)
|
|
154
153
|
response.unprocessable(error?.message || error)
|
|
@@ -161,9 +160,9 @@ async function selectCookie(req, response) {
|
|
|
161
160
|
|
|
162
161
|
async function setProxyFallback(req, response) {
|
|
163
162
|
const fallback = await req.json()
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
response.unprocessable(
|
|
163
|
+
const err = ConfigValidator.proxyFallback(fallback)
|
|
164
|
+
if (err)
|
|
165
|
+
response.unprocessable(err)
|
|
167
166
|
else {
|
|
168
167
|
config.proxyFallback = fallback
|
|
169
168
|
response.ok()
|
|
@@ -173,9 +172,9 @@ async function setProxyFallback(req, response) {
|
|
|
173
172
|
|
|
174
173
|
async function setCollectProxied(req, response) {
|
|
175
174
|
const collectProxied = await req.json()
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
response.unprocessable(
|
|
175
|
+
const err = ConfigValidator.collectProxied(collectProxied)
|
|
176
|
+
if (err)
|
|
177
|
+
response.unprocessable(err)
|
|
179
178
|
else {
|
|
180
179
|
config.collectProxied = collectProxied
|
|
181
180
|
response.ok()
|
|
@@ -263,7 +262,7 @@ async function writeMock(req, response) {
|
|
|
263
262
|
const [file, content] = await req.json()
|
|
264
263
|
if (typeof file !== 'string')
|
|
265
264
|
return response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
|
|
266
|
-
|
|
265
|
+
|
|
267
266
|
const path = await resolveIn(config.mocksDir, file)
|
|
268
267
|
if (!path)
|
|
269
268
|
return response.forbidden('Filename path resolves outside config.mocksDir')
|
package/src/server/MockBroker.js
CHANGED
|
@@ -6,7 +6,7 @@ export default {
|
|
|
6
6
|
userB: jwtCookie('CookieB', { email: 'john@example.test' }),
|
|
7
7
|
},
|
|
8
8
|
extraHeaders: ['custom_header_name', 'custom_header_val'],
|
|
9
|
-
extraMimes: {
|
|
9
|
+
extraMimes: { 'custom_extension': 'custom_mime' },
|
|
10
10
|
logLevel: 'verbose',
|
|
11
11
|
corsOrigins: ['https://example.test'],
|
|
12
12
|
corsExposedHeaders: ['Content-Encoding'],
|
|
@@ -166,7 +166,7 @@ describe('CORS', () => {
|
|
|
166
166
|
test('422 for non boolean', async () => {
|
|
167
167
|
const r = await api.setCorsAllowed('not-a-boolean')
|
|
168
168
|
equal(r.status, 422)
|
|
169
|
-
equal(await r.text(), 'Expected
|
|
169
|
+
equal(await r.text(), 'Expected Boolean')
|
|
170
170
|
})
|
|
171
171
|
|
|
172
172
|
test('200', async () => {
|
|
@@ -271,7 +271,7 @@ describe('Delay', () => {
|
|
|
271
271
|
test('422 for invalid value', async () => {
|
|
272
272
|
const r = await api.setGlobalDelay('not-a-number')
|
|
273
273
|
equal(r.status, 422)
|
|
274
|
-
equal(await r.text(), 'Expected
|
|
274
|
+
equal(await r.text(), 'Expected an integer between 0 and 120000')
|
|
275
275
|
})
|
|
276
276
|
test('200 for valid global delay value', async () => {
|
|
277
277
|
const r = await api.setGlobalDelay(150)
|
|
@@ -284,7 +284,7 @@ describe('Delay', () => {
|
|
|
284
284
|
test('422 for invalid value', async () => {
|
|
285
285
|
const r = await api.setGlobalDelayJitter('not-a-number')
|
|
286
286
|
equal(r.status, 422)
|
|
287
|
-
equal(await r.text(), 'Expected
|
|
287
|
+
equal(await r.text(), 'Expected a float between 0 and 3')
|
|
288
288
|
})
|
|
289
289
|
test('200 for valid value', async () => {
|
|
290
290
|
const r = await api.setGlobalDelayJitter(0.1)
|
|
@@ -391,7 +391,7 @@ describe('Proxy Fallback', () => {
|
|
|
391
391
|
test('422 when value is not a valid URL', async () => {
|
|
392
392
|
const r = await api.setProxyFallback('bad url')
|
|
393
393
|
equal(r.status, 422)
|
|
394
|
-
equal(await r.text(), '
|
|
394
|
+
equal(await r.text(), 'Expected an empty String or URL')
|
|
395
395
|
})
|
|
396
396
|
|
|
397
397
|
test('sets fallback', async () => {
|
|
@@ -411,7 +411,7 @@ describe('Proxy Fallback', () => {
|
|
|
411
411
|
test('422 for invalid collectProxied value', async () => {
|
|
412
412
|
const r = await api.setCollectProxied('not-a-boolean')
|
|
413
413
|
equal(r.status, 422)
|
|
414
|
-
equal(await r.text(), 'Expected
|
|
414
|
+
equal(await r.text(), 'Expected Boolean')
|
|
415
415
|
})
|
|
416
416
|
|
|
417
417
|
test('200 set and unset', async () => {
|
package/src/server/ProxyRelay.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from 'node:path'
|
|
|
2
2
|
import { randomUUID } from 'node:crypto'
|
|
3
3
|
|
|
4
4
|
import { extFor } from './utils/mime.js'
|
|
5
|
-
import { write, isFile } from './utils/fs.js'
|
|
5
|
+
import { write, isFile, resolveIn } from './utils/fs.js'
|
|
6
6
|
import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
|
|
7
7
|
|
|
8
8
|
import { config } from './config.js'
|
|
@@ -41,6 +41,10 @@ export async function proxy(req, response, delay) {
|
|
|
41
41
|
setTimeout(() => response.end(body), delay) // TESTME
|
|
42
42
|
|
|
43
43
|
if (config.collectProxied) {
|
|
44
|
+
if (config.readOnly) {
|
|
45
|
+
logger.info('Write denied: config.readOnly is true')
|
|
46
|
+
return
|
|
47
|
+
}
|
|
44
48
|
const mime = proxyResponse.headers.get('content-type')
|
|
45
49
|
const ext = mime
|
|
46
50
|
? extFor(mime) || EXT_UNKNOWN_MIME
|
|
@@ -59,10 +63,13 @@ async function saveMockToDisk(url, method, status, ext, body) {
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
try {
|
|
62
|
-
|
|
66
|
+
const f = makeUniqueMockFilename(url, method, status, ext)
|
|
67
|
+
if (!resolveIn(config.mocksDir, f))
|
|
68
|
+
throw 'Attempted write outside config.mocksDir'
|
|
69
|
+
await write(f, body)
|
|
63
70
|
}
|
|
64
71
|
catch (err) {
|
|
65
|
-
logger.warn('Write
|
|
72
|
+
logger.warn('Write denied', err)
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
|
package/src/server/cli.test.js
CHANGED
|
@@ -26,7 +26,7 @@ describe('CLI', () => {
|
|
|
26
26
|
const { stderr, status } = cli(
|
|
27
27
|
rel('../../mockaton-mocks'),
|
|
28
28
|
'--port', 'not-a-number')
|
|
29
|
-
equal(stderr.trim(), `port="not-a-number"
|
|
29
|
+
equal(stderr.trim(), `port="not-a-number"\nExpected an integer between 0 and 65535`)
|
|
30
30
|
equal(status, 1)
|
|
31
31
|
})
|
|
32
32
|
|
package/src/server/config.js
CHANGED
|
@@ -1,57 +1,56 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
|
+
import { lstatSync } from 'node:fs'
|
|
2
3
|
import { METHODS } from 'node:http'
|
|
3
4
|
|
|
4
5
|
import { logger } from './utils/logger.js'
|
|
5
|
-
import { isDirectory } from './utils/fs.js'
|
|
6
6
|
import { registerMimes } from './utils/mime.js'
|
|
7
7
|
import { openInBrowser } from './utils/openInBrowser.js'
|
|
8
|
-
import {
|
|
8
|
+
import { is, validate, isInt, isFloat, isOneOf, optionalURL } from './utils/validate.js'
|
|
9
9
|
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
|
|
10
10
|
|
|
11
11
|
import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
/** @type {{
|
|
15
14
|
* [K in keyof Config]-?: [
|
|
16
15
|
* defaultVal: Config[K],
|
|
17
|
-
* validator: (val: unknown) =>
|
|
16
|
+
* validator: (val: unknown) => err:string
|
|
18
17
|
* ]
|
|
19
18
|
* }} */
|
|
20
19
|
const schema = {
|
|
21
|
-
mocksDir: [resolve('mockaton-mocks'), isDirectory],
|
|
20
|
+
mocksDir: [resolve('mockaton-mocks'), p => !lstatSync(p).isDirectory()],
|
|
22
21
|
ignore: [/(\.DS_Store|~)$/, is(RegExp)],
|
|
23
22
|
readOnly: [true, is(Boolean)],
|
|
24
23
|
watcherEnabled: [true, is(Boolean)],
|
|
25
|
-
watcherDebounceMs: [80,
|
|
24
|
+
watcherDebounceMs: [80, isInt(0, 5000)],
|
|
26
25
|
|
|
27
26
|
host: ['127.0.0.1', is(String)],
|
|
28
|
-
port: [0,
|
|
27
|
+
port: [0, isInt(0, 2 ** 16 - 1)], // 0 means auto-assigned
|
|
29
28
|
|
|
30
|
-
logLevel: ['normal',
|
|
29
|
+
logLevel: ['normal', isOneOf('normal', 'quiet', 'verbose')],
|
|
31
30
|
|
|
32
|
-
delay: [1200,
|
|
33
|
-
delayJitter: [0,
|
|
31
|
+
delay: [1200, isInt(0, 120_000)],
|
|
32
|
+
delayJitter: [0, isFloat(0, 3)],
|
|
34
33
|
|
|
35
|
-
proxyFallback: ['',
|
|
34
|
+
proxyFallback: ['', optionalURL], // e.g. http://localhost:9999
|
|
36
35
|
collectProxied: [false, is(Boolean)],
|
|
37
36
|
formatCollectedJSON: [true, is(Boolean)],
|
|
38
37
|
|
|
39
38
|
cookies: [{}, is(Object)], // defaults to the first kv
|
|
40
|
-
extraHeaders: [[],
|
|
39
|
+
extraHeaders: [[], is(Array)],
|
|
41
40
|
extraMimes: [{}, is(Object)],
|
|
42
41
|
|
|
43
42
|
corsAllowed: [true, is(Boolean)],
|
|
44
43
|
corsOrigins: [['*'], validateCorsAllowedOrigins],
|
|
45
44
|
corsMethods: [METHODS, validateCorsAllowedMethods],
|
|
46
|
-
corsHeaders: [['content-type', 'authorization'], Array
|
|
47
|
-
corsExposedHeaders: [[], Array
|
|
45
|
+
corsHeaders: [['content-type', 'authorization'], is(Array)],
|
|
46
|
+
corsExposedHeaders: [[], is(Array)],
|
|
48
47
|
corsCredentials: [true, is(Boolean)],
|
|
49
48
|
corsMaxAge: [0, is(Number)],
|
|
50
49
|
|
|
51
50
|
plugins: [
|
|
52
51
|
[
|
|
53
52
|
[/\.(js|ts)$/, jsToJsonPlugin]
|
|
54
|
-
], Array
|
|
53
|
+
], is(Array)],
|
|
55
54
|
|
|
56
55
|
onReady: [await openInBrowser, is(Function)],
|
|
57
56
|
|
|
@@ -4,14 +4,20 @@ import { methodIsSupported } from './HttpIncomingMessage.js'
|
|
|
4
4
|
|
|
5
5
|
export function validateCorsAllowedOrigins(arr) {
|
|
6
6
|
if (!Array.isArray(arr))
|
|
7
|
-
return
|
|
7
|
+
return 'Expected Array'
|
|
8
8
|
if (arr.length === 1 && arr[0] === '*')
|
|
9
|
-
return
|
|
9
|
+
return ''
|
|
10
10
|
return arr.every(o => URL.canParse(o))
|
|
11
|
+
? ''
|
|
12
|
+
: 'Expected URLs'
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function validateCorsAllowedMethods(arr) {
|
|
14
|
-
|
|
16
|
+
if (!Array.isArray(arr))
|
|
17
|
+
return 'Expected Array'
|
|
18
|
+
return arr.every(methodIsSupported)
|
|
19
|
+
? ''
|
|
20
|
+
: 'Unsupported Method'
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
export function validate(obj, shape) {
|
|
2
2
|
for (const [field, value] of Object.entries(obj))
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
try {
|
|
4
|
+
const err = shape[field](value)
|
|
5
|
+
if (err)
|
|
6
|
+
throw err
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
throw new TypeError(`${field}=${JSON.stringify(value)}\n${err}`)
|
|
10
|
+
}
|
|
5
11
|
}
|
|
6
12
|
|
|
7
|
-
export
|
|
8
|
-
|
|
13
|
+
export function is(ctor) {
|
|
14
|
+
return val => val.constructor === ctor
|
|
15
|
+
? ''
|
|
16
|
+
: `Expected ${ctor.prototype.constructor.name}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isInt(min = Number.MAX_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
|
|
20
|
+
return v => Number.isInteger(v) && v >= min && v <= max
|
|
21
|
+
? ''
|
|
22
|
+
: `Expected an integer between ${min} and ${max}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isFloat(min = Number.MAX_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
|
|
26
|
+
return v => Number.isFinite(v) && v >= min && v <= max
|
|
27
|
+
? ''
|
|
28
|
+
: `Expected a float between ${min} and ${max}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isOneOf(...vals) {
|
|
32
|
+
return v => vals.includes(v)
|
|
33
|
+
? ''
|
|
34
|
+
: `Expected one of: ${vals.join(', ')}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function optionalURL(v) {
|
|
38
|
+
return !v || URL.canParse(v)
|
|
39
|
+
? ''
|
|
40
|
+
: 'Expected an empty String or URL'
|
|
41
|
+
}
|
|
@@ -1,33 +1,14 @@
|
|
|
1
1
|
import { describe, test } from 'node:test'
|
|
2
2
|
import { doesNotThrow, throws } from 'node:assert/strict'
|
|
3
|
-
import { validate, is
|
|
3
|
+
import { validate, is } from './validate.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
describe('validate', () => {
|
|
7
|
-
describe('optional', () => {
|
|
8
|
-
test('accepts undefined', () =>
|
|
9
|
-
doesNotThrow(() =>
|
|
10
|
-
validate({}, { foo: optional(Number.isInteger) })))
|
|
11
|
-
|
|
12
|
-
test('accepts falsy value regardless of type', () =>
|
|
13
|
-
doesNotThrow(() =>
|
|
14
|
-
validate({ foo: 0 }, { foo: optional(Array.isArray) })))
|
|
15
|
-
|
|
16
|
-
test('accepts when tester func returns truthy', () =>
|
|
17
|
-
doesNotThrow(() =>
|
|
18
|
-
validate({ foo: [] }, { foo: optional(Array.isArray) })))
|
|
19
|
-
|
|
20
|
-
test('rejects when tester func returns falsy', () =>
|
|
21
|
-
throws(() =>
|
|
22
|
-
validate({ foo: 1 }, { foo: optional(Array.isArray) }),
|
|
23
|
-
/foo=1 is invalid/))
|
|
24
|
-
})
|
|
25
|
-
|
|
26
7
|
describe('is', () => {
|
|
27
8
|
test('rejects mismatched type', () =>
|
|
28
9
|
throws(() =>
|
|
29
10
|
validate({ foo: 1 }, { foo: is(String) }),
|
|
30
|
-
/foo=1
|
|
11
|
+
/foo=1\nExpected String/))
|
|
31
12
|
|
|
32
13
|
test('accepts matched type', () =>
|
|
33
14
|
doesNotThrow(() =>
|
|
@@ -37,16 +18,16 @@ describe('validate', () => {
|
|
|
37
18
|
describe('custom tester func', () => {
|
|
38
19
|
test('rejects mismatched type', () =>
|
|
39
20
|
throws(() =>
|
|
40
|
-
validate({ foo: 'not-a-number' }, { foo: n => n
|
|
41
|
-
/foo="not-a-number"
|
|
21
|
+
validate({ foo: 'not-a-number' }, { foo: n => Number.isInteger(n) ? '' : 'Expected Integer' }),
|
|
22
|
+
/foo="not-a-number"\nExpected Integer/))
|
|
42
23
|
|
|
43
|
-
test('rejects mismatched
|
|
24
|
+
test('rejects mismatched value', () =>
|
|
44
25
|
throws(() =>
|
|
45
|
-
validate({ foo: 0 }, { foo: n => n
|
|
46
|
-
/foo=0
|
|
26
|
+
validate({ foo: 0 }, { foo: n => n === 1 ? '' : 'Expected 1' }),
|
|
27
|
+
/foo=0\nExpected 1/))
|
|
47
28
|
|
|
48
29
|
test('accepts matched type', () =>
|
|
49
30
|
doesNotThrow(() =>
|
|
50
|
-
validate({ foo: 1 }, { foo: Number.isInteger })))
|
|
31
|
+
validate({ foo: 1 }, { foo: v => !Number.isInteger(v) })))
|
|
51
32
|
})
|
|
52
33
|
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Mockaton
|
|
3
|
+
description: Generates and serves mock HTTP APIs from filesystem conventions. Use when creating, editing, or reasoning about mock endpoints.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Basic Usage
|
|
7
|
+
```sh
|
|
8
|
+
npx mockaton my-mocks-dir/
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Mockaton will serve the files on the given directory. It's a file-system
|
|
12
|
+
based router, so filenames can have dynamic parameters and comments.
|
|
13
|
+
Also, each route can have different mock file variants.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
| Route | Filename | Description |
|
|
17
|
+
| -----| -----| ---|
|
|
18
|
+
| /api/company/123 | api/company/[id].GET.200.json | `[id]` is a dynamic parameter |
|
|
19
|
+
| /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
|
|
20
|
+
| /api/login | api/login(invalid attempt).POST.401.json | Anything within parenthesis is a **comment**, they are ignored when routing |
|
|
21
|
+
| /api/login | api/login(default).GET.200.json | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
|
|
22
|
+
| /api/login | api/login(locked out user).POST.423.ts | TypeScript or JavaScript mocks are sent as JSON by default |
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Docs
|
|
26
|
+
- How to **configure** Mockaton? See [CLI and mockaton.config.js](https://mockaton.com/config) docs.
|
|
27
|
+
- How to **control** Mockaton? Besides the dashboard, there's a [Programmatic API](https://mockaton.com/api).
|
|
28
|
+
- How to **add plugins**? You can write [Plugins](https://mockaton.com/plugins) for customizing responses.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## How to create mocks?
|
|
33
|
+
|
|
34
|
+
Write to your mocks directory. Alternatively, there's an API [PATCH /mockaton/write-mock](https://mockaton.com/api).
|
|
35
|
+
```sh
|
|
36
|
+
mkdir -p my-mocks-dir/api
|
|
37
|
+
echo '{ "name": "John" }' > my-mocks-dir/api/user.GET.200.json
|
|
38
|
+
sleep 0.1 # Wait for the watcher to register it
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Example A: JSON
|
|
42
|
+
- **Route:** /api/company/123
|
|
43
|
+
- **Filename:** api/company/[id].GET.200.json
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"name": "Acme, Inc."
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Example B: TypeScript or JavaScript
|
|
52
|
+
Exporting an Object, Array, or String is sent as JSON.
|
|
53
|
+
|
|
54
|
+
- **Route:** /api/company/abc
|
|
55
|
+
- **Filename:** api/company/[id].GET.200.ts
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export default {
|
|
59
|
+
name: 'Acme, Inc.'
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Example C: [Function Mocks](https://mockaton.com/function-mocks)
|
|
64
|
+
With a function mock you can do pretty much anything you could do with a normal backend handler.</p>
|
|
65
|
+
For example, you can handle complex logic, URL parsing, saving toa database, etc.
|
|
66
|
+
|
|
67
|
+
- **Route:** /api/company/abc/user/999
|
|
68
|
+
- **Filename:** api/company/[companyId]/user/[userId].GET.200.ts
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { IncomingMessage, OutgoingMessage } from 'node:http'
|
|
72
|
+
import { parseSegments } from 'mockaton'
|
|
73
|
+
|
|
74
|
+
export default async function (req: IncomingMessage, response: OutgoingMessage) {
|
|
75
|
+
const { companyId, userId } = parseSegments(req.url, import.meta.filename)
|
|
76
|
+
const foo = await getFoo()
|
|
77
|
+
return JSON.stringify({
|
|
78
|
+
foo,
|
|
79
|
+
companyId,
|
|
80
|
+
userId,
|
|
81
|
+
name: 'Acme, Inc.'
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
```
|