mockaton 13.12.0 → 13.14.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.
package/README.md CHANGED
@@ -6,22 +6,22 @@
6
6
 
7
7
  <img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
8
8
 
9
- No API state should be too difficult to test.
9
+ *No API state should be too difficult to test*
10
10
 
11
11
  ## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog) | [Skills](skills/mockaton/SKILL.md)
12
12
 
13
13
 
14
- Simulate API states that are normally ignored. For example,
15
- click the 500 button to trigger an error on an endpoint. Then
16
- unclick it to test your retry logic. Similarly, click the clock
17
- button 🕓 to delay a response so you can test spinners &mdash;
18
- youve seen them in production at the top left, or restarting
14
+ Simulate API states that are normally ignored. For example, quickly trigger an error on
15
+ an endpoint by clicking the 500 button. Then, unclick it to test your retry logic.
16
+ Similarly, pick a mock variant from the dropdown, say to respond with a 423 (locked account).
17
+ As another example, click the clock button 🕓 to delay a response so you can test
18
+ spinners &mdash; youve seen them in production at the top left, or restart
19
19
  their animation midway.
20
20
 
21
21
 
22
22
  ## Dashboard
23
- Besides the dashboard UI, there’s [an API](https://mockaton.com/api)
24
- you can use to set up tests.
23
+ Besides the dashboard UI, there’s a [programmatic API](https://mockaton.com/api),
24
+ which is handy for setting up tests.
25
25
 
26
26
  <picture>
27
27
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.light.gold.png">
@@ -78,26 +78,27 @@ code variants.
78
78
  | -----| -----| ---|
79
79
  | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
80
80
  | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
81
- | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. |
82
- | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins. |
81
+ | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. You can add many comments, `foo(c0)(c1).png` |
82
+ | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment. Otherwise, the first mock variant in alphabetical order wins. |
83
83
  | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
84
84
 
85
85
  <!-- SKILLS_IGNORE_BEGIN -->
86
86
  ## How to scrape your backend APIs?
87
- Mockaton has a [Browser Extension](https://mockaton.com/scraping) that lets
87
+ There’s a sister [Browser Extension](https://mockaton.com/scraping) that lets
88
88
  you download in bulk all your API responses following Mockaton's filename convention.
89
89
  <!-- SKILLS_IGNORE_END -->
90
90
 
91
91
 
92
92
  ## How to create mocks?
93
- Write it to your mocks directory. `.ts` files are served as JSON by default.
93
+ Write it to your mocks directory. TypeScript files are sent as JSON by default.
94
94
  ```sh
95
95
  mkdir -p my-mocks-dir/api
96
96
  echo "export default { name: 'John' }" > my-mocks-dir/api/user.GET.200.ts
97
97
  ```
98
+ Alternatively, there’s a [write-mock API](https://mockaton.com/api).
98
99
 
99
100
  ### Example A: JSON
100
- For JSON responses, use TypeScript (or JS), and `export default` an Object, Array, or
101
+ For JSON responses, you can use TypeScript (or JS) and `export default` an Object, Array, or
101
102
  String.
102
103
 
103
104
  - **Route:** /api/company/123
package/index.d.ts CHANGED
@@ -46,7 +46,7 @@ export declare interface Config {
46
46
  onReady?: (address: string) => void
47
47
 
48
48
  hotReload?: boolean // For UI dev purposes only
49
- bypassImportCache?: boolean
49
+ bypassImportCache?: boolean
50
50
  }
51
51
 
52
52
 
@@ -66,39 +66,40 @@ export declare function parseJSON(request: IncomingMessage): Promise<any>
66
66
  export declare function parseSegments(reqUrl: string, filename: string): Record<string, string>
67
67
  export declare function parseQueryParams(reqUrl: string): URLSearchParams
68
68
 
69
- export declare type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
70
-
71
-
72
- // API
73
-
74
- export declare type ClientMockBroker = {
75
- mocks: string[]
76
- file: string
77
- status: number
78
- isStatic: boolean
79
- autoStatus: number
80
- delayed: boolean
81
- proxied: boolean
82
- }
83
- export declare type ClientBrokersByMethod = {
84
- [method: string]: {
85
- [urlMask: string]: ClientMockBroker
69
+ declare global {
70
+ type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
71
+
72
+ type ClientMockBroker = {
73
+ mocks: string[]
74
+ file: string
75
+ status: number
76
+ isStatic: boolean
77
+ autoStatus: number
78
+ delayed: boolean
79
+ proxied: boolean
80
+ }
81
+ type ClientBrokersByMethod = {
82
+ [method: string]: {
83
+ [urlMask: string]: ClientMockBroker
84
+ }
86
85
  }
87
- }
88
86
 
89
- export declare interface State {
90
- brokersByMethod: ClientBrokersByMethod
87
+ interface State {
88
+ brokersByMethod: ClientBrokersByMethod
91
89
 
92
- cookies: [label: string, selected: boolean][]
93
- comments: string[]
90
+ cookies: [label: string, selected: boolean][]
91
+ comments: string[]
94
92
 
95
- delay: number
96
- delayJitter: number
93
+ delay: number
94
+ delayJitter: number
97
95
 
98
- collectProxied: boolean
99
- proxyFallback: string
96
+ collectProxied: boolean
97
+ proxyFallback: string
100
98
 
101
- readOnly: boolean
99
+ readOnly: boolean
102
100
 
103
- corsAllowed?: boolean
101
+ corsAllowed?: boolean
102
+ }
104
103
  }
104
+
105
+ export type { JsonPromise, ClientMockBroker, ClientBrokersByMethod, State }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.12.0",
5
+ "version": "13.14.0",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -29,22 +29,23 @@ code variants.
29
29
  | -----| -----| ---|
30
30
  | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
31
31
  | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
32
- | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. |
33
- | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins. |
32
+ | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. You can add many comments, `foo(c0)(c1).png` |
33
+ | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment. Otherwise, the first mock variant in alphabetical order wins. |
34
34
  | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
35
35
 
36
36
 
37
37
 
38
38
 
39
39
  ## How to create mocks?
40
- Write it to your mocks directory. `.ts` files are served as JSON by default.
40
+ Write it to your mocks directory. TypeScript files are sent as JSON by default.
41
41
  ```sh
42
42
  mkdir -p my-mocks-dir/api
43
43
  echo "export default { name: 'John' }" > my-mocks-dir/api/user.GET.200.ts
44
44
  ```
45
+ Alternatively, there’s a [write-mock API](https://mockaton.com/api).
45
46
 
46
47
  ### Example A: JSON
47
- For JSON responses, use TypeScript (or JS), and `export default` an Object, Array, or
48
+ For JSON responses, you can use TypeScript (or JS) and `export default` an Object, Array, or
48
49
  String.
49
50
 
50
51
  - **Route:** /api/company/123
@@ -36,7 +36,7 @@ export class Commander {
36
36
  setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
37
37
 
38
38
 
39
- /** @returns {JsonPromise<State.cookies>} */
39
+ /** @returns {JsonPromise<State['cookies']>} */
40
40
  selectCookie = label => this.#patch(API.cookies, label)
41
41
 
42
42
  setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
@@ -11,13 +11,12 @@ const METHODS = [ // @KeepSync node:http.METHODS
11
11
 
12
12
  const reComments = /\([^()]*\)/g // Anything within parentheses
13
13
 
14
- export function extractComments(file) {
15
- return Array.from(file.matchAll(reComments), ([c]) => c)
16
- }
14
+ export const extractComments = file =>
15
+ Array.from(file.matchAll(reComments), ([c]) => c)
16
+
17
+ export const includesComment = (file, search) =>
18
+ extractComments(file).some(c => c.includes(search))
17
19
 
18
- export function includesComment(file, search) {
19
- return extractComments(file).some(c => c.includes(search))
20
- }
21
20
 
22
21
  export function parseFilename(file) {
23
22
  const tokens = file.replace(reComments, '').split('.')
@@ -62,6 +61,6 @@ export function makeMockFilename(url, method, status, ext, comment = '') {
62
61
  }
63
62
 
64
63
  function replaceIds(filename) {
65
- return filename.replaceAll(replaceIds.reUuidV4, '[id]')
64
+ 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
65
+ return filename.replaceAll(reUuidV4, '[id]')
66
66
  }
67
- replaceIds.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
@@ -142,7 +142,7 @@
142
142
  &.GlobalDelay,
143
143
  &.GlobalDelayJitter {
144
144
  input {
145
- &&::selection {
145
+ &::selection {
146
146
  background-color: transparent;
147
147
  }
148
148
 
package/src/server/Api.js CHANGED
@@ -300,12 +300,17 @@ async function deleteMock(req, response) {
300
300
  const file = await req.json()
301
301
  const path = await resolveIn(config.mocksDir, file)
302
302
 
303
- if (!path)
303
+ if (!path) {
304
304
  response.forbidden('Filename path resolves outside config.mocksDir')
305
- else if (!isFile(path))
305
+ return
306
+ }
307
+
308
+ if (!isFile(path)) {
306
309
  response.unprocessable(`Missing Mock: ${file}`)
307
- else
308
- await rm(path)
310
+ return
311
+ }
312
+
313
+ await rm(path)
309
314
 
310
315
  if (!config.watcherEnabled) {
311
316
  brokers.unregisterMock(file)
@@ -1,4 +1,4 @@
1
- import { register } from 'node:module'
1
+ import { registerHooks } from 'node:module'
2
2
  import { createServer } from 'node:http'
3
3
 
4
4
  import pkgJSON from '../../package.json' with { type: 'json' }
@@ -18,6 +18,9 @@ import { cookie } from './stores/cookies.js'
18
18
  import { config, initConfig } from './stores/config.js'
19
19
  import { watchMocksDir } from './stores/Watcher.js'
20
20
 
21
+ import { resolveExtensionless } from './resolveExtensionless.js'
22
+ import { resolveBypassImportCache } from './resolveBypassImportCache.js'
23
+
21
24
 
22
25
  export function Mockaton(options) {
23
26
  return new Promise((resolve, reject) => {
@@ -25,10 +28,10 @@ export function Mockaton(options) {
25
28
  cookie.init(config.cookies)
26
29
  brokers.init()
27
30
 
28
- register('./ResolverResolveExtensionless.js', import.meta.url)
31
+ registerHooks({ resolve: resolveExtensionless })
29
32
 
30
33
  if (config.bypassImportCache)
31
- register('./ResolverBypassImportCache.js', import.meta.url)
34
+ registerHooks({ resolve: resolveBypassImportCache })
32
35
 
33
36
  if (config.watcherEnabled)
34
37
  watchMocksDir()
@@ -36,7 +36,7 @@ const proc = spawn(join(import.meta.dirname, 'cli.js'), [
36
36
  mocksDir.value,
37
37
  '--config', join(import.meta.dirname, 'Mockaton.test.config.js'),
38
38
  '--no-open'
39
- ])
39
+ ], { env: { ...process.env, FORCE_COLOR: 0, NO_COLOR: 1 } })
40
40
 
41
41
  const DEBUG = false
42
42
  proc.stdout.on('data', data => {
@@ -1178,6 +1178,7 @@ describe('Registering Mocks', () => {
1178
1178
  before(async () => {
1179
1179
  await mocksDir.mkdir('reg0')
1180
1180
  await mocksDir.write(fx0.file, fx0.body)
1181
+ await sleep(config.watcherDebounceMs + 50)
1181
1182
  version = await resolveOnNextSyncVersion(-1)
1182
1183
  })
1183
1184
 
@@ -1217,41 +1218,28 @@ describe('Registering Mocks', () => {
1217
1218
  })
1218
1219
 
1219
1220
 
1220
- /** In Node, there's no EventSource, so we work around it like this.
1221
- * This is for listening to real-time updates. It responds when a new mock is added, deleted, or renamed. */
1222
- async function resolveOnNextSyncVersion(currSyncVer = undefined) {
1221
+ /** Uses the native `EventSource` to listen for real-time sync-version updates.
1222
+ * It resolves when a new version is pushed that differs from `currSyncVer`;
1223
+ * when `currSyncVer` is omitted, the first push after the initial connection is
1224
+ * used. */
1225
+ function resolveOnNextSyncVersion(currSyncVer = undefined) {
1223
1226
  let skipFirst = currSyncVer === undefined
1224
- const reader = (await api.getSyncVersion())
1225
- .body.pipeThrough(new TextDecoderStream())
1226
- .getReader()
1227
- let buffer = ''
1228
-
1229
- try {
1230
- while (true) {
1231
- try {
1232
- const { done, value } = await reader.read()
1233
- if (done) break
1234
- buffer += value
1227
+ return new Promise((resolve, reject) => {
1228
+ const es = new EventSource(api.addr + API.syncVersion)
1229
+ es.onmessage = event => {
1230
+ const v = Number(event.data)
1231
+ if (skipFirst || v === currSyncVer)
1232
+ skipFirst = false
1233
+ else {
1234
+ es.close()
1235
+ resolve(v)
1235
1236
  }
1236
- catch {
1237
- break
1237
+ }
1238
+ es.onerror = () => {
1239
+ if (es.readyState === EventSource.CLOSED) {
1240
+ es.close()
1241
+ reject(new Error('sync-version stream closed'))
1238
1242
  }
1239
- const parts = buffer.split('\n\n')
1240
- buffer = parts.pop() || ''
1241
-
1242
- for (const event of parts)
1243
- for (const line of event.split(/\r?\n/))
1244
- if (line.startsWith('data:')) {
1245
- const v = Number(line.slice(5).trim())
1246
- if (skipFirst || v === currSyncVer)
1247
- skipFirst = false
1248
- else
1249
- return v
1250
- }
1251
1243
  }
1252
- }
1253
- finally {
1254
- reader.cancel().catch(() => {})
1255
- }
1244
+ })
1256
1245
  }
1257
-
@@ -3,9 +3,9 @@ import { resolve as _resolve } from 'node:path'
3
3
  const mockatonSrcRoot = `file://${_resolve(import.meta.dirname, '..')}`
4
4
 
5
5
  // We register this hook at runtime so it doesn’t interfere with non-dynamic imports.
6
- // Cache bust by appending timestamp query param
7
- export async function resolve(specifier, context, nextResolve) {
8
- const result = await nextResolve(specifier, context)
6
+ // It cache-busts by appending timestamp query param
7
+ export function resolveBypassImportCache(specifier, context, nextResolve) {
8
+ const result = nextResolve(specifier, context)
9
9
  if (result.url?.startsWith('file://') && !result.url.startsWith(mockatonSrcRoot)) {
10
10
  const url = new URL(result.url)
11
11
  url.searchParams.set('t', performance.now())
@@ -3,9 +3,9 @@ import { join, dirname } from 'node:path'
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url'
4
4
 
5
5
 
6
- export async function resolve(specifier, context, nextResolve) {
6
+ export function resolveExtensionless(specifier, context, nextResolve) {
7
7
  try {
8
- return await nextResolve(specifier, context)
8
+ return nextResolve(specifier, context)
9
9
  }
10
10
  catch (error) {
11
11
  // Attempt to resolve imports as .ts and .js
@@ -13,7 +13,7 @@ export async function resolve(specifier, context, nextResolve) {
13
13
  const absPath = join(dirname(fileURLToPath(context.parentURL)), specifier)
14
14
  for (const candidate of ['.ts', '.js'].map(ext => absPath + ext))
15
15
  if (existsSync(candidate))
16
- return resolve(pathToFileURL(candidate).href, context, nextResolve)
16
+ return resolveExtensionless(pathToFileURL(candidate).href, context, nextResolve)
17
17
  }
18
18
  throw error
19
19
  }