mockaton 0.9.6 → 0.9.8

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/Api.js CHANGED
@@ -72,8 +72,7 @@ async function updateBroker(req, response) {
72
72
  const broker = mockBrokersCollection.getBrokerByFilename(body[DF.file])
73
73
  if (DF.delayed in body)
74
74
  broker.updateDelay(body[DF.delayed])
75
- else
76
- broker.updateFile(body[DF.file])
75
+ broker.updateFile(body[DF.file])
77
76
  sendOK(response)
78
77
  }
79
78
  catch (error) {
package/MockBroker.js CHANGED
@@ -20,7 +20,6 @@ export class MockBroker {
20
20
  this.mocks = [] // *.json,txt
21
21
  this.currentMock = {
22
22
  file: '',
23
- status: 200,
24
23
  delay: 0
25
24
  }
26
25
 
@@ -36,10 +35,8 @@ export class MockBroker {
36
35
  else if (file.endsWith('.mjs'))
37
36
  this.transforms.push(file)
38
37
  else {
39
- if (!this.mocks.length) {
38
+ if (!this.mocks.length)
40
39
  this.currentMock.file = file // The first mock file option for a particular route becomes the default
41
- this.currentMock.status = Route.parseFilename(file).status
42
- }
43
40
  this.mocks.push(file)
44
41
  }
45
42
  }
@@ -47,12 +44,11 @@ export class MockBroker {
47
44
  urlMaskMatches(url) { return this.#route.urlMaskMatches(url) }
48
45
 
49
46
  get file() { return this.currentMock.file }
50
- get status() { return this.currentMock.status }
51
47
  get delay() { return this.currentMock.delay }
48
+ get status() { return Route.parseFilename(this.currentMock.file).status }
52
49
 
53
50
  updateFile(filename) {
54
51
  this.currentMock.file = filename
55
- this.currentMock.status = Route.parseFilename(filename).status
56
52
  }
57
53
 
58
54
  updateDelay(delayed) {
@@ -83,24 +79,24 @@ export class MockBroker {
83
79
  return comments
84
80
  }
85
81
 
86
- ensureItHas501() {
87
- if (!this.#has501())
88
- this.#write501()
82
+ ensureItHas500() {
83
+ if (!this.#has500())
84
+ this.#write500()
89
85
  }
90
86
 
91
- #has501() {
87
+ #has500() {
92
88
  return this.mocks.some(mock =>
93
- Route.parseFilename(mock).status === 501)
89
+ Route.parseFilename(mock).status === 500)
94
90
  }
95
91
 
96
- #write501() {
92
+ #write500() {
97
93
  // TODO handle route with transforms but without mocks
98
94
  const { urlMask, method } = Route.parseFilename(this.mocks[0])
99
95
  let mask = urlMask
100
96
  const t = join(Config.mocksDir, urlMask)
101
97
  if (existsSync(t) && lstatSync(t).isDirectory())
102
98
  mask = urlMask + '/'
103
- const file = `${mask}.${method}.501.txt`
99
+ const file = `${mask}.${method}.500.txt`
104
100
  writeFileSync(join(Config.mocksDir, file), '')
105
101
  this.register(file)
106
102
  }
package/MockDispatcher.js CHANGED
@@ -10,46 +10,45 @@ import { parseJSON, JsonBodyParserError } from './utils/http-request.js'
10
10
  import { sendInternalServerError, sendNotFound, sendFile, sendBadRequest } from './utils/http-response.js'
11
11
 
12
12
 
13
- function serveDocumentation(req, response) {
14
- sendFile(response, join(Config.mocksDir, decodeURIComponent(req.url)))
15
- }
16
-
17
13
  export async function dispatchMock(req, response) {
14
+ /* Serve Documentation */
18
15
  if (req.method === 'GET' && req.url.endsWith('.md')) {
19
- serveDocumentation(req, response)
16
+ sendFile(response, join(Config.mocksDir, decodeURIComponent(req.url)))
20
17
  return
21
18
  }
22
19
 
23
20
  const mockBroker = MockBrokerCollection.findMatchingBroker(req.method, req.url)
24
- if (!mockBroker)
21
+ if (!mockBroker) {
25
22
  sendNotFound(response)
26
- else
27
- try {
28
- const { file, status, delay, currentTransform } = mockBroker
29
- console.log(decodeURIComponent(req.url), '->', file)
23
+ return
24
+ }
25
+
26
+ try {
27
+ const { file, status, delay, currentTransform } = mockBroker
28
+ console.log('\n', req.url, '→\n ', file)
30
29
 
31
- response.statusCode = status
32
- response.setHeader('content-type', mimeFor(file))
33
- if (cookie.getCurrent())
34
- response.setHeader('set-cookie', cookie.getCurrent())
30
+ response.statusCode = status
31
+ response.setHeader('content-type', mimeFor(file))
32
+ if (cookie.getCurrent())
33
+ response.setHeader('set-cookie', cookie.getCurrent())
35
34
 
36
- let mockAsText = readMock(file)
37
- if (mockBroker.currentTransform) {
38
- const body = await requestBodyForTransform(req, mockAsText)
39
- const transformFunc = await importTransformFunc(currentTransform)
40
- mockAsText = transformFunc(mockAsText, body, Config)
41
- }
42
- setTimeout(() => response.end(mockAsText), delay)
43
- }
44
- catch (error) {
45
- console.error(error)
46
- if (error instanceof JsonBodyParserError)
47
- sendBadRequest(response)
48
- else if (error.code === 'ENOENT')
49
- sendNotFound(response) // file has been deleted
50
- else
51
- sendInternalServerError(response)
35
+ let mockAsText = readMock(file)
36
+ if (mockBroker.currentTransform) {
37
+ const body = await requestBodyForTransform(req, mockAsText)
38
+ const transformFunc = await importTransformFunc(currentTransform)
39
+ mockAsText = transformFunc(mockAsText, body, Config)
52
40
  }
41
+ setTimeout(() => response.end(mockAsText), delay)
42
+ }
43
+ catch (error) {
44
+ console.error(error)
45
+ if (error instanceof JsonBodyParserError)
46
+ sendBadRequest(response)
47
+ else if (error.code === 'ENOENT')
48
+ sendNotFound(response) // file has been deleted
49
+ else
50
+ sendInternalServerError(response)
51
+ }
53
52
  }
54
53
 
55
54
  const nonSafeMethods = ['PATCH', 'POST', 'PUT', 'DELETE', 'CONNECT']
Binary file
Binary file
Binary file
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Mockaton
2
2
  _Mockaton_ is a mock server for developing and testing frontends.
3
3
 
4
- It scans `Config.mocksDir` for files following a specific file name convention, which is
5
- similar to the URLs. For example, the following file will be served for `/api/user/1234`
4
+ It scans `Config.mocksDir` for files following a specific
5
+ file name convention, which is similar to the URL paths. For
6
+ example, the following file will be served for `/api/user/1234`
6
7
  ```
7
8
  api/
8
9
  api/user/
@@ -22,12 +23,12 @@ Each route can have many mocks, which could either be:
22
23
  Those alternatives can be manually selected in the dashboard
23
24
  UI, or programmatically, for instance, for setting up tests.
24
25
 
25
- About the mock precedence, the first file in **alphabetical order** wins.
26
+ About the default mock file, the first file in **alphabetical order** wins.
26
27
 
27
28
 
28
29
  ## Getting Started
29
30
  The best way to learn _Mockaton_ is by checking out this repo and
30
- exploring its [sample-mocks/](./sample-mocks) directory. Then run
31
+ exploring its [sample-mocks/](./sample-mocks) directory. Then, run
31
32
  [`./_usage_example.js`](./_usage_example.js) and you’ll see this dashboard:
32
33
 
33
34
  ![](./README-dashboard.png)
@@ -38,8 +39,8 @@ The **sample-mocks/** directory has three mock alternatives for serving
38
39
  `/api/user/friends`:
39
40
  - _200 - OK_
40
41
  - _204 - No Content_ with an empty list of friends
41
- - _501 - Internal Server Error_
42
- - BTW, 501 mocks get autogenerated for routes that have no 501’s.
42
+ - _500 - Internal Server Error_
43
+ - BTW, 500 mocks get autogenerated for routes that have no 500’s.
43
44
 
44
45
  ![](./README-dashboard-dropdown.png)
45
46
 
@@ -48,7 +49,7 @@ Comments are anything within parentheses, including them.
48
49
  ![](./README-mocks-with-comments.png)
49
50
 
50
51
  ## Delay 🕓
51
- The clock icon next to the mock selector dropdown is a checkbox for delaying a
52
+ The clock icon next to the mock selector is a checkbox for delaying a
52
53
  particular response. They are handy for testing spinners.
53
54
 
54
55
  The milliseconds for the delay is globally configurable via `Config.delay = 1200`
@@ -136,13 +137,13 @@ api/foo.GET.200.json
136
137
  ```
137
138
  api/video?limit=[limit].GET.200.json
138
139
  ```
139
- The query string behaves like comments in the sense it’s
140
- only used for documenting the URL API contract.
140
+ The query string behaves like comments in the sense it’s only used for documenting
141
+ the URL API contract. In other words, the query string is ignored when routing to it.
141
142
 
142
- In other words, the query string is ignored when routing to it. BTW, in Windows,
143
- filenames containing "?" are not permitted, but they are ignored anyway.
143
+ BTW, in Windows, filenames containing "?" are [not
144
+ permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file),
145
+ but since that’s part of the query string, it’s ignored anyway.
144
146
 
145
- https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
146
147
 
147
148
 
148
149
  ### Default (index-like) file
@@ -194,15 +195,6 @@ PATCH /mockaton/edit
194
195
  ---
195
196
 
196
197
  ### `/mockaton/bulk-select` Select all mocks that have a particular comment
197
- Many mocks can be changed at once. We do that by searching the
198
- comments on the filename. For example, `api/foo(demo-a).GET.200.json`
199
-
200
- Non-matching mocks are ignored. For instance, if for a
201
- particular API there is only `demo-a` and `demo-b`, changing to
202
- `demo-c` will preserve the last one that was successfully set.
203
-
204
- Similarly, if there’s no demo mock at all for
205
- a route, the first dev mock (a-z) will be served.
206
198
 
207
199
  ```
208
200
  PATCH /mockaton/bulk-select
@@ -220,7 +212,8 @@ PATCH /mockaton/reset
220
212
  ---
221
213
 
222
214
  ### `/mockaton/cookies` Select a cookie
223
- In `Config.cookies`, each key is a label used to change them.
215
+ In `Config.cookies`, each key is the label used
216
+ for changing it. Only one cookie can be set.
224
217
  ```
225
218
  PATCH /mockaton/cookies
226
219
  {
@@ -229,8 +222,7 @@ PATCH /mockaton/cookies
229
222
  ```
230
223
 
231
224
  ### `/mockaton/cookies` List Cookies
232
- Sends a list of the cookie labels (keys) and
233
- along with a flag indicated if it’s the selected.
225
+ Sends a list of the available cookies along with a flag indicated if it’s the selected.
234
226
  ```
235
227
  GET /mockaton/cookies
236
228
  ```
package/Route.js CHANGED
@@ -45,20 +45,18 @@ export class Route {
45
45
  static parseFilename(file) {
46
46
  const tokens = file.replace(Route.reComments, '').split('.')
47
47
 
48
- let error = ''
49
48
  if (tokens.length < 4)
50
- error = 'Invalid Filename Convention'
51
-
52
- const method = tokens.at(-3)
53
- if (!httpMethods.includes(method))
54
- error = `Unrecognized HTTP Method: "${method}"`
49
+ return { error: 'Invalid Filename Convention' }
55
50
 
56
51
  const status = Number(tokens.at(-2))
57
52
  if (!responseStatusIsValid(status))
58
- error = `Invalid HTTP Response Status: "${status}"`
53
+ return { error: `Invalid HTTP Response Status: "${status}"` }
54
+
55
+ const method = tokens.at(-3)
56
+ if (!httpMethods.includes(method))
57
+ return { error: `Unrecognized HTTP Method: "${method}"` }
59
58
 
60
59
  return {
61
- error,
62
60
  urlMask: '/' + removeTrailingSlash(tokens.at(-4)),
63
61
  method,
64
62
  status
package/Tests.js CHANGED
@@ -90,15 +90,10 @@ const fixtures = [
90
90
  for (const [, file, body] of fixtures)
91
91
  write(file, file.endsWith('.json') ? JSON.stringify(body) : body)
92
92
 
93
- write('api/.GET.501.txt', 'keeps non-autogenerated 501')
93
+ write('api/.GET.500.txt', 'keeps non-autogenerated 500')
94
94
  write('api/alternative(comment-2).GET.200.json', JSON.stringify({ comment: 2 }))
95
95
  write('api/my-route(comment-2).GET.200.json', JSON.stringify({ comment: 2 }))
96
96
 
97
- // These files ensure the server doesn’t crash. We don’t test their console.error
98
- write('api/bad-filename.200.json', 'missing method')
99
- write('api/bad-filename.GET.200', 'missing extension')
100
- write('api/bad-filename.GET.json', 'missing response status')
101
-
102
97
  writeStatic('index.html', '<h1>Static</h1>')
103
98
  writeStatic('assets/app.js', 'const app = 1')
104
99
  writeStatic('another-entry/index.html', '<h1>Another</h1>')
@@ -123,16 +118,17 @@ async function runTests() {
123
118
 
124
119
  await testItUpdatesDelayAndFile(
125
120
  '/api/alternative',
126
- 'api/alternative(comment-1).GET.200.json')
121
+ 'api/alternative(comment-2).GET.200.json',
122
+ JSON.stringify({ comment: 2 }))
127
123
 
128
- await testAutogenerates501(
124
+ await testAutogenerates500(
129
125
  '/api/company-e/123?limit=9',
130
- 'api/company-e/[id]?limit=[limit].GET.501.txt')
126
+ 'api/company-e/[id]?limit=[limit].GET.500.txt')
131
127
 
132
- await testPreservesExiting501(
128
+ await testPreservesExiting500(
133
129
  '/api',
134
- 'api/.GET.501.txt',
135
- 'keeps non-autogenerated 501')
130
+ 'api/.GET.500.txt',
131
+ 'keeps non-autogenerated 500')
136
132
 
137
133
  await reset()
138
134
  await testItUpdatesTheCurrentSelectedMock(
@@ -148,12 +144,10 @@ async function runTests() {
148
144
  '(this is the actual comment)',
149
145
  '(another comment)'
150
146
  ])
151
- await testItBulkSelectsByComment('(comment-2)',
152
- [
153
- ['/api/alternative', 'api/alternative(comment-2).GET.200.json', { comment: 2 }],
154
- ['/api/my-route', 'api/my-route(comment-2).GET.200.json', { comment: 2 }]
155
- ]
156
- )
147
+ await testItBulkSelectsByComment('(comment-2)', [
148
+ ['/api/alternative', 'api/alternative(comment-2).GET.200.json', { comment: 2 }],
149
+ ['/api/my-route', 'api/my-route(comment-2).GET.200.json', { comment: 2 }]
150
+ ])
157
151
 
158
152
  await reset()
159
153
  for (const [url, file, body] of fixtures)
@@ -161,9 +155,8 @@ async function runTests() {
161
155
 
162
156
  await testItUpdatesUserRole()
163
157
  await testTransforms()
164
-
165
158
  await testStaticFileServing()
166
-
159
+ await testInvalidFilenamesAreIgnored()
167
160
  server.close()
168
161
  }
169
162
 
@@ -208,7 +201,7 @@ async function testItUpdatesTheCurrentSelectedMock(url, file, expectedStatus, ex
208
201
  })
209
202
  }
210
203
 
211
- async function testItUpdatesDelayAndFile(url, file) {
204
+ async function testItUpdatesDelayAndFile(url, file, expectedBody) {
212
205
  await request(DP.edit, {
213
206
  method: 'PATCH',
214
207
  body: JSON.stringify({
@@ -217,35 +210,38 @@ async function testItUpdatesDelayAndFile(url, file) {
217
210
  })
218
211
  })
219
212
  const now = new Date()
220
- await request(url)
221
- await describe('url: ' + url, () =>
222
- it('delay is over 1 sec', () => equal((new Date()).getTime() - now.getTime() > 1000, true)))
213
+ const res = await request(url)
214
+ const body = await res.text()
215
+ await describe('url: ' + url, () => {
216
+ it('body is: ' + expectedBody, () => equal(body, expectedBody))
217
+ it('delay is over 1 sec', () => equal((new Date()).getTime() - now.getTime() > 1000, true))
218
+ })
223
219
  }
224
220
 
225
221
 
226
- async function testAutogenerates501(url, file) {
222
+ async function testAutogenerates500(url, file) {
227
223
  await request(DP.edit, {
228
224
  method: 'PATCH',
229
225
  body: JSON.stringify({ [DF.file]: file })
230
226
  })
231
227
  const res = await request(url)
232
228
  const body = await res.text()
233
- await describe('autogenerated 501', () => {
229
+ await describe('autogenerated 500', () => {
234
230
  it('body is empty', () => equal(body, ''))
235
- it('status is: 501', () => equal(res.status, 501))
231
+ it('status is: 500', () => equal(res.status, 500))
236
232
  })
237
233
  }
238
234
 
239
- async function testPreservesExiting501(url, file, expectedBody) {
235
+ async function testPreservesExiting500(url, file, expectedBody) {
240
236
  await request(DP.edit, {
241
237
  method: 'PATCH',
242
238
  body: JSON.stringify({ [DF.file]: file })
243
239
  })
244
240
  const res = await request(url)
245
241
  const body = await res.text()
246
- await describe('preserves existing 501', () => {
242
+ await describe('preserves existing 500', () => {
247
243
  it('body is empty', () => equal(body, expectedBody))
248
- it('status is: 501', () => equal(res.status, 501))
244
+ it('status is: 500', () => equal(res.status, 500))
249
245
  })
250
246
  }
251
247
 
@@ -259,9 +255,7 @@ async function testExtractsAllComments(expected) {
259
255
  async function testItBulkSelectsByComment(comment, tests) {
260
256
  await request(DP.bulkSelect, {
261
257
  method: 'PATCH',
262
- body: JSON.stringify({
263
- [DF.comment]: comment
264
- })
258
+ body: JSON.stringify({ [DF.comment]: comment })
265
259
  })
266
260
  for (const [url, file, body] of tests)
267
261
  await testMockDispatching(url, file, body)
@@ -305,9 +299,7 @@ export default function (mock, reqBody, config) {
305
299
  await reset() // for registering the files
306
300
  await request(DP.transform, {
307
301
  method: 'PATCH',
308
- body: JSON.stringify({
309
- [DF.file]: 'api/transform.POST.200.mjs'
310
- })
302
+ body: JSON.stringify({ [DF.file]: 'api/transform.POST.200.mjs' })
311
303
  })
312
304
  await testMockDispatching('/api/transform',
313
305
  'api/transform.POST.200.json',
@@ -342,6 +334,22 @@ async function testStaticFileServing() {
342
334
  })
343
335
  }
344
336
 
337
+ async function testInvalidFilenamesAreIgnored() {
338
+ await it('Invalid filenames get skipped, so they don’t crash the server', async (t) => {
339
+ const consoleErrorSpy = t.mock.method(console, 'error')
340
+ consoleErrorSpy.mock.mockImplementation(() => {}) // so they don’t render in the test report
341
+
342
+ // An extension is needed for testing because of `Config.allowedExt`
343
+ write('api/_INVALID_FILENAME_CONVENTION_.json', '')
344
+ write('api/bad-filename.GET._INVALID_STATUS_.json', '')
345
+ write('api/bad-filename._INVALID_METHOD_.200.json', '')
346
+ await reset()
347
+ equal(consoleErrorSpy.mock.calls[0].arguments[0], 'Invalid Filename Convention')
348
+ equal(consoleErrorSpy.mock.calls[1].arguments[0], 'Invalid HTTP Response Status: "NaN"')
349
+ equal(consoleErrorSpy.mock.calls[2].arguments[0], 'Unrecognized HTTP Method: "_INVALID_METHOD_"')
350
+ })
351
+ }
352
+
345
353
 
346
354
  // Utils
347
355
 
@@ -35,14 +35,13 @@ export function init() {
35
35
  collection[method][urlMask].register(file)
36
36
  }
37
37
  }
38
- forEachBroker(broker => broker.ensureItHas501())
38
+ forEachBroker(broker => broker.ensureItHas500())
39
39
  }
40
40
 
41
41
  export const getAll = () => collection
42
- export const getBroker = (method, urlMask) => collection[method][urlMask]
43
42
  export const getBrokerByFilename = file => {
44
43
  const { method, urlMask } = Route.parseFilename(file)
45
- return getBroker(method, urlMask)
44
+ return collection[method][urlMask]
46
45
  }
47
46
 
48
47
 
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": "0.9.6",
5
+ "version": "0.9.8",
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/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.
@@ -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 501 - 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 501 is auto-generated for routes that don’t have a 501.