mockaton 1.1.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
@@ -36,6 +36,15 @@ export default [
36
36
  ]
37
37
  ```
38
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
+
39
48
 
40
49
  ### Proxying Routes
41
50
  `Config.proxyFallback` lets you specify a target
@@ -86,10 +95,9 @@ interface Config {
86
95
  port?: number // defaults to 0, which means auto-assigned
87
96
  delay?: number // defaults to 1200 (ms)
88
97
  cookies?: object
89
- database?: object // for "Transforms"
90
98
  skipOpen?: boolean // Prevents opening the dashboard in a browser
91
99
  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 ~)
100
+ allowedExt?: RegExp // /\.(json|txt|md|js)$/ Just for excluding temporary editor files (e.g. JetBrains appends a ~)
93
101
  }
94
102
  ```
95
103
 
@@ -175,19 +183,6 @@ api/foo/[user-id].POST.201.md
175
183
  api/foo/[user-id].POST.201.json
176
184
  ```
177
185
 
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
186
 
192
187
  ## API
193
188
 
@@ -234,14 +229,6 @@ Sends a list of the available cookies along with a flag indicated if it’s the
234
229
  fetch(addr + '/mockaton/cookies')
235
230
  ```
236
231
 
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
232
  ### Update Fallback Proxy
246
233
  ```js
247
234
  fetch(addr + '/mockaton/fallback', {
package/Tests.js CHANGED
@@ -121,8 +121,6 @@ async function runTests() {
121
121
  for (const [url, file, body] of fixtures)
122
122
  await testMockDispatching(url, file, body)
123
123
 
124
- await testMockDispatching('/api/object', 'api/object.GET.200.js', { JSON_FROM_JS: true }, undefined, mimeFor('.json'))
125
-
126
124
  await testItUpdatesDelayAndFile(
127
125
  '/api/alternative',
128
126
  'api/alternative(comment-2).GET.200.json',
@@ -159,9 +157,11 @@ async function runTests() {
159
157
  await reset()
160
158
  for (const [url, file, body] of fixtures)
161
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()
162
163
 
163
164
  await testItUpdatesUserRole()
164
- await testTransforms()
165
165
  await testStaticFileServing()
166
166
  await testInvalidFilenamesAreIgnored()
167
167
  await testEnableFallbackSoRoutesWithoutMocksGetRelayed()
@@ -190,11 +190,11 @@ async function test404() {
190
190
  })
191
191
  }
192
192
 
193
- async function testMockDispatching(url, file, expectedBody, reqBody = void 0, forcedMime = void 0) {
193
+ async function testMockDispatching(url, file, expectedBody, forcedMime = void 0) {
194
194
  const { urlMask, method, status } = Route.parseFilename(file)
195
195
  const mime = forcedMime || mimeFor(file)
196
196
  const now = new Date()
197
- const res = await request(url, { method, body: reqBody })
197
+ const res = await request(url, { method })
198
198
  const body = mime === 'application/json'
199
199
  ? await res.json()
200
200
  : await res.text()
@@ -305,25 +305,18 @@ async function testItUpdatesUserRole() {
305
305
  })
306
306
  }
307
307
 
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);
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'
317
314
  }`)
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']))
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')
327
320
  })
328
321
  }
329
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.1.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 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
@@ -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|js|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) {
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,25 @@ 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))
35
+ response.setHeader('content-type', mimeFor(file))
37
36
  if (cookie.getCurrent())
38
37
  response.setHeader('set-cookie', cookie.getCurrent())
39
38
 
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)
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)
48
46
  }
49
- setTimeout(() => response.end(mockAsText), delay)
47
+ else
48
+ mockText = readMock(file)
49
+ setTimeout(() => response.end(mockText), delay)
50
50
  }
51
51
  catch (error) {
52
52
  console.error(error)
@@ -59,16 +59,6 @@ export async function dispatchMock(req, response) {
59
59
  }
60
60
  }
61
61
 
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
62
  function readMock(file) {
73
63
  return readFileSync(join(Config.mocksDir, file), 'utf8')
74
64
  }
@@ -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
- }
File without changes