mockaton 12.7.1 → 13.0.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 +53 -17
- package/index.d.ts +2 -13
- package/package.json +4 -9
- package/src/client/ApiCommander.js +7 -11
- package/src/client/ApiConstants.js +1 -4
- package/src/client/Filename.js +22 -22
- package/src/client/app-header.js +6 -6
- package/src/client/app-payload-viewer.js +52 -25
- package/src/client/app-store.js +11 -64
- package/src/client/app-store.test.js +15 -1
- package/src/client/app.css +54 -44
- package/src/client/app.js +28 -100
- package/src/client/{app-icons.js → graphics.js} +1 -1
- package/src/client/watcherDev.js +7 -7
- package/src/server/Api.js +4 -38
- package/src/server/MockBroker.js +16 -13
- package/src/server/MockDispatcher.js +18 -4
- package/src/server/Mockaton.js +1 -8
- package/src/server/Mockaton.test.config.js +1 -3
- package/src/server/Mockaton.test.js +98 -194
- package/src/server/ProxyRelay.js +26 -15
- package/src/server/Watcher.js +1 -33
- package/src/server/cli.js +9 -13
- package/src/server/cli.test.js +4 -5
- package/src/server/config.js +0 -7
- package/src/server/mockBrokersCollection.js +11 -13
- package/src/server/utils/mime.js +0 -1
- package/src/server/StaticDispatcher.js +0 -36
- package/src/server/staticCollection.js +0 -56
package/src/server/ProxyRelay.js
CHANGED
|
@@ -29,29 +29,40 @@ export async function proxy(req, response, delay) {
|
|
|
29
29
|
return
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
response.writeHead(proxyResponse.status, {
|
|
33
|
+
...Object.fromEntries(proxyResponse.headers),
|
|
34
|
+
'Set-Cookie': proxyResponse.headers.getSetCookie() // parses multiple into an array
|
|
35
|
+
})
|
|
35
36
|
const body = await proxyResponse.text()
|
|
36
37
|
setTimeout(() => response.end(body), delay) // TESTME
|
|
37
38
|
|
|
38
39
|
if (config.collectProxied) {
|
|
39
40
|
const ext = extFor(proxyResponse.headers.get('content-type'))
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
data = JSON.stringify(JSON.parse(body), null, ' ')
|
|
48
|
-
}
|
|
49
|
-
catch {}
|
|
41
|
+
saveMockToDisk(req.url, req.method, proxyResponse.status, ext, body)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function saveMockToDisk(url, method, status, ext, body) {
|
|
46
|
+
if (config.formatCollectedJSON && ext === 'json')
|
|
50
47
|
try {
|
|
51
|
-
|
|
48
|
+
body = JSON.stringify(JSON.parse(body), null, ' ')
|
|
52
49
|
}
|
|
53
50
|
catch (err) {
|
|
54
|
-
logger.warn('
|
|
51
|
+
logger.warn('Invalid JSON response', err)
|
|
55
52
|
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
write(makeUniqueMockFilename(url, method, status, ext), body)
|
|
56
56
|
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
logger.warn('Write access denied', err)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeUniqueMockFilename(url, method, status, ext) {
|
|
63
|
+
let file = makeMockFilename(url, method, status, ext)
|
|
64
|
+
if (isFile(join(config.mocksDir, file)))
|
|
65
|
+
file = makeMockFilename(url, method, status, ext, `(${randomUUID()})`)
|
|
66
|
+
return join(config.mocksDir, file)
|
|
57
67
|
}
|
|
68
|
+
|
package/src/server/Watcher.js
CHANGED
|
@@ -5,12 +5,10 @@ import { EventEmitter } from 'node:events'
|
|
|
5
5
|
import { config } from './config.js'
|
|
6
6
|
import { isFile, isDirectory } from './utils/fs.js'
|
|
7
7
|
|
|
8
|
-
import * as staticCollection from './staticCollection.js'
|
|
9
8
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
let mocksWatcher = null
|
|
13
|
-
let staticWatcher = null
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
/**
|
|
@@ -67,33 +65,6 @@ export function watchMocksDir() {
|
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
|
|
70
|
-
export function watchStaticDir() {
|
|
71
|
-
const dir = config.staticDir
|
|
72
|
-
if (!dir)
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
staticWatcher = staticWatcher || watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
76
|
-
if (!file)
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
if (isDirectory(join(dir, file))) {
|
|
80
|
-
staticCollection.init()
|
|
81
|
-
uiSyncVersion.increment()
|
|
82
|
-
}
|
|
83
|
-
else if (!isFile(join(dir, file))) { // file deleted
|
|
84
|
-
staticCollection.unregisterMock(file)
|
|
85
|
-
uiSyncVersion.increment()
|
|
86
|
-
}
|
|
87
|
-
else if (staticCollection.registerMock(file))
|
|
88
|
-
uiSyncVersion.increment()
|
|
89
|
-
else {
|
|
90
|
-
// ignore file edits
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
68
|
/** Realtime notify ARR Events */
|
|
98
69
|
export function sseClientSyncVersion(req, response) {
|
|
99
70
|
response.writeHead(200, {
|
|
@@ -113,7 +84,7 @@ export function sseClientSyncVersion(req, response) {
|
|
|
113
84
|
const keepAlive = setInterval(() => {
|
|
114
85
|
response.write(': ping\n\n')
|
|
115
86
|
}, 10_000)
|
|
116
|
-
|
|
87
|
+
|
|
117
88
|
req.on('close', cleanup)
|
|
118
89
|
req.on('error', cleanup)
|
|
119
90
|
function cleanup() {
|
|
@@ -125,12 +96,9 @@ export function sseClientSyncVersion(req, response) {
|
|
|
125
96
|
|
|
126
97
|
export function startWatchers() {
|
|
127
98
|
watchMocksDir()
|
|
128
|
-
watchStaticDir()
|
|
129
99
|
}
|
|
130
100
|
|
|
131
101
|
export function stopWatchers() {
|
|
132
102
|
mocksWatcher?.close()
|
|
133
|
-
staticWatcher?.close()
|
|
134
103
|
mocksWatcher = null
|
|
135
|
-
staticWatcher = null
|
|
136
104
|
}
|
package/src/server/cli.js
CHANGED
|
@@ -11,25 +11,25 @@ import pkgJSON from '../../package.json' with { type: 'json' }
|
|
|
11
11
|
|
|
12
12
|
process.on('unhandledRejection', error => { throw error })
|
|
13
13
|
|
|
14
|
-
let args
|
|
14
|
+
let args, positionals
|
|
15
15
|
try {
|
|
16
|
-
|
|
16
|
+
const result = parseArgs({
|
|
17
17
|
options: {
|
|
18
18
|
config: { short: 'c', type: 'string' },
|
|
19
19
|
|
|
20
20
|
port: { short: 'p', type: 'string' },
|
|
21
21
|
host: { short: 'H', type: 'string' },
|
|
22
22
|
|
|
23
|
-
'mocks-dir': { short: 'm', type: 'string' },
|
|
24
|
-
'static-dir': { short: 's', type: 'string' },
|
|
25
|
-
|
|
26
23
|
quiet: { short: 'q', type: 'boolean' },
|
|
27
24
|
'no-open': { short: 'n', type: 'boolean' },
|
|
28
25
|
|
|
29
26
|
help: { short: 'h', type: 'boolean' },
|
|
30
27
|
version: { short: 'v', type: 'boolean' }
|
|
31
|
-
}
|
|
32
|
-
|
|
28
|
+
},
|
|
29
|
+
allowPositionals: true
|
|
30
|
+
})
|
|
31
|
+
args = result.values
|
|
32
|
+
positionals = result.positionals
|
|
33
33
|
}
|
|
34
34
|
catch (error) {
|
|
35
35
|
console.error(error.message)
|
|
@@ -45,14 +45,11 @@ if (args.version)
|
|
|
45
45
|
|
|
46
46
|
else if (args.help)
|
|
47
47
|
console.log(`
|
|
48
|
-
Usage: mockaton [options]
|
|
48
|
+
Usage: mockaton [mocks-dir] [options]
|
|
49
49
|
|
|
50
50
|
Options:
|
|
51
51
|
-c, --config <file> (default: ./mockaton.config.js)
|
|
52
52
|
|
|
53
|
-
-m, --mocks-dir <dir> (default: ./mockaton-mocks/)
|
|
54
|
-
-s, --static-dir <dir> (default: ./mockaton-static-mocks/)
|
|
55
|
-
|
|
56
53
|
-H, --host <host> (default: 127.0.0.1)
|
|
57
54
|
-p, --port <port> (default: 0) which means auto-assigned
|
|
58
55
|
|
|
@@ -80,8 +77,7 @@ else {
|
|
|
80
77
|
if (args.host) opts.host = args.host
|
|
81
78
|
if (args.port) opts.port = Number.isNaN(Number(args.port)) ? args.port : Number(args.port)
|
|
82
79
|
|
|
83
|
-
if (
|
|
84
|
-
if (args['static-dir']) opts.staticDir = args['static-dir']
|
|
80
|
+
if (positionals[0]) opts.mocksDir = positionals[0]
|
|
85
81
|
|
|
86
82
|
if (args.quiet) opts.logLevel = 'quiet'
|
|
87
83
|
if (args['no-open']) opts.onReady = () => {}
|
package/src/server/cli.test.js
CHANGED
|
@@ -12,7 +12,7 @@ const cli = (...args) => spawnSync(rel('cli.js'), args, { encoding: 'utf8' })
|
|
|
12
12
|
describe('CLI', () => {
|
|
13
13
|
test('invalid flag', () => {
|
|
14
14
|
const { stderr, status } = cli('--invalid-flag')
|
|
15
|
-
equal(stderr.
|
|
15
|
+
equal(stderr.startsWith(`Unknown option '--invalid-flag'`), true)
|
|
16
16
|
equal(status, 1)
|
|
17
17
|
})
|
|
18
18
|
|
|
@@ -24,9 +24,8 @@ describe('CLI', () => {
|
|
|
24
24
|
|
|
25
25
|
test('invalid port', () => {
|
|
26
26
|
const { stderr, status } = cli(
|
|
27
|
-
|
|
28
|
-
'--port', 'not-a-number'
|
|
29
|
-
)
|
|
27
|
+
rel('../../mockaton-mocks'),
|
|
28
|
+
'--port', 'not-a-number')
|
|
30
29
|
equal(stderr.trim(), `port="not-a-number" is invalid`)
|
|
31
30
|
equal(status, 1)
|
|
32
31
|
})
|
|
@@ -39,7 +38,7 @@ describe('CLI', () => {
|
|
|
39
38
|
|
|
40
39
|
test('-h outputs usage message', () => {
|
|
41
40
|
const { stdout, status } = cli('-h')
|
|
42
|
-
equal(stdout.split('\n')[0], 'Usage: mockaton [options]')
|
|
41
|
+
equal(stdout.split('\n')[0], 'Usage: mockaton [mocks-dir] [options]')
|
|
43
42
|
equal(status, 0)
|
|
44
43
|
})
|
|
45
44
|
|
package/src/server/config.js
CHANGED
|
@@ -7,7 +7,6 @@ import { openInBrowser } from './utils/openInBrowser.js'
|
|
|
7
7
|
import { optional, is, validate } from './utils/validate.js'
|
|
8
8
|
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
|
|
12
11
|
|
|
13
12
|
|
|
@@ -19,7 +18,6 @@ import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
|
|
|
19
18
|
* }} */
|
|
20
19
|
const schema = {
|
|
21
20
|
mocksDir: [resolve('mockaton-mocks'), isDirectory],
|
|
22
|
-
staticDir: [resolve('mockaton-static-mocks'), optional(isDirectory)],
|
|
23
21
|
ignore: [/(\.DS_Store|~)$/, is(RegExp)],
|
|
24
22
|
watcherEnabled: [true, is(Boolean)],
|
|
25
23
|
watcherDebounceMs: [80, ms => Number.isInteger(ms) && ms >= 0],
|
|
@@ -78,11 +76,6 @@ export function setup(opts) {
|
|
|
78
76
|
if (opts.mocksDir)
|
|
79
77
|
opts.mocksDir = resolve(opts.mocksDir)
|
|
80
78
|
|
|
81
|
-
if (opts.staticDir)
|
|
82
|
-
opts.staticDir = resolve(opts.staticDir)
|
|
83
|
-
else if (!isDirectory(defaults.staticDir))
|
|
84
|
-
opts.staticDir = ''
|
|
85
|
-
|
|
86
79
|
Object.assign(config, opts)
|
|
87
80
|
validate(config, ConfigValidator)
|
|
88
81
|
logger.setLevel(config.logLevel)
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { basename } from 'node:path'
|
|
2
2
|
|
|
3
|
-
import { logger } from './utils/logger.js'
|
|
4
|
-
import { listFilesRecursively } from './utils/fs.js'
|
|
5
|
-
|
|
6
3
|
import { cookie } from './cookie.js'
|
|
7
4
|
import { MockBroker } from './MockBroker.js'
|
|
5
|
+
import { parseFilename } from '../client/Filename.js'
|
|
6
|
+
import { listFilesRecursively } from './utils/fs.js'
|
|
8
7
|
import { config, isFileAllowed } from './config.js'
|
|
9
|
-
import { parseFilename, validateFilename } from '../client/Filename.js'
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
/**
|
|
@@ -42,8 +40,7 @@ export function init() {
|
|
|
42
40
|
/** @returns {boolean} registered */
|
|
43
41
|
export function registerMock(file, isFromWatcher = false) {
|
|
44
42
|
if (brokerByFilename(file)?.hasMock(file)
|
|
45
|
-
|| !isFileAllowed(basename(file))
|
|
46
|
-
|| !filenameIsValid(file))
|
|
43
|
+
|| !isFileAllowed(basename(file)))
|
|
47
44
|
return false
|
|
48
45
|
|
|
49
46
|
const { method, urlMask } = parseFilename(file)
|
|
@@ -62,13 +59,6 @@ export function registerMock(file, isFromWatcher = false) {
|
|
|
62
59
|
return true
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
function filenameIsValid(file) {
|
|
66
|
-
const error = validateFilename(file)
|
|
67
|
-
if (error)
|
|
68
|
-
logger.warn(error, file)
|
|
69
|
-
return !error
|
|
70
|
-
}
|
|
71
|
-
|
|
72
62
|
export function unregisterMock(file) {
|
|
73
63
|
const broker = brokerByFilename(file)
|
|
74
64
|
const hasNoMoreMocks = broker?.unregister(file)
|
|
@@ -99,6 +89,14 @@ export function brokerByRoute(method, url) {
|
|
|
99
89
|
for (let i = brokers.length - 1; i >= 0; i--)
|
|
100
90
|
if (brokers[i].urlMaskMatches(url))
|
|
101
91
|
return brokers[i]
|
|
92
|
+
|
|
93
|
+
// TODO Verify
|
|
94
|
+
if (method === 'GET') {
|
|
95
|
+
const indexUrl = url.endsWith('/') ? url + 'index.html' : url + '/index.html'
|
|
96
|
+
for (let i = brokers.length - 1; i >= 0; i--)
|
|
97
|
+
if (brokers[i].urlMaskMatches(indexUrl))
|
|
98
|
+
return brokers[i]
|
|
99
|
+
}
|
|
102
100
|
}
|
|
103
101
|
|
|
104
102
|
export function extractAllComments() {
|
package/src/server/utils/mime.js
CHANGED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
3
|
-
|
|
4
|
-
import { isFile } from './utils/fs.js'
|
|
5
|
-
import { logger } from './utils/logger.js'
|
|
6
|
-
import { mimeFor } from './utils/mime.js'
|
|
7
|
-
|
|
8
|
-
import { brokerByRoute } from './staticCollection.js'
|
|
9
|
-
import { config, calcDelay } from './config.js'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// TODO HEAD
|
|
13
|
-
export async function dispatchStatic(req, response) {
|
|
14
|
-
const broker = brokerByRoute(req.url)
|
|
15
|
-
|
|
16
|
-
setTimeout(async () => {
|
|
17
|
-
if (!broker || broker.status === 404) {
|
|
18
|
-
response.mockNotFound()
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const file = join(config.staticDir, broker.route)
|
|
23
|
-
if (!isFile(file)) {
|
|
24
|
-
response.mockNotFound()
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
logger.accessMock(req.url, 'static200')
|
|
29
|
-
if (req.headers.range)
|
|
30
|
-
await response.partialContent(req.headers.range, file)
|
|
31
|
-
else {
|
|
32
|
-
response.setHeader('Content-Type', mimeFor(file))
|
|
33
|
-
response.end(readFileSync(file))
|
|
34
|
-
}
|
|
35
|
-
}, Number(broker.delayed && calcDelay()))
|
|
36
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { join, basename } from 'node:path'
|
|
2
|
-
import { listFilesRecursively } from './utils/fs.js'
|
|
3
|
-
import { config, isFileAllowed } from './config.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class StaticBroker {
|
|
7
|
-
constructor(route) {
|
|
8
|
-
this.route = route
|
|
9
|
-
this.delayed = false
|
|
10
|
-
this.status = 200 // 200 or 404
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
setDelayed(value) { this.delayed = value }
|
|
14
|
-
setStatus(value) { this.status = value }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** @type {{ [route:string]: StaticBroker }} */
|
|
18
|
-
let collection = {}
|
|
19
|
-
|
|
20
|
-
export const all = () => collection
|
|
21
|
-
|
|
22
|
-
export function init() {
|
|
23
|
-
collection = {}
|
|
24
|
-
listFilesRecursively(config.staticDir)
|
|
25
|
-
.sort()
|
|
26
|
-
.forEach(registerMock)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/** @returns {boolean} registered */
|
|
31
|
-
export function registerMock(relativeFile) {
|
|
32
|
-
if (!isFileAllowed(basename(relativeFile)))
|
|
33
|
-
return false
|
|
34
|
-
|
|
35
|
-
const route = '/' + relativeFile
|
|
36
|
-
if (brokerByRoute(route))
|
|
37
|
-
return false
|
|
38
|
-
|
|
39
|
-
collection[route] = new StaticBroker(route)
|
|
40
|
-
return true
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export function unregisterMock(relativeFile) {
|
|
45
|
-
delete collection['/' + relativeFile]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/** @returns {StaticBroker | undefined} */
|
|
50
|
-
export function brokerByRoute(route) {
|
|
51
|
-
return collection[route] || collection[join(route, 'index.html')]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|