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 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 } = Route.parseFilename(file)
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
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "A deterministic server-side for developing and testing frontend clients",
4
4
  "type": "module",
5
- "version": "6.3.7",
5
+ "version": "6.3.8",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
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
- ['/Route.js', serveDashboardAsset],
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 { Route } from '../Route.js'
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 } = Route.parseFilename(selected)
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 } = Route.parseFilename(this.value)
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 = Route.parseFilename(broker.currentMock.file).status === 500
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 => Route.parseFilename(f).status === 500)
259
+ ? items.find(f => parseFilename(f).status === 500)
260
260
  : items[0]
261
261
  })
262
262
  })
@@ -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
- #route
9
+ #urlRegex
10
10
 
11
11
  constructor(file) {
12
- this.#route = new Route(file)
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
- urlMaskMatches(url) { return this.#route.urlMaskMatches(url) }
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 Route.parseFilename(this.file).status }
32
- get isTemp500() { return Route.hasInParentheses(this.file, DEFAULT_500_COMMENT) }
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 (Route.hasInParentheses(file, comment)) {
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(Route.extractComments(file))
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 => Route.parseFilename(mock).status === 500)
74
+ return this.mocks.some(mock => parseFilename(mock).status === 500)
63
75
  }
64
76
  #registerTemp500() {
65
- const { urlMask, method } = Route.parseFilename(this.mocks[0])
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 } = Route.parseFilename(file)
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 } = Route.parseFilename(file)
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
- }