mockaton 8.7.4 → 8.7.6
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 +19 -14
- package/package.json +1 -1
- package/src/Api.js +23 -12
- package/src/Dashboard.css +27 -18
- package/src/Dashboard.js +19 -8
- package/src/Filename.js +1 -0
- package/src/MockDispatcher.js +1 -1
- package/src/Mockaton.js +5 -1
- package/src/Mockaton.test.js +12 -6
- package/src/StaticDispatcher.js +50 -5
- package/src/config.js +4 -0
- package/src/mockBrokersCollection.js +5 -2
- package/src/utils/fs.js +1 -3
- package/src/utils/http-response.js +9 -36
- package/src/utils/mime.js +8 -2
package/README.md
CHANGED
|
@@ -9,27 +9,27 @@ Mockaton is an HTTP mock server with the goal of making
|
|
|
9
9
|
your frontend development and testing easier—and a lot more fun.
|
|
10
10
|
|
|
11
11
|
With Mockaton you don’t need to write code for wiring your mocks.
|
|
12
|
-
Instead, just place your mocks in a directory
|
|
13
|
-
for filenames
|
|
12
|
+
Instead, just place your mocks in a directory and it will be scanned
|
|
13
|
+
for filenames following a convention similar to the URLs.
|
|
14
14
|
|
|
15
|
-
For example, for
|
|
15
|
+
For example, for `/api/user/1234` the mock filename would be:
|
|
16
16
|
```
|
|
17
17
|
my-mocks-dir/api/user/[user-id].GET.200.json
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Scrapping Mocks from you Backend
|
|
21
21
|
|
|
22
|
-
Mockaton can fallback to your real backend on routes you don’t have mocks for.
|
|
23
|
-
|
|
24
|
-
check **Save Mocks**,
|
|
22
|
+
Mockaton can fallback to your real backend on routes you don’t have mocks for. For that,
|
|
23
|
+
type your backend address in the **Fallback Backend** field. And if you
|
|
24
|
+
check **Save Mocks**, it will collect those responses that hit your backend.
|
|
25
25
|
Those mocks will be saved to your `config.mocksDir` following the filename convention.
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
## Multiple Mock Variants
|
|
29
29
|
|
|
30
30
|
### Adding comments in filenames
|
|
31
|
-
Want to mock a locked-out user or an invalid login attempt?
|
|
32
|
-
|
|
31
|
+
Want to mock a locked-out user or an invalid login attempt?
|
|
32
|
+
Add a comment to the filename in parentheses. For example:
|
|
33
33
|
|
|
34
34
|
`api/login(locked out user).POST.423.json`
|
|
35
35
|
|
|
@@ -214,8 +214,8 @@ Response Status Code, and File Extension.
|
|
|
214
214
|
api/user.GET.200.json
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
You can also use `.empty` if you don’t
|
|
218
|
-
`Content-Type` header.
|
|
217
|
+
You can also use `.empty` or `.unknown` if you don’t
|
|
218
|
+
want a `Content-Type` header in the response.
|
|
219
219
|
|
|
220
220
|
|
|
221
221
|
### Dynamic Parameters
|
|
@@ -305,10 +305,10 @@ URL, the filename will have `[id]` in their place. For example,
|
|
|
305
305
|
|
|
306
306
|
```
|
|
307
307
|
/api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
|
|
308
|
-
my-mocks-dir/api/user/[id]/likes.GET.200.json
|
|
308
|
+
my-mocks-dir/api/user/[id]/likes.GET.200.json
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
-
Your existing mocks won’t be overwritten (they don’t hit the fallback server).
|
|
311
|
+
Your existing mocks won’t be overwritten (because they don’t hit the fallback server).
|
|
312
312
|
|
|
313
313
|
<details>
|
|
314
314
|
<summary>Extension Details</summary>
|
|
@@ -464,7 +464,7 @@ If you don’t want to open a browser, pass a noop:
|
|
|
464
464
|
config.onReady = () => {}
|
|
465
465
|
```
|
|
466
466
|
|
|
467
|
-
|
|
467
|
+
At any rate, you can trigger any command besides opening a browser.
|
|
468
468
|
|
|
469
469
|
---
|
|
470
470
|
|
|
@@ -507,9 +507,14 @@ await mockaton.setProxyFallback('http://example.com')
|
|
|
507
507
|
```
|
|
508
508
|
Pass an empty string to disable it.
|
|
509
509
|
|
|
510
|
+
### Set Save Proxied Mocks
|
|
511
|
+
```js
|
|
512
|
+
await mockaton.setCollectProxied(true)
|
|
513
|
+
```
|
|
514
|
+
|
|
510
515
|
### Reset
|
|
511
516
|
Re-initialize the collection. The selected mocks, cookies, and delays go back to
|
|
512
|
-
default, but `
|
|
517
|
+
default, but the `proxyFallback`, `colledProxied`, and `corsAllowed` are not affected.
|
|
513
518
|
```js
|
|
514
519
|
await mockaton.reset()
|
|
515
520
|
```
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -10,17 +10,22 @@ import { DF, API } from './ApiConstants.js'
|
|
|
10
10
|
import { parseJSON } from './utils/http-request.js'
|
|
11
11
|
import { listFilesRecursively } from './utils/fs.js'
|
|
12
12
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
13
|
-
import { sendOK, sendBadRequest, sendJSON,
|
|
13
|
+
import { sendOK, sendBadRequest, sendJSON, sendUnprocessableContent, sendDashboardFile, sendForbidden } from './utils/http-response.js'
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
const dashboardAssets = [
|
|
17
|
+
'/ApiConstants.js',
|
|
18
|
+
'/Commander.js',
|
|
19
|
+
'/Dashboard.css',
|
|
20
|
+
'/Dashboard.js',
|
|
21
|
+
'/Filename.js',
|
|
22
|
+
'/mockaton-logo.svg'
|
|
23
|
+
]
|
|
24
|
+
|
|
16
25
|
export const apiGetRequests = new Map([
|
|
17
26
|
[API.dashboard, serveDashboard],
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
[API.dashboard + '/Dashboard.css', serveDashboardAsset],
|
|
21
|
-
[API.dashboard + '/Dashboard.js', serveDashboardAsset],
|
|
22
|
-
[API.dashboard + '/Filename.js', serveDashboardAsset],
|
|
23
|
-
[API.dashboard + '/mockaton-logo.svg', serveDashboardAsset],
|
|
27
|
+
...dashboardAssets.map(f =>
|
|
28
|
+
[API.dashboard + f, serveDashboardAsset]),
|
|
24
29
|
[API.mocks, listMockBrokers],
|
|
25
30
|
[API.cookies, listCookies],
|
|
26
31
|
[API.comments, listComments],
|
|
@@ -44,10 +49,14 @@ export const apiPatchRequests = new Map([
|
|
|
44
49
|
/* === GET === */
|
|
45
50
|
|
|
46
51
|
function serveDashboard(_, response) {
|
|
47
|
-
|
|
52
|
+
sendDashboardFile(response, join(import.meta.dirname, 'Dashboard.html'))
|
|
48
53
|
}
|
|
49
54
|
function serveDashboardAsset(req, response) {
|
|
50
|
-
|
|
55
|
+
const f = req.url.replace(API.dashboard, '')
|
|
56
|
+
if (dashboardAssets.includes(f))
|
|
57
|
+
sendDashboardFile(response, join(import.meta.dirname, f))
|
|
58
|
+
else
|
|
59
|
+
sendForbidden(response)
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
function listCookies(_, response) { sendJSON(response, cookie.list()) }
|
|
@@ -95,9 +104,11 @@ async function selectMock(req, response) {
|
|
|
95
104
|
const file = await parseJSON(req)
|
|
96
105
|
const broker = mockBrokersCollection.getBrokerByFilename(file)
|
|
97
106
|
if (!broker || !broker.hasMock(file))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
sendUnprocessableContent(response, `Missing Mock: ${file}`)
|
|
108
|
+
else {
|
|
109
|
+
broker.updateFile(file)
|
|
110
|
+
sendOK(response)
|
|
111
|
+
}
|
|
101
112
|
}
|
|
102
113
|
catch (error) {
|
|
103
114
|
sendBadRequest(response, error)
|
package/src/Dashboard.css
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.1), 0 1px 1px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
|
3
|
+
--radius: 6px
|
|
3
4
|
}
|
|
4
5
|
|
|
5
6
|
@media (prefers-color-scheme: light) {
|
|
6
7
|
:root {
|
|
7
8
|
--color4xxBackground: #ffedd1;
|
|
8
|
-
--colorAccent: #
|
|
9
|
-
--colorAccentAlt: #
|
|
9
|
+
--colorAccent: #0075db;
|
|
10
|
+
--colorAccentAlt: #008664;
|
|
10
11
|
--colorBackground: #fff;
|
|
11
|
-
--colorHeaderBackground: #f7f7f7;
|
|
12
|
-
--colorComboBoxBackground: #f7f7f7;
|
|
13
|
-
--colorSecondaryButtonBackground: #f5f5f5;
|
|
14
12
|
--colorComboBoxHeaderBackground: #fff;
|
|
13
|
+
--colorComboBoxBackground: #f7f7f7;
|
|
14
|
+
--colorHeaderBackground: #f3f3f3;
|
|
15
|
+
--colorSecondaryButtonBackground: #f3f3f3;
|
|
15
16
|
--colorDisabled: #444;
|
|
16
17
|
--colorHover: #dfefff;
|
|
17
18
|
--colorLabel: #444;
|
|
@@ -64,13 +65,21 @@ select, a, input, button, summary {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
a, button, input[type=checkbox] {
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
|
|
71
|
+
&:active {
|
|
72
|
+
cursor: grabbing;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
select {
|
|
68
77
|
font-size: 100%;
|
|
69
78
|
background: var(--colorComboBoxBackground);
|
|
70
79
|
color: var(--colorText);
|
|
71
80
|
cursor: pointer;
|
|
72
81
|
outline: 0;
|
|
73
|
-
border-radius:
|
|
82
|
+
border-radius: var(--radius);
|
|
74
83
|
|
|
75
84
|
&:enabled {
|
|
76
85
|
box-shadow: var(--boxShadow1);
|
|
@@ -122,7 +131,11 @@ select {
|
|
|
122
131
|
margin-top: 4px;
|
|
123
132
|
font-size: 11px;
|
|
124
133
|
background: var(--colorComboBoxHeaderBackground);
|
|
125
|
-
border-radius:
|
|
134
|
+
border-radius: var(--radius);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
select:enabled:hover {
|
|
138
|
+
background: var(--colorHover);
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
&.FallbackBackend {
|
|
@@ -165,7 +178,6 @@ select {
|
|
|
165
178
|
background: transparent;
|
|
166
179
|
color: var(--colorRed);
|
|
167
180
|
border-radius: 50px;
|
|
168
|
-
cursor: pointer;
|
|
169
181
|
|
|
170
182
|
&:hover {
|
|
171
183
|
background: var(--colorRed);
|
|
@@ -234,7 +246,7 @@ select {
|
|
|
234
246
|
display: inline-block;
|
|
235
247
|
width: 280px;
|
|
236
248
|
padding: 8px 6px;
|
|
237
|
-
border-radius:
|
|
249
|
+
border-radius: var(--radius);
|
|
238
250
|
color: var(--colorAccent);
|
|
239
251
|
text-decoration: none;
|
|
240
252
|
|
|
@@ -292,8 +304,8 @@ select {
|
|
|
292
304
|
}
|
|
293
305
|
|
|
294
306
|
> svg {
|
|
295
|
-
width:
|
|
296
|
-
height:
|
|
307
|
+
width: 20px;
|
|
308
|
+
height: 20px;
|
|
297
309
|
vertical-align: bottom;
|
|
298
310
|
fill: var(--colorText);
|
|
299
311
|
border-radius: 50%;
|
|
@@ -329,7 +341,7 @@ select {
|
|
|
329
341
|
}
|
|
330
342
|
|
|
331
343
|
> span {
|
|
332
|
-
padding: 4px;
|
|
344
|
+
padding: 5px 4px;
|
|
333
345
|
box-shadow: var(--boxShadow1);
|
|
334
346
|
font-size: 10px;
|
|
335
347
|
color: var(--colorText);
|
|
@@ -374,13 +386,10 @@ select {
|
|
|
374
386
|
}
|
|
375
387
|
|
|
376
388
|
.StaticFilesList {
|
|
377
|
-
margin-top:
|
|
389
|
+
margin-top: 20px;
|
|
378
390
|
|
|
379
|
-
|
|
380
|
-
width: max-content;
|
|
391
|
+
h2 {
|
|
381
392
|
margin-bottom: 8px;
|
|
382
|
-
cursor: pointer;
|
|
383
|
-
font-weight: bold;
|
|
384
393
|
}
|
|
385
394
|
|
|
386
395
|
ul {
|
|
@@ -395,7 +404,7 @@ select {
|
|
|
395
404
|
a {
|
|
396
405
|
display: inline-block;
|
|
397
406
|
padding: 6px;
|
|
398
|
-
border-radius:
|
|
407
|
+
border-radius: var(--radius);
|
|
399
408
|
color: var(--colorAccentAlt);
|
|
400
409
|
text-decoration: none;
|
|
401
410
|
|
package/src/Dashboard.js
CHANGED
|
@@ -3,6 +3,14 @@ import { Commander } from './Commander.js'
|
|
|
3
3
|
import { DEFAULT_500_COMMENT } from './ApiConstants.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
function syntaxHighlightJson(textBody) {
|
|
7
|
+
const prism = window.Prism
|
|
8
|
+
return prism?.highlight && prism?.languages?.json
|
|
9
|
+
? prism.highlight(textBody, prism.languages.json, 'json')
|
|
10
|
+
: false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
const Strings = {
|
|
7
15
|
bulk_select_by_comment: 'Bulk Select by Comment',
|
|
8
16
|
bulk_select_by_comment_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
|
|
@@ -19,7 +27,7 @@ const Strings = {
|
|
|
19
27
|
pick: 'Pick…',
|
|
20
28
|
reset: 'Reset',
|
|
21
29
|
save_proxied: 'Save Mocks',
|
|
22
|
-
|
|
30
|
+
static_get: 'Static GET'
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
const CSS = {
|
|
@@ -397,12 +405,15 @@ async function updatePayloadViewer(method, urlMask, response) {
|
|
|
397
405
|
}))
|
|
398
406
|
}
|
|
399
407
|
else {
|
|
400
|
-
const prism = window.Prism
|
|
401
408
|
const body = await response.text() || Strings.empty_response_body
|
|
402
|
-
if (mime === 'application/json'
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
409
|
+
if (mime === 'application/json') {
|
|
410
|
+
const hBody = syntaxHighlightJson(body)
|
|
411
|
+
if (hBody) {
|
|
412
|
+
payloadViewerRef.current.innerHTML = hBody
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
payloadViewerRef.current.innerText = body
|
|
406
417
|
}
|
|
407
418
|
}
|
|
408
419
|
|
|
@@ -428,11 +439,11 @@ function StaticFilesList({ staticFiles }) {
|
|
|
428
439
|
if (!staticFiles.length)
|
|
429
440
|
return null
|
|
430
441
|
return (
|
|
431
|
-
r('
|
|
442
|
+
r('section', {
|
|
432
443
|
open: true,
|
|
433
444
|
className: CSS.StaticFilesList
|
|
434
445
|
},
|
|
435
|
-
r('
|
|
446
|
+
r('h2', null, Strings.static_get),
|
|
436
447
|
r('ul', null, staticFiles.map(f =>
|
|
437
448
|
r('li', null,
|
|
438
449
|
r('a', { href: f, target: '_blank' }, f))))))
|
package/src/Filename.js
CHANGED
package/src/MockDispatcher.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function dispatchMock(req, response) {
|
|
|
20
20
|
return
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
console.log(decodeURIComponent(req.url),
|
|
23
|
+
console.log('%s → %s', decodeURIComponent(req.url), broker.file)
|
|
24
24
|
response.statusCode = broker.status
|
|
25
25
|
|
|
26
26
|
if (cookie.getCurrent())
|
package/src/Mockaton.js
CHANGED
|
@@ -12,6 +12,8 @@ import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
|
12
12
|
import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
process.on('unhandledRejection', error => { throw error })
|
|
16
|
+
|
|
15
17
|
export function Mockaton(options) {
|
|
16
18
|
setup(options)
|
|
17
19
|
mockBrokerCollection.init()
|
|
@@ -21,7 +23,7 @@ export function Mockaton(options) {
|
|
|
21
23
|
if (!file)
|
|
22
24
|
return
|
|
23
25
|
if (existsSync(join(config.mocksDir, file)))
|
|
24
|
-
mockBrokerCollection.registerMock(file, '
|
|
26
|
+
mockBrokerCollection.registerMock(file, 'isFromWatcher')
|
|
25
27
|
else
|
|
26
28
|
mockBrokerCollection.unregisterMock(file)
|
|
27
29
|
})
|
|
@@ -39,6 +41,8 @@ export function Mockaton(options) {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
async function onRequest(req, response) {
|
|
44
|
+
req.on('error', console.error)
|
|
45
|
+
response.on('error', console.error)
|
|
42
46
|
response.setHeader('Server', 'Mockaton')
|
|
43
47
|
|
|
44
48
|
if (config.corsAllowed)
|
package/src/Mockaton.test.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createServer } from 'node:http'
|
|
|
5
5
|
import { dirname, join } from 'node:path'
|
|
6
6
|
import { randomUUID } from 'node:crypto'
|
|
7
7
|
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
8
|
-
import { writeFileSync, mkdtempSync, mkdirSync, unlinkSync } from 'node:fs'
|
|
8
|
+
import { writeFileSync, mkdtempSync, mkdirSync, unlinkSync, readFileSync } from 'node:fs'
|
|
9
9
|
|
|
10
10
|
import { config } from './config.js'
|
|
11
11
|
import { mimeFor } from './utils/mime.js'
|
|
@@ -14,7 +14,7 @@ import { readBody } from './utils/http-request.js'
|
|
|
14
14
|
import { Commander } from './Commander.js'
|
|
15
15
|
import { CorsHeader } from './utils/http-cors.js'
|
|
16
16
|
import { parseFilename } from './Filename.js'
|
|
17
|
-
import { listFilesRecursively
|
|
17
|
+
import { listFilesRecursively } from './utils/fs.js'
|
|
18
18
|
import { API, DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
19
19
|
|
|
20
20
|
|
|
@@ -351,7 +351,7 @@ async function testRegistering() {
|
|
|
351
351
|
fixtureForRegisteringPutA500[1]
|
|
352
352
|
])
|
|
353
353
|
deepEqual(currentMock, {
|
|
354
|
-
file:
|
|
354
|
+
file: fixtureForRegisteringPutA[1],
|
|
355
355
|
delay: 0
|
|
356
356
|
})
|
|
357
357
|
})
|
|
@@ -409,6 +409,7 @@ async function testItUpdatesRouteDelay(url, file, expectedBody) {
|
|
|
409
409
|
await describe('url: ' + url, () => {
|
|
410
410
|
it('body is: ' + expectedBody, () => equal(body, JSON.stringify(expectedBody)))
|
|
411
411
|
it('delay', () => equal((new Date()).getTime() - now.getTime() > config.delay, true))
|
|
412
|
+
// TODO flaky test ^
|
|
412
413
|
})
|
|
413
414
|
}
|
|
414
415
|
|
|
@@ -416,7 +417,7 @@ async function testBadRequestWhenUpdatingNonExistingMockAlternative() {
|
|
|
416
417
|
await it('There are mocks for /api/the-route but not this one', async () => {
|
|
417
418
|
const missingFile = 'api/the-route(non-existing-variant).GET.200.json'
|
|
418
419
|
const res = await commander.select(missingFile)
|
|
419
|
-
equal(res.status,
|
|
420
|
+
equal(res.status, 422)
|
|
420
421
|
equal(await res.text(), `Missing Mock: ${missingFile}`)
|
|
421
422
|
})
|
|
422
423
|
}
|
|
@@ -499,6 +500,11 @@ export default function (req, response) {
|
|
|
499
500
|
|
|
500
501
|
async function testStaticFileServing() {
|
|
501
502
|
await describe('Static File Serving', () => {
|
|
503
|
+
it('404 path traversal', async () => {
|
|
504
|
+
const res = await request('/../../../etc/passwd')
|
|
505
|
+
equal(res.status, 404)
|
|
506
|
+
})
|
|
507
|
+
|
|
502
508
|
it('Defaults to index.html', async () => {
|
|
503
509
|
const res = await request('/')
|
|
504
510
|
const body = await res.text()
|
|
@@ -549,7 +555,7 @@ async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
|
|
|
549
555
|
const fallbackServer = createServer(async (req, response) => {
|
|
550
556
|
response.writeHead(423, {
|
|
551
557
|
'custom_header': 'my_custom_header',
|
|
552
|
-
'content-type': mimeFor('txt'),
|
|
558
|
+
'content-type': mimeFor('.txt'),
|
|
553
559
|
'set-cookie': [
|
|
554
560
|
'cookieA=A',
|
|
555
561
|
'cookieB=B'
|
|
@@ -573,7 +579,7 @@ async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
|
|
|
573
579
|
equal(res.headers.get('set-cookie'), ['cookieA=A', 'cookieB=B'].join(', '))
|
|
574
580
|
equal(await res.text(), reqBodyPayload)
|
|
575
581
|
|
|
576
|
-
const savedBody =
|
|
582
|
+
const savedBody = readFileSync(join(tmpDir, 'api/non-existing-mock/[id].POST.423.txt'), 'utf8')
|
|
577
583
|
equal(savedBody, reqBodyPayload)
|
|
578
584
|
|
|
579
585
|
fallbackServer.close()
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
1
|
+
import { join, resolve } from 'node:path'
|
|
2
|
+
import fs, { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
2
4
|
import { config } from './config.js'
|
|
5
|
+
import { mimeFor } from './utils/mime.js'
|
|
3
6
|
import { isDirectory, isFile } from './utils/fs.js'
|
|
4
|
-
import {
|
|
7
|
+
import { sendNotFound, sendInternalServerError } from './utils/http-response.js'
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
export function isStatic(req) {
|
|
8
|
-
if (!config.staticDir)
|
|
11
|
+
if (!config.staticDir || !isWithinStaticDir(req.url))
|
|
9
12
|
return false
|
|
10
13
|
const f = resolvePath(req.url)
|
|
11
|
-
return !config.ignore.test(f)
|
|
14
|
+
return f && !config.ignore.test(f)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isWithinStaticDir(url) {
|
|
18
|
+
const candidate = resolve(join(config.staticDir, url))
|
|
19
|
+
return candidate.startsWith(config.staticDir)
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
export async function dispatchStatic(req, response) {
|
|
@@ -24,9 +32,46 @@ export async function dispatchStatic(req, response) {
|
|
|
24
32
|
function resolvePath(url) {
|
|
25
33
|
let candidate = join(config.staticDir, url)
|
|
26
34
|
if (isDirectory(candidate))
|
|
27
|
-
candidate
|
|
35
|
+
candidate = join(candidate, 'index.html')
|
|
28
36
|
if (isFile(candidate))
|
|
29
37
|
return candidate
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
function sendFile(response, file) {
|
|
41
|
+
if (!isFile(file))
|
|
42
|
+
sendNotFound(response)
|
|
43
|
+
else {
|
|
44
|
+
response.setHeader('Content-Type', mimeFor(file))
|
|
45
|
+
response.end(readFileSync(file, 'utf8'))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function sendPartialContent(response, range, file) {
|
|
50
|
+
const { size } = await fs.promises.lstat(file)
|
|
51
|
+
let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
52
|
+
if (isNaN(end)) end = size - 1
|
|
53
|
+
if (isNaN(start)) start = size - end
|
|
54
|
+
|
|
55
|
+
if (start < 0 || start > end || start >= size || end >= size) {
|
|
56
|
+
response.statusCode = 416 // Range Not Satisfiable
|
|
57
|
+
response.setHeader('Content-Range', `bytes */${size}`)
|
|
58
|
+
response.end()
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
response.statusCode = 206 // Partial Content
|
|
62
|
+
response.setHeader('Accept-Ranges', 'bytes')
|
|
63
|
+
response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
64
|
+
response.setHeader('Content-Type', mimeFor(file))
|
|
65
|
+
const reader = fs.createReadStream(file, { start, end })
|
|
66
|
+
reader.on('open', function () {
|
|
67
|
+
this.pipe(response)
|
|
68
|
+
})
|
|
69
|
+
reader.on('error', function (error) {
|
|
70
|
+
sendInternalServerError(response, error)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
32
77
|
|
package/src/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
1
2
|
import { isDirectory } from './utils/fs.js'
|
|
2
3
|
import { openInBrowser } from './utils/openInBrowser.js'
|
|
3
4
|
import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
|
|
@@ -66,6 +67,9 @@ export function setup(options) {
|
|
|
66
67
|
|
|
67
68
|
onReady: is(Function)
|
|
68
69
|
})
|
|
70
|
+
|
|
71
|
+
config.mocksDir = resolve(config.mocksDir)
|
|
72
|
+
config.staticDir = resolve(config.staticDir)
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
|
|
@@ -35,7 +35,7 @@ export function init() {
|
|
|
35
35
|
})
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function registerMock(file,
|
|
38
|
+
export function registerMock(file, isFromWatcher) {
|
|
39
39
|
if (getBrokerByFilename(file)?.hasMock(file)
|
|
40
40
|
|| config.ignore.test(file)
|
|
41
41
|
|| !filenameIsValid(file))
|
|
@@ -48,8 +48,11 @@ export function registerMock(file, shouldEnsure500) {
|
|
|
48
48
|
else
|
|
49
49
|
collection[method][urlMask].register(file)
|
|
50
50
|
|
|
51
|
-
if (
|
|
51
|
+
if (isFromWatcher) {
|
|
52
|
+
if (!this.file)
|
|
53
|
+
collection[method][urlMask].selectDefaultFile()
|
|
52
54
|
collection[method][urlMask].ensureItHas500()
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export function unregisterMock(file) {
|
package/src/utils/fs.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { join, dirname, sep, posix } from 'node:path'
|
|
2
|
-
import { lstatSync,
|
|
2
|
+
import { lstatSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
|
|
6
6
|
export const isDirectory = path => lstatSync(path, { throwIfNoEntry: false })?.isDirectory()
|
|
7
7
|
|
|
8
|
-
export const read = path => readFileSync(path, 'utf8')
|
|
9
|
-
|
|
10
8
|
/** @returns {Array<string>} paths relative to `dir` */
|
|
11
9
|
export const listFilesRecursively = dir => {
|
|
12
10
|
const files = readdirSync(dir, { recursive: true }).filter(f => isFile(join(dir, f)))
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { mimeFor } from './mime.js'
|
|
3
|
-
import { isFile, read } from './fs.js'
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
export function sendOK(response) {
|
|
@@ -17,46 +16,20 @@ export function sendJSON(response, payload) {
|
|
|
17
16
|
response.end(JSON.stringify(payload))
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
else {
|
|
24
|
-
response.setHeader('Content-Type', mimeFor(filePath))
|
|
25
|
-
response.end(read(filePath))
|
|
26
|
-
}
|
|
19
|
+
export function sendForbidden(response) {
|
|
20
|
+
response.statusCode = 403
|
|
21
|
+
response.end()
|
|
27
22
|
}
|
|
28
23
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (isNaN(end)) end = size - 1
|
|
33
|
-
if (isNaN(start)) start = size - end
|
|
34
|
-
|
|
35
|
-
if (start < 0 || start > end || start >= size || end >= size) {
|
|
36
|
-
response.statusCode = 416 // Range Not Satisfiable
|
|
37
|
-
response.setHeader('Content-Range', `bytes */${size}`)
|
|
38
|
-
response.end()
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
response.statusCode = 206 // Partial Content
|
|
42
|
-
response.setHeader('Accept-Ranges', 'bytes')
|
|
43
|
-
response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
44
|
-
response.setHeader('Content-Type', mimeFor(file))
|
|
45
|
-
const reader = fs.createReadStream(file, { start, end })
|
|
46
|
-
reader.on('open', function () {
|
|
47
|
-
this.pipe(response)
|
|
48
|
-
})
|
|
49
|
-
reader.on('error', function (error) {
|
|
50
|
-
sendInternalServerError(response, error)
|
|
51
|
-
})
|
|
52
|
-
}
|
|
24
|
+
export function sendDashboardFile(response, file) {
|
|
25
|
+
response.setHeader('Content-Type', mimeFor(file))
|
|
26
|
+
response.end(readFileSync(file, 'utf8'))
|
|
53
27
|
}
|
|
54
28
|
|
|
55
|
-
|
|
56
29
|
export function sendBadRequest(response, error) {
|
|
57
30
|
console.error(error)
|
|
58
31
|
response.statusCode = 400
|
|
59
|
-
response.end(
|
|
32
|
+
response.end()
|
|
60
33
|
}
|
|
61
34
|
|
|
62
35
|
export function sendNotFound(response) {
|
|
@@ -73,5 +46,5 @@ export function sendUnprocessableContent(response, error) {
|
|
|
73
46
|
export function sendInternalServerError(response, error) {
|
|
74
47
|
console.error(error)
|
|
75
48
|
response.statusCode = 500
|
|
76
|
-
response.end(
|
|
49
|
+
response.end()
|
|
77
50
|
}
|
package/src/utils/mime.js
CHANGED
|
@@ -88,16 +88,22 @@ const mimes = {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function mimeFor(filename) {
|
|
91
|
-
const ext = filename
|
|
91
|
+
const ext = extname(filename).toLowerCase()
|
|
92
92
|
return config.extraMimes[ext] || mimes[ext] || ''
|
|
93
93
|
}
|
|
94
|
+
function extname(filename) {
|
|
95
|
+
const i = filename.lastIndexOf('.')
|
|
96
|
+
return i >= 0
|
|
97
|
+
? filename.substring(i + 1)
|
|
98
|
+
: ''
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
|
|
95
102
|
export function extFor(mime) {
|
|
96
103
|
return mime
|
|
97
104
|
? findExt(mime)
|
|
98
105
|
: 'empty'
|
|
99
106
|
}
|
|
100
|
-
|
|
101
107
|
function findExt(targetMime) {
|
|
102
108
|
for (const [ext, mime] of Object.entries(config.extraMimes))
|
|
103
109
|
if (targetMime === mime)
|