mockaton 13.11.2 → 13.11.3
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/package.json +1 -1
- package/src/server/Api.js +36 -35
- package/src/server/MockDispatcher.js +6 -5
- package/src/server/Mockaton.js +10 -11
- package/src/server/Mockaton.test.js +60 -47
- package/src/server/ProxyRelay.js +3 -2
- package/src/server/UrlParsers.js +1 -1
- package/src/server/UrlParsers.test.js +1 -1
- package/src/server/cli.js +6 -6
- package/src/server/{Watcher.js → stores/Watcher.js} +18 -9
- package/src/server/{mockBrokersCollection.js → stores/brokers.js} +3 -3
- package/src/server/{config.js → stores/config.js} +28 -14
- package/src/server/utils/HttpServerResponse.test.js +2 -2
- package/www/src/assets/openapi.json +1 -1
- /package/src/server/{resolverBypassImportCache.js → ResolverBypassImportCache.js} +0 -0
- /package/src/server/{resolverResolveExtensionless.js → ResolverResolveExtensionless.js} +0 -0
- /package/src/server/{cookie.js → stores/cookies.js} +0 -0
package/package.json
CHANGED
package/src/server/Api.js
CHANGED
|
@@ -4,21 +4,21 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { join, relative } from 'node:path'
|
|
7
|
+
|
|
7
8
|
import { write, rm, isFile, resolveIn } from './utils/fs.js'
|
|
9
|
+
import { removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
|
|
10
|
+
import { sseClientHotReload } from './utils/WatcherDevClient.js'
|
|
8
11
|
|
|
9
|
-
import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
|
|
10
12
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
11
|
-
|
|
12
|
-
import { sseClientHotReload } from './utils/WatcherDevClient.js'
|
|
13
|
-
import { stopMocksDirWatcher, sseClientSyncVersion, uiSyncVersion, watchMocksDir } from './Watcher.js'
|
|
13
|
+
import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
|
|
14
14
|
|
|
15
15
|
import { API } from '../client/ApiConstants.js'
|
|
16
16
|
import { IndexHtml, CSP } from '../client/IndexHtml.js'
|
|
17
17
|
|
|
18
|
-
import { cookie } from './
|
|
19
|
-
import { config, ConfigValidator } from './config.js'
|
|
20
|
-
import * as
|
|
21
|
-
import
|
|
18
|
+
import { cookie } from './stores/cookies.js'
|
|
19
|
+
import { config, ConfigValidator, reinitConfig } from './stores/config.js'
|
|
20
|
+
import * as brokers from './stores/brokers.js'
|
|
21
|
+
import * as Watcher from './stores/Watcher.js'
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
|
|
@@ -32,7 +32,7 @@ const getReqs = new Map([
|
|
|
32
32
|
|
|
33
33
|
[API.root, serveDashboard],
|
|
34
34
|
[API.state, getState],
|
|
35
|
-
[API.syncVersion, sseClientSyncVersion],
|
|
35
|
+
[API.syncVersion, Watcher.sseClientSyncVersion],
|
|
36
36
|
|
|
37
37
|
[API.watchHotReload, onDevWatch],
|
|
38
38
|
[API.openAPI, (_, response) => response.json(openapi)],
|
|
@@ -94,9 +94,9 @@ function serveDashboard(_, response) {
|
|
|
94
94
|
function getState(_, response) {
|
|
95
95
|
response.json({
|
|
96
96
|
cookies: cookie.list(),
|
|
97
|
-
comments:
|
|
97
|
+
comments: brokers.extractAllComments(),
|
|
98
98
|
|
|
99
|
-
brokersByMethod:
|
|
99
|
+
brokersByMethod: brokers.all(),
|
|
100
100
|
|
|
101
101
|
delay: config.delay,
|
|
102
102
|
delayJitter: config.delayJitter,
|
|
@@ -118,10 +118,11 @@ function onDevWatch(req, response) {
|
|
|
118
118
|
/** # PATCH */
|
|
119
119
|
|
|
120
120
|
function reset(_, response) {
|
|
121
|
-
|
|
121
|
+
reinitConfig()
|
|
122
|
+
brokers.init()
|
|
122
123
|
cookie.init(config.cookies)
|
|
123
124
|
response.ok()
|
|
124
|
-
|
|
125
|
+
Watcher.emitChange()
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
|
|
@@ -133,7 +134,7 @@ async function setCorsAllowed(req, response) {
|
|
|
133
134
|
else {
|
|
134
135
|
config.corsAllowed = corsAllowed
|
|
135
136
|
response.ok()
|
|
136
|
-
|
|
137
|
+
Watcher.emitChange()
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -146,7 +147,7 @@ async function setGlobalDelay(req, response) {
|
|
|
146
147
|
else {
|
|
147
148
|
config.delay = delay
|
|
148
149
|
response.ok()
|
|
149
|
-
|
|
150
|
+
Watcher.emitChange()
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -158,7 +159,7 @@ async function setGlobalDelayJitter(req, response) {
|
|
|
158
159
|
else {
|
|
159
160
|
config.delayJitter = jitter
|
|
160
161
|
response.ok()
|
|
161
|
-
|
|
162
|
+
Watcher.emitChange()
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -170,7 +171,7 @@ async function selectCookie(req, response) {
|
|
|
170
171
|
response.unprocessable(error?.message || error)
|
|
171
172
|
else {
|
|
172
173
|
response.json(cookie.list())
|
|
173
|
-
|
|
174
|
+
Watcher.emitChange()
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -183,7 +184,7 @@ async function setProxyFallback(req, response) {
|
|
|
183
184
|
else {
|
|
184
185
|
config.proxyFallback = fallback
|
|
185
186
|
response.ok()
|
|
186
|
-
|
|
187
|
+
Watcher.emitChange()
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
@@ -195,7 +196,7 @@ async function setCollectProxied(req, response) {
|
|
|
195
196
|
else {
|
|
196
197
|
config.collectProxied = collectProxied
|
|
197
198
|
response.ok()
|
|
198
|
-
|
|
199
|
+
Watcher.emitChange()
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
|
|
@@ -203,41 +204,41 @@ async function setCollectProxied(req, response) {
|
|
|
203
204
|
|
|
204
205
|
async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
205
206
|
const comment = await req.json()
|
|
206
|
-
|
|
207
|
+
brokers.setMocksMatchingComment(comment)
|
|
207
208
|
response.ok()
|
|
208
|
-
|
|
209
|
+
Watcher.emitChange()
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
|
|
212
213
|
async function selectMock(req, response) {
|
|
213
214
|
const file = await req.json()
|
|
214
|
-
const broker =
|
|
215
|
+
const broker = brokers.brokerByFilename(file)
|
|
215
216
|
if (!broker || !broker.hasMock(file))
|
|
216
217
|
response.unprocessable(`Missing Mock: ${file}`)
|
|
217
218
|
else {
|
|
218
219
|
broker.selectFile(file)
|
|
219
220
|
response.json(broker)
|
|
220
|
-
|
|
221
|
+
Watcher.emitChange()
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
|
|
225
226
|
async function toggleRouteStatus(req, response) {
|
|
226
227
|
const [method, urlMask, status] = await req.json()
|
|
227
|
-
const broker =
|
|
228
|
+
const broker = brokers.brokerByRoute(method, urlMask)
|
|
228
229
|
if (!broker)
|
|
229
230
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
230
231
|
else {
|
|
231
232
|
broker.toggleStatus(status)
|
|
232
233
|
response.json(broker)
|
|
233
|
-
|
|
234
|
+
Watcher.emitChange()
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
|
|
238
239
|
async function setRouteIsDelayed(req, response) {
|
|
239
240
|
const [method, urlMask, delayed] = await req.json()
|
|
240
|
-
const broker =
|
|
241
|
+
const broker = brokers.brokerByRoute(method, urlMask)
|
|
241
242
|
if (!broker)
|
|
242
243
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
243
244
|
else if (typeof delayed !== 'boolean')
|
|
@@ -245,14 +246,14 @@ async function setRouteIsDelayed(req, response) {
|
|
|
245
246
|
else {
|
|
246
247
|
broker.setDelayed(delayed)
|
|
247
248
|
response.json(broker)
|
|
248
|
-
|
|
249
|
+
Watcher.emitChange()
|
|
249
250
|
}
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
|
|
253
254
|
async function setRouteIsProxied(req, response) {
|
|
254
255
|
const [method, urlMask, proxied] = await req.json()
|
|
255
|
-
const broker =
|
|
256
|
+
const broker = brokers.brokerByRoute(method, urlMask)
|
|
256
257
|
if (!broker)
|
|
257
258
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
258
259
|
else if (typeof proxied !== 'boolean')
|
|
@@ -262,7 +263,7 @@ async function setRouteIsProxied(req, response) {
|
|
|
262
263
|
else {
|
|
263
264
|
broker.setProxied(proxied)
|
|
264
265
|
response.json(broker)
|
|
265
|
-
|
|
266
|
+
Watcher.emitChange()
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
@@ -282,8 +283,8 @@ async function writeMock(req, response) {
|
|
|
282
283
|
await write(path, content)
|
|
283
284
|
|
|
284
285
|
if (!config.watcherEnabled) {
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
brokers.registerMock(file, true)
|
|
287
|
+
Watcher.emitChange()
|
|
287
288
|
}
|
|
288
289
|
response.ok()
|
|
289
290
|
}
|
|
@@ -305,8 +306,8 @@ async function deleteMock(req, response) {
|
|
|
305
306
|
await rm(path)
|
|
306
307
|
|
|
307
308
|
if (!config.watcherEnabled) {
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
brokers.unregisterMock(file)
|
|
310
|
+
Watcher.emitChange()
|
|
310
311
|
}
|
|
311
312
|
response.ok()
|
|
312
313
|
}
|
|
@@ -319,9 +320,9 @@ async function setWatchMocks(req, response) {
|
|
|
319
320
|
response.unprocessable(`Expected boolean for "watchMocks"`)
|
|
320
321
|
else {
|
|
321
322
|
if (enabled)
|
|
322
|
-
watchMocksDir()
|
|
323
|
+
Watcher.watchMocksDir()
|
|
323
324
|
else
|
|
324
|
-
|
|
325
|
+
Watcher.unwatchMocksDir()
|
|
325
326
|
response.ok()
|
|
326
327
|
}
|
|
327
328
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
|
|
3
|
-
import { logger } from './utils/logger.js'
|
|
4
3
|
import { proxy } from './ProxyRelay.js'
|
|
5
|
-
import { cookie } from './
|
|
6
|
-
import { parseFilename } from '../client/Filename.js'
|
|
4
|
+
import { cookie } from './stores/cookies.js'
|
|
7
5
|
import { echoFilePlugin } from './MockDispatcherPlugins.js'
|
|
8
|
-
import { brokerByRoute } from './
|
|
9
|
-
import { config, calcDelay } from './config.js'
|
|
6
|
+
import { brokerByRoute } from './stores/brokers.js'
|
|
7
|
+
import { config, calcDelay } from './stores/config.js'
|
|
8
|
+
|
|
9
|
+
import { logger } from './utils/logger.js'
|
|
10
|
+
import { parseFilename } from '../client/Filename.js'
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
export async function dispatchMock(req, response) {
|
package/src/server/Mockaton.js
CHANGED
|
@@ -7,29 +7,28 @@ import { logger } from './utils/logger.js'
|
|
|
7
7
|
import { ServerResponse } from './utils/HttpServerResponse.js'
|
|
8
8
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
9
9
|
import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
|
|
10
|
+
import { watchDevSPA } from './utils/WatcherDevClient.js'
|
|
10
11
|
|
|
11
12
|
import { API } from '../client/ApiConstants.js'
|
|
12
|
-
import { cookie } from './cookie.js'
|
|
13
|
-
import { config, setup } from './config.js'
|
|
14
|
-
import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
|
|
15
13
|
|
|
14
|
+
import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
|
|
15
|
+
import { cookie } from './stores/cookies.js'
|
|
16
|
+
import { config, initConfig } from './stores/config.js'
|
|
17
|
+
import * as brokers from './stores/brokers.js'
|
|
16
18
|
import { dispatchMock } from './MockDispatcher.js'
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
import { watchDevSPA } from './utils/WatcherDevClient.js'
|
|
20
|
-
import { watchMocksDir } from './Watcher.js'
|
|
19
|
+
import { watchMocksDir } from './stores/Watcher.js'
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
export function Mockaton(options) {
|
|
24
23
|
return new Promise((resolve, reject) => {
|
|
25
|
-
|
|
24
|
+
initConfig(options)
|
|
26
25
|
cookie.init(config.cookies)
|
|
27
|
-
|
|
26
|
+
brokers.init()
|
|
28
27
|
|
|
29
|
-
register('./
|
|
28
|
+
register('./ResolverResolveExtensionless.js', import.meta.url)
|
|
30
29
|
|
|
31
30
|
if (config.bypassImportCache)
|
|
32
|
-
register('./
|
|
31
|
+
register('./ResolverBypassImportCache.js', import.meta.url)
|
|
33
32
|
|
|
34
33
|
if (config.watcherEnabled)
|
|
35
34
|
watchMocksDir()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
1
2
|
import { join } from 'node:path'
|
|
2
3
|
import { spawn } from 'node:child_process'
|
|
3
4
|
import { tmpdir } from 'node:os'
|
|
@@ -7,23 +8,32 @@ import { mkdtempSync } from 'node:fs'
|
|
|
7
8
|
import { randomUUID } from 'node:crypto'
|
|
8
9
|
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
9
10
|
import { describe, test, before, beforeEach, after } from 'node:test'
|
|
10
|
-
import { unlink, mkdir, readFile, rename, readdir, writeFile, rm } from 'node:fs/promises'
|
|
11
11
|
|
|
12
|
+
import CONFIG from './Mockaton.test.config.js'
|
|
13
|
+
import { config } from './stores/config.js'
|
|
12
14
|
import { mimeFor } from './utils/mime.js'
|
|
13
15
|
import { API } from '../client/ApiConstants.js'
|
|
14
16
|
import { Commander } from '../client/ApiCommander.js'
|
|
15
17
|
import { parseFilename } from '../client/Filename.js'
|
|
16
18
|
|
|
17
|
-
import CONFIG from './Mockaton.test.config.js'
|
|
18
|
-
import { config } from './config.js'
|
|
19
19
|
|
|
20
|
+
const mocksDir = new class {
|
|
21
|
+
value = mkdtempSync(join(tmpdir(), 'mocks'))
|
|
22
|
+
|
|
23
|
+
rm = f => fs.unlink(join(this.value, f))
|
|
24
|
+
read = f => fs.readFile(join(this.value, f), 'utf8')
|
|
25
|
+
write = (f, data) => fs.writeFile(join(this.value, f), data)
|
|
26
|
+
rename = (src, target) => fs.rename(join(this.value, src), join(this.value, target))
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
list = d => fs.readdir(join(this.value, d))
|
|
29
|
+
rmdir = d => fs.rm(join(this.value, d), { recursive: true })
|
|
30
|
+
mkdir = d => fs.mkdir(join(this.value, d), { recursive: true })
|
|
31
|
+
}
|
|
22
32
|
|
|
23
33
|
const stdout = []
|
|
24
34
|
const stderr = []
|
|
25
35
|
const proc = spawn(join(import.meta.dirname, 'cli.js'), [
|
|
26
|
-
mocksDir,
|
|
36
|
+
mocksDir.value,
|
|
27
37
|
'--config', join(import.meta.dirname, 'Mockaton.test.config.js'),
|
|
28
38
|
'--no-open'
|
|
29
39
|
])
|
|
@@ -48,17 +58,6 @@ const serverAddr = await new Promise((resolve, reject) => {
|
|
|
48
58
|
|
|
49
59
|
after(() => proc.kill('SIGUSR2'))
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
const rmFromMocksDir = f => unlink(join(mocksDir, f))
|
|
53
|
-
const readFromMocksDir = f => readFile(join(mocksDir, f), 'utf8')
|
|
54
|
-
const writeInMocksDir = (f, data) => writeFile(join(mocksDir, f), data)
|
|
55
|
-
const renameInMocksDir = (src, target) => rename(join(mocksDir, src), join(mocksDir, target))
|
|
56
|
-
|
|
57
|
-
const listFromMocksDir = d => readdir(join(mocksDir, d))
|
|
58
|
-
const rmDirFromMocks = d => rm(join(mocksDir, d), { recursive: true })
|
|
59
|
-
const makeDirInMocks = dir => mkdir(join(mocksDir, dir), { recursive: true })
|
|
60
|
-
|
|
61
|
-
|
|
62
61
|
const api = new Commander(serverAddr)
|
|
63
62
|
|
|
64
63
|
|
|
@@ -304,6 +303,7 @@ describe('Delay', () => {
|
|
|
304
303
|
const r = await api.setGlobalDelayJitter(0.1)
|
|
305
304
|
equal(r.status, 200)
|
|
306
305
|
equal((await fetchState()).delayJitter, 0.1)
|
|
306
|
+
await api.setGlobalDelayJitter(0)
|
|
307
307
|
})
|
|
308
308
|
})
|
|
309
309
|
|
|
@@ -389,14 +389,14 @@ describe('Proxy Fallback', () => {
|
|
|
389
389
|
deepEqual(await r2.json(), BODY_PAYLOAD)
|
|
390
390
|
deepEqual(await r1.json(), BODY_PAYLOAD)
|
|
391
391
|
|
|
392
|
-
const savedMocks = await
|
|
392
|
+
const savedMocks = await mocksDir.list('non-existing-mock')
|
|
393
393
|
equal(savedMocks.length, 2)
|
|
394
394
|
|
|
395
|
-
equal(await
|
|
395
|
+
equal(await mocksDir.read('non-existing-mock/[id].POST.423.json'), expectedBody)
|
|
396
396
|
for (const m of savedMocks) {
|
|
397
397
|
const f = join('non-existing-mock', m)
|
|
398
|
-
equal(await
|
|
399
|
-
await
|
|
398
|
+
equal(await mocksDir.read(f), expectedBody)
|
|
399
|
+
await mocksDir.rm(f)
|
|
400
400
|
}
|
|
401
401
|
})
|
|
402
402
|
})
|
|
@@ -920,7 +920,7 @@ describe('Dynamic Params', () => {
|
|
|
920
920
|
const fx2 = new Fixture('dynamic-params/[id]/suffix/[id].GET.200.txt')
|
|
921
921
|
const fx3 = new Fixture('dynamic-params/exact-route.GET.200.txt')
|
|
922
922
|
before(async () => {
|
|
923
|
-
await
|
|
923
|
+
await mocksDir.mkdir('dynamic-params/[id]/suffix/[id]')
|
|
924
924
|
await fx0.write()
|
|
925
925
|
await fx1.write()
|
|
926
926
|
await fx2.write()
|
|
@@ -956,7 +956,7 @@ describe('Dynamic Params', () => {
|
|
|
956
956
|
|
|
957
957
|
test('Dynamic Params on partial segments', async () => {
|
|
958
958
|
const fx = new Fixture('dynamic-params-partial-[id]/foo.GET.200.txt')
|
|
959
|
-
await
|
|
959
|
+
await mocksDir.mkdir('dynamic-params-partial-[id]')
|
|
960
960
|
await fx.write()
|
|
961
961
|
const r = await request('/dynamic-params-partial-999/foo')
|
|
962
962
|
equal(await r.text(), fx.body)
|
|
@@ -968,7 +968,7 @@ describe('Query String', () => {
|
|
|
968
968
|
const fx0 = new Fixture('query-string?foo=[foo]&bar=[bar].GET.200.json')
|
|
969
969
|
const fx1 = new Fixture('query-string/[id]?limit=[limit].GET.200.json')
|
|
970
970
|
before(async () => {
|
|
971
|
-
await
|
|
971
|
+
await mocksDir.mkdir('query-string')
|
|
972
972
|
await fx0.write()
|
|
973
973
|
await fx1.write()
|
|
974
974
|
await api.reset()
|
|
@@ -1127,10 +1127,10 @@ describe('Registering Mocks', () => {
|
|
|
1127
1127
|
test('when watcher is off, newly added mocks do not get registered', async () => {
|
|
1128
1128
|
await api.setWatchMocks(false)
|
|
1129
1129
|
const fx = new FixtureExternal('non-auto-registered-file.GET.200.json')
|
|
1130
|
-
await
|
|
1130
|
+
await mocksDir.write(fx.file, fx.body)
|
|
1131
1131
|
await sleep(100)
|
|
1132
1132
|
equal(await fx.fetchBroker(), undefined)
|
|
1133
|
-
await
|
|
1133
|
+
await mocksDir.rm(fx.file)
|
|
1134
1134
|
})
|
|
1135
1135
|
|
|
1136
1136
|
test('register', async () => {
|
|
@@ -1176,27 +1176,27 @@ describe('Registering Mocks', () => {
|
|
|
1176
1176
|
const fx0 = new FixtureExternal('reg0/runtime0.GET.200.txt')
|
|
1177
1177
|
let version
|
|
1178
1178
|
before(async () => {
|
|
1179
|
-
await
|
|
1180
|
-
await
|
|
1179
|
+
await mocksDir.mkdir('reg0')
|
|
1180
|
+
await mocksDir.write(fx0.file, fx0.body)
|
|
1181
1181
|
version = await resolveOnNextSyncVersion(-1)
|
|
1182
1182
|
})
|
|
1183
1183
|
|
|
1184
1184
|
const fx = new FixtureExternal('runtime1.GET.200.txt')
|
|
1185
1185
|
test('responds when a file is added', async () => {
|
|
1186
1186
|
const prom = resolveOnNextSyncVersion(version)
|
|
1187
|
-
await
|
|
1187
|
+
await mocksDir.write(fx.file, fx.body)
|
|
1188
1188
|
equal(await prom, version + 1)
|
|
1189
1189
|
})
|
|
1190
1190
|
|
|
1191
1191
|
test('responds when a file is deleted', async () => {
|
|
1192
1192
|
const prom = resolveOnNextSyncVersion(version + 1)
|
|
1193
|
-
await
|
|
1193
|
+
await mocksDir.rm(fx.file)
|
|
1194
1194
|
equal(await prom, version + 2)
|
|
1195
1195
|
})
|
|
1196
1196
|
|
|
1197
1197
|
test('responds when dir is renamed', async () => {
|
|
1198
1198
|
const prom = resolveOnNextSyncVersion(version + 2)
|
|
1199
|
-
await
|
|
1199
|
+
await mocksDir.rename('reg0', 'reg1')
|
|
1200
1200
|
equal(await prom, version + 3)
|
|
1201
1201
|
|
|
1202
1202
|
const s = await fetchState()
|
|
@@ -1209,7 +1209,7 @@ describe('Registering Mocks', () => {
|
|
|
1209
1209
|
await fx.writeExternally()
|
|
1210
1210
|
config.watcherDebounceMs = 100 // Because on macOS rmdir triggers a few events
|
|
1211
1211
|
const nextVerPromise = resolveOnNextSyncVersion()
|
|
1212
|
-
await
|
|
1212
|
+
await mocksDir.rmdir('api/bulk-delete')
|
|
1213
1213
|
await nextVerPromise
|
|
1214
1214
|
equal(await fx.fetchBroker(), undefined)
|
|
1215
1215
|
await sleep(50) // Only for Docker, not sure why we need to delay the server teardown
|
|
@@ -1221,24 +1221,37 @@ describe('Registering Mocks', () => {
|
|
|
1221
1221
|
* This is for listening to real-time updates. It responds when a new mock is added, deleted, or renamed. */
|
|
1222
1222
|
async function resolveOnNextSyncVersion(currSyncVer = undefined) {
|
|
1223
1223
|
let skipFirst = currSyncVer === undefined
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1224
|
+
const reader = (await api.getSyncVersion())
|
|
1225
|
+
.body.pipeThrough(new TextDecoderStream())
|
|
1226
|
+
.getReader()
|
|
1226
1227
|
let buffer = ''
|
|
1227
1228
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1229
|
+
try {
|
|
1230
|
+
while (true) {
|
|
1231
|
+
try {
|
|
1232
|
+
const { done, value } = await reader.read()
|
|
1233
|
+
if (done) break
|
|
1234
|
+
buffer += value
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
break
|
|
1238
|
+
}
|
|
1239
|
+
const parts = buffer.split('\n\n')
|
|
1240
|
+
buffer = parts.pop() || ''
|
|
1241
|
+
|
|
1242
|
+
for (const event of parts)
|
|
1243
|
+
for (const line of event.split(/\r?\n/))
|
|
1244
|
+
if (line.startsWith('data:')) {
|
|
1245
|
+
const v = Number(line.slice(5).trim())
|
|
1246
|
+
if (skipFirst || v === currSyncVer)
|
|
1247
|
+
skipFirst = false
|
|
1248
|
+
else
|
|
1249
|
+
return v
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
finally {
|
|
1254
|
+
reader.cancel().catch(() => {})
|
|
1242
1255
|
}
|
|
1243
1256
|
}
|
|
1244
1257
|
|
package/src/server/ProxyRelay.js
CHANGED
|
@@ -4,12 +4,13 @@ import { randomUUID } from 'node:crypto'
|
|
|
4
4
|
import { extFor } from './utils/mime.js'
|
|
5
5
|
import { write, isFile, resolveIn } from './utils/fs.js'
|
|
6
6
|
import { BodyReaderError } from './utils/HttpIncomingMessage.js'
|
|
7
|
-
|
|
8
|
-
import { config } from './config.js'
|
|
9
7
|
import { logger } from './utils/logger.js'
|
|
8
|
+
|
|
10
9
|
import { makeMockFilename } from '../client/Filename.js'
|
|
11
10
|
import { EXT_EMPTY, EXT_UNKNOWN_MIME } from '../client/ApiConstants.js'
|
|
12
11
|
|
|
12
|
+
import { config } from './stores/config.js'
|
|
13
|
+
|
|
13
14
|
|
|
14
15
|
export async function proxy(req, response, delay) {
|
|
15
16
|
let proxyResponse
|
package/src/server/UrlParsers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { relative } from 'node:path'
|
|
2
|
-
import { config } from './config.js'
|
|
2
|
+
import { config } from './stores/config.js'
|
|
3
3
|
import { decode, removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
|
|
4
4
|
import { parseFilename, removeTrailingSlash } from '../client/Filename.js'
|
|
5
5
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, describe } from 'node:test'
|
|
2
2
|
import { deepEqual, equal } from 'node:assert/strict'
|
|
3
3
|
import { parseSegments, parseQueryParams } from './UrlParsers.js'
|
|
4
|
-
import { config } from './config.js'
|
|
4
|
+
import { config } from './stores/config.js'
|
|
5
5
|
|
|
6
6
|
test('parseQueryParams', () => {
|
|
7
7
|
const searchParams = parseQueryParams('/api/foo?limit=123')
|
package/src/server/cli.js
CHANGED
|
@@ -4,9 +4,9 @@ import { pathToFileURL } from 'node:url'
|
|
|
4
4
|
import { resolve, join } from 'node:path'
|
|
5
5
|
import { parseArgs } from 'node:util'
|
|
6
6
|
|
|
7
|
+
import { config } from './stores/config.js'
|
|
7
8
|
import { isFile, isDirectory } from './utils/fs.js'
|
|
8
9
|
import { Mockaton } from '../../index.js'
|
|
9
|
-
import { config } from './config.js'
|
|
10
10
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
11
11
|
|
|
12
12
|
|
|
@@ -19,14 +19,14 @@ SYNOPSIS
|
|
|
19
19
|
|
|
20
20
|
OPTIONS
|
|
21
21
|
-c, --config <file> (default: ./${DEFAULT_CONFIG_FILE})
|
|
22
|
-
|
|
23
|
-
-H, --host <host> (default:
|
|
24
|
-
-p, --port <port> (default:
|
|
25
|
-
|
|
22
|
+
|
|
23
|
+
-H, --host <host> (default: ${config.host})
|
|
24
|
+
-p, --port <port> (default: ${config.port}) 0 means auto-assigned
|
|
25
|
+
|
|
26
26
|
-q, --quiet Show errors only
|
|
27
27
|
--no-open Don't open dashboard in a browser
|
|
28
28
|
--no-read-only Allow writing and deleting mocks via API
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
--skills Show AI agent SKILL.md file path
|
|
31
31
|
-h, --help
|
|
32
32
|
-v, --version
|
|
@@ -3,8 +3,8 @@ import { watch } from 'node:fs'
|
|
|
3
3
|
import { EventEmitter } from 'node:events'
|
|
4
4
|
|
|
5
5
|
import { config } from './config.js'
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import * as brokers from './brokers.js'
|
|
7
|
+
import { isFile, isDirectory } from '../utils/fs.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
let mocksWatcher = null
|
|
@@ -38,6 +38,10 @@ export const uiSyncVersion = new class extends EventEmitter {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function emitChange() {
|
|
42
|
+
uiSyncVersion.increment()
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
|
|
42
46
|
export function watchMocksDir() {
|
|
43
47
|
const dir = config.mocksDir
|
|
@@ -46,22 +50,25 @@ export function watchMocksDir() {
|
|
|
46
50
|
return
|
|
47
51
|
|
|
48
52
|
if (isDirectory(join(dir, file))) {
|
|
49
|
-
|
|
53
|
+
brokers.init()
|
|
50
54
|
uiSyncVersion.increment()
|
|
51
55
|
}
|
|
52
56
|
else if (!isFile(join(dir, file))) { // file deleted
|
|
53
|
-
|
|
57
|
+
brokers.unregisterMock(file)
|
|
54
58
|
uiSyncVersion.increment()
|
|
55
59
|
}
|
|
56
|
-
else if (
|
|
60
|
+
else if (brokers.registerMock(file, Boolean('isFromWatcher')))
|
|
57
61
|
uiSyncVersion.increment()
|
|
58
62
|
else {
|
|
59
63
|
// ignore file edits
|
|
60
64
|
}
|
|
61
65
|
})
|
|
66
|
+
mocksWatcher?.on('error', () => {
|
|
67
|
+
// on linux, subdir deletion can trigger inotify IN_IGNORED
|
|
68
|
+
})
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
export function
|
|
71
|
+
export function unwatchMocksDir() {
|
|
65
72
|
mocksWatcher?.close()
|
|
66
73
|
mocksWatcher = null
|
|
67
74
|
}
|
|
@@ -75,19 +82,21 @@ export function sseClientSyncVersion(req, response) {
|
|
|
75
82
|
})
|
|
76
83
|
response.flushHeaders()
|
|
77
84
|
|
|
78
|
-
function sendVersion() {
|
|
79
|
-
response.write(`data: ${uiSyncVersion.version}\n\n`)
|
|
80
|
-
}
|
|
81
85
|
|
|
82
86
|
sendVersion()
|
|
83
87
|
uiSyncVersion.subscribe(sendVersion)
|
|
84
88
|
|
|
89
|
+
function sendVersion() {
|
|
90
|
+
response.write(`data: ${uiSyncVersion.version}\n\n`)
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
const keepAlive = setInterval(() => {
|
|
86
94
|
response.write(': ping\n\n')
|
|
87
95
|
}, 10_000)
|
|
88
96
|
|
|
89
97
|
req.on('close', cleanup)
|
|
90
98
|
req.on('error', cleanup)
|
|
99
|
+
response.on('error', cleanup)
|
|
91
100
|
function cleanup() {
|
|
92
101
|
clearInterval(keepAlive)
|
|
93
102
|
uiSyncVersion.unsubscribe(sendVersion)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { basename } from 'node:path'
|
|
2
2
|
|
|
3
|
-
import { MockBroker } from './MockBroker.js'
|
|
4
|
-
import { parseFilename } from '../client/Filename.js'
|
|
5
|
-
import { listFilesRecursively } from './utils/fs.js'
|
|
6
3
|
import { config, isFileAllowed } from './config.js'
|
|
4
|
+
import { MockBroker } from '../MockBroker.js'
|
|
5
|
+
import { listFilesRecursively } from '../utils/fs.js'
|
|
6
|
+
import { parseFilename } from '../../client/Filename.js'
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -2,13 +2,13 @@ import { resolve } from 'node:path'
|
|
|
2
2
|
import { lstatSync } from 'node:fs'
|
|
3
3
|
import { METHODS } from 'node:http'
|
|
4
4
|
|
|
5
|
-
import { logger } from '
|
|
6
|
-
import { registerMimes } from '
|
|
7
|
-
import { openInBrowser } from '
|
|
8
|
-
import { is, validate, isInt, isFloat, isOneOf, optionalURL } from '
|
|
9
|
-
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from '
|
|
5
|
+
import { logger } from '../utils/logger.js'
|
|
6
|
+
import { registerMimes } from '../utils/mime.js'
|
|
7
|
+
import { openInBrowser } from '../utils/openInBrowser.js'
|
|
8
|
+
import { is, validate, isInt, isFloat, isOneOf, optionalURL } from '../utils/validate.js'
|
|
9
|
+
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from '../utils/http-cors.js'
|
|
10
10
|
|
|
11
|
-
import { jsToJsonPlugin } from '
|
|
11
|
+
import { jsToJsonPlugin } from '../MockDispatcherPlugins.js'
|
|
12
12
|
|
|
13
13
|
/** @type {{
|
|
14
14
|
* [K in keyof Config]-?: [
|
|
@@ -47,15 +47,12 @@ const schema = {
|
|
|
47
47
|
corsCredentials: [true, is(Boolean)],
|
|
48
48
|
corsMaxAge: [0, is(Number)],
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
[/\.(js|ts)$/, jsToJsonPlugin]
|
|
53
|
-
], is(Array)],
|
|
50
|
+
hotReload: [false, is(Boolean)],
|
|
51
|
+
bypassImportCache: [true, is(Boolean)],
|
|
54
52
|
|
|
53
|
+
// Non-serializable
|
|
54
|
+
plugins: [[[/\.(js|ts)$/, jsToJsonPlugin]], is(Array)],
|
|
55
55
|
onReady: [await openInBrowser, is(Function)],
|
|
56
|
-
|
|
57
|
-
hotReload: [false, is(Boolean)],
|
|
58
|
-
bypassImportCache: [true, is(Boolean)]
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
|
|
@@ -73,19 +70,36 @@ export const config = Object.seal(defaults)
|
|
|
73
70
|
export const ConfigValidator = Object.freeze(validators)
|
|
74
71
|
|
|
75
72
|
|
|
73
|
+
let originalOpts = {}
|
|
74
|
+
|
|
76
75
|
/** @param {Partial<Config>} opts */
|
|
77
|
-
export function
|
|
76
|
+
export function initConfig(opts) {
|
|
78
77
|
if (opts.mocksDir)
|
|
79
78
|
opts.mocksDir = resolve(opts.mocksDir)
|
|
80
79
|
|
|
81
80
|
Object.assign(config, opts)
|
|
81
|
+
originalOpts = deepCloneExcluding(config, 'plugins', 'onReady')
|
|
82
|
+
|
|
82
83
|
validate(config, ConfigValidator)
|
|
83
84
|
logger.setLevel(config.logLevel)
|
|
84
85
|
registerMimes(config.extraMimes)
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
export function reinitConfig() {
|
|
89
|
+
initConfig(originalOpts)
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
export const isFileAllowed = f => !config.ignore.test(f)
|
|
88
93
|
|
|
89
94
|
export const calcDelay = () => config.delayJitter
|
|
90
95
|
? config.delay * (1 + Math.random() * config.delayJitter)
|
|
91
96
|
: config.delay
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
function deepCloneExcluding(obj, ...exclude) {
|
|
100
|
+
const res = {}
|
|
101
|
+
for (const [k, v] of Object.entries(obj))
|
|
102
|
+
if (!exclude.includes(k))
|
|
103
|
+
res[k] = structuredClone(v)
|
|
104
|
+
return res
|
|
105
|
+
}
|
|
@@ -4,7 +4,7 @@ import { createServer } from 'node:http'
|
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { equal } from 'node:assert/strict'
|
|
6
6
|
import { join } from 'node:path'
|
|
7
|
-
import {
|
|
7
|
+
import { rm } from 'node:fs/promises'
|
|
8
8
|
|
|
9
9
|
import { ServerResponse } from './HttpServerResponse.js'
|
|
10
10
|
|
|
@@ -31,7 +31,7 @@ describe('ServerResponse', { concurrency: true }, () => {
|
|
|
31
31
|
|
|
32
32
|
after(async () => {
|
|
33
33
|
server?.close()
|
|
34
|
-
await
|
|
34
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"/mockaton/reset": {
|
|
117
117
|
"patch": {
|
|
118
118
|
"summary": "Re-initialize Mockaton",
|
|
119
|
-
"description": "
|
|
119
|
+
"description": "Initializes the brokers collections and config options",
|
|
120
120
|
"x-js-client-example": "await mockaton.reset()",
|
|
121
121
|
"responses": {
|
|
122
122
|
"200": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|