mockaton 8.2.23 → 8.3.1

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
@@ -13,13 +13,10 @@ URL paths. For example, the following file will be served on `/api/user/1234`
13
13
  my-mocks-dir/api/user/[user-id].GET.200.json
14
14
  ```
15
15
 
16
- By the way, [this browser
17
- extension](https://github.com/ericfortis/devtools-ext-tar-http-requests)
18
- can create a TAR of your requests following that convention.
19
-
20
- Nonetheless, you don’t need to mock all your APIs. Mockaton
21
- can request from your backend the routes you don’t have mocks for.
22
- That’s done with `config.proxyFallback = 'http://mybackend'`
16
+ By the way, you don’t need to mock all your APIs. Mockaton can request
17
+ from your backend the routes you don’t have mocks for. That’s done
18
+ with `config.proxyFallback = 'http://mybackend'`. For convenience, you
19
+ can save mocks for those responses with `config.collectProxied = true`
23
20
 
24
21
  ## Multiple Mock Variants
25
22
  Each route can have many mocks, which could either be:
@@ -280,6 +277,35 @@ the amount is globally configurable. It defaults to `config.delay=1200` millisec
280
277
  Lets you specify a target server for serving routes you don’t have mocks for.
281
278
  For example, `config.proxyFallback = 'http://example.com'`
282
279
 
280
+ ### `collectProxied?: boolean`
281
+ Defaults to `false`. With this flag you can save mocks that hit
282
+ your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
283
+ URL the filename will have `[id]` in their place. For example,
284
+
285
+ ```
286
+ /api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
287
+ my-mocks-dir/api/user/[id]/likes.GET.200.json
288
+ ```
289
+
290
+ Note that newly saved mocks won’t be served until you
291
+ **register them** by reinitializing Mockaton or clicking "Reset".
292
+
293
+ Registered mocks won’t be overwritten (they don’t hit the fallback server).
294
+ On the other hand, newly saved mocks get overwritten while they are unregistered.
295
+
296
+ <details>
297
+ <summary>Extension Details</summary>
298
+ <p>
299
+ An <code>.empty</code> extension means the <code>Content-Type</code>
300
+ header was not sent by your backend.
301
+ </p>
302
+
303
+ <p>
304
+ An <code>.unknown</code> extension means the <code>Content-Type</code> is not in
305
+ Mockaton’s predefined list. For that, you can add it to <code>config.extraMimes</code>
306
+ </p>
307
+ </details>
308
+
283
309
 
284
310
  ### `staticDir?: string`
285
311
  - Use Case 1: If you have a bunch of static assets you don’t want to add `.GET.200.ext`
@@ -472,7 +498,3 @@ await mockaton.reset()
472
498
  <img src="./sample-mocks/api/user/avatar.GET.200.png" width="170"/>
473
499
  <p style="font-size: 18px">“Use Mockaton”</p>
474
500
  </div>
475
-
476
-
477
- ## TODO
478
- - Refactor Tests
package/TODO.md ADDED
@@ -0,0 +1,7 @@
1
+ # TODO
2
+
3
+ - Refactor tests
4
+ - Rename 'Reset' to 'Reload'
5
+ - Add Collect Proxied checkbox to the dashboard
6
+ - Subscribe button for newly added mocks
7
+ - or perhaps registering them automatically without full reset?
package/index.d.ts CHANGED
@@ -18,6 +18,7 @@ interface Config {
18
18
  host?: string,
19
19
  port?: number
20
20
  proxyFallback?: string
21
+ collectProxied?: boolean
21
22
 
22
23
  delay?: number
23
24
  cookies?: { [label: string]: string }
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": "8.2.23",
5
+ "version": "8.3.1",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -21,3 +21,5 @@ export const DF = { // Dashboard Fields (XHR)
21
21
 
22
22
  export const DEFAULT_500_COMMENT = '(Mockaton 500)'
23
23
  export const DEFAULT_MOCK_COMMENT = '(default)'
24
+
25
+ export const EXT_FOR_UNKNOWN_MIME = 'unknown'
package/src/Config.js CHANGED
@@ -14,6 +14,7 @@ export const Config = Object.seal({
14
14
  host: '127.0.0.1',
15
15
  port: 0, // auto-assigned
16
16
  proxyFallback: '', // e.g. http://localhost:9999
17
+ collectProxied: false,
17
18
 
18
19
  delay: 1200, // milliseconds
19
20
  cookies: {}, // defaults to the first kv
@@ -47,6 +48,7 @@ export function setup(options) {
47
48
  host: is(String),
48
49
  port: port => Number.isInteger(port) && port >= 0 && port < 2 ** 16,
49
50
  proxyFallback: optional(URL.canParse),
51
+ collectProxied: is(Boolean),
50
52
 
51
53
  delay: ms => Number.isInteger(ms) && ms > 0,
52
54
  cookies: is(Object),
package/src/Filename.js CHANGED
@@ -44,7 +44,6 @@ export function parseFilename(file) {
44
44
  }
45
45
  }
46
46
 
47
-
48
47
  function removeTrailingSlash(url = '') {
49
48
  return url
50
49
  .replace(/\/$/, '')
@@ -59,5 +58,14 @@ function responseStatusIsValid(status) {
59
58
  }
60
59
 
61
60
 
61
+ export function makeMockFilename(url, method, status, ext) {
62
+ const urlMask = replaceIds(removeTrailingSlash(url))
63
+ return [urlMask, method, status, ext].join('.')
64
+ }
65
+
66
+ const reUuidV4 = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi
67
+ function replaceIds(filename) {
68
+ return filename.replaceAll(reUuidV4, '[id]')
69
+ }
62
70
 
63
71
 
@@ -5,7 +5,7 @@ import { cookie } from './cookie.js'
5
5
  import { Config } from './Config.js'
6
6
  import { applyPlugins } from './MockDispatcherPlugins.js'
7
7
  import * as mockBrokerCollection from './mockBrokersCollection.js'
8
- import { JsonBodyParserError } from './utils/http-request.js'
8
+ import { BodyReaderError } from './utils/http-request.js'
9
9
  import { sendInternalServerError, sendNotFound, sendBadRequest } from './utils/http-response.js'
10
10
 
11
11
 
@@ -37,7 +37,7 @@ export async function dispatchMock(req, response) {
37
37
  setTimeout(() => response.end(body), broker.delay)
38
38
  }
39
39
  catch (error) {
40
- if (error instanceof JsonBodyParserError)
40
+ if (error instanceof BodyReaderError)
41
41
  sendBadRequest(response, error)
42
42
  else if (error.code === 'ENOENT') // mock-file has been deleted
43
43
  sendNotFound(response)
package/src/ProxyRelay.js CHANGED
@@ -1,11 +1,27 @@
1
+ import { join } from 'node:path'
2
+ import { write } from './utils/fs.js'
1
3
  import { Config } from './Config.js'
4
+ import { extFor } from './utils/mime.js'
5
+ import { readBody } from './utils/http-request.js'
6
+ import { makeMockFilename } from './Filename.js'
2
7
 
3
8
 
4
9
  export async function proxy(req, response) {
5
10
  const proxyResponse = await fetch(Config.proxyFallback + req.url, {
6
11
  method: req.method,
7
- headers: req.headers
12
+ headers: req.headers,
13
+ body: req.method === 'GET' || req.method === 'HEAD' // TESTME
14
+ ? undefined
15
+ : await readBody(req)
8
16
  })
17
+ // TODO investigate how to include many repeated headers such as set-cookie
9
18
  response.writeHead(proxyResponse.status, Object.fromEntries(proxyResponse.headers))
10
- response.end(await proxyResponse.text())
19
+ const body = await proxyResponse.text()
20
+ response.end(body)
21
+
22
+ if (Config.collectProxied) { // TESTME
23
+ const ext = extFor(proxyResponse.headers.get('content-type'))
24
+ const filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
25
+ write(join(Config.mocksDir, filename), body)
26
+ }
11
27
  }
package/src/utils/fs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import path, { join } from 'node:path'
2
- import { lstatSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { lstatSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
3
3
 
4
4
 
5
5
  export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
@@ -14,3 +14,8 @@ export const listFilesRecursively = dir => {
14
14
  ? files.map(f => f.replaceAll(path.sep, path.posix.sep)) // TESTME
15
15
  : files
16
16
  }
17
+
18
+ export const write = (fPath, body) => {
19
+ mkdirSync(path.dirname(fPath), { recursive: true })
20
+ writeFileSync(fPath, body)
21
+ }
@@ -3,10 +3,11 @@ export const StandardMethods = [
3
3
  'HEAD', 'OPTIONS', 'TRACE', 'CONNECT'
4
4
  ]
5
5
 
6
+ export class BodyReaderError extends Error {}
6
7
 
7
- export class JsonBodyParserError extends Error {}
8
+ export const parseJSON = req => readBody(req, JSON.parse)
8
9
 
9
- export function parseJSON(req) {
10
+ export function readBody(req, parser = a => a) {
10
11
  return new Promise((resolve, reject) => {
11
12
  const MAX_BODY_SIZE = 200 * 1024
12
13
  const expectedLength = req.headers['content-length'] | 0
@@ -29,13 +30,13 @@ export function parseJSON(req) {
29
30
  req.removeListener('end', onEnd)
30
31
  req.removeListener('error', onEnd)
31
32
  if (lengthSoFar !== expectedLength)
32
- reject(new JsonBodyParserError())
33
+ reject(new BodyReaderError())
33
34
  else
34
35
  try {
35
- resolve(JSON.parse(Buffer.concat(body).toString()))
36
+ resolve(parser(Buffer.concat(body).toString()))
36
37
  }
37
38
  catch (_) {
38
- reject(new JsonBodyParserError())
39
+ reject(new BodyReaderError())
39
40
  }
40
41
  }
41
42
  })
package/src/utils/mime.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Config } from '../Config.js'
2
+ import { EXT_FOR_UNKNOWN_MIME } from '../ApiConstants.js'
2
3
 
3
4
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
4
5
  // m = {};
@@ -88,8 +89,21 @@ const mimes = {
88
89
 
89
90
  export function mimeFor(filename) {
90
91
  const ext = filename.replace(/.*\./, '').toLowerCase()
91
- const mime = Config.extraMimes[ext] || mimes[ext] || ''
92
- if (!mime)
93
- console.info(`Missing MIME for ${filename}`)
92
+ return Config.extraMimes[ext] || mimes[ext] || ''
93
+ }
94
+
95
+ export function extFor(mime) {
94
96
  return mime
97
+ ? findExt(mime)
98
+ : 'empty'
99
+ }
100
+
101
+ function findExt(targetMime) {
102
+ for (const [ext, mime] of Object.entries(Config.extraMimes))
103
+ if (targetMime === mime)
104
+ return ext
105
+ for (const [ext, mime] of Object.entries(mimes))
106
+ if (targetMime === mime)
107
+ return ext
108
+ return EXT_FOR_UNKNOWN_MIME
95
109
  }