mockaton 8.2.23 → 8.3.1
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 +33 -11
- package/TODO.md +7 -0
- package/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/ApiConstants.js +2 -0
- package/src/Config.js +2 -0
- package/src/Filename.js +9 -1
- package/src/MockDispatcher.js +2 -2
- package/src/ProxyRelay.js +18 -2
- package/src/utils/fs.js +6 -1
- package/src/utils/http-request.js +6 -5
- package/src/utils/mime.js +17 -3
package/README.md
CHANGED
|
@@ -13,13 +13,10 @@ URL paths. For example, the following file will be served on `/api/user/1234`
|
|
|
13
13
|
my-mocks-dir/api/user/[user-id].GET.200.json
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
By the way,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Nonetheless, you don’t need to mock all your APIs. Mockaton
|
|
21
|
-
can request from your backend the routes you don’t have mocks for.
|
|
22
|
-
That’s done with `config.proxyFallback = 'http://mybackend'`
|
|
16
|
+
By the way, you don’t need to mock all your APIs. Mockaton can request
|
|
17
|
+
from your backend the routes you don’t have mocks for. That’s done
|
|
18
|
+
with `config.proxyFallback = 'http://mybackend'`. For convenience, you
|
|
19
|
+
can save mocks for those responses with `config.collectProxied = true`
|
|
23
20
|
|
|
24
21
|
## Multiple Mock Variants
|
|
25
22
|
Each route can have many mocks, which could either be:
|
|
@@ -280,6 +277,35 @@ the amount is globally configurable. It defaults to `config.delay=1200` millisec
|
|
|
280
277
|
Lets you specify a target server for serving routes you don’t have mocks for.
|
|
281
278
|
For example, `config.proxyFallback = 'http://example.com'`
|
|
282
279
|
|
|
280
|
+
### `collectProxied?: boolean`
|
|
281
|
+
Defaults to `false`. With this flag you can save mocks that hit
|
|
282
|
+
your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
|
|
283
|
+
URL the filename will have `[id]` in their place. For example,
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
/api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
|
|
287
|
+
my-mocks-dir/api/user/[id]/likes.GET.200.json
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Note that newly saved mocks won’t be served until you
|
|
291
|
+
**register them** by reinitializing Mockaton or clicking "Reset".
|
|
292
|
+
|
|
293
|
+
Registered mocks won’t be overwritten (they don’t hit the fallback server).
|
|
294
|
+
On the other hand, newly saved mocks get overwritten while they are unregistered.
|
|
295
|
+
|
|
296
|
+
<details>
|
|
297
|
+
<summary>Extension Details</summary>
|
|
298
|
+
<p>
|
|
299
|
+
An <code>.empty</code> extension means the <code>Content-Type</code>
|
|
300
|
+
header was not sent by your backend.
|
|
301
|
+
</p>
|
|
302
|
+
|
|
303
|
+
<p>
|
|
304
|
+
An <code>.unknown</code> extension means the <code>Content-Type</code> is not in
|
|
305
|
+
Mockaton’s predefined list. For that, you can add it to <code>config.extraMimes</code>
|
|
306
|
+
</p>
|
|
307
|
+
</details>
|
|
308
|
+
|
|
283
309
|
|
|
284
310
|
### `staticDir?: string`
|
|
285
311
|
- Use Case 1: If you have a bunch of static assets you don’t want to add `.GET.200.ext`
|
|
@@ -472,7 +498,3 @@ await mockaton.reset()
|
|
|
472
498
|
<img src="./sample-mocks/api/user/avatar.GET.200.png" width="170"/>
|
|
473
499
|
<p style="font-size: 18px">“Use Mockaton”</p>
|
|
474
500
|
</div>
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
## TODO
|
|
478
|
-
- Refactor Tests
|
package/TODO.md
ADDED
package/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/ApiConstants.js
CHANGED
package/src/Config.js
CHANGED
|
@@ -14,6 +14,7 @@ export const Config = Object.seal({
|
|
|
14
14
|
host: '127.0.0.1',
|
|
15
15
|
port: 0, // auto-assigned
|
|
16
16
|
proxyFallback: '', // e.g. http://localhost:9999
|
|
17
|
+
collectProxied: false,
|
|
17
18
|
|
|
18
19
|
delay: 1200, // milliseconds
|
|
19
20
|
cookies: {}, // defaults to the first kv
|
|
@@ -47,6 +48,7 @@ export function setup(options) {
|
|
|
47
48
|
host: is(String),
|
|
48
49
|
port: port => Number.isInteger(port) && port >= 0 && port < 2 ** 16,
|
|
49
50
|
proxyFallback: optional(URL.canParse),
|
|
51
|
+
collectProxied: is(Boolean),
|
|
50
52
|
|
|
51
53
|
delay: ms => Number.isInteger(ms) && ms > 0,
|
|
52
54
|
cookies: is(Object),
|
package/src/Filename.js
CHANGED
|
@@ -44,7 +44,6 @@ export function parseFilename(file) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
47
|
function removeTrailingSlash(url = '') {
|
|
49
48
|
return url
|
|
50
49
|
.replace(/\/$/, '')
|
|
@@ -59,5 +58,14 @@ function responseStatusIsValid(status) {
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
|
|
61
|
+
export function makeMockFilename(url, method, status, ext) {
|
|
62
|
+
const urlMask = replaceIds(removeTrailingSlash(url))
|
|
63
|
+
return [urlMask, method, status, ext].join('.')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const reUuidV4 = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi
|
|
67
|
+
function replaceIds(filename) {
|
|
68
|
+
return filename.replaceAll(reUuidV4, '[id]')
|
|
69
|
+
}
|
|
62
70
|
|
|
63
71
|
|
package/src/MockDispatcher.js
CHANGED
|
@@ -5,7 +5,7 @@ import { cookie } from './cookie.js'
|
|
|
5
5
|
import { Config } from './Config.js'
|
|
6
6
|
import { applyPlugins } from './MockDispatcherPlugins.js'
|
|
7
7
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
8
|
-
import {
|
|
8
|
+
import { BodyReaderError } from './utils/http-request.js'
|
|
9
9
|
import { sendInternalServerError, sendNotFound, sendBadRequest } from './utils/http-response.js'
|
|
10
10
|
|
|
11
11
|
|
|
@@ -37,7 +37,7 @@ export async function dispatchMock(req, response) {
|
|
|
37
37
|
setTimeout(() => response.end(body), broker.delay)
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
|
-
if (error instanceof
|
|
40
|
+
if (error instanceof BodyReaderError)
|
|
41
41
|
sendBadRequest(response, error)
|
|
42
42
|
else if (error.code === 'ENOENT') // mock-file has been deleted
|
|
43
43
|
sendNotFound(response)
|
package/src/ProxyRelay.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { write } from './utils/fs.js'
|
|
1
3
|
import { Config } from './Config.js'
|
|
4
|
+
import { extFor } from './utils/mime.js'
|
|
5
|
+
import { readBody } from './utils/http-request.js'
|
|
6
|
+
import { makeMockFilename } from './Filename.js'
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
export async function proxy(req, response) {
|
|
5
10
|
const proxyResponse = await fetch(Config.proxyFallback + req.url, {
|
|
6
11
|
method: req.method,
|
|
7
|
-
headers: req.headers
|
|
12
|
+
headers: req.headers,
|
|
13
|
+
body: req.method === 'GET' || req.method === 'HEAD' // TESTME
|
|
14
|
+
? undefined
|
|
15
|
+
: await readBody(req)
|
|
8
16
|
})
|
|
17
|
+
// TODO investigate how to include many repeated headers such as set-cookie
|
|
9
18
|
response.writeHead(proxyResponse.status, Object.fromEntries(proxyResponse.headers))
|
|
10
|
-
|
|
19
|
+
const body = await proxyResponse.text()
|
|
20
|
+
response.end(body)
|
|
21
|
+
|
|
22
|
+
if (Config.collectProxied) { // TESTME
|
|
23
|
+
const ext = extFor(proxyResponse.headers.get('content-type'))
|
|
24
|
+
const filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
|
|
25
|
+
write(join(Config.mocksDir, filename), body)
|
|
26
|
+
}
|
|
11
27
|
}
|
package/src/utils/fs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path, { join } from 'node:path'
|
|
2
|
-
import { lstatSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
|
|
@@ -14,3 +14,8 @@ export const listFilesRecursively = dir => {
|
|
|
14
14
|
? files.map(f => f.replaceAll(path.sep, path.posix.sep)) // TESTME
|
|
15
15
|
: files
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
export const write = (fPath, body) => {
|
|
19
|
+
mkdirSync(path.dirname(fPath), { recursive: true })
|
|
20
|
+
writeFileSync(fPath, body)
|
|
21
|
+
}
|
|
@@ -3,10 +3,11 @@ export const StandardMethods = [
|
|
|
3
3
|
'HEAD', 'OPTIONS', 'TRACE', 'CONNECT'
|
|
4
4
|
]
|
|
5
5
|
|
|
6
|
+
export class BodyReaderError extends Error {}
|
|
6
7
|
|
|
7
|
-
export
|
|
8
|
+
export const parseJSON = req => readBody(req, JSON.parse)
|
|
8
9
|
|
|
9
|
-
export function
|
|
10
|
+
export function readBody(req, parser = a => a) {
|
|
10
11
|
return new Promise((resolve, reject) => {
|
|
11
12
|
const MAX_BODY_SIZE = 200 * 1024
|
|
12
13
|
const expectedLength = req.headers['content-length'] | 0
|
|
@@ -29,13 +30,13 @@ export function parseJSON(req) {
|
|
|
29
30
|
req.removeListener('end', onEnd)
|
|
30
31
|
req.removeListener('error', onEnd)
|
|
31
32
|
if (lengthSoFar !== expectedLength)
|
|
32
|
-
reject(new
|
|
33
|
+
reject(new BodyReaderError())
|
|
33
34
|
else
|
|
34
35
|
try {
|
|
35
|
-
resolve(
|
|
36
|
+
resolve(parser(Buffer.concat(body).toString()))
|
|
36
37
|
}
|
|
37
38
|
catch (_) {
|
|
38
|
-
reject(new
|
|
39
|
+
reject(new BodyReaderError())
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
})
|
package/src/utils/mime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Config } from '../Config.js'
|
|
2
|
+
import { EXT_FOR_UNKNOWN_MIME } from '../ApiConstants.js'
|
|
2
3
|
|
|
3
4
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
|
4
5
|
// m = {};
|
|
@@ -88,8 +89,21 @@ const mimes = {
|
|
|
88
89
|
|
|
89
90
|
export function mimeFor(filename) {
|
|
90
91
|
const ext = filename.replace(/.*\./, '').toLowerCase()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
return Config.extraMimes[ext] || mimes[ext] || ''
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function extFor(mime) {
|
|
94
96
|
return mime
|
|
97
|
+
? findExt(mime)
|
|
98
|
+
: 'empty'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findExt(targetMime) {
|
|
102
|
+
for (const [ext, mime] of Object.entries(Config.extraMimes))
|
|
103
|
+
if (targetMime === mime)
|
|
104
|
+
return ext
|
|
105
|
+
for (const [ext, mime] of Object.entries(mimes))
|
|
106
|
+
if (targetMime === mime)
|
|
107
|
+
return ext
|
|
108
|
+
return EXT_FOR_UNKNOWN_MIME
|
|
95
109
|
}
|