mockaton 3.0.0 → 4.0.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
@@ -44,17 +44,16 @@ node my-mockaton.js
44
44
  ## Config Options
45
45
  ```ts
46
46
  interface Config {
47
- mocksDir: string
48
- staticDir?: string
49
- host?: string, // defaults to 'localhost'
50
- port?: number // defaults to 0, which means auto-assigned
51
- delay?: number // defaults to 1200 (ms)
52
- open?: (dashboardUrl: string) => void // pass a noop to prevent opening the dashboard
53
- cookies?: object
54
- proxyFallback?: string // e.g. http://localhost:9999 Target for relaying routes without mocks
55
- allowedExt?: RegExp // /\.(json|txt|md|js)$/ Just for excluding temporary editor files (e.g. JetBrains appends a ~)
56
- generate500?: boolean // autogenerates an Internal Server Error empty mock for routes that have no 500
57
- extraHeaders?: []
47
+ mocksDir: string
48
+ staticDir?: string
49
+ host?: string, // defaults to 'localhost'
50
+ port?: number // defaults to 0, which means auto-assigned
51
+ delay?: number // defaults to 1200 (ms)
52
+ onReady?: (dashboardUrl: string) => void // defaults to openInBrowser. pass a noop to prevent opening the dashboard
53
+ cookies?: object
54
+ proxyFallback?: string // e.g. http://localhost:9999 Target for relaying routes without mocks
55
+ allowedExt?: RegExp // /\.(json|txt|md|js)$/ Just for excluding temporary editor files (e.g. JetBrains appends a ~)
56
+ extraHeaders?: []
58
57
  }
59
58
  ```
60
59
 
package/Tests.js CHANGED
@@ -11,7 +11,7 @@ import { writeFileSync, mkdtempSync, mkdirSync } from 'node:fs'
11
11
  import { Route } from './src/Route.js'
12
12
  import { mimeFor } from './src/utils/mime.js'
13
13
  import { Mockaton } from './src/Mockaton.js'
14
- import { API, DF } from './src/ApiConstants.js'
14
+ import { API, DF, DEFAULT_500_COMMENT } from './src/ApiConstants.js'
15
15
 
16
16
 
17
17
  const tmpDir = mkdtempSync(tmpdir()) + '/'
@@ -106,12 +106,11 @@ writeStatic('another-entry/index.html', '<h1>Another</h1>')
106
106
  const server = Mockaton({
107
107
  mocksDir: tmpDir,
108
108
  staticDir: staticTmpDir,
109
- open: () => {},
109
+ onReady: () => {},
110
110
  cookies: {
111
111
  userA: 'CookieA',
112
112
  userB: 'CookieB'
113
113
  },
114
- generate500: true,
115
114
  extraHeaders: ['Server', 'MockatonTester']
116
115
  })
117
116
  server.on('listening', runTests)
@@ -129,8 +128,8 @@ async function runTests() {
129
128
  JSON.stringify({ comment: 2 }))
130
129
 
131
130
  await testAutogenerates500(
132
- '/api/company-e/123?limit=9',
133
- 'api/company-e/[id]?limit=[limit].GET.500.txt')
131
+ '/api/alternative',
132
+ `api/alternative${DEFAULT_500_COMMENT}.GET.500.txt`)
134
133
 
135
134
  await testPreservesExiting500(
136
135
  '/api',
@@ -148,6 +147,7 @@ async function runTests() {
148
147
  await testExtractsAllComments([
149
148
  '(comment-1)',
150
149
  '(comment-2)',
150
+ DEFAULT_500_COMMENT,
151
151
  '(this is the actual comment)',
152
152
  '(another comment)'
153
153
  ])
@@ -248,7 +248,7 @@ async function testAutogenerates500(url, file) {
248
248
  })
249
249
  const res = await request(url)
250
250
  const body = await res.text()
251
- await describe('autogenerated 500', () => {
251
+ await describe('autogenerated in-memory 500', () => {
252
252
  it('body is empty', () => equal(body, ''))
253
253
  it('status is: 500', () => equal(res.status, 500))
254
254
  })
package/index.d.ts CHANGED
@@ -6,11 +6,10 @@ interface Config {
6
6
  host?: string,
7
7
  port?: number
8
8
  delay?: number
9
- open?: (address: string) => void
9
+ onReady?: (address: string) => void
10
10
  cookies?: object
11
11
  proxyFallback?: string
12
12
  allowedExt?: RegExp
13
- generate500?: boolean,
14
13
  extraHeaders?: [string, string][]
15
14
  }
16
15
 
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": "3.0.0",
5
+ "version": "4.0.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -14,3 +14,5 @@ export const DF = { // Dashboard Fields (XHR)
14
14
  delayed: 'delayed',
15
15
  file: 'file'
16
16
  }
17
+
18
+ export const DEFAULT_500_COMMENT = '(Mockaton Temp 500)'
package/src/Config.js CHANGED
@@ -8,11 +8,10 @@ export const Config = {
8
8
  host: '127.0.0.1',
9
9
  port: 0, // auto-assigned
10
10
  delay: 1200, // milliseconds
11
- open: openInBrowser,
12
11
  cookies: {}, // defaults to the first kv
12
+ onReady: openInBrowser,
13
13
  proxyFallback: '', // e.g. http://localhost:9999
14
14
  allowedExt: /\.(json|txt|md|js)$/, // Just for excluding temporary editor files (e.g. JetBrains appends a ~)
15
- generate500: false,
16
15
  extraHeaders: []
17
16
  }
18
17
 
@@ -24,11 +23,10 @@ export function setup(options) {
24
23
  host: is(String),
25
24
  port: port => Number.isInteger(port) && port >= 0 && port < 2 ** 16,
26
25
  delay: ms => Number.isInteger(ms) && ms > 0,
27
- open: is(Function),
28
26
  cookies: is(Object),
27
+ onReady: is(Function),
29
28
  proxyFallback: optional(URL.canParse),
30
29
  allowedExt: is(RegExp),
31
- generate500: is(Boolean),
32
30
  extraHeaders: Array.isArray
33
31
  })
34
32
  }
package/src/Dashboard.css CHANGED
@@ -8,7 +8,7 @@
8
8
  }
9
9
  html, body {
10
10
  margin: 0;
11
- font-size: 13px;
11
+ font-size: 12px;
12
12
  }
13
13
  body {
14
14
  padding: 16px;
@@ -50,36 +50,40 @@ main {
50
50
  padding-bottom: 2px;
51
51
  text-align: left;
52
52
  }
53
+
54
+ tr {
55
+ border-bottom: 2px solid transparent;
56
+ }
53
57
  }
54
58
  }
55
59
 
56
60
  menu {
57
61
  display: flex;
62
+ align-items: flex-end;
58
63
  margin-bottom: 12px;
59
64
  gap: 14px;
60
- align-items: flex-end;
61
65
 
62
66
  h1 {
63
67
  margin: 0;
64
- margin-right: 14px;
65
- font-size: 2rem;
68
+ margin-right: 12px;
69
+ font-size: 26px;
66
70
  }
67
71
 
68
72
  label {
69
73
  span {
70
74
  display: block;
71
75
  color: #555;
72
- font-size: .85rem;
76
+ font-size: 11px;
73
77
  }
74
78
 
75
79
  select {
76
- width: 143px;
80
+ width: 144px;
77
81
  padding: 3px 0;
78
82
  border: 1px solid #bbb;
79
83
  margin-top: 1px;
80
84
  cursor: pointer;
81
85
  border-radius: 4px;
82
- font-size: 0.9rem;
86
+ font-size: 11px;
83
87
  }
84
88
  }
85
89
 
@@ -105,6 +109,8 @@ menu {
105
109
  margin-left: 16px;
106
110
 
107
111
  pre {
112
+ tab-size: 2;
113
+
108
114
  &:not(:empty) {
109
115
  overflow: auto;
110
116
  max-height: calc(100vh - 160px);
@@ -147,6 +153,7 @@ menu {
147
153
  text-align: right;
148
154
  direction: rtl;
149
155
  text-overflow: ellipsis;
156
+ font-size: 12px;
150
157
 
151
158
  &:disabled {
152
159
  background: transparent;
@@ -160,14 +167,11 @@ menu {
160
167
  &.status4xx {
161
168
  background: var(--colorLightOrange);
162
169
  }
163
- &.status5xx {
164
- background: var(--colorLightRed);
165
- }
166
170
  }
167
171
 
168
- .DelayCheckbox {
172
+ .DelayToggler {
169
173
  display: flex;
170
- margin-left: 6px;
174
+ margin-left: 8px;
171
175
  cursor: pointer;
172
176
 
173
177
  > input {
@@ -189,7 +193,36 @@ menu {
189
193
  background: white;
190
194
 
191
195
  &:hover {
192
- background: #ddd
196
+ background: #bce5ff;
197
+ fill: #00318b;
198
+ }
199
+ }
200
+ }
201
+
202
+ .InternalServerErrorToggler {
203
+ display: flex;
204
+ margin-left: 6px;
205
+ cursor: pointer;
206
+
207
+ > input {
208
+ display: none;
209
+
210
+ &:checked ~ span {
211
+ color: white;
212
+ background: var(--colorRed);
213
+ }
214
+ }
215
+
216
+ > span {
217
+ padding: 4px;
218
+ font-size: 10px;
219
+ color: #333;
220
+ border-radius: 2px;
221
+ background: white;
222
+
223
+ &:hover {
224
+ background: var(--colorLightRed);
225
+ color: var(--colorRed);
193
226
  }
194
227
  }
195
228
  }
package/src/Dashboard.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Route } from '../Route.js'
2
- import { API, DF } from '../ApiConstants.js'
2
+ import { API, DF, DEFAULT_500_COMMENT } from '../ApiConstants.js'
3
3
 
4
4
 
5
5
  const Strings = {
@@ -9,6 +9,7 @@ const Strings = {
9
9
  delay: 'Delay',
10
10
  empty_response_body: '/* Empty Response Body */',
11
11
  fetching: '⌚ Fetching…',
12
+ internal_server_error: 'Internal Server Error',
12
13
  mock: 'Mock',
13
14
  reset: 'Reset',
14
15
  select_one: 'Select One',
@@ -16,7 +17,8 @@ const Strings = {
16
17
  }
17
18
 
18
19
  const CSS = {
19
- DelayCheckbox: 'DelayCheckbox',
20
+ DelayToggler: 'DelayToggler',
21
+ InternalServerErrorToggler: 'InternalServerErrorToggler',
20
22
  Documentation: 'Documentation',
21
23
  MockSelector: 'MockSelector',
22
24
  PayloadViewer: 'PayloadViewer',
@@ -25,7 +27,6 @@ const CSS = {
25
27
  bold: 'bold',
26
28
  chosen: 'chosen',
27
29
  status4xx: 'status4xx',
28
- status5xx: 'status5xx'
29
30
  }
30
31
 
31
32
  const r = createElement
@@ -136,8 +137,10 @@ function SectionByMethod({ method, brokers }) {
136
137
  .map(([urlMask, broker]) =>
137
138
  r('tr', null,
138
139
  r('td', null, r(PreviewLink, { method, urlMask, documentation: broker.documentation })),
139
- r('td', null, r(MockSelector, { items: broker.mocks, selected: broker.currentMock.file })),
140
- r('td', null, r(DelayToggler, { name: broker.currentMock.file, checked: Boolean(broker.currentMock.delay) }))))))
140
+ r('td', null, r(MockSelector, { broker })),
141
+ r('td', null, r(DelayToggler, { broker })),
142
+ r('td', null, r(InternalServerErrorToggler, { broker }))
143
+ ))))
141
144
  }
142
145
 
143
146
  function PreviewLink({ method, urlMask, documentation }) {
@@ -173,19 +176,27 @@ function PreviewLink({ method, urlMask, documentation }) {
173
176
  }, urlMask))
174
177
  }
175
178
 
176
- function MockSelector({ items, selected }) {
179
+ function MockSelector({ broker }) {
177
180
  const className = (defaultIsSelected, status) => cssClass(
178
181
  CSS.MockSelector,
179
182
  !defaultIsSelected && CSS.bold,
180
- status >= 400 && status < 500 && CSS.status4xx,
181
- status >= 500 && CSS.status5xx)
183
+ status >= 400 && status < 500 && CSS.status4xx)
184
+
185
+ const items = broker.mocks
186
+ const selected = broker.currentMock.file
187
+
188
+ const { status } = Route.parseFilename(selected)
189
+ const files = items.filter(item =>
190
+ status === 500 ||
191
+ !item.includes(DEFAULT_500_COMMENT))
192
+
182
193
  return (
183
194
  r('select', {
184
- className: className(selected === items[0], Route.parseFilename(selected).status),
195
+ className: className(selected === files[0], status),
185
196
  autocomplete: 'off',
186
- disabled: items.length <= 1,
197
+ disabled: files.length <= 1,
187
198
  onChange() {
188
- const status = Route.parseFilename(this.value).status
199
+ const { status } = Route.parseFilename(this.value)
189
200
  this.style.fontWeight = this.value === this.options[0].value // default is selected
190
201
  ? 'normal'
191
202
  : 'bold'
@@ -194,20 +205,22 @@ function MockSelector({ items, selected }) {
194
205
  body: JSON.stringify({ [DF.file]: this.value })
195
206
  }).then(() => {
196
207
  this.closest('tr').querySelector('a').click()
197
- this.className = className(this.value === this.options[0].value, this.value === status)
208
+ this.closest('tr').querySelector(`.${CSS.InternalServerErrorToggler}>[type=checkbox]`).checked = status === 500
209
+ this.className = className(this.value === this.options[0].value, status)
198
210
  })
199
211
  }
200
- }, items.map(item =>
201
- r('option', {
202
- value: item,
203
- selected: item === selected
204
- }, item))))
212
+ }, files.map(file => r('option', {
213
+ value: file,
214
+ selected: file === selected
215
+ }, file))))
205
216
  }
206
217
 
207
- function DelayToggler({ name, checked }) {
218
+ function DelayToggler({ broker }) {
219
+ const name = broker.currentMock.file
220
+ const checked = Boolean(broker.currentMock.delay)
208
221
  return (
209
222
  r('label', {
210
- className: CSS.DelayCheckbox,
223
+ className: CSS.DelayToggler,
211
224
  title: Strings.delay
212
225
  },
213
226
  r('input', {
@@ -234,6 +247,36 @@ function TimerIcon() {
234
247
  r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
235
248
  }
236
249
 
250
+ function InternalServerErrorToggler({ broker }) {
251
+ const items = broker.mocks
252
+ const name = broker.currentMock.file
253
+ const checked = Route.parseFilename(broker.currentMock.file).status === 500
254
+ return (
255
+ r('label', {
256
+ className: CSS.InternalServerErrorToggler,
257
+ title: Strings.internal_server_error
258
+ },
259
+ r('input', {
260
+ type: 'checkbox',
261
+ autocomplete: 'off',
262
+ name,
263
+ checked,
264
+ onChange(event) {
265
+ fetch(API.edit, {
266
+ method: 'PATCH',
267
+ body: JSON.stringify({
268
+ [DF.file]: event.currentTarget.checked
269
+ ? items.find(f => f.includes(DEFAULT_500_COMMENT))
270
+ : items[0]
271
+ })
272
+ }).then(init)
273
+ }
274
+ }),
275
+ r('span', null, '500')
276
+ )
277
+ )
278
+ }
279
+
237
280
 
238
281
 
239
282
  /* === Utils === */
package/src/MockBroker.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { join } from 'node:path'
2
- import { existsSync, lstatSync, writeFileSync } from 'node:fs'
2
+ import { existsSync, lstatSync } from 'node:fs'
3
3
 
4
4
  import { Route } from './Route.js'
5
5
  import { Config } from './Config.js'
6
+ import { DEFAULT_500_COMMENT } from './ApiConstants.js'
6
7
 
7
8
 
8
9
  // MockBroker is a state for a particular route. It knows the available
9
10
  // mock files that can be served for the route, the currently selected
10
- // file, and its delay. Also, knows if the route has documentation (md).
11
+ // file, and its delay. Also, knows if the route has documentation (md)
11
12
  export class MockBroker {
12
13
  #route
13
14
 
@@ -40,7 +41,8 @@ export class MockBroker {
40
41
 
41
42
  get file() { return this.currentMock.file }
42
43
  get delay() { return this.currentMock.delay }
43
- get status() { return Route.parseFilename(this.currentMock.file).status }
44
+ get status() { return Route.parseFilename(this.file).status }
45
+ get isTemp500() { return Route.hasInParentheses(this.file, DEFAULT_500_COMMENT) }
44
46
 
45
47
  updateFile(filename) {
46
48
  this.currentMock.file = filename
@@ -67,7 +69,7 @@ export class MockBroker {
67
69
 
68
70
  ensureItHas500() {
69
71
  if (!this.#has500())
70
- this.#write500()
72
+ this.#registerTemp500()
71
73
  }
72
74
 
73
75
  #has500() {
@@ -75,14 +77,14 @@ export class MockBroker {
75
77
  Route.parseFilename(mock).status === 500)
76
78
  }
77
79
 
78
- #write500() {
80
+ #registerTemp500() {
79
81
  const { urlMask, method } = Route.parseFilename(this.mocks[0])
80
82
  let mask = urlMask
81
83
  const t = join(Config.mocksDir, urlMask)
82
84
  if (existsSync(t) && lstatSync(t).isDirectory())
83
85
  mask = urlMask + '/'
84
- const file = `${mask}.${method}.500.txt`
85
- writeFileSync(join(Config.mocksDir, file), '')
86
+ mask = mask.replace(/^\//, '') // remove initial slash
87
+ const file = `${mask}${DEFAULT_500_COMMENT}.${method}.500.txt`
86
88
  this.register(file)
87
89
  }
88
90
  }
@@ -1,13 +1,12 @@
1
1
  import { join } from 'node:path'
2
2
  import { readFileSync } from 'node:fs'
3
3
 
4
- import { DF } from './ApiConstants.js'
5
4
  import { proxy } from './ProxyRelay.js'
6
5
  import { cookie } from './cookie.js'
7
6
  import { Config } from './Config.js'
8
7
  import { mimeFor } from './utils/mime.js'
9
8
  import * as mockBrokerCollection from './mockBrokersCollection.js'
10
- import { parseJSON, JsonBodyParserError } from './utils/http-request.js'
9
+ import { JsonBodyParserError } from './utils/http-request.js'
11
10
  import { sendInternalServerError, sendNotFound, sendFile, sendBadRequest } from './utils/http-response.js'
12
11
 
13
12
 
@@ -33,20 +32,17 @@ export async function dispatchMock(req, response) {
33
32
 
34
33
  let mockText
35
34
  if (file.endsWith('.js')) {
36
- response.setHeader('content-type', mimeFor('.json'))
37
- const jsExport = await importDefault(file)
38
- mockText = typeof jsExport === 'function'
39
- ? await jsExport(req, response)
40
- : JSON.stringify(jsExport, null, 2)
35
+ response.setHeader('Content-Type', mimeFor('.json'))
36
+ mockText = await jsMockText(file, req, response)
41
37
  }
42
38
  else {
43
- response.setHeader('content-type', mimeFor(file))
44
- mockText = readMock(file)
39
+ response.setHeader('Content-Type', mimeFor(file))
40
+ mockText = broker.isTemp500 ? '' : readMock(file)
45
41
  }
46
42
 
47
43
  if (cookie.getCurrent())
48
- response.setHeader('set-cookie', cookie.getCurrent())
49
-
44
+ response.setHeader('Set-Cookie', cookie.getCurrent())
45
+
50
46
  response.writeHead(status, Config.extraHeaders)
51
47
  setTimeout(() => response.end(mockText), delay)
52
48
  }
@@ -61,6 +57,13 @@ export async function dispatchMock(req, response) {
61
57
  }
62
58
  }
63
59
 
60
+ async function jsMockText(file, req, response) {
61
+ const jsExport = await importDefault(file)
62
+ return typeof jsExport === 'function'
63
+ ? await jsExport(req, response)
64
+ : JSON.stringify(jsExport, null, 2)
65
+ }
66
+
64
67
  function readMock(file) {
65
68
  return readFileSync(join(Config.mocksDir, file), 'utf8')
66
69
  }
package/src/Mockaton.js CHANGED
@@ -1,4 +1,3 @@
1
- import { exec } from 'node:child_process'
2
1
  import { createServer } from 'node:http'
3
2
 
4
3
  import { API } from './ApiConstants.js'
@@ -37,6 +36,6 @@ export function Mockaton(options) {
37
36
  if (error)
38
37
  console.error(error)
39
38
  else
40
- Config.open(url + API.dashboard)
39
+ Config.onReady(url + API.dashboard)
41
40
  })
42
41
  }
@@ -40,8 +40,7 @@ export function init() {
40
40
  collection[method][urlMask].register(file)
41
41
  }
42
42
 
43
- if (Config.generate500)
44
- forEachBroker(broker => broker.ensureItHas500())
43
+ forEachBroker(broker => broker.ensureItHas500())
45
44
  }
46
45
 
47
46
  function forEachBroker(fn) {
@@ -49,7 +48,6 @@ function forEachBroker(fn) {
49
48
  Object.values(brokers).forEach(fn)
50
49
  }
51
50
 
52
-
53
51
  export const getAll = () => collection
54
52
 
55
53
  export const getBrokerByFilename = file => {
@@ -72,7 +70,6 @@ export function getBrokerForUrl(method, url) {
72
70
  return brokers[i]
73
71
  }
74
72
 
75
-
76
73
  export function extractAllComments() {
77
74
  const comments = new Set()
78
75
  forEachBroker(broker => {
@@ -1,4 +1,4 @@
1
- import fs, { readFileSync } from 'node:fs'
1
+ import fs, { existsSync, readFileSync } from 'node:fs'
2
2
  import { mimeFor } from './mime.js'
3
3
 
4
4
 
@@ -12,8 +12,12 @@ export function sendJSON(response, payload) {
12
12
  }
13
13
 
14
14
  export function sendFile(response, file) {
15
- response.setHeader('content-type', mimeFor(file))
16
- response.end(readFileSync(file))
15
+ if (!existsSync(file))
16
+ sendNotFound(response)
17
+ else {
18
+ response.setHeader('content-type', mimeFor(file))
19
+ response.end(readFileSync(file))
20
+ }
17
21
  }
18
22
 
19
23
  export async function sendPartialContent(response, range, file) {