mockaton 6.4.6 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-dashboard.png +0 -0
- package/README.md +40 -42
- package/Tests.js +43 -59
- package/index.d.ts +23 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/Api.js +27 -6
- package/src/ApiConstants.js +5 -3
- package/src/Commander.js +54 -0
- package/src/Dashboard.js +12 -12
- package/src/MockDispatcher.js +21 -31
- package/src/Mockaton.js +24 -22
package/README-dashboard.png
CHANGED
|
Binary file
|
package/README.md
CHANGED
|
@@ -12,35 +12,41 @@ my-mocks-dir/api/user/[user-id].GET.200.json
|
|
|
12
12
|
[This browser extension](https://github.com/ericfortis/devtools-ext-tar-http-requests)
|
|
13
13
|
can be used for downloading a TAR of your XHR requests following that convention.
|
|
14
14
|
|
|
15
|
-
## What do I use
|
|
15
|
+
## What do I use it for?
|
|
16
16
|
- I’m a frontend dev, so I don’t have to spin up and maintain hefty or complex backends.
|
|
17
|
-
- For a deterministic and comprehensive state.
|
|
18
|
-
state variants
|
|
17
|
+
- For a deterministic and comprehensive backend state. For example, having all the possible
|
|
18
|
+
state variants of a particular collection helps for spotting inadvertent bugs. And having those
|
|
19
|
+
assorted responses are not easy to trigger from the backend.
|
|
19
20
|
- Testing empty responses.
|
|
20
21
|
- Testing spinners by delaying responses.
|
|
21
|
-
-
|
|
22
|
+
- Testing errors such as _Bad Request_ and _Internal Server Error_.
|
|
22
23
|
- Triggering notifications and alerts.
|
|
23
|
-
-
|
|
24
|
+
- Prototyping before the backend API is developed.
|
|
25
|
+
- Setting up tests.
|
|
26
|
+
- If you commit the mocks in the repo, when bisecting a bug, you don’t
|
|
24
27
|
have to sync the frontend with many backend repos.
|
|
25
28
|
- Similarly, I can check out long-lived branches that have old API contracts.
|
|
26
|
-
- Prototyping before the backend API is developed.
|
|
27
29
|
- As API documentation.
|
|
28
|
-
- Setting up tests.
|
|
29
30
|
|
|
30
31
|
## Alternatives
|
|
31
32
|
- Chrome DevTools allows for [overriding responses](https://developer.chrome.com/docs/devtools/overrides)
|
|
32
33
|
- Reverse Proxies such as [Burp](https://portswigger.net/burp) are also handy for overriding responses.
|
|
33
|
-
- [
|
|
34
|
+
- Storybook’s [MSW](https://storybook.js.org/addons/msw-storybook-addon)
|
|
34
35
|
|
|
35
36
|
### Caveats
|
|
36
|
-
- Syncing the mocks
|
|
37
|
+
- Syncing the mocks, but the browser extension mentioned above helps.
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
## Getting Started
|
|
40
41
|
The best way to learn _Mockaton_ is by checking out this repo and
|
|
41
42
|
exploring its [sample-mocks/](./sample-mocks) directory. Then, run
|
|
42
|
-
[`./_usage_example.js`](./_usage_example.js) and you’ll see
|
|
43
|
+
[`./_usage_example.js`](./_usage_example.js) and you’ll see the dashboard.
|
|
44
|
+
|
|
45
|
+
You can select mock files without resetting Mockaton. The _Reset_
|
|
46
|
+
button is for when you add, remove, or rename a mock file.
|
|
43
47
|
|
|
48
|
+
The dropdown lets you pick a mock variant, details in the next section. Next to it is a
|
|
49
|
+
_Delay_ toggler, and a button for sending _500 - Internal Server Error_ on that endpoint.
|
|
44
50
|
|
|
45
51
|
<img src="./README-dashboard.png" style="max-width:820px"/>
|
|
46
52
|
|
|
@@ -282,60 +288,52 @@ Config.onReady = open
|
|
|
282
288
|
|
|
283
289
|
---
|
|
284
290
|
|
|
285
|
-
## API
|
|
286
|
-
|
|
287
|
-
|
|
291
|
+
## HTTP API
|
|
292
|
+
`Commander` is a wrapper for the Mockaton HTTP API.
|
|
293
|
+
All of its methods return their `fetch` response promise.
|
|
288
294
|
```js
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
})
|
|
295
|
-
})
|
|
295
|
+
import { Commander } from 'mockaton'
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
const myMockatonAddr = 'http://localhost:2345'
|
|
299
|
+
const commander = new Commander(myMockatonAddr)
|
|
296
300
|
```
|
|
297
301
|
|
|
302
|
+
### Select a mock file for a route
|
|
303
|
+
```js
|
|
304
|
+
await commander.select('api/foo.200.GET.json')
|
|
305
|
+
```
|
|
298
306
|
### Select all mocks that have a particular comment
|
|
299
307
|
```js
|
|
300
|
-
|
|
301
|
-
method: 'PATCH',
|
|
302
|
-
body: JSON.stringify('(demo-a)')
|
|
303
|
-
})
|
|
308
|
+
await commander.bulkSelectByComment('(demo-a)')
|
|
304
309
|
```
|
|
305
310
|
|
|
306
|
-
###
|
|
307
|
-
Sends a list of the available cookies along with an "is selected" boolean flag.
|
|
311
|
+
### Set Route is Delayed Flag
|
|
308
312
|
```js
|
|
309
|
-
|
|
313
|
+
await commander.setRouteIsDelayed('GET', '/api/foo', true)
|
|
310
314
|
```
|
|
311
315
|
|
|
312
316
|
### Select a cookie
|
|
313
|
-
In `Config.cookies`, each key is the label used for
|
|
317
|
+
In `Config.cookies`, each key is the label used for selecting it.
|
|
314
318
|
```js
|
|
315
|
-
|
|
316
|
-
method: 'PATCH',
|
|
317
|
-
body: JSON.stringify('My Normal User')
|
|
318
|
-
})
|
|
319
|
+
await commander.selectCookie('My Normal User')
|
|
319
320
|
```
|
|
320
321
|
|
|
321
|
-
###
|
|
322
|
+
### Set Fallback Proxy
|
|
322
323
|
```js
|
|
323
|
-
|
|
324
|
-
method: 'PATCH',
|
|
325
|
-
body: JSON.stringify('http://example.com')
|
|
326
|
-
})
|
|
324
|
+
await commander.setProxyFallback('http://example.com')
|
|
327
325
|
```
|
|
326
|
+
Pass an empty string to disable it.
|
|
328
327
|
|
|
329
328
|
### Reset
|
|
330
329
|
Re-initialize the collection. So if you added or removed mocks they
|
|
331
330
|
will be considered. The selected mocks, cookies, and delays go
|
|
332
|
-
back to default, but `Config.
|
|
331
|
+
back to default, but `Config.proxyFallback` is not affected.
|
|
333
332
|
```js
|
|
334
|
-
|
|
335
|
-
method: 'PATCH'
|
|
336
|
-
})
|
|
333
|
+
await commander.reset()
|
|
337
334
|
```
|
|
338
335
|
|
|
336
|
+
|
|
339
337
|
## TODO
|
|
340
|
-
- Dashboard. List `staticDir` and indicate if it’s overriding some mock.
|
|
341
338
|
- Refactor Tests
|
|
339
|
+
- Dashboard. List `staticDir` and indicate if it’s overriding some mock.
|
package/Tests.js
CHANGED
|
@@ -11,8 +11,9 @@ import { writeFileSync, mkdtempSync, mkdirSync } from 'node:fs'
|
|
|
11
11
|
import { Config } from './src/Config.js'
|
|
12
12
|
import { mimeFor } from './src/utils/mime.js'
|
|
13
13
|
import { Mockaton } from './src/Mockaton.js'
|
|
14
|
+
import { Commander } from './src/Commander.js'
|
|
14
15
|
import { parseFilename } from './src/Filename.js'
|
|
15
|
-
import { API,
|
|
16
|
+
import { API, DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './src/ApiConstants.js'
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
const tmpDir = mkdtempSync(tmpdir()) + '/'
|
|
@@ -36,6 +37,12 @@ const fixtureDefaultInName = [
|
|
|
36
37
|
'default my route body content'
|
|
37
38
|
]
|
|
38
39
|
|
|
40
|
+
const fixtureDelayed = [
|
|
41
|
+
'/api/delayed',
|
|
42
|
+
'api/delayed.GET.200.json',
|
|
43
|
+
'Route_To_Be_Delayed'
|
|
44
|
+
]
|
|
45
|
+
|
|
39
46
|
const fixtures = [
|
|
40
47
|
[
|
|
41
48
|
'/api',
|
|
@@ -45,6 +52,7 @@ const fixtures = [
|
|
|
45
52
|
|
|
46
53
|
// Exact route paths
|
|
47
54
|
fixtureDefaultInName,
|
|
55
|
+
fixtureDelayed,
|
|
48
56
|
[
|
|
49
57
|
'/api/the-route',
|
|
50
58
|
'api/the-route(default).GET.200.json',
|
|
@@ -135,6 +143,8 @@ writeStatic('index.html', '<h1>Static</h1>')
|
|
|
135
143
|
writeStatic('assets/app.js', 'const app = 1')
|
|
136
144
|
writeStatic('another-entry/index.html', '<h1>Another</h1>')
|
|
137
145
|
|
|
146
|
+
|
|
147
|
+
|
|
138
148
|
const server = Mockaton({
|
|
139
149
|
mocksDir: tmpDir,
|
|
140
150
|
staticDir: staticTmpDir,
|
|
@@ -151,7 +161,19 @@ const server = Mockaton({
|
|
|
151
161
|
})
|
|
152
162
|
server.on('listening', runTests)
|
|
153
163
|
|
|
164
|
+
function mockatonAddr() {
|
|
165
|
+
const { address, port } = server.address()
|
|
166
|
+
return `http://${address}:${port}`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function request(path, options = {}) {
|
|
170
|
+
return fetch(`${mockatonAddr()}${path}`, options)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let commander
|
|
154
174
|
async function runTests() {
|
|
175
|
+
commander = new Commander(mockatonAddr())
|
|
176
|
+
|
|
155
177
|
await testItRendersDashboard()
|
|
156
178
|
await test404()
|
|
157
179
|
|
|
@@ -160,11 +182,7 @@ async function runTests() {
|
|
|
160
182
|
|
|
161
183
|
await testDefaultMock()
|
|
162
184
|
|
|
163
|
-
await
|
|
164
|
-
'/api/alternative',
|
|
165
|
-
'api/alternative(comment-2).GET.200.json',
|
|
166
|
-
JSON.stringify({ comment: 2 }))
|
|
167
|
-
|
|
185
|
+
await testItUpdatesRouteDelay(...fixtureDelayed)
|
|
168
186
|
await testBadRequestWhenUpdatingNonExistingMockAlternative()
|
|
169
187
|
|
|
170
188
|
await testAutogenerates500(
|
|
@@ -176,14 +194,14 @@ async function runTests() {
|
|
|
176
194
|
'api/.GET.500.txt',
|
|
177
195
|
'keeps non-autogenerated 500')
|
|
178
196
|
|
|
179
|
-
await reset()
|
|
197
|
+
await commander.reset()
|
|
180
198
|
await testItUpdatesTheCurrentSelectedMock(
|
|
181
199
|
'/api/alternative',
|
|
182
200
|
'api/alternative(comment-2).GET.200.json',
|
|
183
201
|
200,
|
|
184
202
|
JSON.stringify({ comment: 2 }))
|
|
185
203
|
|
|
186
|
-
await reset()
|
|
204
|
+
await commander.reset()
|
|
187
205
|
await testExtractsAllComments([
|
|
188
206
|
'(comment-1)',
|
|
189
207
|
'(comment-2)',
|
|
@@ -197,7 +215,7 @@ async function runTests() {
|
|
|
197
215
|
['/api/my-route', 'api/my-route(comment-2).GET.200.json', { comment: 2 }]
|
|
198
216
|
])
|
|
199
217
|
|
|
200
|
-
await reset()
|
|
218
|
+
await commander.reset()
|
|
201
219
|
for (const [url, file, body] of fixtures)
|
|
202
220
|
await testMockDispatching(url, file, body)
|
|
203
221
|
|
|
@@ -212,9 +230,6 @@ async function runTests() {
|
|
|
212
230
|
server.close()
|
|
213
231
|
}
|
|
214
232
|
|
|
215
|
-
async function reset() {
|
|
216
|
-
await request(API.reset, { method: 'PATCH' })
|
|
217
|
-
}
|
|
218
233
|
|
|
219
234
|
async function testItRendersDashboard() {
|
|
220
235
|
const res = await request(API.dashboard)
|
|
@@ -259,10 +274,7 @@ async function testDefaultMock() {
|
|
|
259
274
|
}
|
|
260
275
|
|
|
261
276
|
async function testItUpdatesTheCurrentSelectedMock(url, file, expectedStatus, expectedBody) {
|
|
262
|
-
await
|
|
263
|
-
method: 'PATCH',
|
|
264
|
-
body: JSON.stringify({ [DF.file]: file })
|
|
265
|
-
})
|
|
277
|
+
await commander.select(file)
|
|
266
278
|
const res = await request(url)
|
|
267
279
|
const body = await res.text()
|
|
268
280
|
await describe('url: ' + url, () => {
|
|
@@ -271,19 +283,14 @@ async function testItUpdatesTheCurrentSelectedMock(url, file, expectedStatus, ex
|
|
|
271
283
|
})
|
|
272
284
|
}
|
|
273
285
|
|
|
274
|
-
async function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
body: JSON.stringify({
|
|
278
|
-
[DF.file]: file,
|
|
279
|
-
[DF.delayed]: true
|
|
280
|
-
})
|
|
281
|
-
})
|
|
286
|
+
async function testItUpdatesRouteDelay(url, file, expectedBody) {
|
|
287
|
+
const { method } = parseFilename(file)
|
|
288
|
+
await commander.setRouteIsDelayed(method, url, true)
|
|
282
289
|
const now = new Date()
|
|
283
290
|
const res = await request(url)
|
|
284
291
|
const body = await res.text()
|
|
285
292
|
await describe('url: ' + url, () => {
|
|
286
|
-
it('body is: ' + expectedBody, () => equal(body, expectedBody))
|
|
293
|
+
it('body is: ' + expectedBody, () => equal(body, JSON.stringify(expectedBody)))
|
|
287
294
|
it('delay', () => equal((new Date()).getTime() - now.getTime() > Config.delay, true))
|
|
288
295
|
})
|
|
289
296
|
}
|
|
@@ -291,20 +298,14 @@ async function testItUpdatesDelayAndFile(url, file, expectedBody) {
|
|
|
291
298
|
async function testBadRequestWhenUpdatingNonExistingMockAlternative() {
|
|
292
299
|
await it('There are mocks for /api/the-route but not this one', async () => {
|
|
293
300
|
const missingFile = 'api/the-route(non-existing-variant).GET.200.json'
|
|
294
|
-
const res = await
|
|
295
|
-
method: 'PATCH',
|
|
296
|
-
body: JSON.stringify({ [DF.file]: missingFile })
|
|
297
|
-
})
|
|
301
|
+
const res = await commander.select(missingFile)
|
|
298
302
|
equal(res.status, 400)
|
|
299
303
|
equal(await res.text(), `Missing Mock: ${missingFile}`)
|
|
300
304
|
})
|
|
301
305
|
}
|
|
302
306
|
|
|
303
307
|
async function testAutogenerates500(url, file) {
|
|
304
|
-
await
|
|
305
|
-
method: 'PATCH',
|
|
306
|
-
body: JSON.stringify({ [DF.file]: file })
|
|
307
|
-
})
|
|
308
|
+
await commander.select(file)
|
|
308
309
|
const res = await request(url)
|
|
309
310
|
const body = await res.text()
|
|
310
311
|
await describe('autogenerated in-memory 500', () => {
|
|
@@ -314,10 +315,7 @@ async function testAutogenerates500(url, file) {
|
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
async function testPreservesExiting500(url, file, expectedBody) {
|
|
317
|
-
await
|
|
318
|
-
method: 'PATCH',
|
|
319
|
-
body: JSON.stringify({ [DF.file]: file })
|
|
320
|
-
})
|
|
318
|
+
await commander.select(file)
|
|
321
319
|
const res = await request(url)
|
|
322
320
|
const body = await res.text()
|
|
323
321
|
await describe('preserves existing 500', () => {
|
|
@@ -327,17 +325,14 @@ async function testPreservesExiting500(url, file, expectedBody) {
|
|
|
327
325
|
}
|
|
328
326
|
|
|
329
327
|
async function testExtractsAllComments(expected) {
|
|
330
|
-
const res = await
|
|
328
|
+
const res = await commander.listComments()
|
|
331
329
|
const body = await res.json()
|
|
332
330
|
await it('Extracts all comments without duplicates', () =>
|
|
333
331
|
deepEqual(body, expected))
|
|
334
332
|
}
|
|
335
333
|
|
|
336
334
|
async function testItBulkSelectsByComment(comment, tests) {
|
|
337
|
-
await
|
|
338
|
-
method: 'PATCH',
|
|
339
|
-
body: JSON.stringify(comment)
|
|
340
|
-
})
|
|
335
|
+
await commander.bulkSelectByComment(comment)
|
|
341
336
|
for (const [url, file, body] of tests)
|
|
342
337
|
await testMockDispatching(url, file, body)
|
|
343
338
|
}
|
|
@@ -346,7 +341,7 @@ async function testItBulkSelectsByComment(comment, tests) {
|
|
|
346
341
|
async function testItUpdatesUserRole() {
|
|
347
342
|
await describe('Cookie', () => {
|
|
348
343
|
it('Defaults to the first key:value', async () => {
|
|
349
|
-
const res = await
|
|
344
|
+
const res = await commander.listCookies()
|
|
350
345
|
deepEqual(await res.json(), [
|
|
351
346
|
['userA', true],
|
|
352
347
|
['userB', false]
|
|
@@ -354,11 +349,8 @@ async function testItUpdatesUserRole() {
|
|
|
354
349
|
})
|
|
355
350
|
|
|
356
351
|
it('Update the selected cookie', async () => {
|
|
357
|
-
await
|
|
358
|
-
|
|
359
|
-
body: JSON.stringify('userB')
|
|
360
|
-
})
|
|
361
|
-
const res = await request(API.cookies)
|
|
352
|
+
await commander.selectCookie('userB')
|
|
353
|
+
const res = await commander.listCookies()
|
|
362
354
|
deepEqual(await res.json(), [
|
|
363
355
|
['userA', false],
|
|
364
356
|
['userB', true]
|
|
@@ -374,7 +366,7 @@ export default function (req, response) {
|
|
|
374
366
|
response.setHeader('content-type', 'custom-mime')
|
|
375
367
|
return 'SOME_STRING'
|
|
376
368
|
}`)
|
|
377
|
-
await reset() // for registering the file
|
|
369
|
+
await commander.reset() // for registering the file
|
|
378
370
|
await testMockDispatching('/api/js-func',
|
|
379
371
|
'api/js-func.POST.200.js',
|
|
380
372
|
'SOME_STRING',
|
|
@@ -416,7 +408,7 @@ async function testInvalidFilenamesAreIgnored() {
|
|
|
416
408
|
write('api/_INVALID_FILENAME_CONVENTION_.json', '')
|
|
417
409
|
write('api/bad-filename-method._INVALID_METHOD_.200.json', '')
|
|
418
410
|
write('api/bad-filename-status.GET._INVALID_STATUS_.json', '')
|
|
419
|
-
await reset()
|
|
411
|
+
await commander.reset()
|
|
420
412
|
equal(consoleErrorSpy.mock.calls[0].arguments[0], 'Invalid Filename Convention')
|
|
421
413
|
equal(consoleErrorSpy.mock.calls[1].arguments[0], 'Unrecognized HTTP Method: "_INVALID_METHOD_"')
|
|
422
414
|
equal(consoleErrorSpy.mock.calls[2].arguments[0], 'Invalid HTTP Response Status: "NaN"')
|
|
@@ -432,10 +424,7 @@ async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
|
|
|
432
424
|
})
|
|
433
425
|
await promisify(fallbackServer.listen).bind(fallbackServer, 0, '127.0.0.1')()
|
|
434
426
|
|
|
435
|
-
await
|
|
436
|
-
method: 'PATCH',
|
|
437
|
-
body: JSON.stringify(`http://localhost:${fallbackServer.address().port}`)
|
|
438
|
-
})
|
|
427
|
+
await commander.setProxyFallback(`http://localhost:${fallbackServer.address().port}`)
|
|
439
428
|
await it('Relays to fallback server', async () => {
|
|
440
429
|
const res = await request('/non-existing-mock')
|
|
441
430
|
equal(res.headers.get('custom_header'), 'my_custom_header')
|
|
@@ -461,8 +450,3 @@ function _write(absPath, data) {
|
|
|
461
450
|
writeFileSync(absPath, data, 'utf8')
|
|
462
451
|
}
|
|
463
452
|
|
|
464
|
-
function request(path, options = {}) {
|
|
465
|
-
const { address, port } = server.address()
|
|
466
|
-
return fetch(`http://${address}:${port}${path}`, options)
|
|
467
|
-
}
|
|
468
|
-
|
package/index.d.ts
CHANGED
|
@@ -18,6 +18,29 @@ interface Config {
|
|
|
18
18
|
onReady?: (address: string) => void
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
export function Mockaton(options: Config): Server
|
|
22
23
|
|
|
24
|
+
|
|
23
25
|
export function jwtCookie(cookieName: string, payload: any): string
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export class Commander {
|
|
29
|
+
constructor(addr: string)
|
|
30
|
+
|
|
31
|
+
select(file: string): Promise<Response>
|
|
32
|
+
|
|
33
|
+
bulkSelectByComment(comment: string): Promise<Response>
|
|
34
|
+
|
|
35
|
+
setRouteIsDelayed(routeMethod: string, routeUrlMask: string, delayed: boolean): Promise<Response>
|
|
36
|
+
|
|
37
|
+
selectCookie(cookieKey: string): Promise<Response>
|
|
38
|
+
|
|
39
|
+
setProxyFallback(proxyAddr: string): Promise<Response>
|
|
40
|
+
|
|
41
|
+
reset(): Promise<Response>
|
|
42
|
+
|
|
43
|
+
listCookies(): Promise<Response>
|
|
44
|
+
|
|
45
|
+
listComments(): Promise<Response>
|
|
46
|
+
}
|
package/index.js
CHANGED
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -25,7 +25,8 @@ export const apiGetRequests = new Map([
|
|
|
25
25
|
])
|
|
26
26
|
|
|
27
27
|
export const apiPatchRequests = new Map([
|
|
28
|
-
[API.
|
|
28
|
+
[API.select, selectMock],
|
|
29
|
+
[API.delay, setRouteIsDelayed],
|
|
29
30
|
[API.reset, reinitialize],
|
|
30
31
|
[API.cookies, selectCookie],
|
|
31
32
|
[API.fallback, updateProxyFallback],
|
|
@@ -45,6 +46,7 @@ function reinitialize(_, response) {
|
|
|
45
46
|
sendOK(response)
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
|
|
48
50
|
async function selectCookie(req, response) {
|
|
49
51
|
try {
|
|
50
52
|
cookie.setCurrent(await parseJSON(req))
|
|
@@ -55,16 +57,14 @@ async function selectCookie(req, response) {
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
async function selectMock(req, response) {
|
|
59
62
|
try {
|
|
60
|
-
const
|
|
61
|
-
const file = body[DF.file]
|
|
63
|
+
const file = await parseJSON(req)
|
|
62
64
|
const broker = mockBrokersCollection.getBrokerByFilename(file)
|
|
63
65
|
if (!broker || !broker.mockExists(file))
|
|
64
66
|
throw `Missing Mock: ${file}`
|
|
65
67
|
|
|
66
|
-
if (DF.delayed in body)
|
|
67
|
-
broker.updateDelay(body[DF.delayed])
|
|
68
68
|
broker.updateFile(file)
|
|
69
69
|
sendOK(response)
|
|
70
70
|
}
|
|
@@ -73,6 +73,26 @@ async function updateBroker(req, response) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
|
|
77
|
+
async function setRouteIsDelayed(req, response) {
|
|
78
|
+
try {
|
|
79
|
+
const body = await parseJSON(req)
|
|
80
|
+
const broker = mockBrokersCollection.getBrokerForUrl(
|
|
81
|
+
body[DF.routeMethod],
|
|
82
|
+
body[DF.routeUrlMask])
|
|
83
|
+
|
|
84
|
+
if (!broker)
|
|
85
|
+
throw `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`
|
|
86
|
+
|
|
87
|
+
broker.updateDelay(body[DF.delayed])
|
|
88
|
+
sendOK(response)
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
sendBadRequest(response, error)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
76
96
|
async function updateProxyFallback(req, response) {
|
|
77
97
|
try {
|
|
78
98
|
Config.proxyFallback = await parseJSON(req)
|
|
@@ -83,6 +103,7 @@ async function updateProxyFallback(req, response) {
|
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
105
|
|
|
106
|
+
|
|
86
107
|
async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
87
108
|
try {
|
|
88
109
|
mockBrokersCollection.setMocksMatchingComment(await parseJSON(req))
|
package/src/ApiConstants.js
CHANGED
|
@@ -3,7 +3,8 @@ export const API = {
|
|
|
3
3
|
dashboard: MOUNT,
|
|
4
4
|
bulkSelect: MOUNT + '/bulk-select-by-comment',
|
|
5
5
|
comments: MOUNT + '/comments',
|
|
6
|
-
|
|
6
|
+
select: MOUNT + '/select',
|
|
7
|
+
delay: MOUNT + '/delay',
|
|
7
8
|
mocks: MOUNT + '/mocks',
|
|
8
9
|
reset: MOUNT + '/reset',
|
|
9
10
|
cookies: MOUNT + '/cookies',
|
|
@@ -11,8 +12,9 @@ export const API = {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export const DF = { // Dashboard Fields (XHR)
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
routeMethod: 'route_method',
|
|
16
|
+
routeUrlMask: 'route_url_mask',
|
|
17
|
+
delayed: 'delayed'
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export const DEFAULT_500_COMMENT = '(Mockaton 500)'
|
package/src/Commander.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { API, DF } from './ApiConstants.js'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export class Commander {
|
|
5
|
+
#addr = ''
|
|
6
|
+
constructor(addr) {
|
|
7
|
+
this.#addr = addr
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
select(file) {
|
|
11
|
+
return this.#patch(API.select, file)
|
|
12
|
+
}
|
|
13
|
+
bulkSelectByComment(comment) {
|
|
14
|
+
return this.#patch(API.bulkSelect, comment)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setRouteIsDelayed(routeMethod, routeUrlMask, delayed) {
|
|
18
|
+
return this.#patch(API.delay, {
|
|
19
|
+
[DF.routeMethod]: routeMethod,
|
|
20
|
+
[DF.routeUrlMask]: routeUrlMask,
|
|
21
|
+
[DF.delayed]: delayed
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
listCookies() {
|
|
26
|
+
return this.#get(API.cookies)
|
|
27
|
+
}
|
|
28
|
+
selectCookie(cookieKey) {
|
|
29
|
+
return this.#patch(API.cookies, cookieKey)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
listComments() {
|
|
33
|
+
return this.#get(API.comments)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setProxyFallback(proxyAddr) {
|
|
37
|
+
return this.#patch(API.fallback, proxyAddr)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
reset() {
|
|
41
|
+
return this.#patch(API.reset)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
#get(api) {
|
|
46
|
+
return fetch(this.#addr + api)
|
|
47
|
+
}
|
|
48
|
+
#patch(api, body) {
|
|
49
|
+
return fetch(this.#addr + api, {
|
|
50
|
+
method: 'PATCH',
|
|
51
|
+
body: JSON.stringify(body)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/Dashboard.js
CHANGED
|
@@ -133,7 +133,7 @@ function SectionByMethod({ method, brokers }) {
|
|
|
133
133
|
r('tr', null,
|
|
134
134
|
r('td', null, r(PreviewLink, { method, urlMask })),
|
|
135
135
|
r('td', null, r(MockSelector, { broker })),
|
|
136
|
-
r('td', null, r(
|
|
136
|
+
r('td', null, r(DelayRouteToggler, { broker })),
|
|
137
137
|
r('td', null, r(InternalServerErrorToggler, { broker }))
|
|
138
138
|
))))
|
|
139
139
|
}
|
|
@@ -188,9 +188,9 @@ function MockSelector({ broker }) {
|
|
|
188
188
|
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
189
189
|
? 'normal'
|
|
190
190
|
: 'bold'
|
|
191
|
-
fetch(API.
|
|
191
|
+
fetch(API.select, {
|
|
192
192
|
method: 'PATCH',
|
|
193
|
-
body: JSON.stringify(
|
|
193
|
+
body: JSON.stringify(this.value)
|
|
194
194
|
})
|
|
195
195
|
.then(() => {
|
|
196
196
|
this.closest('tr').querySelector('a').click()
|
|
@@ -205,7 +205,7 @@ function MockSelector({ broker }) {
|
|
|
205
205
|
}, file))))
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
function
|
|
208
|
+
function DelayRouteToggler({ broker }) {
|
|
209
209
|
const name = broker.currentMock.file
|
|
210
210
|
const checked = Boolean(broker.currentMock.delay)
|
|
211
211
|
return (
|
|
@@ -219,10 +219,12 @@ function DelayToggler({ broker }) {
|
|
|
219
219
|
name,
|
|
220
220
|
checked,
|
|
221
221
|
onChange(event) {
|
|
222
|
-
|
|
222
|
+
const { method, urlMask } = parseFilename(this.name)
|
|
223
|
+
fetch(API.delay, {
|
|
223
224
|
method: 'PATCH',
|
|
224
225
|
body: JSON.stringify({
|
|
225
|
-
[DF.
|
|
226
|
+
[DF.routeMethod]: method,
|
|
227
|
+
[DF.routeUrlMask]: urlMask,
|
|
226
228
|
[DF.delayed]: event.currentTarget.checked
|
|
227
229
|
})
|
|
228
230
|
})
|
|
@@ -252,13 +254,11 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
252
254
|
name,
|
|
253
255
|
checked,
|
|
254
256
|
onChange(event) {
|
|
255
|
-
fetch(API.
|
|
257
|
+
fetch(API.select, {
|
|
256
258
|
method: 'PATCH',
|
|
257
|
-
body: JSON.stringify(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
: items[0]
|
|
261
|
-
})
|
|
259
|
+
body: JSON.stringify(event.currentTarget.checked
|
|
260
|
+
? items.find(f => parseFilename(f).status === 500)
|
|
261
|
+
: items[0])
|
|
262
262
|
})
|
|
263
263
|
.then(init)
|
|
264
264
|
.catch(console.error)
|
package/src/MockDispatcher.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { readFileSync as read } from 'node:fs'
|
|
3
3
|
|
|
4
4
|
import { proxy } from './ProxyRelay.js'
|
|
5
5
|
import { cookie } from './cookie.js'
|
|
@@ -11,34 +11,40 @@ import { sendInternalServerError, sendNotFound, sendBadRequest } from './utils/h
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
export async function dispatchMock(req, response) {
|
|
14
|
-
const broker = mockBrokerCollection.getBrokerForUrl(req.method, req.url)
|
|
15
|
-
if (!broker) {
|
|
16
|
-
if (Config.proxyFallback)
|
|
17
|
-
await proxy(req, response)
|
|
18
|
-
else
|
|
19
|
-
sendNotFound(response)
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
try {
|
|
15
|
+
const broker = mockBrokerCollection.getBrokerForUrl(req.method, req.url)
|
|
16
|
+
if (!broker) {
|
|
17
|
+
if (Config.proxyFallback)
|
|
18
|
+
await proxy(req, response)
|
|
19
|
+
else
|
|
20
|
+
sendNotFound(response)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
24
|
const { file, status, delay } = broker
|
|
25
25
|
console.log(decodeURIComponent(req.url), ' → ', file)
|
|
26
|
+
const filePath = join(Config.mocksDir, file)
|
|
26
27
|
|
|
27
|
-
let
|
|
28
|
+
let mockBody
|
|
28
29
|
if (file.endsWith('.js')) {
|
|
29
30
|
response.setHeader('Content-Type', mimeFor('.json'))
|
|
30
|
-
|
|
31
|
+
const jsExport = (await import(filePath + '?' + Date.now())).default // date for cache busting
|
|
32
|
+
mockBody = typeof jsExport === 'function'
|
|
33
|
+
? await jsExport(req, response)
|
|
34
|
+
: JSON.stringify(jsExport, null, 2)
|
|
31
35
|
}
|
|
32
36
|
else {
|
|
33
37
|
response.setHeader('Content-Type', mimeFor(file))
|
|
34
|
-
|
|
38
|
+
mockBody = broker.isTemp500
|
|
39
|
+
? ''
|
|
40
|
+
: read(filePath)
|
|
35
41
|
}
|
|
36
|
-
|
|
42
|
+
|
|
37
43
|
if (cookie.getCurrent())
|
|
38
44
|
response.setHeader('Set-Cookie', cookie.getCurrent())
|
|
39
45
|
|
|
40
46
|
response.writeHead(status, Config.extraHeaders)
|
|
41
|
-
setTimeout(() => response.end(
|
|
47
|
+
setTimeout(() => response.end(mockBody), delay)
|
|
42
48
|
}
|
|
43
49
|
catch (error) {
|
|
44
50
|
if (error instanceof JsonBodyParserError)
|
|
@@ -49,19 +55,3 @@ export async function dispatchMock(req, response) {
|
|
|
49
55
|
sendInternalServerError(response, error)
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
|
-
|
|
53
|
-
async function jsMockText(file, req, response) {
|
|
54
|
-
const jsExport = await importDefault(file)
|
|
55
|
-
return typeof jsExport === 'function'
|
|
56
|
-
? await jsExport(req, response)
|
|
57
|
-
: JSON.stringify(jsExport, null, 2)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function readMock(file) {
|
|
61
|
-
return readFileSync(join(Config.mocksDir, file))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function importDefault(file) {
|
|
65
|
-
// The date param is just for cache busting
|
|
66
|
-
return (await import(join(Config.mocksDir, file) + '?' + Date.now())).default
|
|
67
|
-
}
|
package/src/Mockaton.js
CHANGED
|
@@ -11,31 +11,33 @@ import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
|
11
11
|
export function Mockaton(options) {
|
|
12
12
|
setup(options)
|
|
13
13
|
mockBrokerCollection.init()
|
|
14
|
+
const server = createServer(onRequest)
|
|
15
|
+
server.listen(Config.port, Config.host, (error) => {
|
|
16
|
+
const { address, port } = server.address()
|
|
17
|
+
const url = `http://${address}:${port}`
|
|
18
|
+
console.log('Listening', url)
|
|
19
|
+
console.log('Dashboard', url + API.dashboard)
|
|
20
|
+
if (error)
|
|
21
|
+
console.error(error)
|
|
22
|
+
else
|
|
23
|
+
Config.onReady(url + API.dashboard)
|
|
24
|
+
})
|
|
25
|
+
return server
|
|
26
|
+
}
|
|
14
27
|
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
async function onRequest(req, response) {
|
|
29
|
+
response.setHeader('Server', 'Mockaton')
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
const { url, method } = req
|
|
32
|
+
if (method === 'GET' && apiGetRequests.has(url))
|
|
33
|
+
apiGetRequests.get(url)(req, response)
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
else if (method === 'PATCH' && apiPatchRequests.has(url))
|
|
36
|
+
await apiPatchRequests.get(url)(req, response)
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
else if (isStatic(req))
|
|
39
|
+
await dispatchStatic(req, response)
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
.listen(Config.port, Config.host, function (error) {
|
|
32
|
-
const { address, port } = this.address()
|
|
33
|
-
const url = `http://${address}:${port}`
|
|
34
|
-
console.log('Listening', url)
|
|
35
|
-
console.log('Dashboard', url + API.dashboard)
|
|
36
|
-
if (error)
|
|
37
|
-
console.error(error)
|
|
38
|
-
else
|
|
39
|
-
Config.onReady(url + API.dashboard)
|
|
40
|
-
})
|
|
41
|
+
else
|
|
42
|
+
await dispatchMock(req, response)
|
|
41
43
|
}
|