mockaton 13.0.2 → 13.2.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 +7 -1
- package/src/client/ApiConstants.js +2 -0
- package/src/client/app-payload-viewer.js +3 -3
- package/src/client/app-store.js +20 -11
- package/src/server/Api.js +79 -24
- 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 +9 -15
- 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,12 +51,18 @@ 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
|
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
|
-
* SSE - Streams an incremental version when a mock is added, deleted, or renamed
|
|
64
|
+
* SSE - Streams an incremental version when a mock is added, deleted, or renamed.
|
|
65
|
+
* Also, when the internal state changes.
|
|
60
66
|
* @returns {Promise<Response>}
|
|
61
67
|
*/
|
|
62
68
|
getSyncVersion = () => fetch(this.addr + API.syncVersion)
|
|
@@ -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/client/app-store.js
CHANGED
|
@@ -10,6 +10,7 @@ export const store = {
|
|
|
10
10
|
onError(err) {},
|
|
11
11
|
render() {},
|
|
12
12
|
renderRow(method, urlMask) {},
|
|
13
|
+
skipNextRender: false,
|
|
13
14
|
|
|
14
15
|
brokersByMethod: /** @type ClientBrokersByMethod */ {},
|
|
15
16
|
|
|
@@ -58,49 +59,53 @@ export const store = {
|
|
|
58
59
|
if (store.showProxyField === null) // isFirstCall
|
|
59
60
|
store.showProxyField = Boolean(store.proxyFallback)
|
|
60
61
|
|
|
61
|
-
store.
|
|
62
|
+
if (store.skipNextRender)
|
|
63
|
+
store.skipNextRender = false
|
|
64
|
+
else
|
|
65
|
+
store.render()
|
|
62
66
|
})
|
|
63
67
|
},
|
|
64
68
|
|
|
65
69
|
reset() {
|
|
66
70
|
store._request(api.reset, () => {
|
|
67
71
|
store.setChosenLink('', '')
|
|
68
|
-
store.fetchState()
|
|
69
72
|
})
|
|
70
73
|
},
|
|
71
74
|
|
|
72
75
|
bulkSelectByComment(value) {
|
|
73
|
-
store._request(() => api.bulkSelectByComment(value)
|
|
74
|
-
store.fetchState()
|
|
75
|
-
})
|
|
76
|
+
store._request(() => api.bulkSelectByComment(value))
|
|
76
77
|
},
|
|
77
78
|
|
|
78
79
|
setGlobalDelay(value) {
|
|
80
|
+
store.skipNextRender = true
|
|
79
81
|
store._request(() => api.setGlobalDelay(value), () => {
|
|
80
82
|
store.delay = value
|
|
81
83
|
})
|
|
82
84
|
},
|
|
83
85
|
|
|
84
86
|
setGlobalDelayJitter(value) {
|
|
87
|
+
store.skipNextRender = true
|
|
85
88
|
store._request(() => api.setGlobalDelayJitter(value), () => {
|
|
86
89
|
store.delayJitter = value
|
|
87
90
|
})
|
|
88
91
|
},
|
|
89
92
|
|
|
90
93
|
selectCookie(name) {
|
|
94
|
+
store.skipNextRender = true
|
|
91
95
|
store._request(() => api.selectCookie(name), async response => {
|
|
92
96
|
store.cookies = await response.json()
|
|
93
97
|
})
|
|
94
98
|
},
|
|
95
99
|
|
|
96
100
|
setProxyFallback(value) {
|
|
101
|
+
store.skipNextRender = true
|
|
97
102
|
store._request(() => api.setProxyFallback(value), () => {
|
|
98
103
|
store.proxyFallback = value
|
|
99
|
-
store.render()
|
|
100
104
|
})
|
|
101
105
|
},
|
|
102
106
|
|
|
103
107
|
setCollectProxied(checked) {
|
|
108
|
+
store.skipNextRender = true
|
|
104
109
|
store._request(() => api.setCollectProxied(checked), () => {
|
|
105
110
|
store.collectProxied = checked
|
|
106
111
|
})
|
|
@@ -111,7 +116,7 @@ export const store = {
|
|
|
111
116
|
return store.brokersByMethod[method]?.[urlMask]
|
|
112
117
|
},
|
|
113
118
|
|
|
114
|
-
|
|
119
|
+
_setBroker(broker) {
|
|
115
120
|
const { method, urlMask } = parseFilename(broker.file)
|
|
116
121
|
store.brokersByMethod[method] ??= {}
|
|
117
122
|
store.brokersByMethod[method][urlMask] = broker
|
|
@@ -153,33 +158,37 @@ export const store = {
|
|
|
153
158
|
},
|
|
154
159
|
|
|
155
160
|
selectFile(file) {
|
|
161
|
+
store.skipNextRender = true
|
|
156
162
|
store._request(() => api.select(file), async response => {
|
|
157
163
|
const { method, urlMask } = parseFilename(file)
|
|
158
|
-
store.
|
|
164
|
+
store._setBroker(await response.json())
|
|
159
165
|
store.setChosenLink(method, urlMask)
|
|
160
166
|
store.renderRow(method, urlMask)
|
|
161
167
|
})
|
|
162
168
|
},
|
|
163
169
|
|
|
164
170
|
toggleStatus(method, urlMask, status) {
|
|
171
|
+
store.skipNextRender = true
|
|
165
172
|
store._request(() => api.toggleStatus(method, urlMask, status), async response => {
|
|
166
|
-
store.
|
|
173
|
+
store._setBroker(await response.json())
|
|
167
174
|
store.setChosenLink(method, urlMask)
|
|
168
175
|
store.renderRow(method, urlMask)
|
|
169
176
|
})
|
|
170
177
|
},
|
|
171
178
|
|
|
172
179
|
setProxied(method, urlMask, checked) {
|
|
180
|
+
store.skipNextRender = true
|
|
173
181
|
store._request(() => api.setRouteIsProxied(method, urlMask, checked), async response => {
|
|
174
|
-
store.
|
|
182
|
+
store._setBroker(await response.json())
|
|
175
183
|
store.setChosenLink(method, urlMask)
|
|
176
184
|
store.renderRow(method, urlMask)
|
|
177
185
|
})
|
|
178
186
|
},
|
|
179
187
|
|
|
180
188
|
setDelayed(method, urlMask, checked) {
|
|
189
|
+
store.skipNextRender = true
|
|
181
190
|
store._request(() => api.setRouteIsDelayed(method, urlMask, checked), async response => {
|
|
182
|
-
store.
|
|
191
|
+
store._setBroker(await response.json())
|
|
183
192
|
})
|
|
184
193
|
}
|
|
185
194
|
}
|
package/src/server/Api.js
CHANGED
|
@@ -5,22 +5,18 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
sseClientHotReload,
|
|
10
|
-
DASHBOARD_ASSETS,
|
|
11
|
-
CLIENT_DIR
|
|
12
|
-
} from './WatcherDevClient.js'
|
|
13
|
-
import { startWatchers, stopWatchers, sseClientSyncVersion } from './Watcher.js'
|
|
14
|
-
|
|
15
8
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
16
9
|
|
|
10
|
+
import { sseClientHotReload, DASHBOARD_ASSETS, CLIENT_DIR } from './WatcherDevClient.js'
|
|
11
|
+
import { stopMocksDirWatcher, sseClientSyncVersion, uiSyncVersion, watchMocksDir } from './Watcher.js'
|
|
12
|
+
|
|
17
13
|
import { API } from '../client/ApiConstants.js'
|
|
18
14
|
import { IndexHtml, CSP } from '../client/IndexHtml.js'
|
|
19
15
|
|
|
20
16
|
import { cookie } from './cookie.js'
|
|
21
17
|
import { config, ConfigValidator } from './config.js'
|
|
22
|
-
|
|
23
18
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
19
|
+
import { write, rm, isFile, resolveIn } from './utils/fs.js'
|
|
24
20
|
|
|
25
21
|
|
|
26
22
|
export const apiGetReqs = new Map([
|
|
@@ -52,6 +48,8 @@ export const apiPatchReqs = new Map([
|
|
|
52
48
|
[API.proxied, setRouteIsProxied],
|
|
53
49
|
[API.toggleStatus, toggleRouteStatus],
|
|
54
50
|
|
|
51
|
+
[API.writeMock, writeMock],
|
|
52
|
+
[API.deleteMock, deleteMock],
|
|
55
53
|
[API.watchMocks, setWatchMocks]
|
|
56
54
|
])
|
|
57
55
|
|
|
@@ -80,6 +78,7 @@ function getState(_, response) {
|
|
|
80
78
|
|
|
81
79
|
proxyFallback: config.proxyFallback,
|
|
82
80
|
collectProxied: config.collectProxied,
|
|
81
|
+
readOnly: config.readOnly,
|
|
83
82
|
corsAllowed: config.corsAllowed
|
|
84
83
|
})
|
|
85
84
|
}
|
|
@@ -90,6 +89,7 @@ function getState(_, response) {
|
|
|
90
89
|
function reset(_, response) {
|
|
91
90
|
mockBrokersCollection.init()
|
|
92
91
|
response.ok()
|
|
92
|
+
uiSyncVersion.increment()
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
|
|
@@ -101,21 +101,7 @@ async function setCorsAllowed(req, response) {
|
|
|
101
101
|
else {
|
|
102
102
|
config.corsAllowed = corsAllowed
|
|
103
103
|
response.ok()
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
async function setWatchMocks(req, response) {
|
|
109
|
-
const enabled = await req.json()
|
|
110
|
-
|
|
111
|
-
if (typeof enabled !== 'boolean')
|
|
112
|
-
response.unprocessable(`Expected boolean for "watchMocks"`)
|
|
113
|
-
else {
|
|
114
|
-
if (enabled)
|
|
115
|
-
startWatchers()
|
|
116
|
-
else
|
|
117
|
-
stopWatchers()
|
|
118
|
-
response.ok()
|
|
104
|
+
uiSyncVersion.increment()
|
|
119
105
|
}
|
|
120
106
|
}
|
|
121
107
|
|
|
@@ -127,7 +113,9 @@ async function setGlobalDelay(req, response) {
|
|
|
127
113
|
response.unprocessable(`Expected non-negative integer for "delay"`)
|
|
128
114
|
else {
|
|
129
115
|
config.delay = delay
|
|
116
|
+
uiSyncVersion.increment()
|
|
130
117
|
response.ok()
|
|
118
|
+
uiSyncVersion.increment()
|
|
131
119
|
}
|
|
132
120
|
}
|
|
133
121
|
|
|
@@ -139,6 +127,7 @@ async function setGlobalDelayJitter(req, response) {
|
|
|
139
127
|
else {
|
|
140
128
|
config.delayJitter = jitter
|
|
141
129
|
response.ok()
|
|
130
|
+
uiSyncVersion.increment()
|
|
142
131
|
}
|
|
143
132
|
}
|
|
144
133
|
|
|
@@ -149,8 +138,10 @@ async function selectCookie(req, response) {
|
|
|
149
138
|
const error = cookie.setCurrent(cookieKey)
|
|
150
139
|
if (error)
|
|
151
140
|
response.unprocessable(error?.message || error)
|
|
152
|
-
else
|
|
141
|
+
else {
|
|
153
142
|
response.json(cookie.list())
|
|
143
|
+
uiSyncVersion.increment()
|
|
144
|
+
}
|
|
154
145
|
}
|
|
155
146
|
|
|
156
147
|
|
|
@@ -162,6 +153,7 @@ async function setProxyFallback(req, response) {
|
|
|
162
153
|
else {
|
|
163
154
|
config.proxyFallback = fallback
|
|
164
155
|
response.ok()
|
|
156
|
+
uiSyncVersion.increment()
|
|
165
157
|
}
|
|
166
158
|
}
|
|
167
159
|
|
|
@@ -173,6 +165,7 @@ async function setCollectProxied(req, response) {
|
|
|
173
165
|
else {
|
|
174
166
|
config.collectProxied = collectProxied
|
|
175
167
|
response.ok()
|
|
168
|
+
uiSyncVersion.increment()
|
|
176
169
|
}
|
|
177
170
|
}
|
|
178
171
|
|
|
@@ -183,6 +176,7 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
|
183
176
|
|
|
184
177
|
mockBrokersCollection.setMocksMatchingComment(comment)
|
|
185
178
|
response.ok()
|
|
179
|
+
uiSyncVersion.increment()
|
|
186
180
|
}
|
|
187
181
|
|
|
188
182
|
|
|
@@ -195,6 +189,7 @@ async function selectMock(req, response) {
|
|
|
195
189
|
else {
|
|
196
190
|
broker.selectFile(file)
|
|
197
191
|
response.json(broker)
|
|
192
|
+
uiSyncVersion.increment()
|
|
198
193
|
}
|
|
199
194
|
}
|
|
200
195
|
|
|
@@ -208,6 +203,7 @@ async function toggleRouteStatus(req, response) {
|
|
|
208
203
|
else {
|
|
209
204
|
broker.toggleStatus(status)
|
|
210
205
|
response.json(broker)
|
|
206
|
+
uiSyncVersion.increment()
|
|
211
207
|
}
|
|
212
208
|
}
|
|
213
209
|
|
|
@@ -223,6 +219,7 @@ async function setRouteIsDelayed(req, response) {
|
|
|
223
219
|
else {
|
|
224
220
|
broker.setDelayed(delayed)
|
|
225
221
|
response.json(broker)
|
|
222
|
+
uiSyncVersion.increment()
|
|
226
223
|
}
|
|
227
224
|
}
|
|
228
225
|
|
|
@@ -240,8 +237,66 @@ async function setRouteIsProxied(req, response) {
|
|
|
240
237
|
else {
|
|
241
238
|
broker.setProxied(proxied)
|
|
242
239
|
response.json(broker)
|
|
240
|
+
uiSyncVersion.increment()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async function writeMock(req, response) {
|
|
246
|
+
if (config.readOnly)
|
|
247
|
+
return response.forbidden()
|
|
248
|
+
|
|
249
|
+
const [file, content] = await req.json()
|
|
250
|
+
const path = await resolveIn(config.mocksDir, file)
|
|
251
|
+
|
|
252
|
+
if (!path)
|
|
253
|
+
return response.forbidden()
|
|
254
|
+
|
|
255
|
+
await write(path, content)
|
|
256
|
+
|
|
257
|
+
if (!config.watcherEnabled) {
|
|
258
|
+
mockBrokersCollection.registerMock(file, true)
|
|
259
|
+
uiSyncVersion.increment()
|
|
243
260
|
}
|
|
261
|
+
response.ok()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async function deleteMock(req, response) {
|
|
266
|
+
if (config.readOnly)
|
|
267
|
+
return response.forbidden()
|
|
268
|
+
|
|
269
|
+
const file = await req.json()
|
|
270
|
+
const path = await resolveIn(config.mocksDir, file)
|
|
271
|
+
|
|
272
|
+
if (!path)
|
|
273
|
+
return response.forbidden()
|
|
274
|
+
|
|
275
|
+
if (!isFile(path))
|
|
276
|
+
return response.unprocessable(`Missing Mock: ${file}`)
|
|
277
|
+
|
|
278
|
+
await rm(path)
|
|
279
|
+
|
|
280
|
+
if (!config.watcherEnabled) {
|
|
281
|
+
mockBrokersCollection.unregisterMock(file)
|
|
282
|
+
uiSyncVersion.increment()
|
|
283
|
+
}
|
|
284
|
+
response.ok()
|
|
244
285
|
}
|
|
245
286
|
|
|
246
287
|
|
|
247
288
|
|
|
289
|
+
async function setWatchMocks(req, response) {
|
|
290
|
+
const enabled = await req.json()
|
|
291
|
+
|
|
292
|
+
if (typeof enabled !== 'boolean')
|
|
293
|
+
response.unprocessable(`Expected boolean for "watchMocks"`)
|
|
294
|
+
else {
|
|
295
|
+
if (enabled)
|
|
296
|
+
watchMocksDir()
|
|
297
|
+
else
|
|
298
|
+
stopMocksDirWatcher()
|
|
299
|
+
response.ok()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
@@ -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
|
@@ -12,24 +12,22 @@ let mocksWatcher = null
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* ARR Event = Add, Remove, or Rename Mock
|
|
16
|
-
*
|
|
17
15
|
* The emitter is debounced so it handles e.g. bulk deletes,
|
|
18
16
|
* and also renames, which are two events (delete + add).
|
|
19
17
|
*/
|
|
20
|
-
const uiSyncVersion = new class extends EventEmitter {
|
|
18
|
+
export const uiSyncVersion = new class extends EventEmitter {
|
|
21
19
|
version = 0
|
|
22
20
|
|
|
23
21
|
increment = /** @type {function} */ this.#debounce(() => {
|
|
24
22
|
this.version++
|
|
25
|
-
super.emit('
|
|
23
|
+
super.emit('INC')
|
|
26
24
|
})
|
|
27
25
|
|
|
28
26
|
subscribe(listener) {
|
|
29
|
-
this.on('
|
|
27
|
+
this.on('INC', listener)
|
|
30
28
|
}
|
|
31
29
|
unsubscribe(listener) {
|
|
32
|
-
this.removeListener('
|
|
30
|
+
this.removeListener('INC', listener)
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
#debounce(fn) { // TESTME
|
|
@@ -64,8 +62,12 @@ export function watchMocksDir() {
|
|
|
64
62
|
})
|
|
65
63
|
}
|
|
66
64
|
|
|
65
|
+
export function stopMocksDirWatcher() {
|
|
66
|
+
mocksWatcher?.close()
|
|
67
|
+
mocksWatcher = null
|
|
68
|
+
}
|
|
69
|
+
|
|
67
70
|
|
|
68
|
-
/** Realtime notify ARR Events */
|
|
69
71
|
export function sseClientSyncVersion(req, response) {
|
|
70
72
|
response.writeHead(200, {
|
|
71
73
|
'Content-Type': 'text/event-stream',
|
|
@@ -94,11 +96,3 @@ export function sseClientSyncVersion(req, response) {
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
|
|
97
|
-
export function startWatchers() {
|
|
98
|
-
watchMocksDir()
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function stopWatchers() {
|
|
102
|
-
mocksWatcher?.close()
|
|
103
|
-
mocksWatcher = null
|
|
104
|
-
}
|
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
|
}
|