mockaton 11.2.5 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -841
- package/index.js +1 -1
- package/package.json +7 -1
- package/src/client/ApiCommander.js +54 -39
- package/src/client/ApiConstants.js +1 -1
- package/src/client/Filename.js +3 -3
- package/src/client/app-store.test.js +81 -0
- package/src/client/app.js +83 -68
- package/src/client/dom-utils.js +5 -3
- package/src/client/styles.css +83 -48
- package/src/server/Api.js +112 -152
- package/src/server/Filename.js +8 -6
- package/src/server/MockBroker.js +1 -1
- package/src/server/MockDispatcher.js +5 -5
- package/src/server/Mockaton.js +23 -20
- package/src/server/Mockaton.test.js +1190 -0
- package/src/server/ProxyRelay.js +5 -5
- package/src/server/StaticDispatcher.js +4 -4
- package/src/server/Watcher.js +30 -3
- package/src/server/WatcherDevClient.js +37 -5
- package/src/server/cli.js +1 -0
- package/src/server/config.js +3 -2
- package/src/server/mockBrokersCollection.js +2 -1
- package/src/server/utils/{http-request.js → HttpIncomingMessage.js} +7 -1
- package/src/server/utils/HttpServerResponse.js +117 -0
- package/src/server/utils/fs.js +1 -0
- package/src/server/utils/http-cors.js +1 -1
- package/src/server/utils/http-cors.test.js +225 -0
- package/src/server/utils/logger.js +1 -1
- package/src/server/utils/mime.test.js +24 -0
- package/src/server/utils/validate.test.js +47 -0
- package/src/server/utils/http-response.js +0 -107
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
4
|
+
import { createServer } from 'node:http'
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
7
|
+
import { describe, test, before, beforeEach, after } from 'node:test'
|
|
8
|
+
|
|
9
|
+
import { mkdtempSync } from 'node:fs'
|
|
10
|
+
import { writeFile, unlink, mkdir, readFile, rename } from 'node:fs/promises'
|
|
11
|
+
|
|
12
|
+
import { logger } from './utils/logger.js'
|
|
13
|
+
import { mimeFor } from './utils/mime.js'
|
|
14
|
+
import { readBody } from './utils/HttpIncomingMessage.js'
|
|
15
|
+
import { CorsHeader } from './utils/http-cors.js'
|
|
16
|
+
|
|
17
|
+
import { API } from './ApiConstants.js'
|
|
18
|
+
import { Mockaton } from './Mockaton.js'
|
|
19
|
+
import { parseFilename } from './Filename.js'
|
|
20
|
+
import { watchMocksDir, watchStaticDir } from './Watcher.js'
|
|
21
|
+
|
|
22
|
+
import { Commander } from '../client/ApiCommander.js'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const mocksDir = mkdtempSync(join(tmpdir(), 'mocks'))
|
|
26
|
+
const staticDir = mkdtempSync(join(tmpdir(), 'static'))
|
|
27
|
+
|
|
28
|
+
const makeDirInMocks = dir => mkdir(join(mocksDir, dir), { recursive: true })
|
|
29
|
+
const makeDirInStaticMocks = dir => mkdir(join(staticDir, dir), { recursive: true })
|
|
30
|
+
|
|
31
|
+
const renameInMocksDir = (src, target) => rename(join(mocksDir, src), join(mocksDir, target))
|
|
32
|
+
const renameInStaticMocksDir = (src, target) => rename(join(staticDir, src), join(staticDir, target))
|
|
33
|
+
|
|
34
|
+
const readFromMocksDir = f => readFile(join(mocksDir, f), 'utf8')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const COOKIES = { userA: 'CookieA', userB: 'CookieB' }
|
|
38
|
+
const CUSTOM_EXT = 'custom_extension'
|
|
39
|
+
const CUSTOM_MIME = 'custom_mime'
|
|
40
|
+
const CUSTOM_HEADER_NAME = 'custom_header_name'
|
|
41
|
+
const CUSTOM_HEADER_VAL = 'custom_header_val'
|
|
42
|
+
const ALLOWED_ORIGIN = 'http://example.com'
|
|
43
|
+
|
|
44
|
+
const server = await Mockaton({
|
|
45
|
+
mocksDir,
|
|
46
|
+
staticDir,
|
|
47
|
+
onReady() {},
|
|
48
|
+
cookies: COOKIES,
|
|
49
|
+
extraHeaders: [CUSTOM_HEADER_NAME, CUSTOM_HEADER_VAL],
|
|
50
|
+
extraMimes: { [CUSTOM_EXT]: CUSTOM_MIME },
|
|
51
|
+
logLevel: 'verbose',
|
|
52
|
+
corsOrigins: [ALLOWED_ORIGIN],
|
|
53
|
+
corsExposedHeaders: ['Content-Encoding'],
|
|
54
|
+
watcherEnabled: false,
|
|
55
|
+
})
|
|
56
|
+
after(() => server?.close())
|
|
57
|
+
|
|
58
|
+
const api = new Commander(
|
|
59
|
+
`http://${server.address().address}:${server.address().port}`)
|
|
60
|
+
|
|
61
|
+
/** @returns {Promise<State>} */
|
|
62
|
+
async function fetchState() {
|
|
63
|
+
return (await api.getState()).json()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function request(path, options = {}) {
|
|
67
|
+
return fetch(api.addr + path, options)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BaseFixture {
|
|
72
|
+
dir = ''
|
|
73
|
+
urlMask = ''
|
|
74
|
+
method = ''
|
|
75
|
+
|
|
76
|
+
constructor(file, body = '') {
|
|
77
|
+
this.file = file
|
|
78
|
+
this.body = body || `Body for ${file}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async #nextMacroTask() {
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async register() {
|
|
86
|
+
const nextVerPromise = api.getSyncVersion()
|
|
87
|
+
await this.#nextMacroTask()
|
|
88
|
+
await this.write()
|
|
89
|
+
await nextVerPromise
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async unregister() {
|
|
93
|
+
const nextVerPromise = api.getSyncVersion()
|
|
94
|
+
await this.#nextMacroTask()
|
|
95
|
+
await this.unlink()
|
|
96
|
+
await nextVerPromise
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async write() { await writeFile(this.#path(), this.body, 'utf8') }
|
|
100
|
+
async unlink() { await unlink(this.#path()) }
|
|
101
|
+
#path() { return join(this.dir, this.file) }
|
|
102
|
+
|
|
103
|
+
async sync() {
|
|
104
|
+
await this.write()
|
|
105
|
+
await api.reset()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
request(options = {}) {
|
|
109
|
+
options.method ??= this.method
|
|
110
|
+
return request(this.urlMask, options)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Fixture extends BaseFixture {
|
|
116
|
+
constructor(file, body = '') {
|
|
117
|
+
super(file, body)
|
|
118
|
+
this.dir = mocksDir
|
|
119
|
+
const t = parseFilename(file)
|
|
120
|
+
this.urlMask = t.urlMask
|
|
121
|
+
this.method = t.method
|
|
122
|
+
this.status = t.status
|
|
123
|
+
this.ext = t.ext
|
|
124
|
+
}
|
|
125
|
+
async fetchBroker() {
|
|
126
|
+
return (await fetchState()).brokersByMethod?.[this.method]?.[this.urlMask]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class FixtureStatic extends BaseFixture {
|
|
131
|
+
constructor(file, body = '') {
|
|
132
|
+
super(file, body)
|
|
133
|
+
this.dir = staticDir
|
|
134
|
+
this.urlMask = '/' + file
|
|
135
|
+
this.method = 'GET'
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
describe('Windows', () => {
|
|
142
|
+
test('path separators are normalized to forward slashes', async () => {
|
|
143
|
+
const fx = new Fixture('win-paths.GET.200.json')
|
|
144
|
+
await fx.sync()
|
|
145
|
+
const b = await fx.fetchBroker()
|
|
146
|
+
equal(b.file, fx.file)
|
|
147
|
+
await fx.unlink()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
describe('Rejects malicious URLs', () => {
|
|
153
|
+
[
|
|
154
|
+
['double-encoded', `/${encodeURIComponent(encodeURIComponent('/'))}user`, 400],
|
|
155
|
+
['encoded null byte', '/user%00/admin', 400],
|
|
156
|
+
['invalid percent-encoding', '/user%ZZ', 400],
|
|
157
|
+
['encoded CRLF sequence', '/user%0d%0aSet-Cookie:%20x=1', 400],
|
|
158
|
+
['overlong/illegal UTF-8 sequence', '/user%C0%AF', 400],
|
|
159
|
+
['double-double-encoding trick', '/%25252Fuser', 400],
|
|
160
|
+
['zero-width/invisible char', '/user%E2%80%8Binfo', 404],
|
|
161
|
+
['encoded path traversal', '/user/..%2Fadmin', 404],
|
|
162
|
+
['raw path traversal', '/../user', 404],
|
|
163
|
+
|
|
164
|
+
['very long path', '/'.repeat(2048 + 1), 414]
|
|
165
|
+
]
|
|
166
|
+
.map(([title, url, status]) =>
|
|
167
|
+
test(title, async () =>
|
|
168
|
+
equal((await request(url)).status, status)))
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
describe('Warnings', () => {
|
|
173
|
+
function spyLogger(t, method) {
|
|
174
|
+
const spy = t.mock.method(logger, method)
|
|
175
|
+
spy.mock.mockImplementation(() => null)
|
|
176
|
+
return spy.mock
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test('rejects invalid filenames', async t => {
|
|
180
|
+
const spy = spyLogger(t, 'warn')
|
|
181
|
+
const fx0 = new Fixture('bar.GET._INVALID_STATUS_.json')
|
|
182
|
+
const fx1 = new Fixture('foo._INVALID_METHOD_.202.json')
|
|
183
|
+
const fx2 = new Fixture('missing-method-and-status.json')
|
|
184
|
+
await fx0.write()
|
|
185
|
+
await fx1.write()
|
|
186
|
+
await fx2.write()
|
|
187
|
+
await api.reset()
|
|
188
|
+
|
|
189
|
+
equal(spy.calls[0].arguments[0], 'Invalid HTTP Response Status: "NaN"')
|
|
190
|
+
equal(spy.calls[1].arguments[0], 'Unrecognized HTTP Method: "_INVALID_METHOD_"')
|
|
191
|
+
equal(spy.calls[2].arguments[0], 'Invalid Filename Convention')
|
|
192
|
+
|
|
193
|
+
await fx0.unlink()
|
|
194
|
+
await fx1.unlink()
|
|
195
|
+
await fx2.unlink()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('body parser rejects invalid JSON in API requests', async t => {
|
|
199
|
+
const spy = spyLogger(t, 'access')
|
|
200
|
+
const r = await request(API.cookies, {
|
|
201
|
+
method: 'PATCH',
|
|
202
|
+
body: '[invalid_json]'
|
|
203
|
+
})
|
|
204
|
+
equal(r.status, 422)
|
|
205
|
+
equal(spy.calls[0].arguments[1], 'BodyReaderError: Could not parse')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('returns 500 when a handler throws', async t => {
|
|
209
|
+
const spy = spyLogger(t, 'error')
|
|
210
|
+
const r = await request(API.throws)
|
|
211
|
+
equal(r.status, 500)
|
|
212
|
+
equal(spy.calls[0].arguments[2], 'Test500')
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
describe('CORS', () => {
|
|
218
|
+
describe('Set CORS allowed', () => {
|
|
219
|
+
test('422 for non boolean', async () => {
|
|
220
|
+
const r = await api.setCorsAllowed('not-a-boolean')
|
|
221
|
+
equal(r.status, 422)
|
|
222
|
+
equal(await r.text(), 'Expected boolean for "corsAllowed"')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('200', async () => {
|
|
226
|
+
const r = await api.setCorsAllowed(true)
|
|
227
|
+
equal(r.status, 200)
|
|
228
|
+
equal((await fetchState()).corsAllowed, true)
|
|
229
|
+
|
|
230
|
+
await api.setCorsAllowed(false)
|
|
231
|
+
equal((await fetchState()).corsAllowed, false)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('preflights', async () => {
|
|
236
|
+
await api.setCorsAllowed(true)
|
|
237
|
+
const r = await request('/does-not-matter', {
|
|
238
|
+
method: 'OPTIONS',
|
|
239
|
+
headers: {
|
|
240
|
+
[CorsHeader.Origin]: ALLOWED_ORIGIN,
|
|
241
|
+
[CorsHeader.AcRequestMethod]: 'GET'
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
equal(r.status, 204)
|
|
245
|
+
equal(r.headers.get(CorsHeader.AcAllowOrigin), ALLOWED_ORIGIN)
|
|
246
|
+
equal(r.headers.get(CorsHeader.AcAllowMethods), 'GET')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('responds', async () => {
|
|
250
|
+
const fx = new Fixture('cors-response.GET.200.json')
|
|
251
|
+
await fx.sync()
|
|
252
|
+
const r = await fx.request({
|
|
253
|
+
headers: {
|
|
254
|
+
[CorsHeader.Origin]: ALLOWED_ORIGIN
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
equal(r.status, 200)
|
|
258
|
+
equal(r.headers.get(CorsHeader.AcAllowOrigin), ALLOWED_ORIGIN)
|
|
259
|
+
equal(r.headers.get(CorsHeader.AcExposeHeaders), 'Content-Encoding')
|
|
260
|
+
await fx.unlink()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
describe('Dashboard', () => {
|
|
266
|
+
test('renders', async () => {
|
|
267
|
+
const r = await request(API.dashboard)
|
|
268
|
+
match(await r.text(), new RegExp('<!DOCTYPE html>'))
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('query string is accepted', async () => {
|
|
272
|
+
const r = await request(API.dashboard + '?foo=bar')
|
|
273
|
+
match(await r.text(), new RegExp('<!DOCTYPE html>'))
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
describe('Cookie', () => {
|
|
279
|
+
test('422 when trying to select non-existing cookie', async () => {
|
|
280
|
+
const r = await api.selectCookie('non-existing-cookie-key')
|
|
281
|
+
equal(r.status, 422)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('defaults to the first key:value', async () =>
|
|
285
|
+
deepEqual((await fetchState()).cookies, [
|
|
286
|
+
['userA', true],
|
|
287
|
+
['userB', false]
|
|
288
|
+
]))
|
|
289
|
+
|
|
290
|
+
test('updates selected cookie', async () => {
|
|
291
|
+
const fx = new Fixture('update-cookie.GET.200.json')
|
|
292
|
+
await fx.sync()
|
|
293
|
+
const resA = await fx.request()
|
|
294
|
+
equal(resA.headers.get('set-cookie'), COOKIES.userA)
|
|
295
|
+
|
|
296
|
+
const response = await api.selectCookie('userB')
|
|
297
|
+
deepEqual(await response.json(), [
|
|
298
|
+
['userA', false],
|
|
299
|
+
['userB', true]
|
|
300
|
+
])
|
|
301
|
+
|
|
302
|
+
const resB = await fx.request()
|
|
303
|
+
equal(resB.headers.get('set-cookie'), COOKIES.userB)
|
|
304
|
+
await fx.unlink()
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
describe('Delay', () => {
|
|
310
|
+
describe('Set Global Delay', () => {
|
|
311
|
+
test('422 for invalid global delay value', async () => {
|
|
312
|
+
const r = await api.setGlobalDelay('not-a-number')
|
|
313
|
+
equal(r.status, 422)
|
|
314
|
+
equal(await r.text(), 'Expected non-negative integer for "delay"')
|
|
315
|
+
})
|
|
316
|
+
test('200 for valid global delay value', async () => {
|
|
317
|
+
const r = await api.setGlobalDelay(150)
|
|
318
|
+
equal(r.status, 200)
|
|
319
|
+
equal((await fetchState()).delay, 150)
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('updates route delay', async () => {
|
|
324
|
+
const fx = new Fixture('route-delay.GET.200.json')
|
|
325
|
+
await fx.sync()
|
|
326
|
+
const DELAY = 100
|
|
327
|
+
await api.setGlobalDelay(DELAY)
|
|
328
|
+
await api.setRouteIsDelayed(fx.method, fx.urlMask, true)
|
|
329
|
+
const t0 = performance.now()
|
|
330
|
+
const r = await fx.request()
|
|
331
|
+
equal(await r.text(), fx.body)
|
|
332
|
+
equal(performance.now() - t0 > DELAY, true)
|
|
333
|
+
await fx.unlink()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('Set Route is Delayed', () => {
|
|
337
|
+
test('422 for non-existing route', async () => {
|
|
338
|
+
const r = await api.setRouteIsDelayed('GET', '/non-existing', true)
|
|
339
|
+
equal(r.status, 422)
|
|
340
|
+
equal(await r.text(), `Route does not exist: GET /non-existing`)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
test('422 for invalid delayed value', async () => {
|
|
344
|
+
const fx = new Fixture('set-route-delay.GET.200.json')
|
|
345
|
+
await fx.sync()
|
|
346
|
+
const r = await api.setRouteIsDelayed(fx.method, fx.urlMask, 'not-a-boolean')
|
|
347
|
+
equal(await r.text(), 'Expected boolean for "delayed"')
|
|
348
|
+
await fx.unlink()
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('200', async () => {
|
|
352
|
+
const fx = new Fixture('set-route-delay.GET.200.json')
|
|
353
|
+
await fx.sync()
|
|
354
|
+
const r = await api.setRouteIsDelayed(fx.method, fx.urlMask, true)
|
|
355
|
+
equal((await r.json()).delayed, true)
|
|
356
|
+
await fx.unlink()
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
describe('Proxy Fallback', () => {
|
|
363
|
+
describe('Fallback', () => {
|
|
364
|
+
let fallbackServer
|
|
365
|
+
const CUSTOM_COOKIES = ['cookieX=x', 'cookieY=y']
|
|
366
|
+
before(async () => {
|
|
367
|
+
fallbackServer = createServer(async (req, response) => {
|
|
368
|
+
response.writeHead(423, {
|
|
369
|
+
'custom_header': 'my_custom_header',
|
|
370
|
+
'content-type': mimeFor('.txt'),
|
|
371
|
+
'set-cookie': CUSTOM_COOKIES
|
|
372
|
+
})
|
|
373
|
+
response.end(await readBody(req)) // echoes the req body payload
|
|
374
|
+
})
|
|
375
|
+
await promisify(fallbackServer.listen).bind(fallbackServer, 0, '127.0.0.1')()
|
|
376
|
+
await api.setProxyFallback(`http://localhost:${fallbackServer.address().port}`)
|
|
377
|
+
await api.setCollectProxied(true)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
after(() => fallbackServer.close())
|
|
381
|
+
|
|
382
|
+
test('Relays to fallback server and saves the mock', async () => {
|
|
383
|
+
const reqBodyPayload = 'text_req_body'
|
|
384
|
+
|
|
385
|
+
const r = await request(`/non-existing-mock/${randomUUID()}`, {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
body: reqBodyPayload
|
|
388
|
+
})
|
|
389
|
+
equal(r.status, 423)
|
|
390
|
+
equal(r.headers.get('custom_header'), 'my_custom_header')
|
|
391
|
+
equal(r.headers.get('set-cookie'), CUSTOM_COOKIES.join(', '))
|
|
392
|
+
equal(await r.text(), reqBodyPayload)
|
|
393
|
+
|
|
394
|
+
const savedBody = await readFromMocksDir('non-existing-mock/[id].POST.423.txt')
|
|
395
|
+
equal(savedBody, reqBodyPayload)
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
describe('Set Proxy Fallback', () => {
|
|
400
|
+
test('422 when value is not a valid URL', async () => {
|
|
401
|
+
const r = await api.setProxyFallback('bad url')
|
|
402
|
+
equal(r.status, 422)
|
|
403
|
+
equal(await r.text(), 'Invalid Proxy Fallback URL')
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test('sets fallback', async () => {
|
|
407
|
+
const r = await api.setProxyFallback('http://example.com')
|
|
408
|
+
equal(r.status, 200)
|
|
409
|
+
equal((await fetchState()).proxyFallback, 'http://example.com')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('unsets fallback', async () => {
|
|
413
|
+
const r = await api.setProxyFallback('')
|
|
414
|
+
equal(r.status, 200)
|
|
415
|
+
equal((await fetchState()).proxyFallback, '')
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
describe('Set Collect Proxied', () => {
|
|
420
|
+
test('422 for invalid collectProxied value', async () => {
|
|
421
|
+
const r = await api.setCollectProxied('not-a-boolean')
|
|
422
|
+
equal(r.status, 422)
|
|
423
|
+
equal(await r.text(), 'Expected a boolean for "collectProxied"')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test('200 set and unset', async () => {
|
|
427
|
+
await api.setCollectProxied(true)
|
|
428
|
+
equal((await fetchState()).collectProxied, true)
|
|
429
|
+
|
|
430
|
+
await api.setCollectProxied(false)
|
|
431
|
+
equal((await fetchState()).collectProxied, false)
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
describe('Set Route is Proxied', () => {
|
|
436
|
+
const fx = new Fixture('route-is-proxied.GET.200.json')
|
|
437
|
+
beforeEach(async () => {
|
|
438
|
+
await fx.sync()
|
|
439
|
+
await api.setProxyFallback('')
|
|
440
|
+
})
|
|
441
|
+
after(async () => {
|
|
442
|
+
await fx.unlink()
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('422 for non-existing route', async () => {
|
|
446
|
+
const r = await api.setRouteIsProxied('GET', '/non-existing', true)
|
|
447
|
+
equal(r.status, 422)
|
|
448
|
+
equal(await r.text(), `Route does not exist: GET /non-existing`)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('422 for invalid proxied value', async () => {
|
|
452
|
+
const r = await api.setRouteIsProxied(fx.method, fx.urlMask, 'not-a-boolean')
|
|
453
|
+
equal(r.status, 422)
|
|
454
|
+
equal(await r.text(), 'Expected boolean for "proxied"')
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
test('422 for missing proxy fallback', async () => {
|
|
458
|
+
const r = await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
459
|
+
equal(r.status, 422)
|
|
460
|
+
equal(await r.text(), `There’s no proxy fallback`)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test('200 when setting', async () => {
|
|
464
|
+
await api.setProxyFallback('https://example.com')
|
|
465
|
+
const r0 = await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
466
|
+
equal(r0.status, 200)
|
|
467
|
+
equal((await r0.json()).proxied, true)
|
|
468
|
+
|
|
469
|
+
const r1 = await api.setRouteIsProxied(fx.method, fx.urlMask, false)
|
|
470
|
+
equal(r1.status, 200)
|
|
471
|
+
equal((await r1.json()).proxied, false)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
test('200 when unsetting', async () => {
|
|
475
|
+
const r = await api.setRouteIsProxied(fx.method, fx.urlMask, false)
|
|
476
|
+
equal(r.status, 200)
|
|
477
|
+
equal((await r.json()).proxied, false)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('unsets auto500', async () => {
|
|
481
|
+
const fx = new Fixture('unset-500-on-proxy.GET.200.txt')
|
|
482
|
+
await fx.sync()
|
|
483
|
+
await api.setProxyFallback('https://example.com')
|
|
484
|
+
|
|
485
|
+
const r0 = await api.toggle500(fx.method, fx.urlMask)
|
|
486
|
+
const b0 = await r0.json()
|
|
487
|
+
equal(b0.proxied, false)
|
|
488
|
+
equal(b0.auto500, true)
|
|
489
|
+
|
|
490
|
+
const r1 = await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
491
|
+
const b1 = await r1.json()
|
|
492
|
+
equal(b1.proxied, true)
|
|
493
|
+
equal(b1.auto500, false)
|
|
494
|
+
|
|
495
|
+
await fx.unlink()
|
|
496
|
+
await api.setProxyFallback('')
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('updating selected mock resets proxied flag', async () => {
|
|
501
|
+
const fx = new Fixture('select-resets-proxied.GET.200.txt')
|
|
502
|
+
await fx.sync()
|
|
503
|
+
await api.setProxyFallback('http://example.com')
|
|
504
|
+
const r0 = await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
505
|
+
equal((await r0.json()).proxied, true)
|
|
506
|
+
|
|
507
|
+
const r1 = await api.select(fx.file)
|
|
508
|
+
equal((await r1.json()).proxied, false)
|
|
509
|
+
|
|
510
|
+
await api.setProxyFallback('')
|
|
511
|
+
await fx.unlink()
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
describe('404', () => {
|
|
517
|
+
test('when there’s no mock', async () => {
|
|
518
|
+
const r = await request('/non-existing')
|
|
519
|
+
equal(r.status, 404)
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test('when there’s no mock at all for a method', async () => {
|
|
523
|
+
const r = await request('/non-existing-too', { method: 'DELETE' })
|
|
524
|
+
equal(r.status, 404)
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
test('404s ignored files', async () => {
|
|
528
|
+
const fx = new Fixture('ignored.GET.200.json~')
|
|
529
|
+
await fx.write()
|
|
530
|
+
await api.reset()
|
|
531
|
+
const r = await fx.request()
|
|
532
|
+
equal(r.status, 404)
|
|
533
|
+
await fx.unlink()
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
test('404s ignored static files', async () => {
|
|
537
|
+
const fx = new FixtureStatic('static-ignored.js~')
|
|
538
|
+
await fx.write()
|
|
539
|
+
await api.reset()
|
|
540
|
+
const r = await fx.request()
|
|
541
|
+
equal(r.status, 404)
|
|
542
|
+
await fx.unlink()
|
|
543
|
+
})
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
describe('Default Mock', () => {
|
|
548
|
+
const fxA = new Fixture('alpha.GET.200.txt', 'A')
|
|
549
|
+
const fxB = new Fixture('alpha(default).GET.200.txt', 'B')
|
|
550
|
+
before(async () => {
|
|
551
|
+
await fxA.write()
|
|
552
|
+
await fxB.write()
|
|
553
|
+
await api.reset()
|
|
554
|
+
})
|
|
555
|
+
after(async () => {
|
|
556
|
+
await fxA.unlink()
|
|
557
|
+
await fxB.unlink()
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test('sorts mocks list with the user specified default first for dashboard display', async () => {
|
|
561
|
+
const { mocks } = await fxA.fetchBroker()
|
|
562
|
+
deepEqual(mocks, [
|
|
563
|
+
fxB.file,
|
|
564
|
+
fxA.file
|
|
565
|
+
])
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
test('Dispatches default mock', async () => {
|
|
569
|
+
const r = await fxA.request()
|
|
570
|
+
equal(await r.text(), fxB.body)
|
|
571
|
+
})
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
describe('Dynamic Mocks', () => {
|
|
576
|
+
test('JS object is sent as JSON', async () => {
|
|
577
|
+
const fx = new Fixture(
|
|
578
|
+
'js-object.GET.200.js',
|
|
579
|
+
'export default { FROM_JS: true }')
|
|
580
|
+
await fx.sync()
|
|
581
|
+
const r = await fx.request()
|
|
582
|
+
equal(r.headers.get('content-type'), mimeFor('.json'))
|
|
583
|
+
deepEqual(await r.json(), { FROM_JS: true })
|
|
584
|
+
await fx.unlink()
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
test('TS array is sent as JSON', async () => {
|
|
588
|
+
const fx = new Fixture(
|
|
589
|
+
'js-object.GET.200.ts',
|
|
590
|
+
'export default ["from ts"]')
|
|
591
|
+
await fx.sync()
|
|
592
|
+
const r = await fx.request()
|
|
593
|
+
equal(r.headers.get('content-type'), mimeFor('.json'))
|
|
594
|
+
deepEqual(await r.json(), ['from ts'])
|
|
595
|
+
await fx.unlink()
|
|
596
|
+
})
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
describe('Dynamic Function Mocks', () => {
|
|
601
|
+
test('honors filename convention', async () => {
|
|
602
|
+
const fx = new Fixture('func.GET.200.js', `
|
|
603
|
+
export default function (req, response) {
|
|
604
|
+
return Buffer.from('A')
|
|
605
|
+
}`)
|
|
606
|
+
await fx.sync()
|
|
607
|
+
const r = await fx.request()
|
|
608
|
+
equal(r.status, 200)
|
|
609
|
+
equal(r.headers.get('content-length'), '1')
|
|
610
|
+
equal(r.headers.get('content-type'), mimeFor('.json'))
|
|
611
|
+
equal(r.headers.get('set-cookie'), COOKIES.userA)
|
|
612
|
+
equal(await r.text(), 'A')
|
|
613
|
+
await fx.unlink()
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
test('can override filename convention (also supports TS)', async () => {
|
|
617
|
+
const fx = new Fixture('func.POST.200.ts', `
|
|
618
|
+
export default function (req, response) {
|
|
619
|
+
response.statusCode = 201
|
|
620
|
+
response.setHeader('content-type', 'custom-mime')
|
|
621
|
+
response.setHeader('set-cookie', 'custom-cookie')
|
|
622
|
+
return new Uint8Array([65, 65])
|
|
623
|
+
}`)
|
|
624
|
+
await fx.sync()
|
|
625
|
+
const r = await fx.request({ method: 'POST' })
|
|
626
|
+
equal(r.status, 201)
|
|
627
|
+
equal(r.headers.get('content-length'), String(2))
|
|
628
|
+
equal(r.headers.get('content-type'), 'custom-mime')
|
|
629
|
+
equal(r.headers.get('set-cookie'), 'custom-cookie')
|
|
630
|
+
equal(await r.text(), 'AA')
|
|
631
|
+
await fx.unlink()
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
describe('Static Files', () => {
|
|
637
|
+
const fxsIndex = new FixtureStatic('index.html', '<h1>Index</h1>')
|
|
638
|
+
const fxsAsset = new FixtureStatic('asset-script.js', 'const a = 1')
|
|
639
|
+
before(async () => {
|
|
640
|
+
await fxsIndex.write()
|
|
641
|
+
await fxsAsset.write()
|
|
642
|
+
await api.reset()
|
|
643
|
+
}) // the last test deletes them
|
|
644
|
+
|
|
645
|
+
describe('Static File Serving', () => {
|
|
646
|
+
test('Defaults to index.html', async () => {
|
|
647
|
+
const r = await request('/')
|
|
648
|
+
equal(r.status, 200)
|
|
649
|
+
equal(r.headers.get('content-type'), mimeFor(fxsIndex.file))
|
|
650
|
+
equal(await r.text(), fxsIndex.body)
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
test('Serves exacts paths', async () => {
|
|
654
|
+
const r = await fxsAsset.request()
|
|
655
|
+
equal(r.status, 200)
|
|
656
|
+
equal(r.headers.get('content-type'), mimeFor(fxsAsset.file))
|
|
657
|
+
equal(await r.text(), fxsAsset.body)
|
|
658
|
+
})
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
test('Static File List', async () => {
|
|
662
|
+
const { staticBrokers } = await fetchState()
|
|
663
|
+
deepEqual(Object.keys(staticBrokers), [
|
|
664
|
+
fxsAsset.urlMask,
|
|
665
|
+
fxsIndex.urlMask
|
|
666
|
+
])
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
describe('Set Static Route is Delayed', () => {
|
|
670
|
+
test('422 for non-existing route', async () => {
|
|
671
|
+
const r = await api.setStaticRouteIsDelayed('/non-existing', true)
|
|
672
|
+
equal(r.status, 422)
|
|
673
|
+
equal(await r.text(), `Static route does not exist: /non-existing`)
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
test('422 for invalid delayed value', async () => {
|
|
677
|
+
const r = await api.setStaticRouteIsDelayed(fxsIndex.urlMask, 'not-a-boolean')
|
|
678
|
+
equal(await r.text(), 'Expected boolean for "delayed"')
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test('200', async () => {
|
|
682
|
+
await api.setStaticRouteIsDelayed(fxsIndex.urlMask, true)
|
|
683
|
+
const { staticBrokers } = await fetchState()
|
|
684
|
+
equal(staticBrokers[fxsIndex.urlMask].delayed, true)
|
|
685
|
+
})
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
describe('Set Static Route Status Code', () => {
|
|
689
|
+
test('422 for non-existing route', async () => {
|
|
690
|
+
const r = await api.setStaticRouteStatus('/non-existing', 200)
|
|
691
|
+
equal(r.status, 422)
|
|
692
|
+
equal(await r.text(), `Static route does not exist: /non-existing`)
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
test('422 for invalid delayed value', async () => {
|
|
696
|
+
const r = await api.setStaticRouteStatus(fxsIndex.urlMask, 'not-200-or-404')
|
|
697
|
+
equal(r.status, 422)
|
|
698
|
+
equal(await r.text(), 'Expected 200 or 404 status code')
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
test('sets 404 and 200', async () => {
|
|
702
|
+
await api.setStaticRouteStatus(fxsIndex.urlMask, 404)
|
|
703
|
+
const r0 = await fxsIndex.request()
|
|
704
|
+
equal(r0.status, 404)
|
|
705
|
+
|
|
706
|
+
await api.setStaticRouteStatus(fxsIndex.urlMask, 200)
|
|
707
|
+
const r1 = await fxsIndex.request()
|
|
708
|
+
equal(r1.status, 200)
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
test('404s on a registered route but its file has been deleted', async () => {
|
|
712
|
+
// Possible: (1) due to watcher delay. (2) or, when not-watching and deleting.
|
|
713
|
+
const fx = new FixtureStatic('to-be-deleted.js')
|
|
714
|
+
await fx.sync()
|
|
715
|
+
await fx.unlink()
|
|
716
|
+
const r = await fx.request()
|
|
717
|
+
equal(r.status, 404)
|
|
718
|
+
})
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
describe('Static Partial Content', () => {
|
|
722
|
+
test('206 serves partial content', async () => {
|
|
723
|
+
await api.reset()
|
|
724
|
+
const r0 = await fxsIndex.request({ headers: { range: 'bytes=0-3' } })
|
|
725
|
+
const r1 = await fxsIndex.request({ headers: { range: 'bytes=4-' } })
|
|
726
|
+
equal(r0.status, 206)
|
|
727
|
+
equal(r1.status, 206)
|
|
728
|
+
const body = await r0.text() + await r1.text()
|
|
729
|
+
equal(body, fxsIndex.body)
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
test('416 on invalid range (end > start)', async () => {
|
|
733
|
+
const r = await fxsIndex.request({ headers: { range: 'bytes=3-0' } })
|
|
734
|
+
equal(r.status, 416)
|
|
735
|
+
})
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
test('unregisters static route', async () => {
|
|
739
|
+
await fxsIndex.unlink()
|
|
740
|
+
await fxsAsset.unlink()
|
|
741
|
+
await api.reset()
|
|
742
|
+
const { staticBrokers } = await fetchState()
|
|
743
|
+
equal(staticBrokers[fxsIndex.urlMask], undefined)
|
|
744
|
+
equal(staticBrokers[fxsAsset.urlMask], undefined)
|
|
745
|
+
})
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
describe('500', () => {
|
|
750
|
+
test('toggling ON 500 on a route without 500 auto-generates one', async () => {
|
|
751
|
+
const fx = new Fixture('toggling-500-without-500.GET.200.json')
|
|
752
|
+
await fx.sync()
|
|
753
|
+
equal((await fx.request()).status, fx.status)
|
|
754
|
+
|
|
755
|
+
const bp0 = await api.toggle500(fx.method, fx.urlMask)
|
|
756
|
+
const b0 = await bp0.json()
|
|
757
|
+
equal(b0.auto500, true)
|
|
758
|
+
equal(b0.status, 500)
|
|
759
|
+
equal((await fx.request()).status, 500)
|
|
760
|
+
|
|
761
|
+
const r1 = await api.toggle500(fx.method, fx.urlMask)
|
|
762
|
+
equal((await r1.json()).auto500, false)
|
|
763
|
+
equal((await fx.request()).status, fx.status)
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
test('toggling ON 500 picks existing 500 and toggling OFF selects default', async () => {
|
|
767
|
+
const fx200 = new Fixture('reg-error.GET.200.txt')
|
|
768
|
+
const fx500 = new Fixture('reg-error.GET.500.txt')
|
|
769
|
+
await fx200.write()
|
|
770
|
+
await fx500.write()
|
|
771
|
+
await api.reset()
|
|
772
|
+
|
|
773
|
+
const bp0 = await api.toggle500(fx200.method, fx200.urlMask)
|
|
774
|
+
const b0 = await bp0.json()
|
|
775
|
+
equal(b0.auto500, false)
|
|
776
|
+
equal(b0.status, 500)
|
|
777
|
+
equal(await (await fx200.request()).text(), fx500.body)
|
|
778
|
+
|
|
779
|
+
const bp1 = await api.toggle500(fx200.method, fx200.urlMask)
|
|
780
|
+
const b1 = await bp1.json()
|
|
781
|
+
equal(b0.auto500, false)
|
|
782
|
+
equal(b1.status, 200)
|
|
783
|
+
equal(await (await fx200.request()).text(), fx200.body)
|
|
784
|
+
|
|
785
|
+
await fx200.unlink()
|
|
786
|
+
await fx500.unlink()
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
test('toggling ON 500 unsets `proxied` flag', async () => {
|
|
790
|
+
const fx = new Fixture('proxied-to-500.GET.200.txt')
|
|
791
|
+
await fx.sync()
|
|
792
|
+
await api.setProxyFallback('http://example.com')
|
|
793
|
+
await api.setRouteIsProxied(fx.method, fx.urlMask, true)
|
|
794
|
+
await api.toggle500(fx.method, fx.urlMask)
|
|
795
|
+
equal((await fx.fetchBroker()).proxied, false)
|
|
796
|
+
await fx.unlink()
|
|
797
|
+
await api.setProxyFallback('')
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
describe('Index-like routes', () => {
|
|
803
|
+
test('resolves dirs to the file without urlMask', async () => {
|
|
804
|
+
const fx = new Fixture('.GET.200.json')
|
|
805
|
+
await fx.sync()
|
|
806
|
+
const r = await request('/')
|
|
807
|
+
equal(await r.text(), fx.body)
|
|
808
|
+
await fx.unlink()
|
|
809
|
+
})
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
describe('MIME', () => {
|
|
814
|
+
test('derives content-type from known mime', async () => {
|
|
815
|
+
const fx = new Fixture('tmp.GET.200.json')
|
|
816
|
+
await fx.sync()
|
|
817
|
+
const r = await fx.request()
|
|
818
|
+
equal(r.headers.get('content-type'), 'application/json')
|
|
819
|
+
await fx.unlink()
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
test('derives content-type from custom mime', async () => {
|
|
823
|
+
const fx = new Fixture(`tmp.GET.200.${CUSTOM_EXT}`)
|
|
824
|
+
await fx.sync()
|
|
825
|
+
const r = await fx.request()
|
|
826
|
+
equal(r.headers.get('content-type'), CUSTOM_MIME)
|
|
827
|
+
await fx.unlink()
|
|
828
|
+
})
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
describe('Headers', () => {
|
|
833
|
+
test('responses have version in "Server" header', async () => {
|
|
834
|
+
const r = await api.getState()
|
|
835
|
+
const val = r.headers.get('server')
|
|
836
|
+
match(val, /^Mockaton \d+\.\d+\.\d+$/)
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
test('custom headers are included', async () => {
|
|
840
|
+
const r = await api.getState()
|
|
841
|
+
const val = r.headers.get(CUSTOM_HEADER_NAME)
|
|
842
|
+
equal(val, CUSTOM_HEADER_VAL)
|
|
843
|
+
})
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
describe('Method and Status', () => {
|
|
848
|
+
const fx = new Fixture('uncommon-method.ACL.201.txt')
|
|
849
|
+
before(async () => await fx.sync())
|
|
850
|
+
after(async () => await fx.unlink())
|
|
851
|
+
|
|
852
|
+
test('dispatches the response status', async () => {
|
|
853
|
+
const r = await fx.request()
|
|
854
|
+
equal(r.status, fx.status)
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
test('dispatches uncommon but supported methods', async () => {
|
|
858
|
+
const r = await fx.request()
|
|
859
|
+
equal(r.status, fx.status)
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
test('404s when method mismatches', async () => {
|
|
863
|
+
const r = await fx.request({ method: 'POST' })
|
|
864
|
+
equal(r.status, 404)
|
|
865
|
+
})
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
describe('Select', () => {
|
|
870
|
+
const fx = new Fixture('select(default).GET.200.txt')
|
|
871
|
+
const fxAlt = new Fixture('select(variant).GET.200.txt')
|
|
872
|
+
const fxUnregistered = new Fixture('select(non-existing).GET.200.txt')
|
|
873
|
+
before(async () => {
|
|
874
|
+
await fx.write()
|
|
875
|
+
await fxAlt.write()
|
|
876
|
+
await api.reset()
|
|
877
|
+
})
|
|
878
|
+
after(async () => {
|
|
879
|
+
await fx.unlink()
|
|
880
|
+
await fxAlt.unlink()
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
test('422 when updating non-existing mock', async () => {
|
|
884
|
+
const file = 'route-does-not-exist.GET.200.txt'
|
|
885
|
+
const r = await api.select(file)
|
|
886
|
+
equal(r.status, 422)
|
|
887
|
+
equal(await r.text(), `Missing Mock: ${file}`)
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
test('422 when updating non-existing mock alternative but there are other mocks for the route', async () => {
|
|
891
|
+
const r = await api.select(fxUnregistered.file)
|
|
892
|
+
equal(r.status, 422)
|
|
893
|
+
equal(await r.text(), `Missing Mock: ${fxUnregistered.file}`)
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
test('selects variant', async () => {
|
|
897
|
+
const r0 = await request('/select')
|
|
898
|
+
equal(await r0.text(), fx.body)
|
|
899
|
+
|
|
900
|
+
await api.select(fxAlt.file)
|
|
901
|
+
const r1 = await request('/select')
|
|
902
|
+
equal(await r1.text(), fxAlt.body)
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
describe('Bulk Select', () => {
|
|
908
|
+
const fxIota = new Fixture('iota.GET.200.txt')
|
|
909
|
+
const fxIotaB = new Fixture('iota(comment B).GET.200.txt')
|
|
910
|
+
const fxKappaA = new Fixture('kappa(comment A).GET.200.txt')
|
|
911
|
+
const fxKappaB = new Fixture('kappa(comment B).GET.200.txt')
|
|
912
|
+
before(async () => {
|
|
913
|
+
await fxIota.write()
|
|
914
|
+
await fxIotaB.write()
|
|
915
|
+
await fxKappaA.write()
|
|
916
|
+
await fxKappaB.write()
|
|
917
|
+
await api.reset()
|
|
918
|
+
})
|
|
919
|
+
after(async () => {
|
|
920
|
+
await fxIota.unlink()
|
|
921
|
+
await fxIotaB.unlink()
|
|
922
|
+
await fxKappaA.unlink()
|
|
923
|
+
await fxKappaB.unlink()
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
test('extracts all comments without duplicates', async () =>
|
|
927
|
+
deepEqual((await fetchState()).comments, [
|
|
928
|
+
'(comment A)',
|
|
929
|
+
'(comment B)',
|
|
930
|
+
]))
|
|
931
|
+
|
|
932
|
+
test('selects exact', async () => {
|
|
933
|
+
await api.bulkSelectByComment('(comment B)')
|
|
934
|
+
equal((await (await fxIota.request()).text()), fxIotaB.body)
|
|
935
|
+
equal((await (await fxKappaA.request()).text()), fxKappaB.body)
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
test('selects partial', async () => {
|
|
939
|
+
await api.reset()
|
|
940
|
+
await api.bulkSelectByComment('(mment A)')
|
|
941
|
+
equal((await (await fxKappaB.request()).text()), fxKappaA.body)
|
|
942
|
+
})
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
describe('Decoding URLs', () => {
|
|
947
|
+
test('allows dots, spaces, amp, etc.', async () => {
|
|
948
|
+
const fx = new Fixture('dot.in.path and amp & and colon:.GET.200.txt')
|
|
949
|
+
await fx.sync()
|
|
950
|
+
equal(await (await fx.request()).text(), fx.body)
|
|
951
|
+
await fx.unlink()
|
|
952
|
+
})
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
describe('Dynamic Params', () => {
|
|
957
|
+
const fx0 = new Fixture('dynamic-params/[id]/.GET.200.txt')
|
|
958
|
+
const fx1 = new Fixture('dynamic-params/[id]/suffix.GET.200.txt')
|
|
959
|
+
const fx2 = new Fixture('dynamic-params/[id]/suffix/[id].GET.200.txt')
|
|
960
|
+
const fx3 = new Fixture('dynamic-params/exact-route.GET.200.txt')
|
|
961
|
+
before(async () => {
|
|
962
|
+
await makeDirInMocks('dynamic-params/[id]/suffix/[id]')
|
|
963
|
+
await fx0.write()
|
|
964
|
+
await fx1.write()
|
|
965
|
+
await fx2.write()
|
|
966
|
+
await fx3.write()
|
|
967
|
+
await api.reset()
|
|
968
|
+
})
|
|
969
|
+
after(async () => {
|
|
970
|
+
await fx0.unlink()
|
|
971
|
+
await fx1.unlink()
|
|
972
|
+
await fx2.unlink()
|
|
973
|
+
await fx3.unlink()
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
test('variable at end', async () => {
|
|
977
|
+
const r = await fx0.request()
|
|
978
|
+
equal(await r.text(), fx0.body)
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
test('sandwich variable present in another route at its end', async () => {
|
|
982
|
+
const r = await fx1.request()
|
|
983
|
+
equal(await r.text(), fx1.body)
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
test('sandwich fixed part in dynamic variables', async () => {
|
|
987
|
+
const r = await fx2.request()
|
|
988
|
+
equal(await r.text(), fx2.body)
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
test('ensure dynamic params do not take precedence over exact routes', async () => {
|
|
992
|
+
const r = await fx3.request()
|
|
993
|
+
equal(await r.text(), fx3.body)
|
|
994
|
+
})
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
describe('Query String', () => {
|
|
999
|
+
const fx0 = new Fixture('query-string?foo=[foo]&bar=[bar].GET.200.json')
|
|
1000
|
+
const fx1 = new Fixture('query-string/[id]?limit=[limit].GET.200.json')
|
|
1001
|
+
before(async () => {
|
|
1002
|
+
await makeDirInMocks('query-string')
|
|
1003
|
+
await fx0.write()
|
|
1004
|
+
await fx1.write()
|
|
1005
|
+
await api.reset()
|
|
1006
|
+
})
|
|
1007
|
+
after(async () => {
|
|
1008
|
+
await fx0.unlink()
|
|
1009
|
+
await fx1.unlink()
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
test('multiple params', async () => {
|
|
1013
|
+
const r = await fx0.request()
|
|
1014
|
+
equal(await r.text(), fx0.body)
|
|
1015
|
+
})
|
|
1016
|
+
test('with pretty-param and without query-params', async () => {
|
|
1017
|
+
const r = await request('/query-string/1234')
|
|
1018
|
+
equal(await r.text(), fx1.body)
|
|
1019
|
+
})
|
|
1020
|
+
test('with pretty-param and without query-params, but with trailing slash and "?"', async () => {
|
|
1021
|
+
const r = await request('/query-string/1234/?')
|
|
1022
|
+
equal(await r.text(), fx1.body)
|
|
1023
|
+
})
|
|
1024
|
+
test('with pretty-param and query-params', async () => {
|
|
1025
|
+
const r = await request('/query-string/1234/?limit=4')
|
|
1026
|
+
equal(await r.text(), fx1.body)
|
|
1027
|
+
})
|
|
1028
|
+
})
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
test('head for get. returns the headers without body only for GETs requested as HEAD', async () => {
|
|
1032
|
+
const fx = new Fixture('head-get.GET.200.json')
|
|
1033
|
+
await fx.sync()
|
|
1034
|
+
const r = await fx.request({ method: 'HEAD' })
|
|
1035
|
+
equal(r.status, 200)
|
|
1036
|
+
equal(r.headers.get('content-length'), String(Buffer.byteLength(fx.body)))
|
|
1037
|
+
equal(await r.text(), '')
|
|
1038
|
+
await fx.unlink()
|
|
1039
|
+
})
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
describe('Registering Mocks', () => {
|
|
1043
|
+
before(watchMocksDir)
|
|
1044
|
+
|
|
1045
|
+
const fxA = new Fixture('register(default).GET.200.json')
|
|
1046
|
+
const fxB = new Fixture('register(alt).GET.200.json')
|
|
1047
|
+
|
|
1048
|
+
test('register', async () => {
|
|
1049
|
+
await fxA.register()
|
|
1050
|
+
await fxB.register()
|
|
1051
|
+
const b = await fxA.fetchBroker()
|
|
1052
|
+
deepEqual(b.mocks, [fxA.file, fxB.file])
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
test('unregistering selected ensures a mock is selected', async () => {
|
|
1056
|
+
await api.select(fxA.file)
|
|
1057
|
+
await fxA.unregister()
|
|
1058
|
+
const b = await fxA.fetchBroker()
|
|
1059
|
+
deepEqual(b.mocks, [fxB.file])
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
test('unregistering the last mock removes broker', async () => {
|
|
1063
|
+
await fxB.unregister()
|
|
1064
|
+
const b = await fxB.fetchBroker()
|
|
1065
|
+
equal(b, undefined)
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
test('registering a 500 unsets auto500', async () => {
|
|
1069
|
+
const fx200 = new Fixture('reg-error.GET.200.txt')
|
|
1070
|
+
const fx500 = new Fixture('reg-error.GET.500.txt')
|
|
1071
|
+
await fx200.register()
|
|
1072
|
+
await api.toggle500(fx200.method, fx200.urlMask)
|
|
1073
|
+
const b0 = await fx200.fetchBroker()
|
|
1074
|
+
equal(b0.auto500, true)
|
|
1075
|
+
await fx500.register()
|
|
1076
|
+
const b1 = await fx200.fetchBroker()
|
|
1077
|
+
equal(b1.auto500, false)
|
|
1078
|
+
deepEqual(b1.mocks, [
|
|
1079
|
+
fx200.file,
|
|
1080
|
+
fx500.file
|
|
1081
|
+
])
|
|
1082
|
+
await fx200.unregister()
|
|
1083
|
+
await fx500.unregister()
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
describe('getSyncVersion', () => {
|
|
1087
|
+
const fx0 = new Fixture('reg0/runtime0.GET.200.txt')
|
|
1088
|
+
before(async () => {
|
|
1089
|
+
await makeDirInMocks('reg0')
|
|
1090
|
+
await fx0.sync()
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
let version
|
|
1094
|
+
|
|
1095
|
+
test('getSyncVersion responds immediately when version mismatches', async () => {
|
|
1096
|
+
const r = await api.getSyncVersion(-1)
|
|
1097
|
+
version = await r.json()
|
|
1098
|
+
})
|
|
1099
|
+
|
|
1100
|
+
const fx = new Fixture('runtime1.GET.200.txt')
|
|
1101
|
+
test('responds when a file is added', async () => {
|
|
1102
|
+
const prom = api.getSyncVersion(version)
|
|
1103
|
+
await fx.write()
|
|
1104
|
+
const r = await prom
|
|
1105
|
+
equal(await r.json(), version + 1)
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
test('responds when a file is deleted', async () => {
|
|
1109
|
+
const prom = api.getSyncVersion(version + 1)
|
|
1110
|
+
await fx.unlink()
|
|
1111
|
+
const r = await prom
|
|
1112
|
+
equal(await r.json(), version + 2)
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
test('responds when dir is renamed', async () => {
|
|
1116
|
+
const p0 = api.getSyncVersion(version + 2)
|
|
1117
|
+
await renameInMocksDir('reg0', 'reg1')
|
|
1118
|
+
const r0 = await p0
|
|
1119
|
+
equal(await r0.json(), version + 3)
|
|
1120
|
+
|
|
1121
|
+
const s = await fetchState()
|
|
1122
|
+
equal(s.brokersByMethod.GET['/reg1/runtime0'].file, 'reg1/runtime0.GET.200.txt')
|
|
1123
|
+
})
|
|
1124
|
+
})
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
describe('Registering Static Mocks', () => {
|
|
1129
|
+
before(watchStaticDir)
|
|
1130
|
+
|
|
1131
|
+
const fx = new FixtureStatic('static-register.txt')
|
|
1132
|
+
|
|
1133
|
+
test('registers static', async () => {
|
|
1134
|
+
await fx.register()
|
|
1135
|
+
const { staticBrokers } = await fetchState()
|
|
1136
|
+
deepEqual(staticBrokers, {
|
|
1137
|
+
['/' + fx.file]: {
|
|
1138
|
+
route: '/' + fx.file,
|
|
1139
|
+
status: 200,
|
|
1140
|
+
delayed: false
|
|
1141
|
+
}
|
|
1142
|
+
})
|
|
1143
|
+
})
|
|
1144
|
+
|
|
1145
|
+
test('unregisters static', async () => {
|
|
1146
|
+
await fx.unregister()
|
|
1147
|
+
const { staticBrokers } = await fetchState()
|
|
1148
|
+
deepEqual(staticBrokers, {})
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
describe('getSyncVersion', () => {
|
|
1152
|
+
const fx0 = new FixtureStatic('reg0/static0.txt')
|
|
1153
|
+
before(async () => {
|
|
1154
|
+
await makeDirInStaticMocks('reg0')
|
|
1155
|
+
await fx0.sync()
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
let version
|
|
1159
|
+
|
|
1160
|
+
test('getSyncVersion responds immediately when version mismatches', async () => {
|
|
1161
|
+
const r = await api.getSyncVersion(-1)
|
|
1162
|
+
version = await r.json()
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
const fx = new FixtureStatic('static1.txt')
|
|
1166
|
+
test('responds when a file is added', async () => {
|
|
1167
|
+
const prom = api.getSyncVersion(version)
|
|
1168
|
+
await fx.write()
|
|
1169
|
+
const r = await prom
|
|
1170
|
+
equal(await r.json(), version + 1)
|
|
1171
|
+
})
|
|
1172
|
+
|
|
1173
|
+
test('responds when a file is deleted', async () => {
|
|
1174
|
+
const prom = api.getSyncVersion(version + 1)
|
|
1175
|
+
await fx.unlink()
|
|
1176
|
+
const r = await prom
|
|
1177
|
+
equal(await r.json(), version + 2)
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
test('responds when dir is renamed', async () => {
|
|
1181
|
+
const p0 = api.getSyncVersion(version + 2)
|
|
1182
|
+
await renameInStaticMocksDir('reg0', 'reg1')
|
|
1183
|
+
const r0 = await p0
|
|
1184
|
+
equal(await r0.json(), version + 3)
|
|
1185
|
+
|
|
1186
|
+
const s = await fetchState()
|
|
1187
|
+
equal(s.staticBrokers['/reg1/static0.txt'].route, '/reg1/static0.txt')
|
|
1188
|
+
})
|
|
1189
|
+
})
|
|
1190
|
+
})
|