mockaton 13.6.0 → 13.6.3

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
@@ -1,9 +1,10 @@
1
+ <!-- SKILLS_IGNORE_BEGIN -->
1
2
  ![NPM Version](https://img.shields.io/npm/v/mockaton)
2
3
  [![Test](https://github.com/ericfortis/mockaton/actions/workflows/test.yml/badge.svg)](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
3
4
  [![codecov](https://codecov.io/github/ericfortis/mockaton/graph/badge.svg?token=90NYLMMG1J)](https://codecov.io/github/ericfortis/mockaton)
4
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
6
 
6
- ## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog)
7
+ ## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog) | [Skills ↗](https://mockaton.com/assets/SKILLS.md)
7
8
 
8
9
  Mockaton is an HTTP mock server for simulating APIs, designed
9
10
  for testing difficult to reproduce backend states with minimal setup.
@@ -29,41 +30,41 @@ Test it:
29
30
  curl localhost:2020/api/user
30
31
  ```
31
32
  Dashboard: [localhost:2020/mockaton](http://localhost:2020/mockaton)
32
-
33
+ <!-- SKILLS_IGNORE_END -->
33
34
 
34
35
  ## Basic Usage
35
36
  ```sh
36
37
  npx mockaton my-mocks-dir/
37
38
  ```
38
39
 
39
- Mockaton will serve the files on the given directory. Its a file-system
40
+ Mockaton will serve the files on the given directory. It's a file-system
40
41
  based router, so filenames can have dynamic parameters and comments.
41
42
  Also, each route can have different mock file variants.
42
43
 
43
44
 
44
45
  | Route | Filename | Description |
45
46
  | -----| -----| ---|
46
- | /api/company/123 | api/company/[id].GET.200.json | `[id]` is a dynamic parameter |
47
- | /media/avatar.png | media/avatar.png | Statics assets dont need the above extension |
48
- | /api/login | api/login(invalid attempt).POST.401.json | Anything within parenthesis is a **comment**, they are ignored when routing |
49
- | /api/login | api/login(default).GET.200.json | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
50
- | /api/login | api/login(locked out user).POST.423.ts | TypeScript or JavaScript mocks are sent as JSON by default |
47
+ | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
48
+ | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
49
+ | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a **comment**, they are ignored when routing |
50
+ | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
51
+ | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
51
52
 
52
53
 
53
54
  ## Docs
54
55
  - How to **configure** Mockaton? See [CLI and mockaton.config.js](https://mockaton.com/config) docs.
55
- - How to **control** Mockaton? Besides the dashboard, theres a [Programmatic API](https://mockaton.com/api).
56
+ - How to **control** Mockaton? Besides the dashboard, there's a [Programmatic API](https://mockaton.com/api).
56
57
  - How to **add plugins**? You can write [Plugins](https://mockaton.com/plugins) for customizing responses.
57
58
 
58
-
59
+ <!-- SKILLS_IGNORE_BEGIN -->
59
60
  ## How to scrape your backend APIs?
60
61
  Mockaton has a [Browser Extension](https://mockaton.com/scraping) that lets
61
- you download in bulk all your API responses following Mockatons filename convention.
62
-
62
+ you download in bulk all your API responses following Mockaton's filename convention.
63
+ <!-- SKILLS_IGNORE_END -->
63
64
 
64
65
  ## How to create mocks?
65
66
 
66
- Write to your mocks directory. Alternatively, theres an API [PATCH /mockaton/write-mock](https://mockaton.com/api).
67
+ Write to your mocks directory. Alternatively, there's an API [PATCH /mockaton/write-mock](https://mockaton.com/api).
67
68
  ```sh
68
69
  mkdir -p my-mocks-dir/api
69
70
  echo '{ "name": "John" }' > my-mocks-dir/api/user.GET.200.json
@@ -71,27 +72,29 @@ sleep 0.1 # Wait for the watcher to register it
71
72
  ```
72
73
 
73
74
  ### Example A: JSON
74
- - **Route:** /api/company/123
75
- - **Filename:** api/company/[id].GET.200.json
76
-
77
- ```json
78
- {
79
- "name": "Acme, Inc."
80
- }
81
- ```
75
+ For JSON responses, use TypeScript (or JavaScript), and export an Object, Array, or String.
82
76
 
83
- ### Example B: TypeScript or JavaScript
84
- Exporting an Object, Array, or String is sent as JSON.
85
-
86
- - **Route:** /api/company/abc
77
+ - **Route:** /api/company/123
87
78
  - **Filename:** api/company/[id].GET.200.ts
88
79
 
89
80
  ```ts
90
81
  export default {
82
+ id: 123,
91
83
  name: 'Acme, Inc.'
92
84
  }
93
85
  ```
94
86
 
87
+ ### Example B: Non-JSON
88
+ - **Route:** /api/company/123
89
+ - **Filename:** api/company/[id].GET.200.xml
90
+
91
+ ```xml
92
+ <company>
93
+ <id>123</id>
94
+ <name>Acme, Inc.</name>
95
+ </company>
96
+ ```
97
+
95
98
  ### Example C: [Function Mocks](https://mockaton.com/function-mocks)
96
99
  With a function mock you can do pretty much anything you could do with a normal backend handler.</p>
97
100
  For example, you can handle complex logic, URL parsing, saving toa database, etc.
package/package.json CHANGED
@@ -2,19 +2,21 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.6.0",
5
+ "version": "13.6.3",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
9
9
  "types": "./index.d.ts"
10
10
  },
11
- "./openapi.json": "./www/src/assets/openapi.json"
11
+ "./openapi.json": "./www/src/assets/openapi.json",
12
+ "./SKILLS.md": "./www/src/assets/SKILLS.md"
12
13
  },
13
14
  "files": [
14
15
  "src",
15
16
  "index.js",
16
17
  "index.d.ts",
17
- "www/src/assets/openapi.json"
18
+ "www/src/assets/openapi.json",
19
+ "www/src/assets/SKILLS.md"
18
20
  ],
19
21
  "license": "MIT",
20
22
  "homepage": "https://mockaton.com",
@@ -45,6 +45,7 @@ function GlobalDelayField() {
45
45
  r('input', {
46
46
  type: 'number',
47
47
  min: 0,
48
+ max: 120_000,
48
49
  step: 100,
49
50
  autocomplete: 'none',
50
51
  value: store.delay,
package/src/server/Api.js CHANGED
@@ -110,9 +110,9 @@ function reset(_, response) {
110
110
 
111
111
  async function setCorsAllowed(req, response) {
112
112
  const corsAllowed = await req.json()
113
-
114
- if (!ConfigValidator.corsAllowed(corsAllowed))
115
- response.unprocessable(`Expected boolean for "corsAllowed"`)
113
+ const err = ConfigValidator.corsAllowed(corsAllowed)
114
+ if (err)
115
+ response.unprocessable(err)
116
116
  else {
117
117
  config.corsAllowed = corsAllowed
118
118
  response.ok()
@@ -123,9 +123,9 @@ async function setCorsAllowed(req, response) {
123
123
 
124
124
  async function setGlobalDelay(req, response) {
125
125
  const delay = await req.json()
126
-
127
- if (!ConfigValidator.delay(delay))
128
- response.unprocessable(`Expected non-negative integer for "delay"`)
126
+ const err = ConfigValidator.delay(delay)
127
+ if (err)
128
+ response.unprocessable(err)
129
129
  else {
130
130
  config.delay = delay
131
131
  response.ok()
@@ -135,9 +135,9 @@ async function setGlobalDelay(req, response) {
135
135
 
136
136
  async function setGlobalDelayJitter(req, response) {
137
137
  const jitter = await req.json()
138
-
139
- if (!ConfigValidator.delayJitter(jitter))
140
- response.unprocessable(`Expected 0 to 3 float for "delayJitter"`)
138
+ const err = ConfigValidator.delayJitter(jitter)
139
+ if (err)
140
+ response.unprocessable(err)
141
141
  else {
142
142
  config.delayJitter = jitter
143
143
  response.ok()
@@ -148,7 +148,6 @@ async function setGlobalDelayJitter(req, response) {
148
148
 
149
149
  async function selectCookie(req, response) {
150
150
  const cookieKey = await req.json()
151
-
152
151
  const error = cookie.setCurrent(cookieKey)
153
152
  if (error)
154
153
  response.unprocessable(error?.message || error)
@@ -161,9 +160,9 @@ async function selectCookie(req, response) {
161
160
 
162
161
  async function setProxyFallback(req, response) {
163
162
  const fallback = await req.json()
164
-
165
- if (!ConfigValidator.proxyFallback(fallback))
166
- response.unprocessable(`Invalid Proxy Fallback URL`)
163
+ const err = ConfigValidator.proxyFallback(fallback)
164
+ if (err)
165
+ response.unprocessable(err)
167
166
  else {
168
167
  config.proxyFallback = fallback
169
168
  response.ok()
@@ -173,9 +172,9 @@ async function setProxyFallback(req, response) {
173
172
 
174
173
  async function setCollectProxied(req, response) {
175
174
  const collectProxied = await req.json()
176
-
177
- if (!ConfigValidator.collectProxied(collectProxied))
178
- response.unprocessable(`Expected a boolean for "collectProxied"`)
175
+ const err = ConfigValidator.collectProxied(collectProxied)
176
+ if (err)
177
+ response.unprocessable(err)
179
178
  else {
180
179
  config.collectProxied = collectProxied
181
180
  response.ok()
@@ -263,7 +262,7 @@ async function writeMock(req, response) {
263
262
  const [file, content] = await req.json()
264
263
  if (typeof file !== 'string')
265
264
  return response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
266
-
265
+
267
266
  const path = await resolveIn(config.mocksDir, file)
268
267
  if (!path)
269
268
  return response.forbidden('Filename path resolves outside config.mocksDir')
@@ -14,7 +14,7 @@ export class MockBroker {
14
14
  proxied = false
15
15
  status = -1
16
16
  autoStatus = 0
17
-
17
+
18
18
  constructor(file) {
19
19
  this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
20
20
  this.register(file)
@@ -6,7 +6,7 @@ export default {
6
6
  userB: jwtCookie('CookieB', { email: 'john@example.test' }),
7
7
  },
8
8
  extraHeaders: ['custom_header_name', 'custom_header_val'],
9
- extraMimes: { ['custom_extension']: 'custom_mime' },
9
+ extraMimes: { 'custom_extension': 'custom_mime' },
10
10
  logLevel: 'verbose',
11
11
  corsOrigins: ['https://example.test'],
12
12
  corsExposedHeaders: ['Content-Encoding'],
@@ -166,7 +166,7 @@ describe('CORS', () => {
166
166
  test('422 for non boolean', async () => {
167
167
  const r = await api.setCorsAllowed('not-a-boolean')
168
168
  equal(r.status, 422)
169
- equal(await r.text(), 'Expected boolean for "corsAllowed"')
169
+ equal(await r.text(), 'Expected Boolean')
170
170
  })
171
171
 
172
172
  test('200', async () => {
@@ -271,7 +271,7 @@ describe('Delay', () => {
271
271
  test('422 for invalid value', async () => {
272
272
  const r = await api.setGlobalDelay('not-a-number')
273
273
  equal(r.status, 422)
274
- equal(await r.text(), 'Expected non-negative integer for "delay"')
274
+ equal(await r.text(), 'Expected an integer between 0 and 120000')
275
275
  })
276
276
  test('200 for valid global delay value', async () => {
277
277
  const r = await api.setGlobalDelay(150)
@@ -284,7 +284,7 @@ describe('Delay', () => {
284
284
  test('422 for invalid value', async () => {
285
285
  const r = await api.setGlobalDelayJitter('not-a-number')
286
286
  equal(r.status, 422)
287
- equal(await r.text(), 'Expected 0 to 3 float for "delayJitter"')
287
+ equal(await r.text(), 'Expected a float between 0 and 3')
288
288
  })
289
289
  test('200 for valid value', async () => {
290
290
  const r = await api.setGlobalDelayJitter(0.1)
@@ -391,7 +391,7 @@ describe('Proxy Fallback', () => {
391
391
  test('422 when value is not a valid URL', async () => {
392
392
  const r = await api.setProxyFallback('bad url')
393
393
  equal(r.status, 422)
394
- equal(await r.text(), 'Invalid Proxy Fallback URL')
394
+ equal(await r.text(), 'Expected an empty String or URL')
395
395
  })
396
396
 
397
397
  test('sets fallback', async () => {
@@ -411,7 +411,7 @@ describe('Proxy Fallback', () => {
411
411
  test('422 for invalid collectProxied value', async () => {
412
412
  const r = await api.setCollectProxied('not-a-boolean')
413
413
  equal(r.status, 422)
414
- equal(await r.text(), 'Expected a boolean for "collectProxied"')
414
+ equal(await r.text(), 'Expected Boolean')
415
415
  })
416
416
 
417
417
  test('200 set and unset', async () => {
@@ -2,7 +2,7 @@ import { join } from 'node:path'
2
2
  import { randomUUID } from 'node:crypto'
3
3
 
4
4
  import { extFor } from './utils/mime.js'
5
- import { write, isFile } from './utils/fs.js'
5
+ import { write, isFile, resolveIn } from './utils/fs.js'
6
6
  import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
7
7
 
8
8
  import { config } from './config.js'
@@ -41,6 +41,10 @@ export async function proxy(req, response, delay) {
41
41
  setTimeout(() => response.end(body), delay) // TESTME
42
42
 
43
43
  if (config.collectProxied) {
44
+ if (config.readOnly) {
45
+ logger.info('Write denied: config.readOnly is true')
46
+ return
47
+ }
44
48
  const mime = proxyResponse.headers.get('content-type')
45
49
  const ext = mime
46
50
  ? extFor(mime) || EXT_UNKNOWN_MIME
@@ -59,10 +63,13 @@ async function saveMockToDisk(url, method, status, ext, body) {
59
63
  }
60
64
 
61
65
  try {
62
- await write(makeUniqueMockFilename(url, method, status, ext), body)
66
+ const f = makeUniqueMockFilename(url, method, status, ext)
67
+ if (!resolveIn(config.mocksDir, f))
68
+ throw 'Attempted write outside config.mocksDir'
69
+ await write(f, body)
63
70
  }
64
71
  catch (err) {
65
- logger.warn('Write access denied', err)
72
+ logger.warn('Write denied', err)
66
73
  }
67
74
  }
68
75
 
package/src/server/cli.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { resolve } from 'node:path'
3
+ import { resolve, join } from 'node:path'
4
4
  import { parseArgs } from 'node:util'
5
5
 
6
6
  import { isFile } from './utils/fs.js'
7
7
  import { Mockaton } from '../../index.js'
8
-
9
8
  import pkgJSON from '../../package.json' with { type: 'json' }
10
9
 
10
+ const rel = f => join(import.meta.dirname, f)
11
11
 
12
12
  process.on('unhandledRejection', error => { throw error })
13
13
 
@@ -25,6 +25,7 @@ try {
25
25
  'no-read-only': { type: 'boolean' },
26
26
 
27
27
  help: { short: 'h', type: 'boolean' },
28
+ skills: { type: 'boolean' },
28
29
  version: { short: 'v', type: 'boolean' }
29
30
  },
30
31
  allowPositionals: true
@@ -44,6 +45,9 @@ process.on('SIGUSR2', () => process.exit(0))
44
45
  if (args.version)
45
46
  console.log(pkgJSON.version)
46
47
 
48
+ if (args.skills)
49
+ console.log(rel('../../www/src/assets/SKILLS.md'))
50
+
47
51
  else if (args.help)
48
52
  console.log(`
49
53
  Usage: mockaton [mocks-dir] [options]
@@ -58,6 +62,7 @@ Options:
58
62
  --no-open Don't open dashboard in a browser
59
63
  --no-read-only Allow writing and deleting mocks via API
60
64
 
65
+ --skills Show AI agent SKILLS.md file path
61
66
  -h, --help
62
67
  -v, --version
63
68
 
@@ -26,7 +26,7 @@ describe('CLI', () => {
26
26
  const { stderr, status } = cli(
27
27
  rel('../../mockaton-mocks'),
28
28
  '--port', 'not-a-number')
29
- equal(stderr.trim(), `port="not-a-number" is invalid`)
29
+ equal(stderr.trim(), `port="not-a-number"\nExpected an integer between 0 and 65535`)
30
30
  equal(status, 1)
31
31
  })
32
32
 
@@ -1,57 +1,56 @@
1
1
  import { resolve } from 'node:path'
2
+ import { lstatSync } from 'node:fs'
2
3
  import { METHODS } from 'node:http'
3
4
 
4
5
  import { logger } from './utils/logger.js'
5
- import { isDirectory } from './utils/fs.js'
6
6
  import { registerMimes } from './utils/mime.js'
7
7
  import { openInBrowser } from './utils/openInBrowser.js'
8
- import { optional, is, validate } from './utils/validate.js'
8
+ import { is, validate, isInt, isFloat, isOneOf, optionalURL } from './utils/validate.js'
9
9
  import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
10
10
 
11
11
  import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
12
12
 
13
-
14
13
  /** @type {{
15
14
  * [K in keyof Config]-?: [
16
15
  * defaultVal: Config[K],
17
- * validator: (val: unknown) => boolean
16
+ * validator: (val: unknown) => err:string
18
17
  * ]
19
18
  * }} */
20
19
  const schema = {
21
- mocksDir: [resolve('mockaton-mocks'), isDirectory],
20
+ mocksDir: [resolve('mockaton-mocks'), p => !lstatSync(p).isDirectory()],
22
21
  ignore: [/(\.DS_Store|~)$/, is(RegExp)],
23
22
  readOnly: [true, is(Boolean)],
24
23
  watcherEnabled: [true, is(Boolean)],
25
- watcherDebounceMs: [80, ms => Number.isInteger(ms) && ms >= 0],
24
+ watcherDebounceMs: [80, isInt(0, 5000)],
26
25
 
27
26
  host: ['127.0.0.1', is(String)],
28
- port: [0, port => Number.isInteger(port) && port >= 0 && port < 2 ** 16], // 0 means auto-assigned
27
+ port: [0, isInt(0, 2 ** 16 - 1)], // 0 means auto-assigned
29
28
 
30
- logLevel: ['normal', val => ['normal', 'quiet', 'verbose'].includes(val)],
29
+ logLevel: ['normal', isOneOf('normal', 'quiet', 'verbose')],
31
30
 
32
- delay: [1200, ms => Number.isInteger(ms) && ms >= 0],
33
- delayJitter: [0, percent => percent >= 0 && percent <= 3],
31
+ delay: [1200, isInt(0, 120_000)],
32
+ delayJitter: [0, isFloat(0, 3)],
34
33
 
35
- proxyFallback: ['', optional(URL.canParse)], // e.g. http://localhost:9999
34
+ proxyFallback: ['', optionalURL], // e.g. http://localhost:9999
36
35
  collectProxied: [false, is(Boolean)],
37
36
  formatCollectedJSON: [true, is(Boolean)],
38
37
 
39
38
  cookies: [{}, is(Object)], // defaults to the first kv
40
- extraHeaders: [[], val => Array.isArray(val) && val.length % 2 === 0],
39
+ extraHeaders: [[], is(Array)],
41
40
  extraMimes: [{}, is(Object)],
42
41
 
43
42
  corsAllowed: [true, is(Boolean)],
44
43
  corsOrigins: [['*'], validateCorsAllowedOrigins],
45
44
  corsMethods: [METHODS, validateCorsAllowedMethods],
46
- corsHeaders: [['content-type', 'authorization'], Array.isArray],
47
- corsExposedHeaders: [[], Array.isArray],
45
+ corsHeaders: [['content-type', 'authorization'], is(Array)],
46
+ corsExposedHeaders: [[], is(Array)],
48
47
  corsCredentials: [true, is(Boolean)],
49
48
  corsMaxAge: [0, is(Number)],
50
49
 
51
50
  plugins: [
52
51
  [
53
52
  [/\.(js|ts)$/, jsToJsonPlugin]
54
- ], Array.isArray],
53
+ ], is(Array)],
55
54
 
56
55
  onReady: [await openInBrowser, is(Function)],
57
56
 
@@ -4,14 +4,20 @@ import { methodIsSupported } from './HttpIncomingMessage.js'
4
4
 
5
5
  export function validateCorsAllowedOrigins(arr) {
6
6
  if (!Array.isArray(arr))
7
- return false
7
+ return 'Expected Array'
8
8
  if (arr.length === 1 && arr[0] === '*')
9
- return true
9
+ return ''
10
10
  return arr.every(o => URL.canParse(o))
11
+ ? ''
12
+ : 'Expected URLs'
11
13
  }
12
14
 
13
15
  export function validateCorsAllowedMethods(arr) {
14
- return Array.isArray(arr) && arr.every(methodIsSupported)
16
+ if (!Array.isArray(arr))
17
+ return 'Expected Array'
18
+ return arr.every(methodIsSupported)
19
+ ? ''
20
+ : 'Unsupported Method'
15
21
  }
16
22
 
17
23
 
@@ -1,8 +1,41 @@
1
1
  export function validate(obj, shape) {
2
2
  for (const [field, value] of Object.entries(obj))
3
- if (!shape[field](value))
4
- throw new TypeError(`${field}=${JSON.stringify(value)} is invalid`)
3
+ try {
4
+ const err = shape[field](value)
5
+ if (err)
6
+ throw err
7
+ }
8
+ catch (err) {
9
+ throw new TypeError(`${field}=${JSON.stringify(value)}\n${err}`)
10
+ }
5
11
  }
6
12
 
7
- export const is = ctor => val => val.constructor === ctor
8
- export const optional = tester => val => !val || tester(val)
13
+ export function is(ctor) {
14
+ return val => val.constructor === ctor
15
+ ? ''
16
+ : `Expected ${ctor.prototype.constructor.name}`
17
+ }
18
+
19
+ export function isInt(min = Number.MAX_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
20
+ return v => Number.isInteger(v) && v >= min && v <= max
21
+ ? ''
22
+ : `Expected an integer between ${min} and ${max}`
23
+ }
24
+
25
+ export function isFloat(min = Number.MAX_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
26
+ return v => Number.isFinite(v) && v >= min && v <= max
27
+ ? ''
28
+ : `Expected a float between ${min} and ${max}`
29
+ }
30
+
31
+ export function isOneOf(...vals) {
32
+ return v => vals.includes(v)
33
+ ? ''
34
+ : `Expected one of: ${vals.join(', ')}`
35
+ }
36
+
37
+ export function optionalURL(v) {
38
+ return !v || URL.canParse(v)
39
+ ? ''
40
+ : 'Expected an empty String or URL'
41
+ }
@@ -1,33 +1,14 @@
1
1
  import { describe, test } from 'node:test'
2
2
  import { doesNotThrow, throws } from 'node:assert/strict'
3
- import { validate, is, optional } from './validate.js'
3
+ import { validate, is } from './validate.js'
4
4
 
5
5
 
6
6
  describe('validate', () => {
7
- describe('optional', () => {
8
- test('accepts undefined', () =>
9
- doesNotThrow(() =>
10
- validate({}, { foo: optional(Number.isInteger) })))
11
-
12
- test('accepts falsy value regardless of type', () =>
13
- doesNotThrow(() =>
14
- validate({ foo: 0 }, { foo: optional(Array.isArray) })))
15
-
16
- test('accepts when tester func returns truthy', () =>
17
- doesNotThrow(() =>
18
- validate({ foo: [] }, { foo: optional(Array.isArray) })))
19
-
20
- test('rejects when tester func returns falsy', () =>
21
- throws(() =>
22
- validate({ foo: 1 }, { foo: optional(Array.isArray) }),
23
- /foo=1 is invalid/))
24
- })
25
-
26
7
  describe('is', () => {
27
8
  test('rejects mismatched type', () =>
28
9
  throws(() =>
29
10
  validate({ foo: 1 }, { foo: is(String) }),
30
- /foo=1 is invalid/))
11
+ /foo=1\nExpected String/))
31
12
 
32
13
  test('accepts matched type', () =>
33
14
  doesNotThrow(() =>
@@ -37,16 +18,16 @@ describe('validate', () => {
37
18
  describe('custom tester func', () => {
38
19
  test('rejects mismatched type', () =>
39
20
  throws(() =>
40
- validate({ foo: 'not-a-number' }, { foo: n => n > 1 }),
41
- /foo="not-a-number" is invalid/))
21
+ validate({ foo: 'not-a-number' }, { foo: n => Number.isInteger(n) ? '' : 'Expected Integer' }),
22
+ /foo="not-a-number"\nExpected Integer/))
42
23
 
43
- test('rejects mismatched type', () =>
24
+ test('rejects mismatched value', () =>
44
25
  throws(() =>
45
- validate({ foo: 0 }, { foo: n => n > 1 }),
46
- /foo=0 is invalid/))
26
+ validate({ foo: 0 }, { foo: n => n === 1 ? '' : 'Expected 1' }),
27
+ /foo=0\nExpected 1/))
47
28
 
48
29
  test('accepts matched type', () =>
49
30
  doesNotThrow(() =>
50
- validate({ foo: 1 }, { foo: Number.isInteger })))
31
+ validate({ foo: 1 }, { foo: v => !Number.isInteger(v) })))
51
32
  })
52
33
  })
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: Mockaton
3
+ description: Generates and serves mock HTTP APIs from filesystem conventions. Use when creating, editing, or reasoning about mock endpoints.
4
+ ---
5
+
6
+ ## Basic Usage
7
+ ```sh
8
+ npx mockaton my-mocks-dir/
9
+ ```
10
+
11
+ Mockaton will serve the files on the given directory. It's a file-system
12
+ based router, so filenames can have dynamic parameters and comments.
13
+ Also, each route can have different mock file variants.
14
+
15
+
16
+ | Route | Filename | Description |
17
+ | -----| -----| ---|
18
+ | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
19
+ | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
20
+ | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a **comment**, they are ignored when routing |
21
+ | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
22
+ | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
23
+
24
+
25
+ ## Docs
26
+ - How to **configure** Mockaton? See [CLI and mockaton.config.js](https://mockaton.com/config) docs.
27
+ - How to **control** Mockaton? Besides the dashboard, there's a [Programmatic API](https://mockaton.com/api).
28
+ - How to **add plugins**? You can write [Plugins](https://mockaton.com/plugins) for customizing responses.
29
+
30
+
31
+
32
+ ## How to create mocks?
33
+
34
+ Write to your mocks directory. Alternatively, there's an API [PATCH /mockaton/write-mock](https://mockaton.com/api).
35
+ ```sh
36
+ mkdir -p my-mocks-dir/api
37
+ echo '{ "name": "John" }' > my-mocks-dir/api/user.GET.200.json
38
+ sleep 0.1 # Wait for the watcher to register it
39
+ ```
40
+
41
+ ### Example A: JSON
42
+ For JSON responses, use TypeScript (or JavaScript), and export an Object, Array, or String.
43
+
44
+ - **Route:** /api/company/123
45
+ - **Filename:** api/company/[id].GET.200.ts
46
+
47
+ ```ts
48
+ export default {
49
+ id: 123,
50
+ name: 'Acme, Inc.'
51
+ }
52
+ ```
53
+
54
+ ### Example B: Non-JSON
55
+ - **Route:** /api/company/123
56
+ - **Filename:** api/company/[id].GET.200.xml
57
+
58
+ ```xml
59
+ <company>
60
+ <id>123</id>
61
+ <name>Acme, Inc.</name>
62
+ </company>
63
+ ```
64
+
65
+ ### Example C: [Function Mocks](https://mockaton.com/function-mocks)
66
+ With a function mock you can do pretty much anything you could do with a normal backend handler.</p>
67
+ For example, you can handle complex logic, URL parsing, saving toa database, etc.
68
+
69
+ - **Route:** /api/company/abc/user/999
70
+ - **Filename:** api/company/[companyId]/user/[userId].GET.200.ts
71
+
72
+ ```ts
73
+ import { IncomingMessage, OutgoingMessage } from 'node:http'
74
+ import { parseSegments } from 'mockaton'
75
+
76
+ export default async function (req: IncomingMessage, response: OutgoingMessage) {
77
+ const { companyId, userId } = parseSegments(req.url, import.meta.filename)
78
+ const foo = await getFoo()
79
+ return JSON.stringify({
80
+ foo,
81
+ companyId,
82
+ userId,
83
+ name: 'Acme, Inc.'
84
+ })
85
+ }
86
+ ```