mockaton 1.1.0 → 2.1.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.
Binary file
package/README.md CHANGED
@@ -16,8 +16,7 @@ be used for downloading a TAR of your XHR requests following that convention.
16
16
  ### Mock Variants
17
17
  Each route can have many mocks, which could either be:
18
18
  - Different response __status code__.
19
- - e.g. for testing error responses. As a side note, an _Internal Server
20
- Error_ mock is autogenerated for routes that have no 500.
19
+ - e.g. for testing error responses.
21
20
  - __Comment__ on the filename, which is anything within parentheses.
22
21
  - e.g. `api/user(my-comment).POST.201.json`
23
22
 
@@ -36,6 +35,15 @@ export default [
36
35
  ]
37
36
  ```
38
37
 
38
+ Or, export default a function. There, you
39
+ can override the response status and the default JSON content
40
+ type. But don’t call `response.end()`, just return a string.
41
+ ```js
42
+ export default function (req, response) {
43
+ return JSON.stringify({ a: 1 })
44
+ }
45
+ ```
46
+
39
47
 
40
48
  ### Proxying Routes
41
49
  `Config.proxyFallback` lets you specify a target
@@ -47,7 +55,7 @@ The best way to learn _Mockaton_ is by checking out this repo and
47
55
  exploring its [sample-mocks/](./sample-mocks) directory. Then, run
48
56
  [`./_usage_example.js`](./_usage_example.js) and you’ll see this dashboard:
49
57
 
50
- ![](./README-dashboard.png)
58
+ <img src="./README-dashboard.png" style="max-width:890px"/>
51
59
 
52
60
 
53
61
  ## Delay 🕓
@@ -86,10 +94,10 @@ interface Config {
86
94
  port?: number // defaults to 0, which means auto-assigned
87
95
  delay?: number // defaults to 1200 (ms)
88
96
  cookies?: object
89
- database?: object // for "Transforms"
90
97
  skipOpen?: boolean // Prevents opening the dashboard in a browser
91
98
  proxyFallback?: string // e.g. http://localhost:9999 Target for relaying routes without mocks
92
- allowedExt?: RegExp // /\.(json|txt|md|js|mjs)$/ Just for excluding temporary editor files (e.g. JetBrains appends a ~)
99
+ allowedExt?: RegExp // /\.(json|txt|md|js)$/ Just for excluding temporary editor files (e.g. JetBrains appends a ~)
100
+ generate500?: boolean // autogenerates an Internal Server Error empty mock for routes that have no 500
93
101
  }
94
102
  ```
95
103
 
@@ -119,10 +127,9 @@ words, it’s useful if you only care about its payload.
119
127
  ### Extension
120
128
  `.Method.HttpResponseStatusCode.FileExt`
121
129
 
122
- The **file extension** can anything, but `.md` and `.mjs` are reserved
123
- for documentation, and mock processors (more on that later).
130
+ The **file extension** can be anything, but `.md` is reserved for documentation.
124
131
 
125
- The `Config.allowedExt` regex defaults to: `/\.(json|txt|md|mjs)$/`
132
+ The `Config.allowedExt` regex defaults to: `/\.(json|txt|md|js)$/`
126
133
 
127
134
 
128
135
  ### Dynamic Parameters
@@ -175,19 +182,6 @@ api/foo/[user-id].POST.201.md
175
182
  api/foo/[user-id].POST.201.json
176
183
  ```
177
184
 
178
- ## Transforms (.mjs)
179
- Using the same filename convention, files ending
180
- with `.mjs` will process the mock before serving it.
181
-
182
- For example, this handler will capitalize the mock body and increment a counter.
183
- ```js
184
- export default function capitalizeAllText(mockAsText, requestBody, config) {
185
- config.database.myCount ??= 0
186
- config.database.myCount++
187
- return mockAsText.toUpperCase()
188
- }
189
- ```
190
-
191
185
 
192
186
  ## API
193
187
 
@@ -234,14 +228,6 @@ Sends a list of the available cookies along with a flag indicated if it’s the
234
228
  fetch(addr + '/mockaton/cookies')
235
229
  ```
236
230
 
237
- ### Select a Transform
238
- ```js
239
- fetch(addr + '/mockaton/transform', {
240
- method: 'PATCH',
241
- body: JSON.stringify('api/video/list(concat newly uploaded).GET.200.mjs')
242
- })
243
- ```
244
-
245
231
  ### Update Fallback Proxy
246
232
  ```js
247
233
  fetch(addr + '/mockaton/fallback', {
package/Tests.js CHANGED
@@ -110,7 +110,8 @@ const server = Mockaton({
110
110
  cookies: {
111
111
  userA: 'CookieA',
112
112
  userB: 'CookieB'
113
- }
113
+ },
114
+ generate500: true
114
115
  })
115
116
  server.on('listening', runTests)
116
117
 
@@ -121,8 +122,6 @@ async function runTests() {
121
122
  for (const [url, file, body] of fixtures)
122
123
  await testMockDispatching(url, file, body)
123
124
 
124
- await testMockDispatching('/api/object', 'api/object.GET.200.js', { JSON_FROM_JS: true }, undefined, mimeFor('.json'))
125
-
126
125
  await testItUpdatesDelayAndFile(
127
126
  '/api/alternative',
128
127
  'api/alternative(comment-2).GET.200.json',
@@ -159,9 +158,11 @@ async function runTests() {
159
158
  await reset()
160
159
  for (const [url, file, body] of fixtures)
161
160
  await testMockDispatching(url, file, body)
161
+
162
+ await testMockDispatching('/api/object', 'api/object.GET.200.js', { JSON_FROM_JS: true }, mimeFor('.json'))
163
+ await testJsFunctionMocks()
162
164
 
163
165
  await testItUpdatesUserRole()
164
- await testTransforms()
165
166
  await testStaticFileServing()
166
167
  await testInvalidFilenamesAreIgnored()
167
168
  await testEnableFallbackSoRoutesWithoutMocksGetRelayed()
@@ -190,11 +191,11 @@ async function test404() {
190
191
  })
191
192
  }
192
193
 
193
- async function testMockDispatching(url, file, expectedBody, reqBody = void 0, forcedMime = void 0) {
194
+ async function testMockDispatching(url, file, expectedBody, forcedMime = void 0) {
194
195
  const { urlMask, method, status } = Route.parseFilename(file)
195
196
  const mime = forcedMime || mimeFor(file)
196
197
  const now = new Date()
197
- const res = await request(url, { method, body: reqBody })
198
+ const res = await request(url, { method })
198
199
  const body = mime === 'application/json'
199
200
  ? await res.json()
200
201
  : await res.text()
@@ -305,25 +306,18 @@ async function testItUpdatesUserRole() {
305
306
  })
306
307
  }
307
308
 
308
- async function testTransforms() {
309
- await describe('Applies transform', async () => {
310
- write('api/transform.POST.200.json', JSON.stringify(['initial']))
311
- write('api/transform.POST.200.mjs', `
312
- export default function (mock, reqBody, config) {
313
- const body = JSON.parse(mock);
314
- body.push(reqBody[0]);
315
- body.push(config.mocksDir);
316
- return JSON.stringify(body);
309
+ async function testJsFunctionMocks() {
310
+ await describe('JS Function Mocks', async () => {
311
+ write('api/js-func.POST.200.js', `
312
+ export default function (req, response) {
313
+ response.setHeader('content-type', 'custom-mime')
314
+ return 'SOME_STRING'
317
315
  }`)
318
- await reset() // for registering the files
319
- await request(API.transform, {
320
- method: 'PATCH',
321
- body: JSON.stringify('api/transform.POST.200.mjs')
322
- })
323
- await testMockDispatching('/api/transform',
324
- 'api/transform.POST.200.json',
325
- ['initial', 'another', tmpDir],
326
- JSON.stringify(['another']))
316
+ await reset() // for registering the file
317
+ await testMockDispatching('/api/js-func',
318
+ 'api/js-func.POST.200.js',
319
+ 'SOME_STRING',
320
+ 'custom-mime')
327
321
  })
328
322
  }
329
323
 
package/index.d.ts CHANGED
@@ -7,10 +7,10 @@ interface Config {
7
7
  port?: number
8
8
  delay?: number
9
9
  cookies?: object
10
- database?: object
11
10
  skipOpen?: boolean
12
11
  proxyFallback?: string
13
12
  allowedExt?: RegExp
13
+ generate500?: boolean
14
14
  }
15
15
 
16
16
  export function Mockaton(options: Config): Server
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": "1.1.0",
5
+ "version": "2.1.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -0,0 +1,7 @@
1
+ This is a plain text response for (/api/friends).
2
+
3
+ In this case, it’s for mocking up a 500 - Internal Server Error.
4
+
5
+ This file could have been empty, or some JSON if it had a `.json` extension.
6
+
7
+ By the way, on initialization an 500 is auto-generated for routes that don’t have a 500.
@@ -0,0 +1,7 @@
1
+ // You can write JSON responses in JavaScript.
2
+
3
+ export default function (req, response) {
4
+ return JSON.stringify([
5
+ { id: 0 }
6
+ ])
7
+ }
package/src/Api.js CHANGED
@@ -28,7 +28,6 @@ export const apiPatchRequests = new Map([
28
28
  [API.edit, updateBroker],
29
29
  [API.reset, reinitialize],
30
30
  [API.cookies, selectCookie],
31
- [API.transform, updateBrokerTransform],
32
31
  [API.fallback, updateProxyFallback]
33
32
  ])
34
33
 
@@ -61,7 +60,6 @@ async function selectCookie(req, response) {
61
60
  }
62
61
 
63
62
  function reinitialize(_, response) {
64
- Config.database = {}
65
63
  mockBrokersCollection.init()
66
64
  sendOK(response)
67
65
  }
@@ -92,19 +90,6 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
92
90
  }
93
91
  }
94
92
 
95
- async function updateBrokerTransform(req, response) {
96
- try {
97
- const file = await parseJSON(req)
98
- const broker = mockBrokersCollection.getBrokerByFilename(file)
99
- broker.updateTransform(file)
100
- sendOK(response)
101
- }
102
- catch (error) {
103
- console.error(error)
104
- sendBadRequest(response)
105
- }
106
- }
107
-
108
93
  async function updateProxyFallback(req, response) {
109
94
  try {
110
95
  Config.proxyFallback = await parseJSON(req)
@@ -6,13 +6,11 @@ export const API = {
6
6
  edit: MOUNT + '/edit',
7
7
  mocks: MOUNT + '/mocks',
8
8
  reset: MOUNT + '/reset',
9
- transform: MOUNT + '/transform',
10
9
  cookies: MOUNT + '/cookies',
11
10
  fallback: MOUNT + '/fallback'
12
11
  }
13
12
 
14
13
  export const DF = { // Dashboard Fields (XHR)
15
14
  delayed: 'delayed',
16
- file: 'file',
17
- isForDashboard: 'mock_request_payload'
15
+ file: 'file'
18
16
  }
package/src/Config.js CHANGED
@@ -9,10 +9,10 @@ export const Config = {
9
9
  port: 0, // auto-assigned
10
10
  delay: 1200, // milliseconds
11
11
  cookies: {}, // defaults to the first kv
12
- database: {},
13
12
  skipOpen: false,
14
13
  proxyFallback: '', // e.g. http://localhost:9999
15
- allowedExt: /\.(json|txt|md|js|mjs)$/ // Just for excluding temporary editor files (e.g. JetBrains appends a ~)
14
+ allowedExt: /\.(json|txt|md|js)$/, // Just for excluding temporary editor files (e.g. JetBrains appends a ~)
15
+ generate500: false
16
16
  }
17
17
 
18
18
  export function setup(options) {
@@ -24,10 +24,10 @@ export function setup(options) {
24
24
  port: port => Number.isInteger(port) && port >= 0 && port < 2 ** 16,
25
25
  delay: ms => Number.isInteger(ms) && ms > 0,
26
26
  cookies: is(Object),
27
- database: is(Object),
28
27
  skipOpen: is(Boolean),
29
28
  proxyFallback: is(String),
30
- allowedExt: is(RegExp)
29
+ allowedExt: is(RegExp),
30
+ generate500: is(Boolean)
31
31
  })
32
32
  }
33
33
 
package/src/Dashboard.css CHANGED
@@ -135,17 +135,11 @@ main {
135
135
  .BulkSelectSection {
136
136
  margin: 20px 0;
137
137
  }
138
- .TransformsSection {
139
- padding-top: 30px;
140
- border-top: 1px solid #ccc;
141
- margin: 30px 0;
142
- }
143
138
 
144
139
  .BulkSelectSection select {
145
140
  margin-top: 5px;
146
141
  }
147
142
 
148
- .TransformSelector,
149
143
  .MockSelector {
150
144
  width: 300px;
151
145
  padding: 8px 1px;
package/src/Dashboard.js CHANGED
@@ -10,11 +10,9 @@ const Strings = {
10
10
  empty_response_body: '/* Empty Response Body */',
11
11
  fetching: '⌚ Fetching…',
12
12
  mock: 'Mock',
13
- none: 'None',
14
13
  reset: 'Reset',
15
14
  select_one: 'Select One',
16
- title: 'Mockaton',
17
- transforms: 'Transforms'
15
+ title: 'Mockaton'
18
16
  }
19
17
 
20
18
  const CSS = {
@@ -26,8 +24,6 @@ const CSS = {
26
24
  PayloadViewer: 'PayloadViewer',
27
25
  PreviewLink: 'PreviewLink',
28
26
  TitleWrap: 'TitleWrap',
29
- TransformSelector: 'TransformSelector',
30
- TransformsSection: 'TransformsSection',
31
27
 
32
28
  bold: 'bold',
33
29
  chosen: 'chosen',
@@ -74,10 +70,7 @@ function DevPanel(brokersByMethod, cookies, comments) {
74
70
  r('div', { className: CSS.PayloadViewer },
75
71
  r('pre', { ref: refDocumentation, className: CSS.Documentation }),
76
72
  r('h2', { ref: refPayloadFile }, Strings.mock),
77
- r('pre', { ref: refPayloadViewer }, Strings.click_link_to_preview))),
78
- r('div', { className: CSS.TransformsSection },
79
- r('h2', null, Strings.transforms),
80
- r(Transforms, { brokersByMethod }))))
73
+ r('pre', { ref: refPayloadViewer }, Strings.click_link_to_preview)))))
81
74
  }
82
75
 
83
76
 
@@ -142,7 +135,7 @@ function SectionByMethod({ method, brokers }) {
142
135
  r('th', null, method),
143
136
  Object.entries(brokers)
144
137
  .sort((a, b) => a[0].localeCompare(b[0]))
145
- .filter(([, broker]) => broker.mocks.length) // handles Markdown doc or js transform without mocks
138
+ .filter(([, broker]) => broker.mocks.length) // handles Markdown doc
146
139
  .map(([urlMask, broker]) =>
147
140
  r('tr', null,
148
141
  r('td', null, r(PreviewLink, { method, urlMask, documentation: broker.documentation })),
@@ -168,8 +161,7 @@ function PreviewLink({ method, urlMask, documentation }) {
168
161
 
169
162
  const spinner = setTimeout(() => refPayloadViewer.current.innerText = Strings.fetching, 180)
170
163
  const res = await fetch(this.href, {
171
- method: this.getAttribute('data-method'),
172
- headers: { [DF.isForDashboard]: '1' }
164
+ method: this.getAttribute('data-method')
173
165
  })
174
166
  document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
175
167
  this.classList.add(CSS.chosen)
@@ -246,47 +238,6 @@ function TimerIcon() {
246
238
  }
247
239
 
248
240
 
249
- function Transforms({ brokersByMethod }) {
250
- const brokersWithTransforms = []
251
- for (const brokers of Object.values(brokersByMethod))
252
- for (const [urlMask, broker] of Object.entries(brokers))
253
- if (broker.transforms.length)
254
- brokersWithTransforms.push([urlMask, broker])
255
- return (
256
- r('table', null, brokersWithTransforms.map(([urlMask, broker]) =>
257
- r('tr', null,
258
- r('td', null, r(PreviewLink, { method: broker.method, urlMask })),
259
- r('td', null, r(TransformSelector, {
260
- items: ['', ...broker.transforms],
261
- selected: broker.currentTransform
262
- })))
263
- )))
264
- }
265
-
266
- function TransformSelector({ items, selected }) {
267
- const className = defaultIsSelected => cssClass(
268
- CSS.TransformSelector,
269
- !defaultIsSelected && CSS.bold)
270
- return (
271
- r('select', {
272
- className: className(selected === items[0]),
273
- autocomplete: 'off',
274
- onChange() {
275
- fetch(API.transform, {
276
- method: 'PATCH',
277
- body: JSON.stringify(this.value)
278
- }).then(() => {
279
- this.closest('tr').querySelector('a').click()
280
- this.className = className(this.value === this.options[0].value)
281
- })
282
- }
283
- }, items.map(item =>
284
- r('option', {
285
- value: item,
286
- selected: item === selected
287
- }, item || Strings.none))))
288
- }
289
-
290
241
 
291
242
  /* === Utils === */
292
243
  function cssClass(...args) {
package/src/MockBroker.js CHANGED
@@ -5,9 +5,9 @@ import { Route } from './Route.js'
5
5
  import { Config } from './Config.js'
6
6
 
7
7
 
8
- // MockBroker is a state for a particular route. It knows the available mock files
9
- // that can be served for the route, the currently selected file, and its delay. Also,
10
- // knows if the route has js preprocessors (transforms) and documentation (md).
8
+ // MockBroker is a state for a particular route. It knows the available
9
+ // 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
11
  export class MockBroker {
12
12
  #route
13
13
 
@@ -17,23 +17,18 @@ export class MockBroker {
17
17
 
18
18
  this.documentation = '' // .md
19
19
 
20
- this.mocks = [] // *.json,txt
20
+ this.mocks = [] // *.json,txt,js
21
21
  this.currentMock = {
22
22
  file: '',
23
23
  delay: 0
24
24
  }
25
25
 
26
- this.transforms = [] // *.mjs
27
- this.currentTransform = ''
28
-
29
26
  this.register(file)
30
27
  }
31
28
 
32
29
  register(file) {
33
30
  if (file.endsWith('.md'))
34
31
  this.documentation = file
35
- else if (file.endsWith('.mjs'))
36
- this.transforms.push(file)
37
32
  else {
38
33
  if (!this.mocks.length)
39
34
  this.currentMock.file = file // The first mock file option for a particular route becomes the default
@@ -55,26 +50,17 @@ export class MockBroker {
55
50
  this.currentMock.delay = Number(delayed) * Config.delay
56
51
  }
57
52
 
58
- updateTransform(filename) {
59
- this.currentTransform = filename
60
- }
61
-
62
53
  setByMatchingComment(comment) {
63
54
  for (const file of this.mocks)
64
55
  if (Route.hasInParentheses(file, comment)) {
65
56
  this.updateFile(file)
66
57
  break
67
58
  }
68
- for (const file of this.transforms)
69
- if (Route.hasInParentheses(file, comment)) {
70
- this.updateTransform(file)
71
- break
72
- }
73
59
  }
74
60
 
75
61
  extractComments() {
76
62
  let comments = []
77
- for (const file of [...this.mocks, ...this.transforms])
63
+ for (const file of this.mocks)
78
64
  comments = comments.concat(Route.extractComments(file))
79
65
  return comments
80
66
  }
@@ -90,7 +76,6 @@ export class MockBroker {
90
76
  }
91
77
 
92
78
  #write500() {
93
- // TODO handle route with transforms but without mocks
94
79
  const { urlMask, method } = Route.parseFilename(this.mocks[0])
95
80
  let mask = urlMask
96
81
  const t = join(Config.mocksDir, urlMask)
@@ -28,25 +28,26 @@ export async function dispatchMock(req, response) {
28
28
  }
29
29
 
30
30
  try {
31
- const { file, status, delay, currentTransform } = broker
31
+ const { file, status, delay } = broker
32
32
  console.log('\n', req.url, '→\n ', file)
33
33
 
34
- const shouldJavaScriptToJSON = file.endsWith('.js')
35
34
  response.statusCode = status
36
- response.setHeader('content-type', mimeFor(shouldJavaScriptToJSON ? '.json' : file))
37
35
  if (cookie.getCurrent())
38
36
  response.setHeader('set-cookie', cookie.getCurrent())
39
37
 
40
- let mockAsText = shouldJavaScriptToJSON
41
- ? JSON.stringify(await importDefault(file))
42
- : readMock(file)
43
-
44
- if (broker.currentTransform) {
45
- const body = await requestBodyForTransform(req, mockAsText)
46
- const transformFunc = await importDefault(currentTransform)
47
- mockAsText = transformFunc(mockAsText, body, Config)
38
+ let mockText
39
+ if (file.endsWith('.js')) {
40
+ response.setHeader('content-type', mimeFor('.json'))
41
+ const jsExport = await importDefault(file)
42
+ mockText = typeof jsExport === 'function'
43
+ ? jsExport(req, response)
44
+ : JSON.stringify(jsExport)
45
+ }
46
+ else {
47
+ response.setHeader('content-type', mimeFor(file))
48
+ mockText = readMock(file)
48
49
  }
49
- setTimeout(() => response.end(mockAsText), delay)
50
+ setTimeout(() => response.end(mockText), delay)
50
51
  }
51
52
  catch (error) {
52
53
  console.error(error)
@@ -59,16 +60,6 @@ export async function dispatchMock(req, response) {
59
60
  }
60
61
  }
61
62
 
62
- const nonSafeMethods = ['PATCH', 'POST', 'PUT', 'DELETE', 'CONNECT']
63
-
64
- async function requestBodyForTransform(req, mockAsText) {
65
- if (nonSafeMethods.includes(req.method))
66
- return req.headers[DF.isForDashboard] // TODO unit TESTME
67
- ? JSON.parse(mockAsText)
68
- : await parseJSON(req)
69
- return ''
70
- }
71
-
72
63
  function readMock(file) {
73
64
  return readFileSync(join(Config.mocksDir, file), 'utf8')
74
65
  }
@@ -39,7 +39,9 @@ export function init() {
39
39
  else
40
40
  collection[method][urlMask].register(file)
41
41
  }
42
- forEachBroker(broker => broker.ensureItHas500())
42
+
43
+ if (Config.generate500)
44
+ forEachBroker(broker => broker.ensureItHas500())
43
45
  }
44
46
 
45
47
  function forEachBroker(fn) {
@@ -1,7 +0,0 @@
1
- This is a plain text response for (/api/user).
2
-
3
- In this case, it’s for mocking up a 500 - Internal Server Error.
4
-
5
- This file could have been empty, or some JSON if it had a `.json` extension.
6
-
7
- By the way, on initialization an 500 is auto-generated for routes that don’t have a 500.
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,8 +0,0 @@
1
- // This is an example "transform". It takes the mock for the same route as
2
- // input, so you can modify it. In this case, it uses the `database` field.
3
-
4
- export default function concatNewlyUploadedVideos(mockAsText, _, config) {
5
- const mockList = JSON.parse(mockAsText)
6
- mockList.videos = mockList.videos.concat(config.database.videos || [])
7
- return JSON.stringify(mockList, null, 2)
8
- }
@@ -1,11 +0,0 @@
1
- {
2
- "_": "This file has query string params, but they are fully ignored. i.e. /api/video/list and /api/video/list?page_num=1 would match as well",
3
- "videos": [
4
- {
5
- "url": "https://example.com/1"
6
- },
7
- {
8
- "url": "https://example.com/2"
9
- }
10
- ]
11
- }
File without changes
@@ -1,10 +0,0 @@
1
- // An example "transform" for saving a POST request payload into the `config.database`
2
-
3
- export default function concatNewlyUploadedVideos(mockAsText, requestBody, config) {
4
- config.database.videos ??= []
5
- config.database.videos.push({
6
- createdAt: Date.now(),
7
- ...requestBody
8
- })
9
- return JSON.stringify({})
10
- }
@@ -1,3 +0,0 @@
1
- {
2
- "created": "OK"
3
- }
File without changes