mockaton 13.0.2 → 13.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/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/client/ApiCommander.js +5 -0
- package/src/client/ApiConstants.js +2 -0
- package/src/client/app-payload-viewer.js +3 -3
- package/src/server/Api.js +49 -1
- package/src/server/Mockaton.test.config.js +1 -0
- package/src/server/Mockaton.test.js +164 -133
- package/src/server/ProxyRelay.js +3 -3
- package/src/server/Watcher.js +5 -0
- package/src/server/cli.js +2 -7
- package/src/server/config.js +1 -0
- package/src/server/utils/HttpServerResponse.js +6 -0
- package/src/server/utils/fs.js +26 -5
package/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface Config {
|
|
|
14
14
|
ignore?: RegExp
|
|
15
15
|
watcherEnabled?: boolean
|
|
16
16
|
watcherDebounceMs?: number
|
|
17
|
+
readOnly?: boolean
|
|
17
18
|
|
|
18
19
|
host?: string,
|
|
19
20
|
port?: number
|
|
@@ -96,5 +97,7 @@ export interface State {
|
|
|
96
97
|
collectProxied: boolean
|
|
97
98
|
proxyFallback: string
|
|
98
99
|
|
|
100
|
+
readOnly: boolean
|
|
101
|
+
|
|
99
102
|
corsAllowed?: boolean
|
|
100
103
|
}
|
package/package.json
CHANGED
|
@@ -51,6 +51,11 @@ export class Commander {
|
|
|
51
51
|
setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
writeMock = (file, content) => this.#patch(API.writeMock, [file, content])
|
|
55
|
+
|
|
56
|
+
deleteMock = file => this.#patch(API.deleteMock, file)
|
|
57
|
+
|
|
58
|
+
|
|
54
59
|
/** @returns {JsonPromise<State>} */
|
|
55
60
|
getState = () => fetch(this.addr + API.state)
|
|
56
61
|
|
|
@@ -21,6 +21,8 @@ export const API = {
|
|
|
21
21
|
toggleStatus: MOUNT + '/toggle-status',
|
|
22
22
|
watchHotReload: MOUNT + '/watch-hot-reload',
|
|
23
23
|
watchMocks: MOUNT + '/watch-mocks',
|
|
24
|
+
writeMock: MOUNT + '/write-mock',
|
|
25
|
+
deleteMock: MOUNT + '/delete-mock',
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export const HEADER_502 = 'Mockaton502'
|
|
@@ -80,9 +80,9 @@ export async function previewMock() {
|
|
|
80
80
|
signal: previewMock.controller.signal
|
|
81
81
|
})
|
|
82
82
|
clearTimeout(spinnerTimer)
|
|
83
|
-
const
|
|
84
|
-
if (proxied || file)
|
|
85
|
-
await updatePayloadViewer(proxied, file, response)
|
|
83
|
+
const broker = store.brokerFor(method, urlMask)
|
|
84
|
+
if (broker?.proxied || broker?.file)
|
|
85
|
+
await updatePayloadViewer(broker.proxied, broker.file, response)
|
|
86
86
|
}
|
|
87
87
|
catch (error) {
|
|
88
88
|
clearTimeout(spinnerTimer)
|
package/src/server/Api.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
DASHBOARD_ASSETS,
|
|
11
11
|
CLIENT_DIR
|
|
12
12
|
} from './WatcherDevClient.js'
|
|
13
|
-
import { startWatchers, stopWatchers, sseClientSyncVersion } from './Watcher.js'
|
|
13
|
+
import { startWatchers, stopWatchers, sseClientSyncVersion, notifyARR } from './Watcher.js'
|
|
14
14
|
|
|
15
15
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
16
16
|
|
|
@@ -20,6 +20,8 @@ import { IndexHtml, CSP } from '../client/IndexHtml.js'
|
|
|
20
20
|
import { cookie } from './cookie.js'
|
|
21
21
|
import { config, ConfigValidator } from './config.js'
|
|
22
22
|
|
|
23
|
+
import { write, rm, isFile, resolveIn } from './utils/fs.js'
|
|
24
|
+
|
|
23
25
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
24
26
|
|
|
25
27
|
|
|
@@ -52,6 +54,8 @@ export const apiPatchReqs = new Map([
|
|
|
52
54
|
[API.proxied, setRouteIsProxied],
|
|
53
55
|
[API.toggleStatus, toggleRouteStatus],
|
|
54
56
|
|
|
57
|
+
[API.writeMock, writeMock],
|
|
58
|
+
[API.deleteMock, deleteMock],
|
|
55
59
|
[API.watchMocks, setWatchMocks]
|
|
56
60
|
])
|
|
57
61
|
|
|
@@ -80,6 +84,7 @@ function getState(_, response) {
|
|
|
80
84
|
|
|
81
85
|
proxyFallback: config.proxyFallback,
|
|
82
86
|
collectProxied: config.collectProxied,
|
|
87
|
+
readOnly: config.readOnly,
|
|
83
88
|
corsAllowed: config.corsAllowed
|
|
84
89
|
})
|
|
85
90
|
}
|
|
@@ -244,4 +249,47 @@ async function setRouteIsProxied(req, response) {
|
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
|
|
252
|
+
async function writeMock(req, response) {
|
|
253
|
+
if (config.readOnly)
|
|
254
|
+
return response.forbidden()
|
|
255
|
+
|
|
256
|
+
const [file, content] = await req.json()
|
|
257
|
+
const path = await resolveIn(config.mocksDir, file)
|
|
258
|
+
|
|
259
|
+
if (!path)
|
|
260
|
+
return response.forbidden()
|
|
261
|
+
|
|
262
|
+
await write(path, content)
|
|
263
|
+
|
|
264
|
+
if (!config.watcherEnabled) {
|
|
265
|
+
mockBrokersCollection.registerMock(file, true)
|
|
266
|
+
notifyARR()
|
|
267
|
+
}
|
|
268
|
+
response.ok()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
async function deleteMock(req, response) {
|
|
273
|
+
if (config.readOnly)
|
|
274
|
+
return response.forbidden()
|
|
275
|
+
|
|
276
|
+
const file = await req.json()
|
|
277
|
+
const path = await resolveIn(config.mocksDir, file)
|
|
278
|
+
|
|
279
|
+
if (!path)
|
|
280
|
+
return response.forbidden()
|
|
281
|
+
|
|
282
|
+
if (!isFile(path))
|
|
283
|
+
return response.unprocessable(`Missing Mock: ${file}`)
|
|
284
|
+
|
|
285
|
+
await rm(path)
|
|
286
|
+
|
|
287
|
+
if (!config.watcherEnabled) {
|
|
288
|
+
mockBrokersCollection.unregisterMock(file)
|
|
289
|
+
notifyARR()
|
|
290
|
+
}
|
|
291
|
+
response.ok()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
247
295
|
|
|
@@ -7,7 +7,7 @@ import { mkdtempSync } from 'node:fs'
|
|
|
7
7
|
import { randomUUID } from 'node:crypto'
|
|
8
8
|
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
9
9
|
import { describe, test, before, beforeEach, after } from 'node:test'
|
|
10
|
-
import {
|
|
10
|
+
import { unlink, mkdir, readFile, rename, readdir, writeFile } from 'node:fs/promises'
|
|
11
11
|
|
|
12
12
|
import { mimeFor } from './utils/mime.js'
|
|
13
13
|
import { parseFilename } from '../client/Filename.js'
|
|
@@ -26,8 +26,8 @@ const proc = spawn(join(import.meta.dirname, 'cli.js'), [
|
|
|
26
26
|
'--no-open'
|
|
27
27
|
])
|
|
28
28
|
|
|
29
|
-
proc.stdout.on('data', data =>
|
|
30
|
-
proc.stderr.on('data', data =>
|
|
29
|
+
proc.stdout.on('data', data => stdout.push(data.toString()))
|
|
30
|
+
proc.stderr.on('data', data => stderr.push(data.toString()))
|
|
31
31
|
|
|
32
32
|
const serverAddr = await new Promise((resolve, reject) => {
|
|
33
33
|
proc.stdout.once('data', () => {
|
|
@@ -43,9 +43,8 @@ after(() => proc.kill('SIGUSR2'))
|
|
|
43
43
|
const rmFromMocksDir = f => unlink(join(mocksDir, f))
|
|
44
44
|
const listFromMocksDir = d => readdir(join(mocksDir, d))
|
|
45
45
|
const readFromMocksDir = f => readFile(join(mocksDir, f), 'utf8')
|
|
46
|
-
|
|
46
|
+
const writeInMocksDir = (f, data) => writeFile(join(mocksDir, f), data)
|
|
47
47
|
const makeDirInMocks = dir => mkdir(join(mocksDir, dir), { recursive: true })
|
|
48
|
-
|
|
49
48
|
const renameInMocksDir = (src, target) => rename(join(mocksDir, src), join(mocksDir, target))
|
|
50
49
|
|
|
51
50
|
|
|
@@ -71,33 +70,8 @@ class BaseFixture {
|
|
|
71
70
|
this.file = file
|
|
72
71
|
this.body = body || `Body for ${file}`
|
|
73
72
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
await new Promise(resolve => setTimeout(resolve, 0))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async register() {
|
|
80
|
-
const nextVerPromise = resolveOnNextSyncVersion()
|
|
81
|
-
await this.#nextMacroTask()
|
|
82
|
-
await this.write()
|
|
83
|
-
await nextVerPromise
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async unregister() {
|
|
87
|
-
const nextVerPromise = resolveOnNextSyncVersion()
|
|
88
|
-
await this.#nextMacroTask()
|
|
89
|
-
await this.unlink()
|
|
90
|
-
await nextVerPromise
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async write() { await writeFile(this.#path(), this.body, 'utf8') }
|
|
94
|
-
async unlink() { await unlink(this.#path()) }
|
|
95
|
-
#path() { return join(this.dir, this.file) }
|
|
96
|
-
|
|
97
|
-
async sync() {
|
|
98
|
-
await this.write()
|
|
99
|
-
await api.reset()
|
|
100
|
-
}
|
|
73
|
+
write() { return api.writeMock(this.file, this.body) }
|
|
74
|
+
delete() { return api.deleteMock(this.file) }
|
|
101
75
|
|
|
102
76
|
request(options = {}) {
|
|
103
77
|
options.method ??= this.method
|
|
@@ -135,10 +109,10 @@ class FixtureStatic extends BaseFixture {
|
|
|
135
109
|
describe('Windows', () => {
|
|
136
110
|
test('path separators are normalized to forward slashes', async () => {
|
|
137
111
|
const fx = new Fixture('win-paths.GET.200.json')
|
|
138
|
-
await fx.
|
|
112
|
+
await fx.write()
|
|
139
113
|
const b = await fx.fetchBroker()
|
|
140
114
|
equal(b.file, fx.file)
|
|
141
|
-
await fx.
|
|
115
|
+
await fx.delete()
|
|
142
116
|
})
|
|
143
117
|
})
|
|
144
118
|
|
|
@@ -163,22 +137,22 @@ describe('Rejects malicious URLs', () => {
|
|
|
163
137
|
|
|
164
138
|
describe('Filename Convention', () => {
|
|
165
139
|
test('registers invalid filenames as GET 200', async () => {
|
|
140
|
+
await api.reset()
|
|
166
141
|
const fx0 = new Fixture('bar.GET._INVALID_STATUS_.json')
|
|
167
142
|
const fx1 = new Fixture('foo._INVALID_METHOD_.202.json')
|
|
168
143
|
const fx2 = new Fixture('missing-method-and-status.json')
|
|
169
144
|
await fx0.write()
|
|
170
145
|
await fx1.write()
|
|
171
146
|
await fx2.write()
|
|
172
|
-
await api.reset()
|
|
173
147
|
|
|
174
148
|
const s = await fetchState()
|
|
175
149
|
equal(s.brokersByMethod.GET['/bar.GET._INVALID_STATUS_.json'].file, 'bar.GET._INVALID_STATUS_.json')
|
|
176
150
|
equal(s.brokersByMethod.GET['/foo._INVALID_METHOD_.202.json'].file, 'foo._INVALID_METHOD_.202.json')
|
|
177
151
|
equal(s.brokersByMethod.GET['/missing-method-and-status.json'].file, 'missing-method-and-status.json')
|
|
178
152
|
|
|
179
|
-
await fx0.
|
|
180
|
-
await fx1.
|
|
181
|
-
await fx2.
|
|
153
|
+
await fx0.delete()
|
|
154
|
+
await fx1.delete()
|
|
155
|
+
await fx2.delete()
|
|
182
156
|
})
|
|
183
157
|
|
|
184
158
|
test('body parser rejects invalid JSON in API requests', async () => {
|
|
@@ -232,7 +206,7 @@ describe('CORS', () => {
|
|
|
232
206
|
|
|
233
207
|
test('responds', async () => {
|
|
234
208
|
const fx = new Fixture('cors-response.GET.200.json')
|
|
235
|
-
await fx.
|
|
209
|
+
await fx.write()
|
|
236
210
|
const r = await fx.request({
|
|
237
211
|
headers: {
|
|
238
212
|
'origin': CONFIG.corsOrigins[0]
|
|
@@ -241,7 +215,7 @@ describe('CORS', () => {
|
|
|
241
215
|
equal(r.status, 200)
|
|
242
216
|
equal(r.headers.get('access-control-allow-origin'), CONFIG.corsOrigins[0])
|
|
243
217
|
equal(r.headers.get('access-control-expose-headers'), 'Content-Encoding')
|
|
244
|
-
await fx.
|
|
218
|
+
await fx.delete()
|
|
245
219
|
})
|
|
246
220
|
})
|
|
247
221
|
|
|
@@ -278,7 +252,7 @@ describe('Cookie', () => {
|
|
|
278
252
|
|
|
279
253
|
test('updates selected cookie', async () => {
|
|
280
254
|
const fx = new Fixture('update-cookie.GET.200.json')
|
|
281
|
-
await fx.
|
|
255
|
+
await fx.write()
|
|
282
256
|
const resA = await fx.request()
|
|
283
257
|
equal(resA.headers.get('set-cookie'), CONFIG.cookies.userA)
|
|
284
258
|
|
|
@@ -290,7 +264,7 @@ describe('Cookie', () => {
|
|
|
290
264
|
|
|
291
265
|
const resB = await fx.request()
|
|
292
266
|
equal(resB.headers.get('set-cookie'), CONFIG.cookies.userB)
|
|
293
|
-
await fx.
|
|
267
|
+
await fx.delete()
|
|
294
268
|
})
|
|
295
269
|
})
|
|
296
270
|
|
|
@@ -324,7 +298,7 @@ describe('Delay', () => {
|
|
|
324
298
|
|
|
325
299
|
test('updates route delay', async () => {
|
|
326
300
|
const fx = new Fixture('route-delay.GET.200.json')
|
|
327
|
-
await fx.
|
|
301
|
+
await fx.write()
|
|
328
302
|
const DELAY = 100
|
|
329
303
|
await api.setGlobalDelay(DELAY)
|
|
330
304
|
await api.setRouteIsDelayed(fx.method, fx.urlMask, true)
|
|
@@ -332,7 +306,7 @@ describe('Delay', () => {
|
|
|
332
306
|
const r = await fx.request()
|
|
333
307
|
equal(await r.text(), fx.body)
|
|
334
308
|
equal(performance.now() - t0 > DELAY, true)
|
|
335
|
-
await fx.
|
|
309
|
+
await fx.delete()
|
|
336
310
|
})
|
|
337
311
|
|
|
338
312
|
describe('Set Route is Delayed', () => {
|
|
@@ -344,18 +318,18 @@ describe('Delay', () => {
|
|
|
344
318
|
|
|
345
319
|
test('422 for invalid delayed value', async () => {
|
|
346
320
|
const fx = new Fixture('set-route-delay.GET.200.json')
|
|
347
|
-
await fx.
|
|
321
|
+
await fx.write()
|
|
348
322
|
const r = await api.setRouteIsDelayed(fx.method, fx.urlMask, 'not-a-boolean')
|
|
349
323
|
equal(await r.text(), 'Expected boolean for "delayed"')
|
|
350
|
-
await fx.
|
|
324
|
+
await fx.delete()
|
|
351
325
|
})
|
|
352
326
|
|
|
353
327
|
test('200', async () => {
|
|
354
328
|
const fx = new Fixture('set-route-delay.GET.200.json')
|
|
355
|
-
await fx.
|
|
329
|
+
await fx.write()
|
|
356
330
|
const r = await api.setRouteIsDelayed(fx.method, fx.urlMask, true)
|
|
357
331
|
equal((await r.json()).delayed, true)
|
|
358
|
-
await fx.
|
|
332
|
+
await fx.delete()
|
|
359
333
|
})
|
|
360
334
|
})
|
|
361
335
|
})
|
|
@@ -451,11 +425,11 @@ describe('Proxy Fallback', () => {
|
|
|
451
425
|
describe('Set Route is Proxied', () => {
|
|
452
426
|
const fx = new Fixture('route-is-proxied.GET.200.json')
|
|
453
427
|
beforeEach(async () => {
|
|
454
|
-
await fx.
|
|
428
|
+
await fx.write()
|
|
455
429
|
await api.setProxyFallback('')
|
|
456
430
|
})
|
|
457
431
|
after(async () => {
|
|
458
|
-
await fx.
|
|
432
|
+
await fx.delete()
|
|
459
433
|
})
|
|
460
434
|
|
|
461
435
|
test('422 for non-existing route', async () => {
|
|
@@ -495,7 +469,7 @@ describe('Proxy Fallback', () => {
|
|
|
495
469
|
|
|
496
470
|
test('unsets autoStatus', async () => {
|
|
497
471
|
const fx = new Fixture('unset-500-on-proxy.GET.200.txt')
|
|
498
|
-
await fx.
|
|
472
|
+
await fx.write()
|
|
499
473
|
await api.setProxyFallback('https://example.test')
|
|
500
474
|
|
|
501
475
|
const r0 = await api.toggleStatus(fx.method, fx.urlMask, 500)
|
|
@@ -508,14 +482,14 @@ describe('Proxy Fallback', () => {
|
|
|
508
482
|
equal(b1.proxied, true)
|
|
509
483
|
equal(b1.autoStatus, 0)
|
|
510
484
|
|
|
511
|
-
await fx.
|
|
485
|
+
await fx.delete()
|
|
512
486
|
await api.setProxyFallback('')
|
|
513
487
|
})
|
|
514
488
|
})
|
|
515
489
|
|
|
516
490
|
test('updating selected mock resets proxied flag', async () => {
|
|
517
491
|
const fx = new Fixture('select-resets-proxied.GET.200.txt')
|
|
518
|
-
await fx.
|
|
492
|
+
await fx.write()
|
|
519
493
|
await api.setProxyFallback('https://example.test')
|
|
520
494
|
const r0 = await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
521
495
|
equal((await r0.json()).proxied, true)
|
|
@@ -524,7 +498,7 @@ describe('Proxy Fallback', () => {
|
|
|
524
498
|
equal((await r1.json()).proxied, false)
|
|
525
499
|
|
|
526
500
|
await api.setProxyFallback('')
|
|
527
|
-
await fx.
|
|
501
|
+
await fx.delete()
|
|
528
502
|
})
|
|
529
503
|
})
|
|
530
504
|
|
|
@@ -543,12 +517,10 @@ describe('404', () => {
|
|
|
543
517
|
test('404s ignored files', async () => {
|
|
544
518
|
const fx = new Fixture('ignored.GET.200.json~')
|
|
545
519
|
await fx.write()
|
|
546
|
-
await api.reset()
|
|
547
520
|
const r = await fx.request()
|
|
548
521
|
equal(r.status, 404)
|
|
549
|
-
await fx.
|
|
522
|
+
await fx.delete()
|
|
550
523
|
})
|
|
551
|
-
|
|
552
524
|
})
|
|
553
525
|
|
|
554
526
|
|
|
@@ -561,8 +533,8 @@ describe('Default Mock', () => {
|
|
|
561
533
|
await api.reset()
|
|
562
534
|
})
|
|
563
535
|
after(async () => {
|
|
564
|
-
await fxA.
|
|
565
|
-
await fxB.
|
|
536
|
+
await fxA.delete()
|
|
537
|
+
await fxB.delete()
|
|
566
538
|
})
|
|
567
539
|
|
|
568
540
|
test('sorts mocks list with the user specified default first for dashboard display', async () => {
|
|
@@ -585,22 +557,22 @@ describe('Dynamic Mocks', () => {
|
|
|
585
557
|
const fx = new Fixture(
|
|
586
558
|
'js-object.GET.200.js',
|
|
587
559
|
'export default { FROM_JS: true }')
|
|
588
|
-
await fx.
|
|
560
|
+
await fx.write()
|
|
589
561
|
const r = await fx.request()
|
|
590
562
|
equal(r.headers.get('content-type'), mimeFor('.json'))
|
|
591
563
|
deepEqual(await r.json(), { FROM_JS: true })
|
|
592
|
-
await fx.
|
|
564
|
+
await fx.delete()
|
|
593
565
|
})
|
|
594
566
|
|
|
595
567
|
test('TS array is sent as JSON', async () => {
|
|
596
568
|
const fx = new Fixture(
|
|
597
569
|
'js-object.GET.200.ts',
|
|
598
570
|
'export default ["from ts"]')
|
|
599
|
-
await fx.
|
|
571
|
+
await fx.write()
|
|
600
572
|
const r = await fx.request()
|
|
601
573
|
equal(r.headers.get('content-type'), mimeFor('.json'))
|
|
602
574
|
deepEqual(await r.json(), ['from ts'])
|
|
603
|
-
await fx.
|
|
575
|
+
await fx.delete()
|
|
604
576
|
})
|
|
605
577
|
})
|
|
606
578
|
|
|
@@ -611,14 +583,14 @@ describe('Dynamic Function Mocks', () => {
|
|
|
611
583
|
export default function (req, response) {
|
|
612
584
|
return Buffer.from('A')
|
|
613
585
|
}`)
|
|
614
|
-
await fx.
|
|
586
|
+
await fx.write()
|
|
615
587
|
const r = await fx.request()
|
|
616
588
|
equal(r.status, 200)
|
|
617
589
|
equal(r.headers.get('content-length'), '1')
|
|
618
590
|
equal(r.headers.get('content-type'), mimeFor('.json'))
|
|
619
591
|
equal(r.headers.get('set-cookie'), CONFIG.cookies.userA)
|
|
620
592
|
equal(await r.text(), 'A')
|
|
621
|
-
await fx.
|
|
593
|
+
await fx.delete()
|
|
622
594
|
})
|
|
623
595
|
|
|
624
596
|
test('can override filename convention (also supports TS)', async () => {
|
|
@@ -629,14 +601,14 @@ describe('Dynamic Function Mocks', () => {
|
|
|
629
601
|
response.setHeader('set-cookie', 'custom-cookie')
|
|
630
602
|
return new Uint8Array([65, 65])
|
|
631
603
|
}`)
|
|
632
|
-
await fx.
|
|
604
|
+
await fx.write()
|
|
633
605
|
const r = await fx.request({ method: 'POST' })
|
|
634
606
|
equal(r.status, 201)
|
|
635
607
|
equal(r.headers.get('content-length'), String(2))
|
|
636
608
|
equal(r.headers.get('content-type'), 'custom-mime')
|
|
637
609
|
equal(r.headers.get('set-cookie'), 'custom-cookie')
|
|
638
610
|
equal(await r.text(), 'AA')
|
|
639
|
-
await fx.
|
|
611
|
+
await fx.delete()
|
|
640
612
|
})
|
|
641
613
|
})
|
|
642
614
|
|
|
@@ -645,9 +617,9 @@ describe('Static Files', () => {
|
|
|
645
617
|
const fxsIndex = new FixtureStatic('index.html', '<h1>Index</h1>')
|
|
646
618
|
const fxsAsset = new FixtureStatic('asset-script.js', 'const a = 1')
|
|
647
619
|
before(async () => {
|
|
620
|
+
await api.reset()
|
|
648
621
|
await fxsIndex.write()
|
|
649
622
|
await fxsAsset.write()
|
|
650
|
-
await api.reset()
|
|
651
623
|
})
|
|
652
624
|
|
|
653
625
|
describe('Static File Serving', () => {
|
|
@@ -690,9 +662,9 @@ describe('Static Files', () => {
|
|
|
690
662
|
})
|
|
691
663
|
|
|
692
664
|
test('unregisters static route', async () => {
|
|
693
|
-
await fxsIndex.unlink()
|
|
694
|
-
await fxsAsset.unlink()
|
|
695
665
|
await api.reset()
|
|
666
|
+
await fxsIndex.delete()
|
|
667
|
+
await fxsAsset.delete()
|
|
696
668
|
const s = await fetchState()
|
|
697
669
|
equal(s.brokersByMethod.GET?.[fxsIndex.urlMask], undefined)
|
|
698
670
|
equal(s.brokersByMethod.GET?.[fxsAsset.urlMask], undefined)
|
|
@@ -703,7 +675,7 @@ describe('Static Files', () => {
|
|
|
703
675
|
describe('Auto Status', () => {
|
|
704
676
|
test('toggling ON 500 on a route without 500 auto-generates one', async () => {
|
|
705
677
|
const fx = new Fixture('toggling-500-without-500.GET.200.json')
|
|
706
|
-
await fx.
|
|
678
|
+
await fx.write()
|
|
707
679
|
equal((await fx.request()).status, fx.status)
|
|
708
680
|
|
|
709
681
|
const bp0 = await api.toggleStatus(fx.method, fx.urlMask, 500)
|
|
@@ -718,11 +690,11 @@ describe('Auto Status', () => {
|
|
|
718
690
|
})
|
|
719
691
|
|
|
720
692
|
test('toggling ON 500 picks existing 500 and toggling OFF selects default', async () => {
|
|
693
|
+
await api.reset()
|
|
721
694
|
const fx200 = new Fixture('reg-error.GET.200.txt')
|
|
722
695
|
const fx500 = new Fixture('reg-error.GET.500.txt')
|
|
723
696
|
await fx200.write()
|
|
724
697
|
await fx500.write()
|
|
725
|
-
await api.reset()
|
|
726
698
|
|
|
727
699
|
const bp0 = await api.toggleStatus(fx200.method, fx200.urlMask, 500)
|
|
728
700
|
const b0 = await bp0.json()
|
|
@@ -736,24 +708,24 @@ describe('Auto Status', () => {
|
|
|
736
708
|
equal(b1.status, 200)
|
|
737
709
|
equal(await (await fx200.request()).text(), fx200.body)
|
|
738
710
|
|
|
739
|
-
await fx200.
|
|
740
|
-
await fx500.
|
|
711
|
+
await fx200.delete()
|
|
712
|
+
await fx500.delete()
|
|
741
713
|
})
|
|
742
714
|
|
|
743
715
|
test('toggling ON 500 unsets `proxied` flag', async () => {
|
|
744
716
|
const fx = new Fixture('proxied-to-500.GET.200.txt')
|
|
745
|
-
await fx.
|
|
717
|
+
await fx.write()
|
|
746
718
|
await api.setProxyFallback('https://example.test')
|
|
747
719
|
await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
748
720
|
await api.toggleStatus(fx.method, fx.urlMask, 500)
|
|
749
721
|
equal((await fx.fetchBroker()).proxied, false)
|
|
750
|
-
await fx.
|
|
722
|
+
await fx.delete()
|
|
751
723
|
await api.setProxyFallback('')
|
|
752
724
|
})
|
|
753
725
|
|
|
754
726
|
test('toggling ON 404 for static routes', async () => {
|
|
755
727
|
const fx = new FixtureStatic('static-404.txt')
|
|
756
|
-
await fx.
|
|
728
|
+
await fx.write()
|
|
757
729
|
equal((await fx.request()).status, 200)
|
|
758
730
|
|
|
759
731
|
const bp0 = await api.toggleStatus(fx.method, fx.urlMask, 404)
|
|
@@ -766,7 +738,7 @@ describe('Auto Status', () => {
|
|
|
766
738
|
equal((await r1.json()).autoStatus, 0)
|
|
767
739
|
equal((await fx.request()).status, 200)
|
|
768
740
|
|
|
769
|
-
await fx.
|
|
741
|
+
await fx.delete()
|
|
770
742
|
})
|
|
771
743
|
})
|
|
772
744
|
|
|
@@ -774,10 +746,10 @@ describe('Auto Status', () => {
|
|
|
774
746
|
describe('Index-like routes', () => {
|
|
775
747
|
test('resolves dirs to the file without urlMask', async () => {
|
|
776
748
|
const fx = new Fixture('.GET.200.json')
|
|
777
|
-
await fx.
|
|
749
|
+
await fx.write()
|
|
778
750
|
const r = await request('/')
|
|
779
751
|
equal(await r.text(), fx.body)
|
|
780
|
-
await fx.
|
|
752
|
+
await fx.delete()
|
|
781
753
|
})
|
|
782
754
|
})
|
|
783
755
|
|
|
@@ -785,20 +757,20 @@ describe('Index-like routes', () => {
|
|
|
785
757
|
describe('MIME', () => {
|
|
786
758
|
test('derives content-type from known mime', async () => {
|
|
787
759
|
const fx = new Fixture('tmp.GET.200.json')
|
|
788
|
-
await fx.
|
|
760
|
+
await fx.write()
|
|
789
761
|
const r = await fx.request()
|
|
790
762
|
equal(r.headers.get('content-type'), 'application/json')
|
|
791
|
-
await fx.
|
|
763
|
+
await fx.delete()
|
|
792
764
|
})
|
|
793
765
|
|
|
794
766
|
test('derives content-type from custom mime', async () => {
|
|
795
767
|
const ext = Object.keys(CONFIG.extraMimes)[0]
|
|
796
768
|
const mime = Object.values(CONFIG.extraMimes)[0]
|
|
797
769
|
const fx = new Fixture(`tmp.GET.200.${ext}`)
|
|
798
|
-
await fx.
|
|
770
|
+
await fx.write()
|
|
799
771
|
const r = await fx.request()
|
|
800
772
|
equal(r.headers.get('content-type'), mime)
|
|
801
|
-
await fx.
|
|
773
|
+
await fx.delete()
|
|
802
774
|
})
|
|
803
775
|
})
|
|
804
776
|
|
|
@@ -819,8 +791,8 @@ describe('Headers', () => {
|
|
|
819
791
|
|
|
820
792
|
describe('Method and Status', () => {
|
|
821
793
|
const fx = new Fixture('uncommon-method.ACL.201.txt')
|
|
822
|
-
before(async () => await fx.
|
|
823
|
-
after(async () => await fx.
|
|
794
|
+
before(async () => await fx.write())
|
|
795
|
+
after(async () => await fx.delete())
|
|
824
796
|
|
|
825
797
|
test('dispatches the response status', async () => {
|
|
826
798
|
const r = await fx.request()
|
|
@@ -846,11 +818,10 @@ describe('Select', () => {
|
|
|
846
818
|
before(async () => {
|
|
847
819
|
await fx.write()
|
|
848
820
|
await fxAlt.write()
|
|
849
|
-
await api.reset()
|
|
850
821
|
})
|
|
851
822
|
after(async () => {
|
|
852
|
-
await fx.
|
|
853
|
-
await fxAlt.
|
|
823
|
+
await fx.delete()
|
|
824
|
+
await fxAlt.delete()
|
|
854
825
|
})
|
|
855
826
|
|
|
856
827
|
test('422 when updating non-existing mock', async () => {
|
|
@@ -887,13 +858,12 @@ describe('Bulk Select', () => {
|
|
|
887
858
|
await fxIotaB.write()
|
|
888
859
|
await fxKappaA.write()
|
|
889
860
|
await fxKappaB.write()
|
|
890
|
-
await api.reset()
|
|
891
861
|
})
|
|
892
862
|
after(async () => {
|
|
893
|
-
await fxIota.
|
|
894
|
-
await fxIotaB.
|
|
895
|
-
await fxKappaA.
|
|
896
|
-
await fxKappaB.
|
|
863
|
+
await fxIota.delete()
|
|
864
|
+
await fxIotaB.delete()
|
|
865
|
+
await fxKappaA.delete()
|
|
866
|
+
await fxKappaB.delete()
|
|
897
867
|
})
|
|
898
868
|
|
|
899
869
|
test('extracts all comments without duplicates', async () =>
|
|
@@ -919,9 +889,9 @@ describe('Bulk Select', () => {
|
|
|
919
889
|
describe('Decoding URLs', () => {
|
|
920
890
|
test('allows dots, spaces, amp, etc.', async () => {
|
|
921
891
|
const fx = new Fixture('dot.in.path and amp & and colon:.GET.200.txt')
|
|
922
|
-
await fx.
|
|
892
|
+
await fx.write()
|
|
923
893
|
equal(await (await fx.request()).text(), fx.body)
|
|
924
|
-
await fx.
|
|
894
|
+
await fx.delete()
|
|
925
895
|
})
|
|
926
896
|
})
|
|
927
897
|
|
|
@@ -937,13 +907,12 @@ describe('Dynamic Params', () => {
|
|
|
937
907
|
await fx1.write()
|
|
938
908
|
await fx2.write()
|
|
939
909
|
await fx3.write()
|
|
940
|
-
await api.reset()
|
|
941
910
|
})
|
|
942
911
|
after(async () => {
|
|
943
|
-
await fx0.
|
|
944
|
-
await fx1.
|
|
945
|
-
await fx2.
|
|
946
|
-
await fx3.
|
|
912
|
+
await fx0.delete()
|
|
913
|
+
await fx1.delete()
|
|
914
|
+
await fx2.delete()
|
|
915
|
+
await fx3.delete()
|
|
947
916
|
})
|
|
948
917
|
|
|
949
918
|
test('variable at end', async () => {
|
|
@@ -978,8 +947,8 @@ describe('Query String', () => {
|
|
|
978
947
|
await api.reset()
|
|
979
948
|
})
|
|
980
949
|
after(async () => {
|
|
981
|
-
await fx0.
|
|
982
|
-
await fx1.
|
|
950
|
+
await fx0.delete()
|
|
951
|
+
await fx1.delete()
|
|
983
952
|
})
|
|
984
953
|
|
|
985
954
|
test('multiple params', async () => {
|
|
@@ -1003,12 +972,54 @@ describe('Query String', () => {
|
|
|
1003
972
|
|
|
1004
973
|
test('head for get. returns the headers without body only for GETs requested as HEAD', async () => {
|
|
1005
974
|
const fx = new Fixture('head-get.GET.200.json')
|
|
1006
|
-
await fx.
|
|
975
|
+
await fx.write()
|
|
1007
976
|
const r = await fx.request({ method: 'HEAD' })
|
|
1008
977
|
equal(r.status, 200)
|
|
1009
978
|
equal(r.headers.get('content-length'), String(Buffer.byteLength(fx.body)))
|
|
1010
979
|
equal(await r.text(), '')
|
|
1011
|
-
await fx.
|
|
980
|
+
await fx.delete()
|
|
981
|
+
})
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
describe('Write and Delete Mock', () => {
|
|
985
|
+
test('guards mocksDir', async () => {
|
|
986
|
+
const r = await api.writeMock('../outside.txt', '')
|
|
987
|
+
equal(r.status, 403)
|
|
988
|
+
|
|
989
|
+
const r2 = await api.deleteMock('../outside.txt')
|
|
990
|
+
equal(r2.status, 403)
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
test('write and delete (with watcher)', async () => {
|
|
994
|
+
await api.setWatchMocks(true)
|
|
995
|
+
const file = 'new-mock.GET.200.txt'
|
|
996
|
+
|
|
997
|
+
const nextVerPromise = resolveOnNextSyncVersion()
|
|
998
|
+
const res = await api.writeMock(file, '')
|
|
999
|
+
equal(res.status, 200)
|
|
1000
|
+
await nextVerPromise
|
|
1001
|
+
const r = await request('/new-mock')
|
|
1002
|
+
equal(r.status, 200)
|
|
1003
|
+
|
|
1004
|
+
const nextVerPromise2 = resolveOnNextSyncVersion()
|
|
1005
|
+
await api.deleteMock(file)
|
|
1006
|
+
await nextVerPromise2
|
|
1007
|
+
const r2 = await request('/new-mock')
|
|
1008
|
+
equal(r2.status, 404)
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
test('write and delete (without watcher)', async () => {
|
|
1012
|
+
await api.setWatchMocks(false)
|
|
1013
|
+
const file = 'manual-mock.GET.200.txt'
|
|
1014
|
+
|
|
1015
|
+
await api.writeMock(file, '')
|
|
1016
|
+
const r = await request('/manual-mock')
|
|
1017
|
+
equal(r.status, 200)
|
|
1018
|
+
|
|
1019
|
+
await api.deleteMock(file)
|
|
1020
|
+
const r2 = await request('/manual-mock')
|
|
1021
|
+
equal(r2.status, 404)
|
|
1022
|
+
})
|
|
1012
1023
|
})
|
|
1013
1024
|
|
|
1014
1025
|
|
|
@@ -1027,76 +1038,102 @@ describe('Watch mocks API toggler', () => {
|
|
|
1027
1038
|
|
|
1028
1039
|
|
|
1029
1040
|
describe('Registering Mocks', () => {
|
|
1030
|
-
|
|
1031
|
-
|
|
1041
|
+
// simulates user interacting with the file-system directly
|
|
1042
|
+
class FixtureExternal extends Fixture {
|
|
1043
|
+
constructor(props) {
|
|
1044
|
+
super(props)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async writeExternally() {
|
|
1048
|
+
const nextVerPromise = resolveOnNextSyncVersion()
|
|
1049
|
+
await sleep(0) // next macro task
|
|
1050
|
+
await this.write()
|
|
1051
|
+
await nextVerPromise
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async deleteExternally() {
|
|
1055
|
+
const nextVerPromise = resolveOnNextSyncVersion()
|
|
1056
|
+
await sleep(0)
|
|
1057
|
+
await this.delete()
|
|
1058
|
+
await nextVerPromise
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function sleep(ms) {
|
|
1063
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
const fxA = new FixtureExternal('register(default).GET.200.json')
|
|
1068
|
+
const fxB = new FixtureExternal('register(alt).GET.200.json')
|
|
1032
1069
|
|
|
1033
1070
|
test('when watcher is off, newly added mocks do not get registered', async () => {
|
|
1034
1071
|
await api.setWatchMocks(false)
|
|
1035
|
-
const fx = new
|
|
1036
|
-
await fx.
|
|
1037
|
-
await sleep()
|
|
1072
|
+
const fx = new FixtureExternal('non-auto-registered-file.GET.200.json')
|
|
1073
|
+
await writeInMocksDir(fx.file, fx.body)
|
|
1074
|
+
await sleep(100)
|
|
1038
1075
|
equal(await fx.fetchBroker(), undefined)
|
|
1039
|
-
await fx.
|
|
1076
|
+
await rmFromMocksDir(fx.file)
|
|
1040
1077
|
})
|
|
1041
1078
|
|
|
1042
1079
|
test('register', async () => {
|
|
1043
1080
|
await api.setWatchMocks(true)
|
|
1044
|
-
await fxA.
|
|
1045
|
-
await fxB.
|
|
1081
|
+
await fxA.writeExternally()
|
|
1082
|
+
await fxB.writeExternally()
|
|
1046
1083
|
const b = await fxA.fetchBroker()
|
|
1047
1084
|
deepEqual(b.mocks, [fxA.file, fxB.file])
|
|
1048
1085
|
})
|
|
1049
1086
|
|
|
1050
1087
|
test('unregistering selected ensures a mock is selected', async () => {
|
|
1051
1088
|
await api.select(fxA.file)
|
|
1052
|
-
await fxA.
|
|
1089
|
+
await fxA.deleteExternally()
|
|
1053
1090
|
const b = await fxA.fetchBroker()
|
|
1054
1091
|
deepEqual(b.mocks, [fxB.file])
|
|
1055
1092
|
})
|
|
1056
1093
|
|
|
1057
1094
|
test('unregistering the last mock removes broker', async () => {
|
|
1058
|
-
await fxB.
|
|
1095
|
+
await fxB.deleteExternally()
|
|
1059
1096
|
const b = await fxB.fetchBroker()
|
|
1060
1097
|
equal(b, undefined)
|
|
1061
1098
|
})
|
|
1062
1099
|
|
|
1063
1100
|
test('registering a 500 unsets autoStatus', async () => {
|
|
1064
|
-
const fx200 = new
|
|
1065
|
-
const fx500 = new
|
|
1066
|
-
await fx200.
|
|
1101
|
+
const fx200 = new FixtureExternal('reg-error.GET.200.txt')
|
|
1102
|
+
const fx500 = new FixtureExternal('reg-error.GET.500.txt')
|
|
1103
|
+
await fx200.writeExternally()
|
|
1067
1104
|
await api.toggleStatus(fx200.method, fx200.urlMask, 500)
|
|
1068
1105
|
const b0 = await fx200.fetchBroker()
|
|
1069
1106
|
equal(b0.autoStatus, 500)
|
|
1070
|
-
await fx500.
|
|
1107
|
+
await fx500.writeExternally()
|
|
1071
1108
|
const b1 = await fx200.fetchBroker()
|
|
1072
1109
|
equal(b1.autoStatus, 0)
|
|
1073
1110
|
deepEqual(b1.mocks, [
|
|
1074
1111
|
fx200.file,
|
|
1075
1112
|
fx500.file
|
|
1076
1113
|
])
|
|
1077
|
-
await fx200.
|
|
1078
|
-
await fx500.
|
|
1114
|
+
await fx200.deleteExternally()
|
|
1115
|
+
await fx500.deleteExternally()
|
|
1079
1116
|
})
|
|
1080
1117
|
|
|
1081
1118
|
describe('getSyncVersion', () => {
|
|
1082
|
-
const fx0 = new
|
|
1119
|
+
const fx0 = new FixtureExternal('reg0/runtime0.GET.200.txt')
|
|
1083
1120
|
let version
|
|
1084
1121
|
before(async () => {
|
|
1085
1122
|
await makeDirInMocks('reg0')
|
|
1086
|
-
await fx0.
|
|
1123
|
+
await writeInMocksDir(fx0.file, fx0.body)
|
|
1087
1124
|
version = await resolveOnNextSyncVersion(-1)
|
|
1088
1125
|
})
|
|
1089
1126
|
|
|
1090
|
-
const fx = new
|
|
1127
|
+
const fx = new FixtureExternal('runtime1.GET.200.txt')
|
|
1091
1128
|
test('responds when a file is added', async () => {
|
|
1092
1129
|
const prom = resolveOnNextSyncVersion(version)
|
|
1093
|
-
await fx.
|
|
1130
|
+
await writeInMocksDir(fx.file, fx.body)
|
|
1094
1131
|
equal(await prom, version + 1)
|
|
1095
1132
|
})
|
|
1096
1133
|
|
|
1097
1134
|
test('responds when a file is deleted', async () => {
|
|
1098
1135
|
const prom = resolveOnNextSyncVersion(version + 1)
|
|
1099
|
-
await fx.
|
|
1136
|
+
await rmFromMocksDir(fx.file)
|
|
1100
1137
|
equal(await prom, version + 2)
|
|
1101
1138
|
})
|
|
1102
1139
|
|
|
@@ -1112,13 +1149,6 @@ describe('Registering Mocks', () => {
|
|
|
1112
1149
|
})
|
|
1113
1150
|
|
|
1114
1151
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
function sleep(ms = 100) {
|
|
1118
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
1152
|
/** In Node, there's no EventSource, so we work around it like this.
|
|
1123
1153
|
* This is for listening to real-time updates. It responds when a new mock is added, deleted, or renamed. */
|
|
1124
1154
|
async function resolveOnNextSyncVersion(currSyncVer = undefined) {
|
|
@@ -1143,3 +1173,4 @@ async function resolveOnNextSyncVersion(currSyncVer = undefined) {
|
|
|
1143
1173
|
}
|
|
1144
1174
|
}
|
|
1145
1175
|
}
|
|
1176
|
+
|
package/src/server/ProxyRelay.js
CHANGED
|
@@ -38,11 +38,11 @@ export async function proxy(req, response, delay) {
|
|
|
38
38
|
|
|
39
39
|
if (config.collectProxied) {
|
|
40
40
|
const ext = extFor(proxyResponse.headers.get('content-type'))
|
|
41
|
-
saveMockToDisk(req.url, req.method, proxyResponse.status, ext, body)
|
|
41
|
+
await saveMockToDisk(req.url, req.method, proxyResponse.status, ext, body)
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function saveMockToDisk(url, method, status, ext, body) {
|
|
45
|
+
async function saveMockToDisk(url, method, status, ext, body) {
|
|
46
46
|
if (config.formatCollectedJSON && ext === 'json')
|
|
47
47
|
try {
|
|
48
48
|
body = JSON.stringify(JSON.parse(body), null, ' ')
|
|
@@ -52,7 +52,7 @@ function saveMockToDisk(url, method, status, ext, body) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
|
-
write(makeUniqueMockFilename(url, method, status, ext), body)
|
|
55
|
+
await write(makeUniqueMockFilename(url, method, status, ext), body)
|
|
56
56
|
}
|
|
57
57
|
catch (err) {
|
|
58
58
|
logger.warn('Write access denied', err)
|
package/src/server/Watcher.js
CHANGED
|
@@ -42,6 +42,11 @@ const uiSyncVersion = new class extends EventEmitter {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
export function notifyARR() {
|
|
46
|
+
uiSyncVersion.increment()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
45
50
|
export function watchMocksDir() {
|
|
46
51
|
const dir = config.mocksDir
|
|
47
52
|
mocksWatcher = mocksWatcher || watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
package/src/server/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { resolve
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
4
|
import { parseArgs } from 'node:util'
|
|
5
5
|
|
|
6
6
|
import { isFile } from './utils/fs.js'
|
|
@@ -24,8 +24,7 @@ try {
|
|
|
24
24
|
'no-open': { short: 'n', type: 'boolean' },
|
|
25
25
|
|
|
26
26
|
help: { short: 'h', type: 'boolean' },
|
|
27
|
-
version: { short: 'v', type: 'boolean' }
|
|
28
|
-
skills: { type: 'boolean' },
|
|
27
|
+
version: { short: 'v', type: 'boolean' }
|
|
29
28
|
},
|
|
30
29
|
allowPositionals: true
|
|
31
30
|
})
|
|
@@ -44,9 +43,6 @@ process.on('SIGUSR2', () => process.exit(0))
|
|
|
44
43
|
if (args.version)
|
|
45
44
|
console.log(pkgJSON.version)
|
|
46
45
|
|
|
47
|
-
else if (args.skills)
|
|
48
|
-
console.log(join(import.meta.dirname, 'skills'))
|
|
49
|
-
|
|
50
46
|
else if (args.help)
|
|
51
47
|
console.log(`
|
|
52
48
|
Usage: mockaton [mocks-dir] [options]
|
|
@@ -62,7 +58,6 @@ Options:
|
|
|
62
58
|
|
|
63
59
|
-h, --help Show this help
|
|
64
60
|
-v, --version Show version
|
|
65
|
-
--skills Show AI agent skills path
|
|
66
61
|
|
|
67
62
|
Notes:
|
|
68
63
|
* mockaton.config.js supports more options, see: https://mockaton.com/config
|
package/src/server/config.js
CHANGED
|
@@ -19,6 +19,7 @@ import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
|
|
|
19
19
|
const schema = {
|
|
20
20
|
mocksDir: [resolve('mockaton-mocks'), isDirectory],
|
|
21
21
|
ignore: [/(\.DS_Store|~)$/, is(RegExp)],
|
|
22
|
+
readOnly: [true, is(Boolean)],
|
|
22
23
|
watcherEnabled: [true, is(Boolean)],
|
|
23
24
|
watcherDebounceMs: [80, ms => Number.isInteger(ms) && ms >= 0],
|
|
24
25
|
|
package/src/server/utils/fs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { join, dirname, sep, posix } from 'node:path'
|
|
2
|
-
import { lstatSync, readdirSync
|
|
1
|
+
import { join, dirname, sep, posix, resolve } from 'node:path'
|
|
2
|
+
import { lstatSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { mkdir, writeFile, unlink, realpath } from 'node:fs/promises'
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
|
|
@@ -19,7 +20,27 @@ export function listFilesRecursively(dir) {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export function write(path, body) {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
export async function write(path, body) {
|
|
24
|
+
await mkdir(dirname(path), { recursive: true })
|
|
25
|
+
await writeFile(path, body)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function rm(path) {
|
|
29
|
+
await unlink(path)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/** @returns {string | null} absolute path if it’s within `baseDir` */
|
|
34
|
+
export async function resolveIn(baseDir, file) {
|
|
35
|
+
try {
|
|
36
|
+
const parent = await realpath(baseDir)
|
|
37
|
+
const child = resolve(parent, file)
|
|
38
|
+
return child.startsWith(parent + sep)
|
|
39
|
+
? child
|
|
40
|
+
: null
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
console.error('DDDDDD', e)
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
25
46
|
}
|