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 +14 -13
- package/index.d.ts +30 -29
- package/package.json +1 -1
- package/skills/mockaton/SKILL.md +5 -4
- package/src/client/ApiCommander.js +1 -1
- package/src/client/Filename.js +7 -8
- package/src/client/Header.css +1 -1
- package/src/server/Api.js +9 -4
- package/src/server/Mockaton.js +6 -3
- package/src/server/Mockaton.test.js +22 -34
- package/src/server/{ResolverBypassImportCache.js → resolveBypassImportCache.js} +3 -3
- package/src/server/{ResolverResolveExtensionless.js → resolveExtensionless.js} +3 -3
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
|
-
|
|
16
|
-
|
|
17
|
-
button 🕓 to delay a response so you can test
|
|
18
|
-
you
|
|
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 — you’ve 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 [
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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)
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
87
|
+
interface State {
|
|
88
|
+
brokersByMethod: ClientBrokersByMethod
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
cookies: [label: string, selected: boolean][]
|
|
91
|
+
comments: string[]
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
delay: number
|
|
94
|
+
delayJitter: number
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
collectProxied: boolean
|
|
97
|
+
proxyFallback: string
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
readOnly: boolean
|
|
102
100
|
|
|
103
|
-
|
|
101
|
+
corsAllowed?: boolean
|
|
102
|
+
}
|
|
104
103
|
}
|
|
104
|
+
|
|
105
|
+
export type { JsonPromise, ClientMockBroker, ClientBrokersByMethod, State }
|
package/package.json
CHANGED
package/skills/mockaton/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
|
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)
|
|
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
|
|
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)
|
package/src/client/Filename.js
CHANGED
|
@@ -11,13 +11,12 @@ const METHODS = [ // @KeepSync node:http.METHODS
|
|
|
11
11
|
|
|
12
12
|
const reComments = /\([^()]*\)/g // Anything within parentheses
|
|
13
13
|
|
|
14
|
-
export
|
|
15
|
-
|
|
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
|
-
|
|
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
|
package/src/client/Header.css
CHANGED
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
|
-
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!isFile(path)) {
|
|
306
309
|
response.unprocessable(`Missing Mock: ${file}`)
|
|
307
|
-
|
|
308
|
-
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await rm(path)
|
|
309
314
|
|
|
310
315
|
if (!config.watcherEnabled) {
|
|
311
316
|
brokers.unregisterMock(file)
|
package/src/server/Mockaton.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
31
|
+
registerHooks({ resolve: resolveExtensionless })
|
|
29
32
|
|
|
30
33
|
if (config.bypassImportCache)
|
|
31
|
-
|
|
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
|
-
/**
|
|
1221
|
-
*
|
|
1222
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
.
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
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
|
-
//
|
|
7
|
-
export
|
|
8
|
-
const result =
|
|
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
|
|
6
|
+
export function resolveExtensionless(specifier, context, nextResolve) {
|
|
7
7
|
try {
|
|
8
|
-
return
|
|
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
|
|
16
|
+
return resolveExtensionless(pathToFileURL(candidate).href, context, nextResolve)
|
|
17
17
|
}
|
|
18
18
|
throw error
|
|
19
19
|
}
|