mockaton 6.3.5 → 6.3.8
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/Tests.js +15 -7
- package/package.json +1 -1
- package/src/Api.js +1 -1
- package/src/Dashboard.js +5 -5
- package/src/Filename.js +53 -0
- package/src/MockBroker.js +32 -10
- package/src/MockDispatcher.js +2 -2
- package/src/mockBrokersCollection.js +3 -3
- package/src/utils/http-response.js +1 -1
- package/src/{mime.js → utils/mime.js} +1 -1
- package/src/Route.js +0 -86
package/Tests.js
CHANGED
|
@@ -8,9 +8,10 @@ import { createServer } from 'node:http'
|
|
|
8
8
|
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
9
9
|
import { writeFileSync, mkdtempSync, mkdirSync } from 'node:fs'
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { mimeFor } from './src/mime.js'
|
|
11
|
+
import { Config } from './src/Config.js'
|
|
12
|
+
import { mimeFor } from './src/utils/mime.js'
|
|
13
13
|
import { Mockaton } from './src/Mockaton.js'
|
|
14
|
+
import { parseFilename } from './src/Filename.js'
|
|
14
15
|
import { API, DF, DEFAULT_500_COMMENT } from './src/ApiConstants.js'
|
|
15
16
|
|
|
16
17
|
|
|
@@ -51,6 +52,14 @@ const fixtures = [
|
|
|
51
52
|
'/api/alternative',
|
|
52
53
|
'api/alternative(comment-1).GET.200.json',
|
|
53
54
|
'With_Comment_1'
|
|
55
|
+
], [
|
|
56
|
+
'/api/dot.in.path',
|
|
57
|
+
'api/dot.in.path.GET.200.json',
|
|
58
|
+
'Dot_in_Path'
|
|
59
|
+
], [
|
|
60
|
+
'/api/space & colon:',
|
|
61
|
+
'api/space & colon:.GET.200.json',
|
|
62
|
+
'Decodes URI'
|
|
54
63
|
],
|
|
55
64
|
|
|
56
65
|
|
|
@@ -115,6 +124,7 @@ writeStatic('another-entry/index.html', '<h1>Another</h1>')
|
|
|
115
124
|
const server = Mockaton({
|
|
116
125
|
mocksDir: tmpDir,
|
|
117
126
|
staticDir: staticTmpDir,
|
|
127
|
+
delay: 40,
|
|
118
128
|
onReady: () => {},
|
|
119
129
|
cookies: {
|
|
120
130
|
userA: 'CookieA',
|
|
@@ -209,10 +219,9 @@ async function test404() {
|
|
|
209
219
|
})
|
|
210
220
|
}
|
|
211
221
|
|
|
212
|
-
async function testMockDispatching(url, file, expectedBody, forcedMime =
|
|
213
|
-
const { urlMask, method, status } =
|
|
222
|
+
async function testMockDispatching(url, file, expectedBody, forcedMime = undefined) {
|
|
223
|
+
const { urlMask, method, status } = parseFilename(file)
|
|
214
224
|
const mime = forcedMime || mimeFor(file)
|
|
215
|
-
const now = new Date()
|
|
216
225
|
const res = await request(url, { method })
|
|
217
226
|
const body = mime === 'application/json'
|
|
218
227
|
? await res.json()
|
|
@@ -222,7 +231,6 @@ async function testMockDispatching(url, file, expectedBody, forcedMime = void 0)
|
|
|
222
231
|
it('mime: ' + mime, () => equal(res.headers.get('content-type'), mime))
|
|
223
232
|
it('status: ' + status, () => equal(res.status, status))
|
|
224
233
|
it('cookie: ' + mime, () => equal(res.headers.get('set-cookie'), 'CookieA'))
|
|
225
|
-
it('delay is under 1 sec', () => equal((new Date()).getTime() - now.getTime() < 1000, true))
|
|
226
234
|
it('extra header', () => equal(res.headers.get('server'), 'MockatonTester'))
|
|
227
235
|
})
|
|
228
236
|
}
|
|
@@ -253,7 +261,7 @@ async function testItUpdatesDelayAndFile(url, file, expectedBody) {
|
|
|
253
261
|
const body = await res.text()
|
|
254
262
|
await describe('url: ' + url, () => {
|
|
255
263
|
it('body is: ' + expectedBody, () => equal(body, expectedBody))
|
|
256
|
-
it('delay
|
|
264
|
+
it('delay', () => equal((new Date()).getTime() - now.getTime() > Config.delay, true))
|
|
257
265
|
})
|
|
258
266
|
}
|
|
259
267
|
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -14,7 +14,7 @@ import { sendOK, sendBadRequest, sendJSON, sendFile, sendUnprocessableContent }
|
|
|
14
14
|
|
|
15
15
|
export const apiGetRequests = new Map([
|
|
16
16
|
[API.dashboard, serveDashboard],
|
|
17
|
-
['/
|
|
17
|
+
['/Filename.js', serveDashboardAsset],
|
|
18
18
|
['/Dashboard.js', serveDashboardAsset],
|
|
19
19
|
['/Dashboard.css', serveDashboardAsset],
|
|
20
20
|
['/ApiConstants.js', serveDashboardAsset],
|
package/src/Dashboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseFilename } from '../Filename.js'
|
|
2
2
|
import { API, DF, DEFAULT_500_COMMENT } from '../ApiConstants.js'
|
|
3
3
|
|
|
4
4
|
|
|
@@ -173,7 +173,7 @@ function MockSelector({ broker }) {
|
|
|
173
173
|
const items = broker.mocks
|
|
174
174
|
const selected = broker.currentMock.file
|
|
175
175
|
|
|
176
|
-
const { status } =
|
|
176
|
+
const { status } = parseFilename(selected)
|
|
177
177
|
const files = items.filter(item =>
|
|
178
178
|
status === 500 ||
|
|
179
179
|
!item.includes(DEFAULT_500_COMMENT))
|
|
@@ -184,7 +184,7 @@ function MockSelector({ broker }) {
|
|
|
184
184
|
autocomplete: 'off',
|
|
185
185
|
disabled: files.length <= 1,
|
|
186
186
|
onChange() {
|
|
187
|
-
const { status } =
|
|
187
|
+
const { status } = parseFilename(this.value)
|
|
188
188
|
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
189
189
|
? 'normal'
|
|
190
190
|
: 'bold'
|
|
@@ -240,7 +240,7 @@ function TimerIcon() {
|
|
|
240
240
|
function InternalServerErrorToggler({ broker }) {
|
|
241
241
|
const items = broker.mocks
|
|
242
242
|
const name = broker.currentMock.file
|
|
243
|
-
const checked =
|
|
243
|
+
const checked = parseFilename(broker.currentMock.file).status === 500
|
|
244
244
|
return (
|
|
245
245
|
r('label', {
|
|
246
246
|
className: CSS.InternalServerErrorToggler,
|
|
@@ -256,7 +256,7 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
256
256
|
method: 'PATCH',
|
|
257
257
|
body: JSON.stringify({
|
|
258
258
|
[DF.file]: event.currentTarget.checked
|
|
259
|
-
? items.find(f =>
|
|
259
|
+
? items.find(f => parseFilename(f).status === 500)
|
|
260
260
|
: items[0]
|
|
261
261
|
})
|
|
262
262
|
})
|
package/src/Filename.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const httpMethods = [
|
|
2
|
+
'CONNECT', 'DELETE', 'GET',
|
|
3
|
+
'HEAD', 'OPTIONS', 'PATCH',
|
|
4
|
+
'POST', 'PUT', 'TRACE'
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const reComments = /\(.*?\)/g // Anything within parentheses
|
|
9
|
+
|
|
10
|
+
export const extractComments = filename =>
|
|
11
|
+
Array.from(filename.matchAll(reComments), ([comment]) => comment)
|
|
12
|
+
|
|
13
|
+
export const includesComment = (filename, search) =>
|
|
14
|
+
extractComments(filename).some(comment => comment.includes(search))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export function parseFilename(file) {
|
|
18
|
+
const tokens = file.replace(reComments, '').split('.')
|
|
19
|
+
if (tokens.length < 4)
|
|
20
|
+
return { error: 'Invalid Filename Convention' }
|
|
21
|
+
|
|
22
|
+
const method = tokens.at(-3)
|
|
23
|
+
const status = Number(tokens.at(-2))
|
|
24
|
+
|
|
25
|
+
if (!httpMethods.includes(method))
|
|
26
|
+
return { error: `Unrecognized HTTP Method: "${method}"` }
|
|
27
|
+
|
|
28
|
+
if (!responseStatusIsValid(status))
|
|
29
|
+
return { error: `Invalid HTTP Response Status: "${status}"` }
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
urlMask: '/' + removeTrailingSlash(tokens.slice(0, -3).join('.')),
|
|
33
|
+
method,
|
|
34
|
+
status
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function removeTrailingSlash(url = '') {
|
|
39
|
+
return url
|
|
40
|
+
.replace(/\/$/, '')
|
|
41
|
+
.replace('/?', '?')
|
|
42
|
+
.replace('/#', '#')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function responseStatusIsValid(status) {
|
|
46
|
+
return Number.isInteger(status)
|
|
47
|
+
&& status >= 100
|
|
48
|
+
&& status <= 599
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
package/src/MockBroker.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import { Route } from './Route.js'
|
|
2
1
|
import { Config } from './Config.js'
|
|
3
2
|
import { DEFAULT_500_COMMENT } from './ApiConstants.js'
|
|
3
|
+
import { includesComment, extractComments, parseFilename } from './Filename.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// MockBroker is a state for a particular route. It knows the available mock files
|
|
7
7
|
// that can be served for the route, the currently selected file, and its delay.
|
|
8
8
|
export class MockBroker {
|
|
9
|
-
#
|
|
9
|
+
#urlRegex
|
|
10
10
|
|
|
11
11
|
constructor(file) {
|
|
12
|
-
|
|
12
|
+
const { urlMask } = parseFilename(file)
|
|
13
|
+
this.#urlRegex = new RegExp('^' + disregardVariables(removeQueryStringAndFragment(urlMask)) + '/*$')
|
|
14
|
+
|
|
13
15
|
this.mocks = []
|
|
14
16
|
this.currentMock = {
|
|
15
17
|
file: '',
|
|
16
18
|
delay: 0
|
|
17
19
|
}
|
|
20
|
+
|
|
18
21
|
this.register(file)
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -24,12 +27,21 @@ export class MockBroker {
|
|
|
24
27
|
this.mocks.push(file)
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
// Appending a '/' so URLs ending with variables don't match
|
|
31
|
+
// URLs that have a path after that variable. For example,
|
|
32
|
+
// without it, the following regex would match both of these URLs:
|
|
33
|
+
// api/foo/[route_id] => api/foo/.* (wrong match because it’s too greedy)
|
|
34
|
+
// api/foo/[route_id]/suffix => api/foo/.*/suffix
|
|
35
|
+
// By the same token, the regex handles many trailing
|
|
36
|
+
// slashes. For instance, for routing api/foo/[id]?qs…
|
|
37
|
+
urlMaskMatches(url) {
|
|
38
|
+
return this.#urlRegex.test(removeQueryStringAndFragment(decodeURIComponent(url)) + '/')
|
|
39
|
+
}
|
|
28
40
|
|
|
29
41
|
get file() { return this.currentMock.file }
|
|
30
42
|
get delay() { return this.currentMock.delay }
|
|
31
|
-
get status() { return
|
|
32
|
-
get isTemp500() { return
|
|
43
|
+
get status() { return parseFilename(this.file).status }
|
|
44
|
+
get isTemp500() { return includesComment(this.file, DEFAULT_500_COMMENT) }
|
|
33
45
|
|
|
34
46
|
updateFile(filename) {
|
|
35
47
|
this.currentMock.file = filename
|
|
@@ -41,7 +53,7 @@ export class MockBroker {
|
|
|
41
53
|
|
|
42
54
|
setByMatchingComment(comment) {
|
|
43
55
|
for (const file of this.mocks)
|
|
44
|
-
if (
|
|
56
|
+
if (includesComment(file, comment)) {
|
|
45
57
|
this.updateFile(file)
|
|
46
58
|
break
|
|
47
59
|
}
|
|
@@ -50,7 +62,7 @@ export class MockBroker {
|
|
|
50
62
|
extractComments() {
|
|
51
63
|
let comments = []
|
|
52
64
|
for (const file of this.mocks)
|
|
53
|
-
comments = comments.concat(
|
|
65
|
+
comments = comments.concat(extractComments(file))
|
|
54
66
|
return comments
|
|
55
67
|
}
|
|
56
68
|
|
|
@@ -59,11 +71,21 @@ export class MockBroker {
|
|
|
59
71
|
this.#registerTemp500()
|
|
60
72
|
}
|
|
61
73
|
#has500() {
|
|
62
|
-
return this.mocks.some(mock =>
|
|
74
|
+
return this.mocks.some(mock => parseFilename(mock).status === 500)
|
|
63
75
|
}
|
|
64
76
|
#registerTemp500() {
|
|
65
|
-
const { urlMask, method } =
|
|
77
|
+
const { urlMask, method } = parseFilename(this.mocks[0])
|
|
66
78
|
const file = urlMask.replace(/^\//, '') // Removes leading slash TESTME
|
|
67
79
|
this.register(`${file}${DEFAULT_500_COMMENT}.${method}.500.txt`)
|
|
68
80
|
}
|
|
69
81
|
}
|
|
82
|
+
|
|
83
|
+
// Stars out (for regex) all the paths that are in square brackets
|
|
84
|
+
function disregardVariables(urlMask) {
|
|
85
|
+
return urlMask.replace(/\[.*?]/g, '[^/]*')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function removeQueryStringAndFragment(urlMask) {
|
|
89
|
+
return urlMask.replace(/[?#].*/, '')
|
|
90
|
+
}
|
|
91
|
+
|
package/src/MockDispatcher.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs'
|
|
|
4
4
|
import { proxy } from './ProxyRelay.js'
|
|
5
5
|
import { cookie } from './cookie.js'
|
|
6
6
|
import { Config } from './Config.js'
|
|
7
|
-
import { mimeFor } from './mime.js'
|
|
7
|
+
import { mimeFor } from './utils/mime.js'
|
|
8
8
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
9
9
|
import { JsonBodyParserError } from './utils/http-request.js'
|
|
10
10
|
import { sendInternalServerError, sendNotFound, sendBadRequest } from './utils/http-response.js'
|
|
@@ -22,7 +22,7 @@ export async function dispatchMock(req, response) {
|
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
24
|
const { file, status, delay } = broker
|
|
25
|
-
console.log(req.url, ' → ', file)
|
|
25
|
+
console.log(decodeURIComponent(req.url), ' → ', file)
|
|
26
26
|
|
|
27
27
|
let mockText
|
|
28
28
|
if (file.endsWith('.js')) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { readdirSync as readDir } from 'node:fs'
|
|
3
3
|
|
|
4
|
-
import { Route } from './Route.js'
|
|
5
4
|
import { Config } from './Config.js'
|
|
6
5
|
import { cookie } from './cookie.js'
|
|
7
6
|
import { isFile } from './utils/fs.js'
|
|
8
7
|
import { MockBroker } from './MockBroker.js'
|
|
8
|
+
import { parseFilename } from './Filename.js'
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -29,7 +29,7 @@ export function init() {
|
|
|
29
29
|
.sort()
|
|
30
30
|
|
|
31
31
|
for (const file of files) {
|
|
32
|
-
const { error, method, urlMask } =
|
|
32
|
+
const { error, method, urlMask } = parseFilename(file)
|
|
33
33
|
if (error) {
|
|
34
34
|
console.error(error, file)
|
|
35
35
|
continue
|
|
@@ -52,7 +52,7 @@ function forEachBroker(fn) {
|
|
|
52
52
|
export const getAll = () => collection
|
|
53
53
|
|
|
54
54
|
export const getBrokerByFilename = file => {
|
|
55
|
-
const { method, urlMask } =
|
|
55
|
+
const { method, urlMask } = parseFilename(file)
|
|
56
56
|
if (collection[method])
|
|
57
57
|
return collection[method][urlMask]
|
|
58
58
|
}
|
package/src/Route.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
const httpMethods = [
|
|
2
|
-
'CONNECT',
|
|
3
|
-
'DELETE',
|
|
4
|
-
'GET',
|
|
5
|
-
'HEAD',
|
|
6
|
-
'OPTIONS',
|
|
7
|
-
'PATCH',
|
|
8
|
-
'POST',
|
|
9
|
-
'PUT',
|
|
10
|
-
'TRACE'
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
export class Route {
|
|
14
|
-
#urlRegex
|
|
15
|
-
|
|
16
|
-
constructor(file) {
|
|
17
|
-
const { urlMask } = Route.parseFilename(file)
|
|
18
|
-
this.#urlRegex = new RegExp('^' + disregardVariables(removeQueryStringAndFragment(urlMask)) + '/*$')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
urlMaskMatches(url) {
|
|
22
|
-
// Appending a '/' so URLs ending with variables don't match
|
|
23
|
-
// URLs that have a path after that variable. For example,
|
|
24
|
-
// without it, the following regex would match both of these URLs:
|
|
25
|
-
// api/foo/[route_id] => api/foo/.* (wrong match because it’s too greedy)
|
|
26
|
-
// api/foo/[route_id]/suffix => api/foo/.*/suffix
|
|
27
|
-
// By the same token, the regex handles many trailing
|
|
28
|
-
// slashes. For instance, for routing api/foo/[id]?qs…
|
|
29
|
-
return this.#urlRegex.test(removeQueryStringAndFragment(url) + '/')
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Anything within parentheses in the filename is a comment, including the parentheses.
|
|
33
|
-
static reComments = /\(.*?\)/g
|
|
34
|
-
|
|
35
|
-
static extractComments(filename) {
|
|
36
|
-
return Array.from(filename.matchAll(Route.reComments), ([comment]) => comment)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static hasInParentheses(filename, search) {
|
|
40
|
-
return Route.extractComments(filename)
|
|
41
|
-
.some(comment => comment.includes(search))
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
static parseFilename(file) {
|
|
45
|
-
const tokens = file.replace(Route.reComments, '').split('.')
|
|
46
|
-
if (tokens.length < 4)
|
|
47
|
-
return { error: 'Invalid Filename Convention' }
|
|
48
|
-
|
|
49
|
-
const status = Number(tokens.at(-2))
|
|
50
|
-
if (!responseStatusIsValid(status))
|
|
51
|
-
return { error: `Invalid HTTP Response Status: "${status}"` }
|
|
52
|
-
|
|
53
|
-
const method = tokens.at(-3)
|
|
54
|
-
if (!httpMethods.includes(method))
|
|
55
|
-
return { error: `Unrecognized HTTP Method: "${method}"` }
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
urlMask: '/' + removeTrailingSlash(tokens.at(-4)),
|
|
59
|
-
method,
|
|
60
|
-
status
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Stars out (for regex) all the paths that are in square brackets
|
|
67
|
-
function disregardVariables(str) {
|
|
68
|
-
return str.replace(/\[.*?]/g, '[^/]*')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function removeQueryStringAndFragment(urlMask) {
|
|
72
|
-
return urlMask.replace(/[?#].*/, '')
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function removeTrailingSlash(url = '') {
|
|
76
|
-
return decodeURIComponent(url
|
|
77
|
-
.replace(/\/$/, '')
|
|
78
|
-
.replace('/?', '?')
|
|
79
|
-
.replace('/#', '#'))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function responseStatusIsValid(status) {
|
|
83
|
-
return Number.isInteger(status)
|
|
84
|
-
&& status >= 100
|
|
85
|
-
&& status <= 599
|
|
86
|
-
}
|