mockaton 13.11.3 → 13.12.0

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 CHANGED
@@ -1,14 +1,27 @@
1
1
  <!-- SKILLS_IGNORE_BEGIN -->
2
- <img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
3
2
 
4
3
  ![NPM Version](https://img.shields.io/npm/v/mockaton)
5
4
  [![Test](https://github.com/ericfortis/mockaton/actions/workflows/test.yml/badge.svg)](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
6
5
  [![codecov](https://codecov.io/github/ericfortis/mockaton/graph/badge.svg?token=90NYLMMG1J)](https://codecov.io/github/ericfortis/mockaton)
7
6
 
7
+ <img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
8
+
9
+ No API state should be too difficult to test.
10
+
8
11
  ## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog) | [Skills](skills/mockaton/SKILL.md)
9
12
 
10
- Mockaton is an HTTP mock server for simulating APIs. With it, you can
11
- test API errors, edge cases, and difficult to reproduce states in general.
13
+
14
+ Simulate API states that are normally ignored. For example,
15
+ click the 500 button to trigger an error on an endpoint. Then
16
+ unclick it to test your retry logic. Similarly, click the clock
17
+ button 🕓 to delay a response so you can test spinners &mdash;
18
+ you‘ve seen them in production at the top left, or restarting
19
+ their animation midway.
20
+
21
+
22
+ ## Dashboard
23
+ Besides the dashboard UI, there’s [an API](https://mockaton.com/api)
24
+ you can use to set up tests.
12
25
 
13
26
  <picture>
14
27
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.light.gold.png">
@@ -35,47 +48,44 @@ curl localhost:2020/api/user
35
48
 
36
49
 
37
50
  # Installation ([more options ↗](https://mockaton.com/installation))
38
- Mockaton has no dependencies.
39
-
40
51
  ```sh
41
52
  npm install -g mockaton
42
53
  ```
54
+ <i>Mockaton is a Node.js app with no dependencies.</i>
55
+
56
+ <!-- SKILLS_IGNORE_BEGIN -->
57
+ ## Skills
58
+ ```sh
59
+ npx skills add ericfortis/mockaton
60
+ ```
61
+ <!-- SKILLS_IGNORE_END -->
62
+
63
+
43
64
 
44
65
  ## Basic Usage
45
66
  ```sh
46
- mockaton --port 2020 my-mocks-dir/
67
+ mockaton --port 2020 my-mocks-dir
47
68
  ```
48
69
 
49
- Mockaton will serve the files on the given directory. It's a file-system based router, so
50
- filenames can have dynamic parameters. Also, filenames can have comments, which are
51
- anything within parentheses, this way each route can have different mock file variants.
52
- Similarly, each route can have different response status code variants.
70
+ Mockaton will serve the files on the given directory. It's a file-system based router in which
71
+ filenames can have dynamic parameters and comments. For paraments use square brackets `[]`,
72
+ and for comments use parentheses `()`. Comments are handy because this way each route
73
+ can have different mock file variants. Similarly, each route can have different response status
74
+ code variants.
53
75
 
54
76
 
55
77
  | Route | Filename | Description |
56
78
  | -----| -----| ---|
57
- | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
58
- | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
59
- | /api/login | api/login(invalid attempt).POST.401.ts | **Anything within parenthesis is a comment**, they are ignored when routing |
60
- | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
61
- | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
62
-
63
-
64
- ## Docs
65
- - [Configuration: CLI and mockaton.config.js](https://mockaton.com/config).
66
- - [Programmatic API](https://mockaton.com/api), in which
67
- you can delay a route, select a different mock file, such as a 500 error, among other options.
79
+ | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
80
+ | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
81
+ | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. |
82
+ | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins. |
83
+ | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
68
84
 
69
85
  <!-- SKILLS_IGNORE_BEGIN -->
70
86
  ## How to scrape your backend APIs?
71
87
  Mockaton has a [Browser Extension](https://mockaton.com/scraping) that lets
72
88
  you download in bulk all your API responses following Mockaton's filename convention.
73
-
74
-
75
- ## Skills
76
- ```sh
77
- npx skills add ericfortis/mockaton
78
- ```
79
89
  <!-- SKILLS_IGNORE_END -->
80
90
 
81
91
 
@@ -135,3 +145,8 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
135
145
  })
136
146
  }
137
147
  ```
148
+
149
+ ## Docs
150
+ - [Configuration](https://mockaton.com/config): CLI and mockaton.config.js
151
+ - [API](https://mockaton.com/api): Programatically, you can delay a route, select a different mock file, etc.
152
+
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.11.3",
5
+ "version": "13.12.0",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -4,36 +4,34 @@ description: Generates and serves mock HTTP APIs. Use when creating, editing, or
4
4
  user-invocable: false
5
5
  ---
6
6
  # Installation ([more options ↗](https://mockaton.com/installation))
7
- Mockaton has no dependencies.
8
-
9
7
  ```sh
10
8
  npm install -g mockaton
11
9
  ```
10
+ <i>Mockaton is a Node.js app with no dependencies.</i>
11
+
12
+
13
+
14
+
12
15
 
13
16
  ## Basic Usage
14
17
  ```sh
15
- mockaton --port 2020 my-mocks-dir/
18
+ mockaton --port 2020 my-mocks-dir
16
19
  ```
17
20
 
18
- Mockaton will serve the files on the given directory. It's a file-system based router, so
19
- filenames can have dynamic parameters. Also, filenames can have comments, which are
20
- anything within parentheses, this way each route can have different mock file variants.
21
- Similarly, each route can have different response status code variants.
21
+ Mockaton will serve the files on the given directory. It's a file-system based router in which
22
+ filenames can have dynamic parameters and comments. For paraments use square brackets `[]`,
23
+ and for comments use parentheses `()`. Comments are handy because this way each route
24
+ can have different mock file variants. Similarly, each route can have different response status
25
+ code variants.
22
26
 
23
27
 
24
28
  | Route | Filename | Description |
25
29
  | -----| -----| ---|
26
- | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
27
- | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
28
- | /api/login | api/login(invalid attempt).POST.401.ts | **Anything within parenthesis is a comment**, they are ignored when routing |
29
- | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
30
- | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
31
-
32
-
33
- ## Docs
34
- - [Configuration: CLI and mockaton.config.js](https://mockaton.com/config).
35
- - [Programmatic API](https://mockaton.com/api), in which
36
- you can delay a route, select a different mock file, such as a 500 error, among other options.
30
+ | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
31
+ | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
32
+ | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. |
33
+ | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins. |
34
+ | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
37
35
 
38
36
 
39
37
 
@@ -93,4 +91,8 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
93
91
  name: 'Acme, Inc.'
94
92
  })
95
93
  }
96
- ```
94
+ ```
95
+
96
+ ## Docs
97
+ - [Configuration](https://mockaton.com/config): CLI and mockaton.config.js
98
+ - [API](https://mockaton.com/api): Programatically, you can delay a route, select a different mock file, etc.
@@ -1,11 +1,15 @@
1
1
  import { API } from './ApiConstants.js'
2
2
 
3
3
 
4
- /** Client for controlling Mockaton via its HTTP API */
4
+ /**
5
+ * Client for controlling Mockaton via its HTTP API
6
+ * @see https://mockaton.com/api
7
+ * @example const mockaton = new Commander('http://localhost:2020')
8
+ */
5
9
  export class Commander {
6
- /** @param {string} addr */
7
- constructor(addr) {
8
- this.addr = addr
10
+ /** @param {string} mockatonServerAddr */
11
+ constructor(mockatonServerAddr) {
12
+ this.addr = mockatonServerAddr
9
13
  }
10
14
 
11
15
  /** @returns {Promise<Response>} */
@@ -16,38 +20,35 @@ export class Commander {
16
20
 
17
21
  reset = () => this.#patch(API.reset)
18
22
 
19
- setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
20
- setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
21
- setCorsAllowed = value => this.#patch(API.cors, value)
22
- setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
23
- setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
24
- setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
25
-
26
- /** @returns {JsonPromise<State.cookies>} */
27
- selectCookie = label => this.#patch(API.cookies, label)
28
-
29
-
30
- bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
31
23
 
32
24
  /** @returns {JsonPromise<ClientMockBroker>} */
33
25
  select = file => this.#patch(API.select, file)
34
26
 
27
+ bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
35
28
 
36
29
  /** @returns {JsonPromise<ClientMockBroker>} */
37
30
  toggleStatus = (method, urlMask, status) => this.#patch(API.toggleStatus, [method, urlMask, status])
38
31
 
32
+ /** @returns {JsonPromise<ClientMockBroker>} */
33
+ setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
39
34
 
40
35
  /** @returns {JsonPromise<ClientMockBroker>} */
41
36
  setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
42
37
 
43
- /** @returns {JsonPromise<ClientMockBroker>} */
44
- setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
45
38
 
39
+ /** @returns {JsonPromise<State.cookies>} */
40
+ selectCookie = label => this.#patch(API.cookies, label)
41
+
42
+ setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
43
+ setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
44
+ setCorsAllowed = value => this.#patch(API.cors, value)
45
+ setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
46
+ setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
47
+ setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
46
48
 
47
49
  writeMock = (file, content) => this.#patch(API.writeMock, [file, content])
48
50
  deleteMock = file => this.#patch(API.deleteMock, file)
49
51
 
50
-
51
52
  /** @returns {JsonPromise<State>} */
52
53
  getState = () => fetch(this.addr + API.state)
53
54
 
@@ -1,14 +1,14 @@
1
1
  import { createElement as r, t, restoreFocus, Fragment } from './utils/dom.js'
2
2
 
3
- import { store } from './app-store.js'
3
+ import { store } from './store.js'
4
4
  import { API } from './ApiConstants.js'
5
- import { Header } from './app-header.js'
5
+ import { Header } from './Header.js'
6
6
  import { adoptSheet } from './utils/css.js'
7
- import { PayloadViewer, previewMock } from './app-payload-viewer.js'
8
- import { MockList, initKeyboardNavigation, renderRow } from './app-mock-list.js'
7
+ import { PayloadViewer, previewMock } from './PayloadViewer.js'
8
+ import { MockList, initKeyboardNavigation, renderRow } from './MockList.js'
9
9
 
10
- import CSS from './app.css' with { type: 'css' }
11
- adoptSheet(CSS, './app.css')
10
+ import CSS from './App.css' with { type: 'css' }
11
+ adoptSheet(CSS, './App.css')
12
12
 
13
13
  store.onError = onError
14
14
  store.render = render
@@ -1,10 +1,10 @@
1
1
  import { createElement as r, t } from './utils/dom.js'
2
- import { Logo, HelpIcon } from './graphics.js'
2
+ import { Logo, HelpIcon } from './icons.js'
3
3
  import { adoptSheet } from './utils/css.js'
4
- import { store } from './app-store.js'
4
+ import { store } from './store.js'
5
5
 
6
- import CSS from './app-header.css' with { type: 'css' }
7
- adoptSheet(CSS, './app-header.css')
6
+ import CSS from './Header.css' with { type: 'css' }
7
+ adoptSheet(CSS, './Header.css')
8
8
 
9
9
 
10
10
  export function Header() {
@@ -13,7 +13,7 @@ export const IndexHtml = (hotReloadEnabled, version) => `
13
13
  <meta charset="UTF-8">
14
14
  <base href="${API.root}/">
15
15
 
16
- <script type="module" src="app.js"></script>
16
+ <script type="module" src="App.js"></script>
17
17
  <link rel="preload" href="${API.state}" as="fetch" crossorigin>
18
18
 
19
19
  <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
@@ -1,12 +1,12 @@
1
1
  import { createElement as r, t, Fragment } from './utils/dom.js'
2
2
 
3
- import { store } from './app-store.js'
4
- import { previewMock } from './app-payload-viewer.js'
3
+ import { store } from './store.js'
4
+ import { previewMock } from './PayloadViewer.js'
5
5
  import { classNames, adoptSheet } from './utils/css.js'
6
- import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
6
+ import { TimerIcon, CloudIcon, ChevronDownIcon } from './icons.js'
7
7
 
8
- import CSS from './app-mock-list.css' with { type: 'css' }
9
- adoptSheet(CSS, './app-mock-list.css')
8
+ import CSS from './MockList.css' with { type: 'css' }
9
+ adoptSheet(CSS, './MockList.css')
10
10
 
11
11
 
12
12
  export function MockList() {
@@ -76,9 +76,8 @@ function TableContent() {
76
76
 
77
77
  if (store.groupByMethod)
78
78
  return Object.keys(store.brokersByMethod).map(method => Fragment(
79
- r('div', {
80
- className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
81
- }, method),
79
+ r('div', { className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy) },
80
+ method),
82
81
  FolderGroups(store.folderGroupsByMethod(method))))
83
82
 
84
83
  return FolderGroups(store.folderGroupsByMethod('*'))
@@ -1,10 +1,10 @@
1
1
  import { createElement as r, t } from './utils/dom.js'
2
2
  import { parseFilename } from './Filename.js'
3
- import { store } from './app-store.js'
3
+ import { store } from './store.js'
4
4
  import { adoptSheet } from './utils/css.js'
5
5
 
6
- import CSS from './app-payload-viewer.css' with { type: 'css' }
7
- adoptSheet(CSS, './app-payload-viewer.css')
6
+ import CSS from './PayloadViewer.css' with { type: 'css' }
7
+ adoptSheet(CSS, './PayloadViewer.css')
8
8
 
9
9
 
10
10
  const titleRef = {}
@@ -1,7 +1,7 @@
1
1
  import { test } from 'node:test'
2
2
  import { deepEqual } from 'node:assert/strict'
3
3
 
4
- import { BrokerRowModel, t } from './app-store.js'
4
+ import { BrokerRowModel, t } from './store.js'
5
5
 
6
6
 
7
7
  test('BrokerRowModel', () => {
package/src/server/Api.js CHANGED
@@ -18,7 +18,7 @@ import { IndexHtml, CSP } from '../client/IndexHtml.js'
18
18
  import { cookie } from './stores/cookies.js'
19
19
  import { config, ConfigValidator, reinitConfig } from './stores/config.js'
20
20
  import * as brokers from './stores/brokers.js'
21
- import * as Watcher from './stores/Watcher.js'
21
+ import * as Watcher from './stores/Watcher.js'
22
22
 
23
23
 
24
24
  export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
@@ -269,19 +269,21 @@ async function setRouteIsProxied(req, response) {
269
269
 
270
270
 
271
271
  async function writeMock(req, response) {
272
- if (config.readOnly)
273
- return response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly, or --no-read-only (CLI)')
274
-
272
+ if (config.readOnly) {
273
+ response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly')
274
+ return
275
+ }
275
276
  const [file, content] = await req.json()
276
- if (typeof file !== 'string')
277
- return response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
278
-
277
+ if (typeof file !== 'string') {
278
+ response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
279
+ return
280
+ }
279
281
  const path = await resolveIn(config.mocksDir, file)
280
- if (!path)
281
- return response.forbidden('Filename path resolves outside config.mocksDir')
282
-
282
+ if (!path) {
283
+ response.forbidden('Filename path resolves outside config.mocksDir')
284
+ return
285
+ }
283
286
  await write(path, content)
284
-
285
287
  if (!config.watcherEnabled) {
286
288
  brokers.registerMock(file, true)
287
289
  Watcher.emitChange()
@@ -291,19 +293,19 @@ async function writeMock(req, response) {
291
293
 
292
294
 
293
295
  async function deleteMock(req, response) {
294
- if (config.readOnly)
295
- return response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly, or --no-read-only (CLI)')
296
-
296
+ if (config.readOnly) {
297
+ response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly')
298
+ return
299
+ }
297
300
  const file = await req.json()
298
301
  const path = await resolveIn(config.mocksDir, file)
299
302
 
300
303
  if (!path)
301
- return response.forbidden('Filename path resolves outside config.mocksDir')
302
-
303
- if (!isFile(path))
304
- return response.unprocessable(`Missing Mock: ${file}`)
305
-
306
- await rm(path)
304
+ response.forbidden('Filename path resolves outside config.mocksDir')
305
+ else if (!isFile(path))
306
+ response.unprocessable(`Missing Mock: ${file}`)
307
+ else
308
+ await rm(path)
307
309
 
308
310
  if (!config.watcherEnabled) {
309
311
  brokers.unregisterMock(file)
@@ -108,21 +108,15 @@ class UrlMatcher {
108
108
  this.#urlRegex = this.#buildUrlRegex(file)
109
109
  }
110
110
 
111
- #escapeRegex(str) {
112
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
113
- }
114
-
115
111
  #buildUrlRegex(file) {
116
112
  let { urlMask } = parseFilename(file)
117
113
  urlMask = removeQueryStringAndFragment(urlMask)
118
- urlMask = this.#escapeRegex(urlMask)
114
+ urlMask = RegExp.escape(urlMask)
119
115
  urlMask = this.#disregardVariables(urlMask)
120
116
  return new RegExp('^' + urlMask + '/*$')
121
117
  }
122
118
 
123
- #disregardVariables(str) {
124
- return str.replace(/\\\[.*?\\]/g, '[^/]+')
125
- }
119
+ #disregardVariables = str => str.replace(/\\\[.*?\\]/g, '[^/]+')
126
120
 
127
121
  // Appending a '/' so URLs ending with variables don't match
128
122
  // URLs that have a path after that variable. For example,
@@ -6,16 +6,16 @@ import pkgJSON from '../../package.json' with { type: 'json' }
6
6
  import { logger } from './utils/logger.js'
7
7
  import { ServerResponse } from './utils/HttpServerResponse.js'
8
8
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
9
- import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
9
+ import { IncomingMessage } from './utils/HttpIncomingMessage.js'
10
10
  import { watchDevSPA } from './utils/WatcherDevClient.js'
11
11
 
12
12
  import { API } from '../client/ApiConstants.js'
13
-
13
+ import { dispatchMock } from './MockDispatcher.js'
14
14
  import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
15
+
16
+ import * as brokers from './stores/brokers.js'
15
17
  import { cookie } from './stores/cookies.js'
16
18
  import { config, initConfig } from './stores/config.js'
17
- import * as brokers from './stores/brokers.js'
18
- import { dispatchMock } from './MockDispatcher.js'
19
19
  import { watchMocksDir } from './stores/Watcher.js'
20
20
 
21
21
 
@@ -60,13 +60,11 @@ async function onRequest(req, response) {
60
60
  logger.verbose('APP', response)
61
61
  })
62
62
 
63
- const url = req.url || ''
64
-
65
- if (url.length > 2048) {
63
+ if (req.url?.length > 2048) {
66
64
  response.uriTooLong()
67
65
  return
68
66
  }
69
- if (hasControlChars(url)) {
67
+ if (req.urlHasControlChars()) {
70
68
  response.badRequest()
71
69
  return
72
70
  }
@@ -83,7 +81,7 @@ async function onRequest(req, response) {
83
81
  }
84
82
  }
85
83
  catch (error) {
86
- if (error instanceof BodyReaderError)
84
+ if (error instanceof IncomingMessage.BodyReaderError)
87
85
  response.unprocessable(`${error.name}: ${error.message}`)
88
86
  else {
89
87
  logger.error(500, req.url, error?.message || error, error?.stack || '')
@@ -234,7 +234,7 @@ describe('Dashboard', () => {
234
234
  })
235
235
 
236
236
  test('serves assets', async () => {
237
- const r = await request(API.root + '/app.css')
237
+ const r = await request(API.root + '/App.css')
238
238
  match(await r.text(), new RegExp(':root {'))
239
239
  })
240
240
  })
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'
3
3
 
4
4
  import { extFor } from './utils/mime.js'
5
5
  import { write, isFile, resolveIn } from './utils/fs.js'
6
- import { BodyReaderError } from './utils/HttpIncomingMessage.js'
6
+ import { IncomingMessage } from './utils/HttpIncomingMessage.js'
7
7
  import { logger } from './utils/logger.js'
8
8
 
9
9
  import { makeMockFilename } from '../client/Filename.js'
@@ -24,7 +24,7 @@ export async function proxy(req, response, delay) {
24
24
  })
25
25
  }
26
26
  catch (error) { // TESTME
27
- if (error instanceof BodyReaderError)
27
+ if (error instanceof IncomingMessage.BodyReaderError)
28
28
  response.unprocessable(error.name)
29
29
  else {
30
30
  response.badGateway()
@@ -1,5 +1,3 @@
1
- import { basename } from 'node:path'
2
-
3
1
  import { config, isFileAllowed } from './config.js'
4
2
  import { MockBroker } from '../MockBroker.js'
5
3
  import { listFilesRecursively } from '../utils/fs.js'
@@ -35,8 +33,7 @@ export function init() {
35
33
 
36
34
  /** @returns {boolean} registered */
37
35
  export function registerMock(file, isFromWatcher = false) {
38
- if (brokerByFilename(file)?.hasMock(file) ||
39
- !isFileAllowed(basename(file)))
36
+ if (brokerByFilename(file)?.hasMock(file) || !isFileAllowed(file))
40
37
  return false
41
38
 
42
39
  const { method, urlMask } = parseFilename(file)
@@ -1,4 +1,4 @@
1
- import { resolve } from 'node:path'
1
+ import { resolve, basename } from 'node:path'
2
2
  import { lstatSync } from 'node:fs'
3
3
  import { METHODS } from 'node:http'
4
4
 
@@ -89,7 +89,7 @@ export function reinitConfig() {
89
89
  initConfig(originalOpts)
90
90
  }
91
91
 
92
- export const isFileAllowed = f => !config.ignore.test(f)
92
+ export const isFileAllowed = f => !config.ignore.test(basename(f))
93
93
 
94
94
  export const calcDelay = () => config.delayJitter
95
95
  ? config.delay * (1 + Math.random() * config.delayJitter)
@@ -1,6 +1,8 @@
1
1
  import http, { METHODS } from 'node:http'
2
2
 
3
3
 
4
+ export const reControlAndDelChars = /[\x00-\x1f\x7f]/
5
+
4
6
  export const methodIsSupported = method => METHODS.includes(method)
5
7
 
6
8
  export function removeQueryStringAndFragment(url = '') {
@@ -16,6 +18,8 @@ export class BodyReaderError extends Error {
16
18
  }
17
19
 
18
20
  export class IncomingMessage extends http.IncomingMessage {
21
+ static BodyReaderError = BodyReaderError
22
+
19
23
  json() {
20
24
  return this.body(JSON.parse)
21
25
  }
@@ -29,18 +33,28 @@ export class IncomingMessage extends http.IncomingMessage {
29
33
  for await (const chunk of this) {
30
34
  lengthSoFar += chunk.length
31
35
  if (lengthSoFar > MAX_BODY_SIZE)
32
- throw new BodyReaderError(`Body too large. Max is ${MAX_BODY_SIZE} bytes`)
36
+ throw new IncomingMessage.BodyReaderError(`Body too large. Max is ${MAX_BODY_SIZE} bytes`)
33
37
  chunks.push(chunk)
34
38
  }
35
39
 
36
40
  if (lengthSoFar !== expectedLength)
37
- throw new BodyReaderError('Length mismatch')
41
+ throw new IncomingMessage.BodyReaderError('Length mismatch')
38
42
 
39
43
  try {
40
44
  return parser(Buffer.concat(chunks).toString())
41
45
  }
42
46
  catch (_) {
43
- throw new BodyReaderError('Could not parse')
47
+ throw new IncomingMessage.BodyReaderError('Could not parse')
48
+ }
49
+ }
50
+
51
+ urlHasControlChars() {
52
+ try {
53
+ const decoded = decode(this.url || '')
54
+ return !decoded || reControlAndDelChars.test(decoded)
55
+ }
56
+ catch {
57
+ return true
44
58
  }
45
59
  }
46
60
  }
@@ -49,19 +63,6 @@ export const parseJSON = req =>
49
63
  IncomingMessage.prototype.body.call(req, JSON.parse)
50
64
 
51
65
 
52
-
53
- export const reControlAndDelChars = /[\x00-\x1f\x7f]/
54
-
55
- export function hasControlChars(url) {
56
- try {
57
- const decoded = decode(url)
58
- return !decoded || reControlAndDelChars.test(decoded)
59
- }
60
- catch {
61
- return true
62
- }
63
- }
64
-
65
66
  export function decode(url) {
66
67
  const candidate = decodeURIComponent(url)
67
68
  return candidate === decodeURIComponent(candidate)
@@ -1,3 +1,4 @@
1
+ import { styleText } from 'node:util'
1
2
  import { decode, reControlAndDelChars } from './HttpIncomingMessage.js'
2
3
 
3
4
 
@@ -39,7 +40,12 @@ export const logger = new class {
39
40
  #msg(...msg) {
40
41
  if (!msg.at(-1))
41
42
  msg.pop()
42
- return [new Date().toISOString(), ...msg.map(this.#sanitize)].join('::')
43
+ const parts = msg.map(this.#sanitize)
44
+ parts[1] = styleText(['bold', 'green'], parts[1])
45
+ return [
46
+ styleText('yellow', new Date().toISOString()),
47
+ ...parts
48
+ ].join(styleText('dim', '::'))
43
49
  }
44
50
 
45
51
  #sanitize(url) {
@@ -57,6 +57,7 @@ const extToMime = {
57
57
  js: 'application/javascript',
58
58
  json: 'application/json',
59
59
  jsonld: 'application/ld+json',
60
+ jxl: 'image/jxl',
60
61
  lz: 'application/x-lzip',
61
62
  m4a: 'audio/mp4',
62
63
  md: 'text/markdown',
@@ -91,6 +92,7 @@ const extToMime = {
91
92
  stl: 'model/stl',
92
93
  svg: 'image/svg+xml',
93
94
  tar: 'application/x-tar',
95
+ tga: 'image/x-targa',
94
96
  tif: 'image/tiff',
95
97
  ts: 'video/mp2t',
96
98
  ttf: 'font/ttf',
File without changes
File without changes
File without changes
File without changes
File without changes