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 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 { Route } from './src/Route.js'
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 = void 0) {
213
- const { urlMask, method, status } = Route.parseFilename(file)
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 is over 1 sec', () => equal((new Date()).getTime() - now.getTime() > 1000, true))
264
+ it('delay', () => equal((new Date()).getTime() - now.getTime() > Config.delay, true))
257
265
  })
258
266
  }
259
267
 
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.5",
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
+
@@ -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 } = 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
  }
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs'
2
- import { mimeFor } from '../mime.js'
2
+ import { mimeFor } from './mime.js'
3
3
  import { isFile, read } from './fs.js'
4
4
 
5
5
 
@@ -1,4 +1,4 @@
1
- import { Config } from './Config.js'
1
+ import { Config } from '../Config.js'
2
2
 
3
3
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
4
4
  // m = {};
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
- }