mockaton 1.0.0 → 2.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.
Binary file
package/README.md CHANGED
@@ -26,6 +26,26 @@ UI, or programmatically, for instance, for setting up tests.
26
26
 
27
27
  The first file in **alphabetical order** becomes the default mock.
28
28
 
29
+ ### Optionally, you can write mocks in JavaScript
30
+ An Object, Array, or String is sent as JSON.
31
+
32
+ `api/user/likes.GET.200.js`
33
+ ```js
34
+ export default [
35
+ { id: 0 }
36
+ ]
37
+ ```
38
+
39
+ Or, export default a function. There, you
40
+ can override the response status and the default JSON content
41
+ type. But don’t call `response.end()`, just return a string.
42
+ ```js
43
+ export default function (req, response) {
44
+ return JSON.stringify({ a: 1 })
45
+ }
46
+ ```
47
+
48
+
29
49
  ### Proxying Routes
30
50
  `Config.proxyFallback` lets you specify a target
31
51
  server for serving routes you don’t have mocks for.
@@ -75,10 +95,9 @@ interface Config {
75
95
  port?: number // defaults to 0, which means auto-assigned
76
96
  delay?: number // defaults to 1200 (ms)
77
97
  cookies?: object
78
- database?: object // for "Transforms"
79
98
  skipOpen?: boolean // Prevents opening the dashboard in a browser
80
99
  proxyFallback?: string // e.g. http://localhost:9999 Target for relaying routes without mocks
81
- allowedExt?: RegExp // /\.(json|txt|md|mjs)$/ Just for excluding temporary editor files (e.g. JetBrains appends a ~)
100
+ allowedExt?: RegExp // /\.(json|txt|md|js)$/ Just for excluding temporary editor files (e.g. JetBrains appends a ~)
82
101
  }
83
102
  ```
84
103
 
@@ -164,19 +183,6 @@ api/foo/[user-id].POST.201.md
164
183
  api/foo/[user-id].POST.201.json
165
184
  ```
166
185
 
167
- ## Transforms (.mjs)
168
- Using the same filename convention, files ending
169
- with `.mjs` will process the mock before serving it.
170
-
171
- For example, this handler will capitalize the mock body and increment a counter.
172
- ```js
173
- export default function capitalizeAllText(mockAsText, requestBody, config) {
174
- config.database.myCount ??= 0
175
- config.database.myCount++
176
- return mockAsText.toUpperCase()
177
- }
178
- ```
179
-
180
186
 
181
187
  ## API
182
188
 
@@ -223,14 +229,6 @@ Sends a list of the available cookies along with a flag indicated if it’s the
223
229
  fetch(addr + '/mockaton/cookies')
224
230
  ```
225
231
 
226
- ### Select a Transform
227
- ```js
228
- fetch(addr + '/mockaton/transform', {
229
- method: 'PATCH',
230
- body: JSON.stringify('api/video/list(concat newly uploaded).GET.200.mjs')
231
- })
232
- ```
233
-
234
232
  ### Update Fallback Proxy
235
233
  ```js
236
234
  fetch(addr + '/mockaton/fallback', {
package/Tests.js CHANGED
@@ -96,6 +96,9 @@ write('api/.GET.500.txt', 'keeps non-autogenerated 500')
96
96
  write('api/alternative(comment-2).GET.200.json', JSON.stringify({ comment: 2 }))
97
97
  write('api/my-route(comment-2).GET.200.json', JSON.stringify({ comment: 2 }))
98
98
 
99
+ // JavaScript to JSON
100
+ write('/api/object.GET.200.js', 'export default { JSON_FROM_JS: true }')
101
+
99
102
  writeStatic('index.html', '<h1>Static</h1>')
100
103
  writeStatic('assets/app.js', 'const app = 1')
101
104
  writeStatic('another-entry/index.html', '<h1>Another</h1>')
@@ -154,9 +157,11 @@ async function runTests() {
154
157
  await reset()
155
158
  for (const [url, file, body] of fixtures)
156
159
  await testMockDispatching(url, file, body)
160
+
161
+ await testMockDispatching('/api/object', 'api/object.GET.200.js', { JSON_FROM_JS: true }, mimeFor('.json'))
162
+ await testJsFunctionMocks()
157
163
 
158
164
  await testItUpdatesUserRole()
159
- await testTransforms()
160
165
  await testStaticFileServing()
161
166
  await testInvalidFilenamesAreIgnored()
162
167
  await testEnableFallbackSoRoutesWithoutMocksGetRelayed()
@@ -185,11 +190,11 @@ async function test404() {
185
190
  })
186
191
  }
187
192
 
188
- async function testMockDispatching(url, file, expectedBody, reqBody = void 0) {
193
+ async function testMockDispatching(url, file, expectedBody, forcedMime = void 0) {
189
194
  const { urlMask, method, status } = Route.parseFilename(file)
190
- const mime = mimeFor(file)
195
+ const mime = forcedMime || mimeFor(file)
191
196
  const now = new Date()
192
- const res = await request(url, { method, body: reqBody })
197
+ const res = await request(url, { method })
193
198
  const body = mime === 'application/json'
194
199
  ? await res.json()
195
200
  : await res.text()
@@ -300,25 +305,18 @@ async function testItUpdatesUserRole() {
300
305
  })
301
306
  }
302
307
 
303
- async function testTransforms() {
304
- await describe('Applies transform', async () => {
305
- write('api/transform.POST.200.json', JSON.stringify(['initial']))
306
- write('api/transform.POST.200.mjs', `
307
- export default function (mock, reqBody, config) {
308
- const body = JSON.parse(mock);
309
- body.push(reqBody[0]);
310
- body.push(config.mocksDir);
311
- return JSON.stringify(body);
308
+ async function testJsFunctionMocks() {
309
+ await describe('JS Function Mocks', async () => {
310
+ write('api/js-func.POST.200.js', `
311
+ export default function (req, response) {
312
+ response.setHeader('content-type', 'custom-mime')
313
+ return 'SOME_STRING'
312
314
  }`)
313
- await reset() // for registering the files
314
- await request(API.transform, {
315
- method: 'PATCH',
316
- body: JSON.stringify('api/transform.POST.200.mjs')
317
- })
318
- await testMockDispatching('/api/transform',
319
- 'api/transform.POST.200.json',
320
- ['initial', 'another', tmpDir],
321
- JSON.stringify(['another']))
315
+ await reset() // for registering the file
316
+ await testMockDispatching('/api/js-func',
317
+ 'api/js-func.POST.200.js',
318
+ 'SOME_STRING',
319
+ 'custom-mime')
322
320
  })
323
321
  }
324
322
 
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.0.0",
5
+ "version": "2.0.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -0,0 +1,7 @@
1
+ // You can write JSON responses in JavaScript.
2
+
3
+ export default [
4
+ { id: 0 },
5
+ { id: 1 },
6
+ { id: 2 },
7
+ ]
@@ -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
 
@@ -92,19 +91,6 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
92
91
  }
93
92
  }
94
93
 
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
94
  async function updateProxyFallback(req, response) {
109
95
  try {
110
96
  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
@@ -1,4 +1,4 @@
1
- import { existsSync, lstatSync } from 'node:fs'
1
+ import { existsSync as exists, lstatSync } from 'node:fs'
2
2
  import { validate, is, optional } from './utils/validate.js'
3
3
 
4
4
 
@@ -12,7 +12,7 @@ export const Config = {
12
12
  database: {},
13
13
  skipOpen: false,
14
14
  proxyFallback: '', // e.g. http://localhost:9999
15
- allowedExt: /\.(json|txt|md|mjs)$/ // Just for excluding temporary editor files (e.g. JetBrains appends a ~)
15
+ allowedExt: /\.(json|txt|md|js)$/ // Just for excluding temporary editor files (e.g. JetBrains appends a ~)
16
16
  }
17
17
 
18
18
  export function setup(options) {
@@ -32,7 +32,7 @@ export function setup(options) {
32
32
  }
33
33
 
34
34
  function isDirectory(dir) {
35
- return existsSync(dir) && lstatSync(dir).isDirectory()
35
+ return exists(dir) && lstatSync(dir).isDirectory()
36
36
  }
37
37
 
38
38
 
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,7 +28,7 @@ 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
34
  response.statusCode = status
@@ -36,13 +36,17 @@ export async function dispatchMock(req, response) {
36
36
  if (cookie.getCurrent())
37
37
  response.setHeader('set-cookie', cookie.getCurrent())
38
38
 
39
- let mockAsText = readMock(file)
40
- if (broker.currentTransform) {
41
- const body = await requestBodyForTransform(req, mockAsText)
42
- const transformFunc = await importTransformFunc(currentTransform)
43
- mockAsText = transformFunc(mockAsText, body, Config)
39
+ let mockText
40
+ if (file.endsWith('.js')) {
41
+ response.setHeader('content-type', mimeFor('.json'))
42
+ const jsExport = await importDefault(file)
43
+ mockText = typeof jsExport === 'function'
44
+ ? jsExport(req, response)
45
+ : JSON.stringify(jsExport)
44
46
  }
45
- setTimeout(() => response.end(mockAsText), delay)
47
+ else
48
+ mockText = readMock(file)
49
+ setTimeout(() => response.end(mockText), delay)
46
50
  }
47
51
  catch (error) {
48
52
  console.error(error)
@@ -55,21 +59,11 @@ export async function dispatchMock(req, response) {
55
59
  }
56
60
  }
57
61
 
58
- const nonSafeMethods = ['PATCH', 'POST', 'PUT', 'DELETE', 'CONNECT']
59
-
60
- async function requestBodyForTransform(req, mockAsText) {
61
- if (nonSafeMethods.includes(req.method))
62
- return req.headers[DF.isForDashboard] // TODO unit TESTME
63
- ? JSON.parse(mockAsText)
64
- : await parseJSON(req)
65
- return ''
66
- }
67
-
68
62
  function readMock(file) {
69
63
  return readFileSync(join(Config.mocksDir, file), 'utf8')
70
64
  }
71
65
 
72
- async function importTransformFunc(file) {
66
+ async function importDefault(file) {
73
67
  // The date param is just for cache busting
74
68
  return (await import(join(Config.mocksDir, file) + '?' + Date.now())).default
75
69
  }
@@ -34,7 +34,7 @@ export function init() {
34
34
  continue
35
35
  }
36
36
  collection[method] ??= {}
37
- if (!(urlMask in collection[method]))
37
+ if (!collection[method][urlMask])
38
38
  collection[method][urlMask] = new MockBroker(file)
39
39
  else
40
40
  collection[method][urlMask].register(file)
@@ -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
- }
@@ -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
- }