mockaton 8.3.1 → 8.3.3
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 +1 -1
- package/index.d.ts +1 -2
- package/package.json +1 -1
- package/src/Api.js +27 -13
- package/src/ApiConstants.js +1 -1
- package/src/Commander.js +4 -0
- package/src/Dashboard.css +31 -4
- package/src/Dashboard.js +0 -1
- package/src/MockBroker.js +5 -5
- package/src/MockDispatcher.js +5 -5
- package/src/MockDispatcherPlugins.js +2 -2
- package/src/Mockaton.js +10 -10
- package/src/Mockaton.test.js +99 -24
- package/src/ProxyRelay.js +9 -7
- package/src/StaticDispatcher.js +4 -6
- package/src/{Config.js → config.js} +15 -21
- package/src/cookie.js +1 -1
- package/src/mockBrokersCollection.js +4 -4
- package/src/utils/fs.js +6 -6
- package/src/utils/http-cors.js +22 -16
- package/src/utils/mime.js +3 -3
- package/src/utils/validate.js +0 -8
package/README.md
CHANGED
package/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { cookie } from './cookie.js'
|
|
8
|
-
import {
|
|
8
|
+
import { config } from './config.js'
|
|
9
9
|
import { DF, API } from './ApiConstants.js'
|
|
10
10
|
import { parseJSON } from './utils/http-request.js'
|
|
11
11
|
import { listFilesRecursively } from './utils/fs.js'
|
|
@@ -35,11 +35,12 @@ export const apiPatchRequests = new Map([
|
|
|
35
35
|
[API.reset, reinitialize],
|
|
36
36
|
[API.cookies, selectCookie],
|
|
37
37
|
[API.fallback, updateProxyFallback],
|
|
38
|
+
[API.collectProxied, setCollectProxied],
|
|
38
39
|
[API.bulkSelect, bulkUpdateBrokersByCommentTag],
|
|
39
40
|
[API.cors, setCorsAllowed]
|
|
40
41
|
])
|
|
41
42
|
|
|
42
|
-
/* GET */
|
|
43
|
+
/* === GET === */
|
|
43
44
|
|
|
44
45
|
function serveDashboard(_, response) { sendFile(response, join(import.meta.dirname, 'Dashboard.html')) }
|
|
45
46
|
function serveDashboardAsset(req, response) { sendFile(response, join(import.meta.dirname, req.url)) }
|
|
@@ -47,13 +48,13 @@ function serveDashboardAsset(req, response) { sendFile(response, join(import.met
|
|
|
47
48
|
function listCookies(_, response) { sendJSON(response, cookie.list()) }
|
|
48
49
|
function listComments(_, response) { sendJSON(response, mockBrokersCollection.extractAllComments()) }
|
|
49
50
|
function listMockBrokers(_, response) { sendJSON(response, mockBrokersCollection.getAll()) }
|
|
50
|
-
function getProxyFallback(_, response) { sendJSON(response,
|
|
51
|
-
function getIsCorsAllowed(_, response) { sendJSON(response,
|
|
51
|
+
function getProxyFallback(_, response) { sendJSON(response, config.proxyFallback) }
|
|
52
|
+
function getIsCorsAllowed(_, response) { sendJSON(response, config.corsAllowed) }
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
function listStaticFiles(req, response) {
|
|
54
55
|
try {
|
|
55
|
-
const files =
|
|
56
|
-
? listFilesRecursively(
|
|
56
|
+
const files = config.staticDir
|
|
57
|
+
? listFilesRecursively(config.staticDir).filter(f => !config.ignore.test(f))
|
|
57
58
|
: []
|
|
58
59
|
sendJSON(response, files)
|
|
59
60
|
}
|
|
@@ -63,7 +64,7 @@ async function listStaticFiles(req, response) { // TESTME
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
/* PATCH */
|
|
67
|
+
/* === PATCH === */
|
|
67
68
|
|
|
68
69
|
function reinitialize(_, response) {
|
|
69
70
|
mockBrokersCollection.init()
|
|
@@ -72,8 +73,11 @@ function reinitialize(_, response) {
|
|
|
72
73
|
|
|
73
74
|
async function selectCookie(req, response) {
|
|
74
75
|
try {
|
|
75
|
-
cookie.setCurrent(await parseJSON(req))
|
|
76
|
-
|
|
76
|
+
const error = cookie.setCurrent(await parseJSON(req))
|
|
77
|
+
if (error)
|
|
78
|
+
sendUnprocessableContent(response, error)
|
|
79
|
+
else
|
|
80
|
+
sendOK(response)
|
|
77
81
|
}
|
|
78
82
|
catch (error) {
|
|
79
83
|
sendBadRequest(response, error)
|
|
@@ -113,10 +117,10 @@ async function setRouteIsDelayed(req, response) {
|
|
|
113
117
|
async function updateProxyFallback(req, response) {
|
|
114
118
|
try {
|
|
115
119
|
const fallback = await parseJSON(req)
|
|
116
|
-
if (fallback && !URL.canParse(fallback))
|
|
120
|
+
if (fallback && !URL.canParse(fallback))
|
|
117
121
|
sendUnprocessableContent(response)
|
|
118
122
|
else {
|
|
119
|
-
|
|
123
|
+
config.proxyFallback = fallback
|
|
120
124
|
sendOK(response)
|
|
121
125
|
}
|
|
122
126
|
}
|
|
@@ -125,6 +129,16 @@ async function updateProxyFallback(req, response) {
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
async function setCollectProxied(req, response) {
|
|
133
|
+
try {
|
|
134
|
+
config.collectProxied = await parseJSON(req)
|
|
135
|
+
sendOK(response)
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
sendBadRequest(response, error)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
129
143
|
try {
|
|
130
144
|
mockBrokersCollection.setMocksMatchingComment(await parseJSON(req))
|
|
@@ -137,7 +151,7 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
|
137
151
|
|
|
138
152
|
async function setCorsAllowed(req, response) {
|
|
139
153
|
try {
|
|
140
|
-
|
|
154
|
+
config.corsAllowed = await parseJSON(req)
|
|
141
155
|
sendOK(response)
|
|
142
156
|
}
|
|
143
157
|
catch (error) {
|
package/src/ApiConstants.js
CHANGED
|
@@ -9,6 +9,7 @@ export const API = {
|
|
|
9
9
|
reset: MOUNT + '/reset',
|
|
10
10
|
cookies: MOUNT + '/cookies',
|
|
11
11
|
fallback: MOUNT + '/fallback',
|
|
12
|
+
collectProxied: MOUNT + '/collect-proxied',
|
|
12
13
|
cors: MOUNT + '/cors',
|
|
13
14
|
static: MOUNT + '/static'
|
|
14
15
|
}
|
|
@@ -21,5 +22,4 @@ export const DF = { // Dashboard Fields (XHR)
|
|
|
21
22
|
|
|
22
23
|
export const DEFAULT_500_COMMENT = '(Mockaton 500)'
|
|
23
24
|
export const DEFAULT_MOCK_COMMENT = '(default)'
|
|
24
|
-
|
|
25
25
|
export const EXT_FOR_UNKNOWN_MIME = 'unknown'
|
package/src/Commander.js
CHANGED
package/src/Dashboard.css
CHANGED
|
@@ -54,6 +54,13 @@ body {
|
|
|
54
54
|
margin: 0;
|
|
55
55
|
font-family: system-ui, sans-serif;
|
|
56
56
|
font-size: 100%;
|
|
57
|
+
outline: 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
select, a, input, button, summary {
|
|
61
|
+
&:focus-visible {
|
|
62
|
+
outline: 2px solid var(--colorAccent);
|
|
63
|
+
}
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
select {
|
|
@@ -116,7 +123,6 @@ menu {
|
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
input[type=url] {
|
|
119
|
-
outline: 0;
|
|
120
126
|
padding: 0 6px;
|
|
121
127
|
box-shadow: var(--boxShadow1);
|
|
122
128
|
color: var(--colorText);
|
|
@@ -244,7 +250,14 @@ main {
|
|
|
244
250
|
cursor: pointer;
|
|
245
251
|
|
|
246
252
|
> input {
|
|
247
|
-
|
|
253
|
+
appearance: none;
|
|
254
|
+
|
|
255
|
+
&:focus-visible {
|
|
256
|
+
outline: 0;
|
|
257
|
+
& ~ svg {
|
|
258
|
+
outline: 2px solid var(--colorAccent)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
248
261
|
|
|
249
262
|
&:checked ~ svg {
|
|
250
263
|
background: var(--colorAccent);
|
|
@@ -274,7 +287,14 @@ main {
|
|
|
274
287
|
cursor: pointer;
|
|
275
288
|
|
|
276
289
|
> input {
|
|
277
|
-
|
|
290
|
+
appearance: none;
|
|
291
|
+
|
|
292
|
+
&:focus-visible {
|
|
293
|
+
outline: 0;
|
|
294
|
+
& ~ span {
|
|
295
|
+
outline: 2px solid var(--colorAccent)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
278
298
|
|
|
279
299
|
&:checked ~ span {
|
|
280
300
|
color: white;
|
|
@@ -331,18 +351,25 @@ main {
|
|
|
331
351
|
margin-top: 40px;
|
|
332
352
|
|
|
333
353
|
summary {
|
|
354
|
+
width: max-content;
|
|
334
355
|
margin-bottom: 8px;
|
|
335
356
|
cursor: pointer;
|
|
336
357
|
font-weight: bold;
|
|
337
358
|
}
|
|
338
359
|
|
|
360
|
+
ul {
|
|
361
|
+
position: relative;
|
|
362
|
+
left: -6px;
|
|
363
|
+
}
|
|
364
|
+
|
|
339
365
|
li {
|
|
340
366
|
list-style: none;
|
|
341
367
|
}
|
|
342
368
|
|
|
343
369
|
a {
|
|
344
370
|
display: inline-block;
|
|
345
|
-
|
|
371
|
+
border-radius: 6px;
|
|
372
|
+
padding: 6px;
|
|
346
373
|
color: var(--colorAccentAlt);
|
|
347
374
|
text-decoration: none;
|
|
348
375
|
|
package/src/Dashboard.js
CHANGED
package/src/MockBroker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { config } from './config.js'
|
|
2
2
|
import { DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
3
3
|
import { includesComment, extractComments, parseFilename } from './Filename.js'
|
|
4
4
|
|
|
@@ -38,7 +38,7 @@ export class MockBroker {
|
|
|
38
38
|
|
|
39
39
|
selectDefaultFile() {
|
|
40
40
|
const userSpecifiedDefault = this.#findMockWithDefaultComment()
|
|
41
|
-
if (userSpecifiedDefault) // Sort for dashboard list
|
|
41
|
+
if (userSpecifiedDefault) // Sort for dashboard list
|
|
42
42
|
this.mocks = [
|
|
43
43
|
userSpecifiedDefault,
|
|
44
44
|
...this.mocks.filter(m => m !== userSpecifiedDefault)
|
|
@@ -53,7 +53,7 @@ export class MockBroker {
|
|
|
53
53
|
|
|
54
54
|
mockExists(file) { return this.mocks.includes(file) }
|
|
55
55
|
updateFile(filename) { this.currentMock.file = filename }
|
|
56
|
-
updateDelay(delayed) { this.currentMock.delay = Number(delayed) *
|
|
56
|
+
updateDelay(delayed) { this.currentMock.delay = Number(delayed) * config.delay }
|
|
57
57
|
|
|
58
58
|
setByMatchingComment(comment) {
|
|
59
59
|
for (const file of this.mocks)
|
|
@@ -79,8 +79,8 @@ export class MockBroker {
|
|
|
79
79
|
}
|
|
80
80
|
#registerTemp500() {
|
|
81
81
|
const { urlMask, method } = parseFilename(this.mocks[0])
|
|
82
|
-
const file = urlMask.replace(/^\//, '') // Removes leading slash
|
|
83
|
-
this.register(`${file}${DEFAULT_500_COMMENT}.${method}.500.
|
|
82
|
+
const file = urlMask.replace(/^\//, '') // Removes leading slash
|
|
83
|
+
this.register(`${file}${DEFAULT_500_COMMENT}.${method}.500.empty`)
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
package/src/MockDispatcher.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from 'node:path'
|
|
|
2
2
|
|
|
3
3
|
import { proxy } from './ProxyRelay.js'
|
|
4
4
|
import { cookie } from './cookie.js'
|
|
5
|
-
import {
|
|
5
|
+
import { config } from './config.js'
|
|
6
6
|
import { applyPlugins } from './MockDispatcherPlugins.js'
|
|
7
7
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
8
8
|
import { BodyReaderError } from './utils/http-request.js'
|
|
@@ -13,7 +13,7 @@ export async function dispatchMock(req, response) {
|
|
|
13
13
|
try {
|
|
14
14
|
const broker = mockBrokerCollection.getBrokerForUrl(req.method, req.url)
|
|
15
15
|
if (!broker) {
|
|
16
|
-
if (
|
|
16
|
+
if (config.proxyFallback)
|
|
17
17
|
await proxy(req, response)
|
|
18
18
|
else
|
|
19
19
|
sendNotFound(response)
|
|
@@ -26,12 +26,12 @@ export async function dispatchMock(req, response) {
|
|
|
26
26
|
if (cookie.getCurrent())
|
|
27
27
|
response.setHeader('Set-Cookie', cookie.getCurrent())
|
|
28
28
|
|
|
29
|
-
for (let i = 0; i <
|
|
30
|
-
response.setHeader(
|
|
29
|
+
for (let i = 0; i < config.extraHeaders.length; i += 2)
|
|
30
|
+
response.setHeader(config.extraHeaders[i], config.extraHeaders[i + 1])
|
|
31
31
|
|
|
32
32
|
const { mime, body } = broker.isTemp500
|
|
33
33
|
? { mime: '', body: '' }
|
|
34
|
-
: await applyPlugins(join(
|
|
34
|
+
: await applyPlugins(join(config.mocksDir, broker.file), req, response)
|
|
35
35
|
|
|
36
36
|
response.setHeader('Content-Type', mime)
|
|
37
37
|
setTimeout(() => response.end(body), broker.delay)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFileSync as read } from 'node:fs'
|
|
2
2
|
import { mimeFor } from './utils/mime.js'
|
|
3
|
-
import {
|
|
3
|
+
import { config } from './config.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export async function applyPlugins(filePath, req, response) {
|
|
7
|
-
for (const [regex, plugin] of
|
|
7
|
+
for (const [regex, plugin] of config.plugins)
|
|
8
8
|
if (regex.test(filePath))
|
|
9
9
|
return await plugin(filePath, req, response)
|
|
10
10
|
return {
|
package/src/Mockaton.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createServer } from 'node:http'
|
|
|
2
2
|
|
|
3
3
|
import { API } from './ApiConstants.js'
|
|
4
4
|
import { dispatchMock } from './MockDispatcher.js'
|
|
5
|
-
import {
|
|
5
|
+
import { config, setup } from './config.js'
|
|
6
6
|
import { sendNoContent } from './utils/http-response.js'
|
|
7
7
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
8
8
|
import { dispatchStatic, isStatic } from './StaticDispatcher.js'
|
|
@@ -13,7 +13,7 @@ import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
|
13
13
|
export function Mockaton(options) {
|
|
14
14
|
setup(options)
|
|
15
15
|
mockBrokerCollection.init()
|
|
16
|
-
return createServer(onRequest).listen(
|
|
16
|
+
return createServer(onRequest).listen(config.port, config.host, function (error) {
|
|
17
17
|
const { address, port } = this.address()
|
|
18
18
|
const url = `http://${address}:${port}`
|
|
19
19
|
console.log('Listening', url)
|
|
@@ -21,21 +21,21 @@ export function Mockaton(options) {
|
|
|
21
21
|
if (error)
|
|
22
22
|
console.error(error)
|
|
23
23
|
else
|
|
24
|
-
|
|
24
|
+
config.onReady(url + API.dashboard)
|
|
25
25
|
})
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async function onRequest(req, response) {
|
|
29
29
|
response.setHeader('Server', 'Mockaton')
|
|
30
30
|
|
|
31
|
-
if (
|
|
31
|
+
if (config.corsAllowed)
|
|
32
32
|
setCorsHeaders(req, response, {
|
|
33
|
-
origins:
|
|
34
|
-
headers:
|
|
35
|
-
methods:
|
|
36
|
-
maxAge:
|
|
37
|
-
credentials:
|
|
38
|
-
exposedHeaders:
|
|
33
|
+
origins: config.corsOrigins,
|
|
34
|
+
headers: config.corsHeaders,
|
|
35
|
+
methods: config.corsMethods,
|
|
36
|
+
maxAge: config.corsMaxAge,
|
|
37
|
+
credentials: config.corsCredentials,
|
|
38
|
+
exposedHeaders: config.corsExposedHeaders
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
const { url, method } = req
|
package/src/Mockaton.test.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os'
|
|
2
|
-
import { dirname } from 'node:path'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
3
|
import { promisify } from 'node:util'
|
|
4
4
|
import { describe, it } from 'node:test'
|
|
5
5
|
import { createServer } from 'node:http'
|
|
6
|
+
import { randomUUID } from 'node:crypto'
|
|
6
7
|
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
7
8
|
import { writeFileSync, mkdtempSync, mkdirSync } from 'node:fs'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
import { config } from './config.js'
|
|
10
12
|
import { mimeFor } from './utils/mime.js'
|
|
11
13
|
import { Mockaton } from './Mockaton.js'
|
|
14
|
+
import { readBody } from './utils/http-request.js'
|
|
12
15
|
import { Commander } from './Commander.js'
|
|
13
|
-
import { parseFilename } from './Filename.js'
|
|
14
16
|
import { CorsHeader } from './utils/http-cors.js'
|
|
17
|
+
import { parseFilename } from './Filename.js'
|
|
18
|
+
import { listFilesRecursively, read } from './utils/fs.js'
|
|
15
19
|
import { API, DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
16
20
|
|
|
17
21
|
|
|
@@ -26,7 +30,7 @@ const fixtureCustomMime = [
|
|
|
26
30
|
]
|
|
27
31
|
const fixtureNonDefaultInName = [
|
|
28
32
|
'/api/the-route',
|
|
29
|
-
'api/the-route
|
|
33
|
+
'api/the-route.GET.200.json',
|
|
30
34
|
'default my route body content'
|
|
31
35
|
]
|
|
32
36
|
const fixtureDefaultInName = [
|
|
@@ -137,11 +141,14 @@ write('api/ignored.GET.200.json~', '')
|
|
|
137
141
|
// JavaScript to JSON
|
|
138
142
|
write('/api/object.GET.200.js', 'export default { JSON_FROM_JS: true }')
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
const staticFiles = [
|
|
145
|
+
['index.html', '<h1>Static</h1>'],
|
|
146
|
+
['assets/app.js', 'const app = 1'],
|
|
147
|
+
['another-entry/index.html', '<h1>Another</h1>']
|
|
148
|
+
]
|
|
149
|
+
writeStatic('ignored.js~', 'ignored_file_body')
|
|
150
|
+
for (const [file, body] of staticFiles)
|
|
151
|
+
writeStatic(file, body)
|
|
145
152
|
|
|
146
153
|
const server = Mockaton({
|
|
147
154
|
mocksDir: tmpDir,
|
|
@@ -156,7 +163,8 @@ const server = Mockaton({
|
|
|
156
163
|
extraMimes: {
|
|
157
164
|
my_custom_extension: 'my_custom_mime'
|
|
158
165
|
},
|
|
159
|
-
corsOrigins: ['http://example.com']
|
|
166
|
+
corsOrigins: ['http://example.com'],
|
|
167
|
+
corsExposedHeaders: ['Content-Encoding']
|
|
160
168
|
})
|
|
161
169
|
server.on('listening', runTests)
|
|
162
170
|
|
|
@@ -186,7 +194,7 @@ async function runTests() {
|
|
|
186
194
|
|
|
187
195
|
await testAutogenerates500(
|
|
188
196
|
'/api/alternative',
|
|
189
|
-
`api/alternative${DEFAULT_500_COMMENT}.GET.500.
|
|
197
|
+
`api/alternative${DEFAULT_500_COMMENT}.GET.500.empty`)
|
|
190
198
|
|
|
191
199
|
await testPreservesExiting500(
|
|
192
200
|
'/api',
|
|
@@ -225,11 +233,14 @@ async function runTests() {
|
|
|
225
233
|
await testMockDispatching(...fixtureCustomMime, 'my_custom_mime')
|
|
226
234
|
await testJsFunctionMocks()
|
|
227
235
|
|
|
228
|
-
await
|
|
236
|
+
await testItUpdatesCookie()
|
|
229
237
|
await testStaticFileServing()
|
|
238
|
+
await testStaticFileList()
|
|
230
239
|
await testInvalidFilenamesAreIgnored()
|
|
231
240
|
await testEnableFallbackSoRoutesWithoutMocksGetRelayed()
|
|
241
|
+
await testValidatesProxyFallbackURL()
|
|
232
242
|
await testCorsAllowed()
|
|
243
|
+
testWindowsPaths()
|
|
233
244
|
|
|
234
245
|
server.close()
|
|
235
246
|
}
|
|
@@ -255,6 +266,10 @@ async function test404() {
|
|
|
255
266
|
const res = await request('/api/ignored')
|
|
256
267
|
equal(res.status, 404)
|
|
257
268
|
})
|
|
269
|
+
await it('Ignores static files ending in ~ by default, e.g. JetBrains temp files', async () => {
|
|
270
|
+
const res = await request('/ignored.js~')
|
|
271
|
+
equal(res.status, 404)
|
|
272
|
+
})
|
|
258
273
|
}
|
|
259
274
|
|
|
260
275
|
async function testMockDispatching(url, file, expectedBody, forcedMime = undefined) {
|
|
@@ -275,6 +290,13 @@ async function testMockDispatching(url, file, expectedBody, forcedMime = undefin
|
|
|
275
290
|
|
|
276
291
|
async function testDefaultMock() {
|
|
277
292
|
await testMockDispatching(...fixtureDefaultInName)
|
|
293
|
+
await it('sorts mocks list with the user specified default first for dashboard display', async () => {
|
|
294
|
+
const res = await commander.listMocks()
|
|
295
|
+
const body = await res.json()
|
|
296
|
+
const { mocks } = body.GET[fixtureDefaultInName[0]]
|
|
297
|
+
equal(mocks[0], fixtureDefaultInName[1])
|
|
298
|
+
equal(mocks[1], fixtureNonDefaultInName[1])
|
|
299
|
+
})
|
|
278
300
|
}
|
|
279
301
|
|
|
280
302
|
async function testItUpdatesTheCurrentSelectedMock(url, file, expectedStatus, expectedBody) {
|
|
@@ -295,7 +317,7 @@ async function testItUpdatesRouteDelay(url, file, expectedBody) {
|
|
|
295
317
|
const body = await res.text()
|
|
296
318
|
await describe('url: ' + url, () => {
|
|
297
319
|
it('body is: ' + expectedBody, () => equal(body, JSON.stringify(expectedBody)))
|
|
298
|
-
it('delay', () => equal((new Date()).getTime() - now.getTime() >
|
|
320
|
+
it('delay', () => equal((new Date()).getTime() - now.getTime() > config.delay, true))
|
|
299
321
|
})
|
|
300
322
|
}
|
|
301
323
|
|
|
@@ -342,7 +364,7 @@ async function testItBulkSelectsByComment(comment, tests) {
|
|
|
342
364
|
}
|
|
343
365
|
|
|
344
366
|
|
|
345
|
-
async function
|
|
367
|
+
async function testItUpdatesCookie() {
|
|
346
368
|
await describe('Cookie', () => {
|
|
347
369
|
it('Defaults to the first key:value', async () => {
|
|
348
370
|
const res = await commander.listCookies()
|
|
@@ -352,7 +374,7 @@ async function testItUpdatesUserRole() {
|
|
|
352
374
|
])
|
|
353
375
|
})
|
|
354
376
|
|
|
355
|
-
it('
|
|
377
|
+
it('Updates selected cookie', async () => {
|
|
356
378
|
await commander.selectCookie('userB')
|
|
357
379
|
const res = await commander.listCookies()
|
|
358
380
|
deepEqual(await res.json(), [
|
|
@@ -360,6 +382,11 @@ async function testItUpdatesUserRole() {
|
|
|
360
382
|
['userB', true]
|
|
361
383
|
])
|
|
362
384
|
})
|
|
385
|
+
|
|
386
|
+
it('422 when trying to select non-existing cookie', async () => {
|
|
387
|
+
const res = await commander.selectCookie('non-existing-cookie-key')
|
|
388
|
+
equal(res.status, 422)
|
|
389
|
+
})
|
|
363
390
|
})
|
|
364
391
|
}
|
|
365
392
|
|
|
@@ -404,6 +431,13 @@ async function testStaticFileServing() {
|
|
|
404
431
|
})
|
|
405
432
|
}
|
|
406
433
|
|
|
434
|
+
async function testStaticFileList() {
|
|
435
|
+
await it('Static File List', async () => {
|
|
436
|
+
const res = await commander.listStaticFiles()
|
|
437
|
+
deepEqual((await res.json()).sort(), staticFiles.map(([file]) => file).sort())
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
407
441
|
async function testInvalidFilenamesAreIgnored() {
|
|
408
442
|
await it('Invalid filenames get skipped, so they don’t crash the server', async (t) => {
|
|
409
443
|
const consoleErrorSpy = t.mock.method(console, 'error')
|
|
@@ -421,26 +455,50 @@ async function testInvalidFilenamesAreIgnored() {
|
|
|
421
455
|
|
|
422
456
|
async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
|
|
423
457
|
await describe('Fallback', async () => {
|
|
424
|
-
const fallbackServer = createServer((
|
|
425
|
-
response.
|
|
426
|
-
|
|
427
|
-
|
|
458
|
+
const fallbackServer = createServer(async (req, response) => {
|
|
459
|
+
response.writeHead(423, {
|
|
460
|
+
'custom_header': 'my_custom_header',
|
|
461
|
+
'content-type': mimeFor('txt'),
|
|
462
|
+
'set-cookie': [
|
|
463
|
+
'cookieA=A',
|
|
464
|
+
'cookieB=B'
|
|
465
|
+
]
|
|
466
|
+
})
|
|
467
|
+
response.end(await readBody(req)) // echoes they req body payload
|
|
428
468
|
})
|
|
429
469
|
await promisify(fallbackServer.listen).bind(fallbackServer, 0, '127.0.0.1')()
|
|
430
470
|
|
|
431
471
|
await commander.setProxyFallback(`http://localhost:${fallbackServer.address().port}`)
|
|
432
|
-
await
|
|
433
|
-
|
|
434
|
-
|
|
472
|
+
await commander.setCollectProxied(true)
|
|
473
|
+
await it('Relays to fallback server and saves the mock', async () => {
|
|
474
|
+
const reqBodyPayload = 'text_req_body'
|
|
475
|
+
|
|
476
|
+
const res = await request(`/api/non-existing-mock/${randomUUID()}`, {
|
|
477
|
+
method: 'POST',
|
|
478
|
+
body: reqBodyPayload
|
|
479
|
+
})
|
|
435
480
|
equal(res.status, 423)
|
|
436
|
-
equal(
|
|
481
|
+
equal(res.headers.get('custom_header'), 'my_custom_header')
|
|
482
|
+
equal(res.headers.get('set-cookie'), ['cookieA=A', 'cookieB=B'].join(', '))
|
|
483
|
+
equal(await res.text(), reqBodyPayload)
|
|
484
|
+
|
|
485
|
+
const savedBody = read(join(tmpDir, 'api/non-existing-mock/[id].POST.423.txt'))
|
|
486
|
+
equal(savedBody, reqBodyPayload)
|
|
487
|
+
|
|
437
488
|
fallbackServer.close()
|
|
438
489
|
})
|
|
439
490
|
})
|
|
440
491
|
}
|
|
441
492
|
|
|
493
|
+
async function testValidatesProxyFallbackURL() {
|
|
494
|
+
await it('422 when value is not a valid URL', async () => {
|
|
495
|
+
const res = await commander.setProxyFallback('bad url')
|
|
496
|
+
equal(res.status, 422)
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
|
|
442
500
|
async function testCorsAllowed() {
|
|
443
|
-
await it('cors', async () => {
|
|
501
|
+
await it('cors preflight', async () => {
|
|
444
502
|
await commander.setCorsAllowed(true)
|
|
445
503
|
const res = await request('/does-not-matter', {
|
|
446
504
|
method: 'OPTIONS',
|
|
@@ -453,6 +511,23 @@ async function testCorsAllowed() {
|
|
|
453
511
|
equal(res.headers.get(CorsHeader.AccessControlAllowOrigin), 'http://example.com')
|
|
454
512
|
equal(res.headers.get(CorsHeader.AccessControlAllowMethods), 'GET')
|
|
455
513
|
})
|
|
514
|
+
await it('cors actual response', async () => {
|
|
515
|
+
const res = await request(fixtureDefaultInName[0], {
|
|
516
|
+
headers: {
|
|
517
|
+
[CorsHeader.Origin]: 'http://example.com'
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
equal(res.status, 200)
|
|
521
|
+
equal(res.headers.get(CorsHeader.AccessControlAllowOrigin), 'http://example.com')
|
|
522
|
+
equal(res.headers.get(CorsHeader.AccessControlExposeHeaders), 'Content-Encoding')
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function testWindowsPaths() {
|
|
527
|
+
it('normalizes backslashes with forward ones', () => {
|
|
528
|
+
const files = listFilesRecursively(config.mocksDir)
|
|
529
|
+
equal(files[0], 'api/.GET.200.json')
|
|
530
|
+
})
|
|
456
531
|
}
|
|
457
532
|
|
|
458
533
|
|
package/src/ProxyRelay.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { write } from './utils/fs.js'
|
|
3
|
-
import {
|
|
3
|
+
import { config } from './config.js'
|
|
4
4
|
import { extFor } from './utils/mime.js'
|
|
5
5
|
import { readBody } from './utils/http-request.js'
|
|
6
6
|
import { makeMockFilename } from './Filename.js'
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
export async function proxy(req, response) {
|
|
10
|
-
const proxyResponse = await fetch(
|
|
10
|
+
const proxyResponse = await fetch(config.proxyFallback + req.url, {
|
|
11
11
|
method: req.method,
|
|
12
12
|
headers: req.headers,
|
|
13
|
-
body: req.method === 'GET' || req.method === 'HEAD'
|
|
13
|
+
body: req.method === 'GET' || req.method === 'HEAD'
|
|
14
14
|
? undefined
|
|
15
15
|
: await readBody(req)
|
|
16
16
|
})
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
const headers = Object.fromEntries(proxyResponse.headers)
|
|
19
|
+
headers['set-cookie'] = proxyResponse.headers.getSetCookie() // parses multiple into an array
|
|
20
|
+
response.writeHead(proxyResponse.status, headers)
|
|
19
21
|
const body = await proxyResponse.text()
|
|
20
22
|
response.end(body)
|
|
21
23
|
|
|
22
|
-
if (
|
|
24
|
+
if (config.collectProxied) {
|
|
23
25
|
const ext = extFor(proxyResponse.headers.get('content-type'))
|
|
24
26
|
const filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
|
|
25
|
-
write(join(
|
|
27
|
+
write(join(config.mocksDir, filename), body)
|
|
26
28
|
}
|
|
27
29
|
}
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import {
|
|
2
|
+
import { config } from './config.js'
|
|
3
3
|
import { isDirectory, isFile } from './utils/fs.js'
|
|
4
4
|
import { sendFile, sendPartialContent, sendNotFound } from './utils/http-response.js'
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
export function isStatic(req) {
|
|
8
|
-
if (!
|
|
8
|
+
if (!config.staticDir)
|
|
9
9
|
return false
|
|
10
|
-
|
|
11
10
|
const f = resolvePath(req.url)
|
|
12
|
-
return !
|
|
13
|
-
&& Boolean(f)
|
|
11
|
+
return !config.ignore.test(f) && Boolean(f)
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
export async function dispatchStatic(req, response) {
|
|
@@ -24,7 +22,7 @@ export async function dispatchStatic(req, response) {
|
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
function resolvePath(url) {
|
|
27
|
-
let candidate = join(
|
|
25
|
+
let candidate = join(config.staticDir, url)
|
|
28
26
|
if (isDirectory(candidate))
|
|
29
27
|
candidate += '/index.html'
|
|
30
28
|
if (isFile(candidate))
|
|
@@ -2,14 +2,13 @@ import { isDirectory } from './utils/fs.js'
|
|
|
2
2
|
import { openInBrowser } from './utils/openInBrowser.js'
|
|
3
3
|
import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
|
|
4
4
|
import { StandardMethods } from './utils/http-request.js'
|
|
5
|
-
import {
|
|
5
|
+
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
export const
|
|
8
|
+
export const config = Object.seal({
|
|
9
9
|
mocksDir: '',
|
|
10
|
-
ignore: /(\.DS_Store|~)$/,
|
|
11
|
-
|
|
12
10
|
staticDir: '',
|
|
11
|
+
ignore: /(\.DS_Store|~)$/,
|
|
13
12
|
|
|
14
13
|
host: '127.0.0.1',
|
|
15
14
|
port: 0, // auto-assigned
|
|
@@ -38,12 +37,11 @@ export const Config = Object.seal({
|
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
export function setup(options) {
|
|
41
|
-
Object.assign(
|
|
42
|
-
validate(
|
|
40
|
+
Object.assign(config, options)
|
|
41
|
+
validate(config, {
|
|
43
42
|
mocksDir: isDirectory,
|
|
44
|
-
ignore: is(RegExp),
|
|
45
|
-
|
|
46
43
|
staticDir: optional(isDirectory),
|
|
44
|
+
ignore: is(RegExp),
|
|
47
45
|
|
|
48
46
|
host: is(String),
|
|
49
47
|
port: port => Number.isInteger(port) && port >= 0 && port < 2 ** 16,
|
|
@@ -70,20 +68,16 @@ export function setup(options) {
|
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (arr.length === 1 && arr[0] === '*')
|
|
78
|
-
return true
|
|
79
|
-
|
|
80
|
-
return arr.every(o => URL.canParse(o))
|
|
71
|
+
function validate(obj, shape) {
|
|
72
|
+
for (const [field, value] of Object.entries(obj))
|
|
73
|
+
if (!shape[field](value))
|
|
74
|
+
throw new TypeError(`config.${field}=${JSON.stringify(value)} is invalid`)
|
|
81
75
|
}
|
|
82
76
|
|
|
77
|
+
function is(ctor) {
|
|
78
|
+
return val => val.constructor === ctor
|
|
79
|
+
}
|
|
83
80
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
return false
|
|
87
|
-
|
|
88
|
-
return arr.every(m => StandardMethods.includes(m))
|
|
81
|
+
function optional(tester) {
|
|
82
|
+
return val => !val || tester(val)
|
|
89
83
|
}
|
package/src/cookie.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { config } from './config.js'
|
|
2
2
|
import { cookie } from './cookie.js'
|
|
3
3
|
import { MockBroker } from './MockBroker.js'
|
|
4
4
|
import { listFilesRecursively } from './utils/fs.js'
|
|
@@ -19,11 +19,11 @@ let collection = {}
|
|
|
19
19
|
|
|
20
20
|
export function init() {
|
|
21
21
|
collection = {}
|
|
22
|
-
cookie.init(
|
|
22
|
+
cookie.init(config.cookies)
|
|
23
23
|
|
|
24
|
-
const files = listFilesRecursively(
|
|
24
|
+
const files = listFilesRecursively(config.mocksDir)
|
|
25
25
|
.sort()
|
|
26
|
-
.filter(f => !
|
|
26
|
+
.filter(f => !config.ignore.test(f) && filenameIsValid(f))
|
|
27
27
|
|
|
28
28
|
for (const file of files) {
|
|
29
29
|
const { method, urlMask } = parseFilename(file)
|
package/src/utils/fs.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { join, dirname, sep, posix } from 'node:path'
|
|
2
2
|
import { lstatSync, readFileSync, 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)
|
|
8
|
+
export const read = path => readFileSync(path, 'utf8')
|
|
9
9
|
|
|
10
10
|
/** @returns {Array<string>} paths relative to `dir` */
|
|
11
11
|
export const listFilesRecursively = dir => {
|
|
12
12
|
const files = readdirSync(dir, { recursive: true }).filter(f => isFile(join(dir, f)))
|
|
13
13
|
return process.platform === 'win32'
|
|
14
|
-
? files.map(f => f.replaceAll(
|
|
14
|
+
? files.map(f => f.replaceAll(sep, posix.sep))
|
|
15
15
|
: files
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export const write = (
|
|
19
|
-
mkdirSync(
|
|
20
|
-
writeFileSync(
|
|
18
|
+
export const write = (path, body) => {
|
|
19
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
20
|
+
writeFileSync(path, body)
|
|
21
21
|
}
|
package/src/utils/http-cors.js
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
import { StandardMethods } from './http-request.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
/* https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-processing-model */
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function validateCorsAllowedOrigins(arr) {
|
|
8
|
+
if (!Array.isArray(arr))
|
|
9
|
+
return false
|
|
10
|
+
if (arr.length === 1 && arr[0] === '*')
|
|
11
|
+
return true
|
|
12
|
+
return arr.every(o => URL.canParse(o))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validateCorsAllowedMethods(arr) {
|
|
16
|
+
return Array.isArray(arr)
|
|
17
|
+
&& arr.every(m => StandardMethods.includes(m))
|
|
18
|
+
}
|
|
19
|
+
|
|
4
20
|
|
|
5
21
|
export const CorsHeader = {
|
|
6
|
-
//
|
|
22
|
+
// Request
|
|
7
23
|
Origin: 'origin',
|
|
8
24
|
AccessControlRequestMethod: 'access-control-request-method',
|
|
9
25
|
AccessControlRequestHeaders: 'access-control-request-headers', // Comma separated
|
|
10
26
|
|
|
11
|
-
//
|
|
27
|
+
// Response
|
|
12
28
|
AccessControlMaxAge: 'Access-Control-Max-Age',
|
|
13
29
|
AccessControlAllowOrigin: 'Access-Control-Allow-Origin', // '*' | Space delimited | null
|
|
14
30
|
AccessControlAllowMethods: 'Access-Control-Allow-Methods', // '*' | Comma delimited
|
|
15
31
|
AccessControlAllowHeaders: 'Access-Control-Allow-Headers', // '*' | Comma delimited
|
|
16
|
-
AccessControlExposeHeaders: 'Access-Control-Expose-Headers', // '*' | Comma delimited
|
|
32
|
+
AccessControlExposeHeaders: 'Access-Control-Expose-Headers', // '*' | Comma delimited (headers client-side JS can read)
|
|
17
33
|
AccessControlAllowCredentials: 'Access-Control-Allow-Credentials' // 'true'
|
|
18
34
|
}
|
|
19
35
|
const CH = CorsHeader
|
|
@@ -45,26 +61,16 @@ export function setCorsHeaders(req, response, {
|
|
|
45
61
|
|
|
46
62
|
if (req.headers[CH.AccessControlRequestMethod])
|
|
47
63
|
setPreflightSpecificHeaders(req, response, methods, headers, maxAge)
|
|
48
|
-
else
|
|
49
|
-
|
|
64
|
+
else if (exposedHeaders.length)
|
|
65
|
+
response.setHeader(CH.AccessControlExposeHeaders, exposedHeaders.join(','))
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
|
|
53
68
|
function setPreflightSpecificHeaders(req, response, methods, headers, maxAge) {
|
|
54
69
|
const methodAskingFor = req.headers[CH.AccessControlRequestMethod]
|
|
55
70
|
if (!methods.includes(methodAskingFor))
|
|
56
71
|
return
|
|
57
|
-
|
|
58
72
|
response.setHeader(CH.AccessControlMaxAge, maxAge)
|
|
59
73
|
response.setHeader(CH.AccessControlAllowMethods, methodAskingFor)
|
|
60
74
|
if (headers.length)
|
|
61
75
|
response.setHeader(CH.AccessControlAllowHeaders, headers.join(','))
|
|
62
76
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// TESTME
|
|
66
|
-
function setActualRequestHeaders(response, exposedHeaders) {
|
|
67
|
-
// Exposed means the client-side JavaScript can read them
|
|
68
|
-
if (exposedHeaders.length)
|
|
69
|
-
response.setHeader(CH.AccessControlExposeHeaders, exposedHeaders.join(','))
|
|
70
|
-
}
|
package/src/utils/mime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { config } from '../config.js'
|
|
2
2
|
import { EXT_FOR_UNKNOWN_MIME } from '../ApiConstants.js'
|
|
3
3
|
|
|
4
4
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
|
@@ -89,7 +89,7 @@ const mimes = {
|
|
|
89
89
|
|
|
90
90
|
export function mimeFor(filename) {
|
|
91
91
|
const ext = filename.replace(/.*\./, '').toLowerCase()
|
|
92
|
-
return
|
|
92
|
+
return config.extraMimes[ext] || mimes[ext] || ''
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export function extFor(mime) {
|
|
@@ -99,7 +99,7 @@ export function extFor(mime) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function findExt(targetMime) {
|
|
102
|
-
for (const [ext, mime] of Object.entries(
|
|
102
|
+
for (const [ext, mime] of Object.entries(config.extraMimes))
|
|
103
103
|
if (targetMime === mime)
|
|
104
104
|
return ext
|
|
105
105
|
for (const [ext, mime] of Object.entries(mimes))
|
package/src/utils/validate.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export function validate(obj, shape) {
|
|
2
|
-
for (const [field, value] of Object.entries(obj))
|
|
3
|
-
if (!shape[field](value))
|
|
4
|
-
throw new TypeError(`Config.${field}=${JSON.stringify(value)} is invalid`)
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export const is = ctor => val => val.constructor === ctor
|
|
8
|
-
export const optional = tester => val => !val || tester(val)
|