mockaton 6.3.7 → 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 +2 -2
- 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/mockBrokersCollection.js +3 -3
- package/src/Route.js +0 -87
package/Tests.js
CHANGED
|
@@ -8,10 +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 { Route } from './src/Route.js'
|
|
12
11
|
import { Config } from './src/Config.js'
|
|
13
12
|
import { mimeFor } from './src/utils/mime.js'
|
|
14
13
|
import { Mockaton } from './src/Mockaton.js'
|
|
14
|
+
import { parseFilename } from './src/Filename.js'
|
|
15
15
|
import { API, DF, DEFAULT_500_COMMENT } from './src/ApiConstants.js'
|
|
16
16
|
|
|
17
17
|
|
|
@@ -220,7 +220,7 @@ async function test404() {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
async function testMockDispatching(url, file, expectedBody, forcedMime = undefined) {
|
|
223
|
-
const { urlMask, method, status } =
|
|
223
|
+
const { urlMask, method, status } = parseFilename(file)
|
|
224
224
|
const mime = forcedMime || mimeFor(file)
|
|
225
225
|
const res = await request(url, { method })
|
|
226
226
|
const body = mime === 'application/json'
|
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
|
+
|
|
@@ -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,87 +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(decodeURIComponent(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 method = tokens.at(-3)
|
|
50
|
-
const status = Number(tokens.at(-2))
|
|
51
|
-
|
|
52
|
-
if (!httpMethods.includes(method))
|
|
53
|
-
return { error: `Unrecognized HTTP Method: "${method}"` }
|
|
54
|
-
|
|
55
|
-
if (!responseStatusIsValid(status))
|
|
56
|
-
return { error: `Invalid HTTP Response Status: "${status}"` }
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
urlMask: '/' + removeTrailingSlash(tokens.slice(0, -3).join('.')),
|
|
60
|
-
method,
|
|
61
|
-
status
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Stars out (for regex) all the paths that are in square brackets
|
|
68
|
-
function disregardVariables(str) {
|
|
69
|
-
return str.replace(/\[.*?]/g, '[^/]*')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function removeQueryStringAndFragment(urlMask) {
|
|
73
|
-
return urlMask.replace(/[?#].*/, '')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function removeTrailingSlash(url = '') {
|
|
77
|
-
return url
|
|
78
|
-
.replace(/\/$/, '')
|
|
79
|
-
.replace('/?', '?')
|
|
80
|
-
.replace('/#', '#')
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function responseStatusIsValid(status) {
|
|
84
|
-
return Number.isInteger(status)
|
|
85
|
-
&& status >= 100
|
|
86
|
-
&& status <= 599
|
|
87
|
-
}
|