mockaton 13.2.0 → 13.3.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/package.json CHANGED
@@ -2,17 +2,19 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.2.0",
5
+ "version": "13.3.0",
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
12
  },
12
13
  "files": [
13
14
  "src",
14
15
  "index.js",
15
- "index.d.ts"
16
+ "index.d.ts",
17
+ "www/src/assets/openapi.json"
16
18
  ],
17
19
  "license": "MIT",
18
20
  "homepage": "https://mockaton.com",
@@ -42,7 +42,6 @@ export class Commander {
42
42
  /** @returns {JsonPromise<ClientMockBroker>} */
43
43
  toggleStatus = (method, urlMask, status) => this.#patch(API.toggleStatus, [method, urlMask, status])
44
44
 
45
- // TODO change Status or Toggle404?
46
45
 
47
46
  /** @returns {JsonPromise<ClientMockBroker>} */
48
47
  setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
@@ -132,7 +132,7 @@ async function updatePayloadViewer(proxied, file, response) {
132
132
  else if (['text/xml', 'application/xml'].includes(mime))
133
133
  codeRef.elem.replaceChildren(SyntaxXML(await bodyAsText()))
134
134
 
135
- else if (mime.startsWith('text/'))
135
+ else if (mime.startsWith('text/') || mime === 'application/yaml')
136
136
  codeRef.elem.textContent = await bodyAsText()
137
137
 
138
138
  else
@@ -33,6 +33,16 @@ export const store = {
33
33
  store.render()
34
34
  },
35
35
 
36
+ collapsedFolders: new Set(JSON.parse(globalThis.localStorage?.getItem('collapsedFolders') || '[]')),
37
+ setFolderCollapsed(folder, collapsed) {
38
+ if (collapsed)
39
+ store.collapsedFolders.add(folder)
40
+ else
41
+ store.collapsedFolders.delete(folder)
42
+
43
+ globalThis.localStorage?.setItem('collapsedFolders', JSON.stringify([...store.collapsedFolders]))
44
+ },
45
+
36
46
  chosenLink: { method: '', urlMask: '' },
37
47
  setChosenLink(method, urlMask) {
38
48
  store.chosenLink = { method, urlMask }
@@ -112,27 +122,40 @@ export const store = {
112
122
  },
113
123
 
114
124
 
125
+ _dittoCache: new Map(),
126
+
115
127
  brokerFor(method, urlMask) {
116
128
  return store.brokersByMethod[method]?.[urlMask]
117
129
  },
118
130
 
131
+ brokerAsRow(method, urlMask) {
132
+ const b = store.brokerFor(method, urlMask)
133
+ const r = new BrokerRowModel(b, store.canProxy)
134
+ r.setUrlMaskDittoed(store._dittoCache.get(r.key))
135
+ return r
136
+ },
137
+
119
138
  _setBroker(broker) {
120
139
  const { method, urlMask } = parseFilename(broker.file)
121
140
  store.brokersByMethod[method] ??= {}
122
141
  store.brokersByMethod[method][urlMask] = broker
123
142
  },
124
143
 
125
- _dittoCache: new Map(),
126
-
127
- _brokersAsArray(byMethod = '*') {
128
- const arr = []
129
- for (const [method, brokers] of Object.entries(store.brokersByMethod))
130
- if (byMethod === '*' || byMethod === method)
131
- arr.push(...Object.values(brokers))
132
- return arr
144
+ folderGroupsByMethod(method) {
145
+ const groups = []
146
+ let g = null
147
+ for (const row of store._brokersAsRowsByMethod(method)) {
148
+ const folder = row.urlMask.substring(0, row.urlMask.lastIndexOf('/') + 1)
149
+ if (!g || g.folder !== folder) {
150
+ g = { folder, children: [] }
151
+ groups.push(g)
152
+ }
153
+ g.children.push(row)
154
+ }
155
+ return groups
133
156
  },
134
157
 
135
- brokersAsRowsByMethod(method) {
158
+ _brokersAsRowsByMethod(method) {
136
159
  const rows = store._brokersAsArray(method)
137
160
  .map(b => new BrokerRowModel(b, store.canProxy))
138
161
  .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
@@ -145,12 +168,14 @@ export const store = {
145
168
  return rows
146
169
  },
147
170
 
148
- brokerAsRow(method, urlMask) {
149
- const b = store.brokerFor(method, urlMask)
150
- const r = new BrokerRowModel(b, store.canProxy)
151
- r.setUrlMaskDittoed(store._dittoCache.get(r.key))
152
- return r
171
+ _brokersAsArray(byMethod = '*') {
172
+ const arr = []
173
+ for (const [method, brokers] of Object.entries(store.brokersByMethod))
174
+ if (byMethod === '*' || byMethod === method)
175
+ arr.push(...Object.values(brokers))
176
+ return arr
153
177
  },
178
+
154
179
 
155
180
  previewLink(method, urlMask) {
156
181
  store.setChosenLink(method, urlMask)
@@ -423,7 +423,7 @@ main {
423
423
  padding-bottom: 4px;
424
424
  padding-left: 1px;
425
425
  border-top: 24px solid transparent;
426
- margin-left: 71px;
426
+ margin-left: 94px;
427
427
  font-weight: bold;
428
428
  text-align: left;
429
429
 
@@ -438,7 +438,7 @@ main {
438
438
 
439
439
  .TableRow {
440
440
  display: flex;
441
- align-items: center;
441
+ margin-left: 24px;
442
442
 
443
443
  &.animIn {
444
444
  opacity: 0;
@@ -446,7 +446,79 @@ main {
446
446
  animation: _kfRowIn 180ms ease-in-out forwards;
447
447
  }
448
448
  }
449
+
450
+ .FolderGroup {
451
+ position: relative;
452
+
453
+ > summary {
454
+ left: 0;
455
+ display: flex;
456
+ overflow: hidden;
457
+ align-items: center;
458
+ padding: 5px 0;
459
+ font-size: 12px;
460
+ list-style: none;
461
+ cursor: pointer;
462
+ user-select: none;
463
+ color: var(--colorLabel);
464
+ white-space: nowrap;
465
+ text-overflow: ellipsis;
466
+ border-radius: var(--radius);
467
+
468
+
469
+ &:active {
470
+ cursor: grabbing;
471
+ }
472
+
473
+ &::-webkit-details-marker {
474
+ display: none;
475
+ }
476
+
477
+ &:hover {
478
+ color: var(--colorText);
479
+ }
480
+
481
+ .FolderName {
482
+ margin-left: 125px;
483
+ &.groupedByMethod {
484
+ margin-left: 79px;
485
+ }
486
+ }
487
+
488
+ .FolderChevron {
489
+ display: flex;
490
+ width: 16px;
491
+ height: 16px;
492
+ flex-shrink: 0;
493
+ align-items: center;
494
+ justify-content: center;
495
+ opacity: .7;
496
+ transition: transform cubic-bezier(.2, .7, .8, 1.4) 400ms;
497
+ transform: rotate(-90deg);
498
+
499
+ svg {
500
+ width: 100%;
501
+ height: 100%;
502
+ }
503
+ }
504
+ }
505
+
506
+ &[open] > summary {
507
+ position: absolute;
508
+ top: 0;
509
+ left: 0;
510
+ width: 16px;
511
+
512
+ .FolderChevron {
513
+ transform: rotate(0deg);
514
+ }
515
+ .FolderName {
516
+ display: none;
517
+ }
518
+ }
519
+ }
449
520
  }
521
+
450
522
  @keyframes _kfRowIn {
451
523
  to {
452
524
  opacity: 1;
@@ -458,6 +530,7 @@ main {
458
530
  overflow: hidden;
459
531
  min-width: 38px;
460
532
  padding: 4px 0;
533
+ margin-top: 3px;
461
534
  margin-right: 8px;
462
535
  color: var(--colorLabel);
463
536
  font-size: 11px;
@@ -523,6 +596,7 @@ main {
523
596
 
524
597
  .Toggler {
525
598
  display: flex;
599
+ margin-top: 3px;
526
600
 
527
601
  input {
528
602
  /* For click drag target */
package/src/client/app.js CHANGED
@@ -3,8 +3,8 @@ import { createElement as r, t, classNames, restoreFocus, Fragment, defineClassN
3
3
  import { store } from './app-store.js'
4
4
  import { API } from './ApiConstants.js'
5
5
  import { Header } from './app-header.js'
6
- import { TimerIcon, CloudIcon } from './graphics.js'
7
6
  import { PayloadViewer, previewMock } from './app-payload-viewer.js'
7
+ import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
8
8
 
9
9
  import CSS from './app.css' with { type: 'css' }
10
10
  document.adoptedStyleSheets.push(CSS)
@@ -100,9 +100,30 @@ function MockList() {
100
100
  r('div', {
101
101
  className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
102
102
  }, method),
103
- store.brokersAsRowsByMethod(method).map(Row)))
103
+ FolderGroups(store.folderGroupsByMethod(method))))
104
104
 
105
- return store.brokersAsRowsByMethod('*').map(Row)
105
+ return FolderGroups(store.folderGroupsByMethod('*'))
106
+ }
107
+
108
+ function FolderGroups(groups) {
109
+ return groups.map(({ folder, children }) => {
110
+ if (children.length === 1)
111
+ return Row(children[0], 0)
112
+
113
+ return r('details', {
114
+ className: CSS.FolderGroup,
115
+ open: !store.collapsedFolders.has(folder),
116
+ onToggle() {
117
+ // TODO alt+click exclusive open
118
+ store.setFolderCollapsed(folder, !this.open)
119
+ }
120
+ },
121
+ r('summary', null,
122
+ r('span', { className: CSS.FolderChevron }, ChevronDownIcon()),
123
+ r('span', { className: classNames(CSS.FolderName, store.groupByMethod && CSS.groupedByMethod) },
124
+ folder + '…')),
125
+ children.map(Row))
126
+ })
106
127
  }
107
128
 
108
129
  /**
@@ -27,3 +27,9 @@ export function HelpIcon() {
27
27
  s('svg', { viewBox: '0 0 24 24' },
28
28
  s('path', { d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25' })))
29
29
  }
30
+
31
+ export function ChevronDownIcon() {
32
+ return (
33
+ s('svg', { viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', 'stroke-width': '2' },
34
+ s('path', { d: 'M6 9l6 6 6-6' })))
35
+ }
package/src/server/Api.js CHANGED
@@ -113,7 +113,6 @@ async function setGlobalDelay(req, response) {
113
113
  response.unprocessable(`Expected non-negative integer for "delay"`)
114
114
  else {
115
115
  config.delay = delay
116
- uiSyncVersion.increment()
117
116
  response.ok()
118
117
  uiSyncVersion.increment()
119
118
  }
@@ -219,7 +218,7 @@ async function setRouteIsDelayed(req, response) {
219
218
  else {
220
219
  broker.setDelayed(delayed)
221
220
  response.json(broker)
222
- uiSyncVersion.increment()
221
+ uiSyncVersion.increment()
223
222
  }
224
223
  }
225
224
 
@@ -53,11 +53,8 @@ export async function dispatchMock(req, response) {
53
53
  response.setHeader('Content-Type', mime)
54
54
  response.setHeader('Content-Length', length(body))
55
55
 
56
- setTimeout(() =>
57
- response.end(isHead
58
- ? null
59
- : body
60
- ), Number(broker.delayed && calcDelay()))
56
+ setTimeout(() => response.end(isHead ? null : body),
57
+ Number(broker.delayed && calcDelay()))
61
58
 
62
59
  logger.accessMock(req.url, broker.file)
63
60
  }
@@ -5,9 +5,8 @@ import pkgJSON from '../../package.json' with { type: 'json' }
5
5
 
6
6
  import { logger } from './utils/logger.js'
7
7
  import { ServerResponse } from './utils/HttpServerResponse.js'
8
- import { IncomingMessage } from './utils/HttpIncomingMessage.js'
8
+ import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
9
9
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
10
- import { BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
11
10
 
12
11
  import { API } from '../client/ApiConstants.js'
13
12
 
@@ -61,29 +61,10 @@ function request(path, options = {}) {
61
61
  }
62
62
 
63
63
 
64
- class BaseFixture {
65
- dir = ''
66
- urlMask = ''
67
- method = ''
68
-
64
+ class Fixture {
69
65
  constructor(file, body = '') {
70
66
  this.file = file
71
67
  this.body = body || `Body for ${file}`
72
- }
73
- write() { return api.writeMock(this.file, this.body) }
74
- delete() { return api.deleteMock(this.file) }
75
-
76
- request(options = {}) {
77
- options.method ??= this.method
78
- return request(this.urlMask, options)
79
- }
80
- }
81
-
82
-
83
- class Fixture extends BaseFixture {
84
- constructor(file, body = '') {
85
- super(file, body)
86
- this.dir = mocksDir
87
68
  const t = parseFilename(file)
88
69
  this.urlMask = t.urlMask
89
70
  this.method = t.method
@@ -93,14 +74,12 @@ class Fixture extends BaseFixture {
93
74
  async fetchBroker() {
94
75
  return (await fetchState()).brokersByMethod?.[this.method]?.[this.urlMask]
95
76
  }
96
- }
77
+ write() { return api.writeMock(this.file, this.body) }
78
+ delete() { return api.deleteMock(this.file) }
97
79
 
98
- class FixtureStatic extends BaseFixture {
99
- constructor(file, body = '') {
100
- super(file, body)
101
- this.dir = mocksDir
102
- this.urlMask = '/' + file
103
- this.method = 'GET'
80
+ request(options = {}) {
81
+ options.method ??= this.method
82
+ return request(this.urlMask, options)
104
83
  }
105
84
  }
106
85
 
@@ -614,8 +593,8 @@ describe('Dynamic Function Mocks', () => {
614
593
 
615
594
 
616
595
  describe('Static Files', () => {
617
- const fxsIndex = new FixtureStatic('index.html', '<h1>Index</h1>')
618
- const fxsAsset = new FixtureStatic('asset-script.js', 'const a = 1')
596
+ const fxsIndex = new Fixture('index.html', '<h1>Index</h1>')
597
+ const fxsAsset = new Fixture('asset-script.js', 'const a = 1')
619
598
  before(async () => {
620
599
  await api.reset()
621
600
  await fxsIndex.write()
@@ -724,7 +703,7 @@ describe('Auto Status', () => {
724
703
  })
725
704
 
726
705
  test('toggling ON 404 for static routes', async () => {
727
- const fx = new FixtureStatic('static-404.txt')
706
+ const fx = new Fixture('static-404.txt')
728
707
  await fx.write()
729
708
  equal((await fx.request()).status, 200)
730
709
 
@@ -1040,10 +1019,6 @@ describe('Watch mocks API toggler', () => {
1040
1019
  describe('Registering Mocks', () => {
1041
1020
  // simulates user interacting with the file-system directly
1042
1021
  class FixtureExternal extends Fixture {
1043
- constructor(props) {
1044
- super(props)
1045
- }
1046
-
1047
1022
  async writeExternally() {
1048
1023
  const nextVerPromise = resolveOnNextSyncVersion()
1049
1024
  await sleep(0) // next macro task
@@ -1063,7 +1038,6 @@ describe('Registering Mocks', () => {
1063
1038
  return new Promise(resolve => setTimeout(resolve, ms))
1064
1039
  }
1065
1040
 
1066
-
1067
1041
  const fxA = new FixtureExternal('register(default).GET.200.json')
1068
1042
  const fxB = new FixtureExternal('register(alt).GET.200.json')
1069
1043
 
@@ -1138,9 +1112,9 @@ describe('Registering Mocks', () => {
1138
1112
  })
1139
1113
 
1140
1114
  test('responds when dir is renamed', async () => {
1141
- const p0 = resolveOnNextSyncVersion(version + 2)
1115
+ const prom = resolveOnNextSyncVersion(version + 2)
1142
1116
  await renameInMocksDir('reg0', 'reg1')
1143
- equal(await p0, version + 3)
1117
+ equal(await prom, version + 3)
1144
1118
 
1145
1119
  const s = await fetchState()
1146
1120
  equal(s.brokersByMethod.GET['/reg1/runtime0'].file, 'reg1/runtime0.GET.200.txt')
@@ -61,8 +61,8 @@ export function registerMock(file, isFromWatcher = false) {
61
61
 
62
62
  export function unregisterMock(file) {
63
63
  const broker = brokerByFilename(file)
64
- const hasNoMoreMocks = broker?.unregister(file)
65
- if (hasNoMoreMocks) {
64
+ const methodHasNoMoreMocks = broker?.unregister(file) // TODO or it was a directory of many mocks
65
+ if (methodHasNoMoreMocks) {
66
66
  const { method, urlMask } = parseFilename(file)
67
67
  delete collection[method][urlMask]
68
68
  if (!Object.keys(collection[method]).length)
@@ -19,14 +19,14 @@ export class ServerResponse extends http.ServerResponse {
19
19
 
20
20
  html(html, csp) {
21
21
  logger.access(this)
22
- this.setHeader('Content-Type', mimeFor('html'))
22
+ this.setHeader('Content-Type', mimeFor('.html'))
23
23
  this.setHeader('Content-Security-Policy', csp)
24
24
  this.end(html)
25
25
  }
26
26
 
27
27
  json(payload) {
28
28
  logger.access(this)
29
- this.setHeader('Content-Type', mimeFor('json'))
29
+ this.setHeader('Content-Type', mimeFor('.json'))
30
30
  this.end(JSON.stringify(payload))
31
31
  }
32
32
 
@@ -40,7 +40,6 @@ export async function resolveIn(baseDir, file) {
40
40
  : null
41
41
  }
42
42
  catch (e) {
43
- console.error('DDDDDD', e)
44
43
  return null
45
44
  }
46
45
  }
@@ -0,0 +1,661 @@
1
+ {
2
+ "openapi": "3.1.2",
3
+ "info": {
4
+ "title": "Mockaton Control API",
5
+ "version": "1.0.0"
6
+ },
7
+ "servers": [
8
+ {
9
+ "url": "http://localhost:2020"
10
+ }
11
+ ],
12
+ "paths": {
13
+ "/mockaton/reset": {
14
+ "patch": {
15
+ "summary": "Re-initialize Mockaton",
16
+ "description": "The selected mocks, cookies, and delays go back to default, but `proxyFallback`, `collectProxied`, and `corsAllowed` are not affected.",
17
+ "x-js-client-example": "await mockaton.reset()",
18
+ "responses": {
19
+ "200": {
20
+ "description": "OK"
21
+ }
22
+ }
23
+ }
24
+ },
25
+ "/mockaton/bulk-select-by-comment": {
26
+ "patch": {
27
+ "summary": "Select all mocks that have a particular comment",
28
+ "x-js-client-example": "await mockaton.bulkSelectByComment('(demo-a)')",
29
+ "requestBody": {
30
+ "required": true,
31
+ "content": {
32
+ "application/json": {
33
+ "schema": {
34
+ "type": "string",
35
+ "description": "Parentheses are optional, so you can pass a partial match. For example, passing `'demo-'` (without the final `a`) works too. On routes with many partial matches, their first mock in alphabetical order wins.",
36
+ "example": "(demo-a)"
37
+ }
38
+ }
39
+ }
40
+ },
41
+ "responses": {
42
+ "200": {
43
+ "$ref": "#/components/responses/Broker"
44
+ }
45
+ }
46
+ }
47
+ },
48
+ "/mockaton/select": {
49
+ "patch": {
50
+ "summary": "Select a mock file for a route",
51
+ "x-js-client-example": "await mockaton.select('api/user/avatar.GET.json')",
52
+ "requestBody": {
53
+ "required": true,
54
+ "content": {
55
+ "application/json": {
56
+ "schema": {
57
+ "$ref": "#/components/schemas/Filename"
58
+ }
59
+ }
60
+ }
61
+ },
62
+ "responses": {
63
+ "200": {
64
+ "$ref": "#/components/responses/Broker"
65
+ },
66
+ "422": {
67
+ "description": "Mock file doesn’t exist",
68
+ "content": {
69
+ "application/json": {
70
+ "schema": {
71
+ "type": "string"
72
+ },
73
+ "example": "Mock file does not exist"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ },
80
+ "/mockaton/toggle-status": {
81
+ "patch": {
82
+ "summary": "Toggle a specific status for a route",
83
+ "description": "Selects the first found mock with the given status, which could be the autogenerated one. Or, selects the default file.",
84
+ "x-js-client-example": "await mockaton.toggleStatus('GET', '/api/user/friends', 500)",
85
+ "requestBody": {
86
+ "required": true,
87
+ "content": {
88
+ "application/json": {
89
+ "schema": {
90
+ "type": "array",
91
+ "minItems": 3,
92
+ "maxItems": 3,
93
+ "prefixItems": [
94
+ {
95
+ "$ref": "#/components/schemas/Method"
96
+ },
97
+ {
98
+ "$ref": "#/components/schemas/UrlMask"
99
+ },
100
+ {
101
+ "type": "number",
102
+ "description": "HTTP Status to toggle",
103
+ "example": 500
104
+ }
105
+ ],
106
+ "additionalItems": false
107
+ },
108
+ "example": [
109
+ "GET",
110
+ "/api/user",
111
+ 500
112
+ ]
113
+ }
114
+ }
115
+ },
116
+ "responses": {
117
+ "200": {
118
+ "$ref": "#/components/responses/Broker"
119
+ },
120
+ "422": {
121
+ "description": "Route does not exist"
122
+ }
123
+ }
124
+ }
125
+ },
126
+ "/mockaton/proxied": {
127
+ "patch": {
128
+ "summary": "Set whether a route is proxied",
129
+ "description": "Applicable only when there’s a proxy fallback server URL already set. See `PATCH /mockaton/fallback`",
130
+ "x-js-client-example": "await mockaton.setRouteIsProxied('GET', '/api/user/friends', true)",
131
+ "requestBody": {
132
+ "required": true,
133
+ "content": {
134
+ "application/json": {
135
+ "schema": {
136
+ "type": "array",
137
+ "minItems": 3,
138
+ "maxItems": 3,
139
+ "prefixItems": [
140
+ {
141
+ "$ref": "#/components/schemas/Method"
142
+ },
143
+ {
144
+ "$ref": "#/components/schemas/UrlMask"
145
+ },
146
+ {
147
+ "type": "boolean",
148
+ "description": "proxied",
149
+ "example": true
150
+ }
151
+ ],
152
+ "additionalItems": false
153
+ },
154
+ "example": [
155
+ "GET",
156
+ "/api/user",
157
+ true
158
+ ]
159
+ }
160
+ }
161
+ },
162
+ "responses": {
163
+ "200": {
164
+ "$ref": "#/components/responses/Broker"
165
+ },
166
+ "422": {
167
+ "description": "Invalid request. Possible reasons:\n- Route does not exist\n- Expected boolean for `proxied`\n- No `proxyFallback` configured\n"
168
+ }
169
+ }
170
+ }
171
+ },
172
+ "/mockaton/delay": {
173
+ "patch": {
174
+ "summary": "Set whether a route is delayed",
175
+ "x-js-client-example": "await mockaton.setRouteIsDelayed('GET', '/api/user/friends', true)",
176
+ "requestBody": {
177
+ "required": true,
178
+ "content": {
179
+ "application/json": {
180
+ "schema": {
181
+ "type": "array",
182
+ "minItems": 3,
183
+ "maxItems": 3,
184
+ "prefixItems": [
185
+ {
186
+ "$ref": "#/components/schemas/Method"
187
+ },
188
+ {
189
+ "$ref": "#/components/schemas/UrlMask"
190
+ },
191
+ {
192
+ "type": "boolean",
193
+ "description": "delayed",
194
+ "example": true
195
+ }
196
+ ],
197
+ "additionalItems": false
198
+ },
199
+ "example": [
200
+ "GET",
201
+ "/api/user",
202
+ true
203
+ ]
204
+ }
205
+ }
206
+ },
207
+ "responses": {
208
+ "200": {
209
+ "$ref": "#/components/responses/Broker"
210
+ },
211
+ "422": {
212
+ "description": "Invalid request. Possible reasons:\n- Route does not exist\n- Expected boolean for `delayed`\n"
213
+ }
214
+ }
215
+ }
216
+ },
217
+ "/mockaton/global-delay": {
218
+ "patch": {
219
+ "summary": "Set global delay for all responses",
220
+ "x-js-client-example": "await mockaton.setGlobalDelay(1500)",
221
+ "requestBody": {
222
+ "required": true,
223
+ "content": {
224
+ "application/json": {
225
+ "schema": {
226
+ "type": "integer",
227
+ "description": "Delay in milliseconds",
228
+ "example": 1500
229
+ }
230
+ }
231
+ }
232
+ },
233
+ "responses": {
234
+ "200": {
235
+ "description": "OK"
236
+ },
237
+ "422": {
238
+ "description": "Expected non-negative integer"
239
+ }
240
+ }
241
+ }
242
+ },
243
+ "/mockaton/global-delay-jitter": {
244
+ "patch": {
245
+ "summary": "Set global delay jitter for all responses",
246
+ "x-js-client-example": "await mockaton.setGlobalDelayJitter(0.5)",
247
+ "requestBody": {
248
+ "required": true,
249
+ "content": {
250
+ "application/json": {
251
+ "schema": {
252
+ "type": "number",
253
+ "description": "0 to 3 float percent",
254
+ "example": 0.5
255
+ }
256
+ }
257
+ }
258
+ },
259
+ "responses": {
260
+ "200": {
261
+ "description": "OK"
262
+ },
263
+ "422": {
264
+ "description": "Expected 0 to 3 float"
265
+ }
266
+ }
267
+ }
268
+ },
269
+ "/mockaton/cors": {
270
+ "patch": {
271
+ "summary": "Enable or disable CORS",
272
+ "x-js-client-example": "await mockaton.setCorsAllowed(false)",
273
+ "requestBody": {
274
+ "required": true,
275
+ "content": {
276
+ "application/json": {
277
+ "schema": {
278
+ "type": "boolean",
279
+ "description": "Is enabled?"
280
+ }
281
+ }
282
+ }
283
+ },
284
+ "responses": {
285
+ "200": {
286
+ "description": "OK"
287
+ },
288
+ "422": {
289
+ "description": "Expected boolean"
290
+ }
291
+ }
292
+ }
293
+ },
294
+ "/mockaton/fallback": {
295
+ "patch": {
296
+ "summary": "Set proxy fallback address",
297
+ "x-js-client-example": "await mockaton.setProxyFallback('http://example.test')",
298
+ "requestBody": {
299
+ "required": true,
300
+ "content": {
301
+ "application/json": {
302
+ "schema": {
303
+ "type": "string",
304
+ "description": "URL",
305
+ "example": "https://example.test"
306
+ }
307
+ }
308
+ }
309
+ },
310
+ "responses": {
311
+ "200": {
312
+ "description": "OK"
313
+ },
314
+ "422": {
315
+ "description": "Invalid Proxy Fallback URL"
316
+ }
317
+ }
318
+ }
319
+ },
320
+ "/mockaton/collect-proxied": {
321
+ "patch": {
322
+ "summary": "Enable or disable collection of proxied responses",
323
+ "description": "Applicable only when there’s a proxy fallback server URL already set. See `PATCH /mockaton/fallback`",
324
+ "x-js-client-example": "await mockaton.setCollectProxied(true)",
325
+ "requestBody": {
326
+ "required": true,
327
+ "content": {
328
+ "application/json": {
329
+ "schema": {
330
+ "type": "boolean",
331
+ "description": "Should Collect?",
332
+ "example": true
333
+ }
334
+ }
335
+ }
336
+ },
337
+ "responses": {
338
+ "200": {
339
+ "description": "OK"
340
+ },
341
+ "422": {
342
+ "description": "Expected boolean"
343
+ }
344
+ }
345
+ }
346
+ },
347
+ "/mockaton/cookies": {
348
+ "patch": {
349
+ "summary": "Select a cookie label",
350
+ "x-js-client-example": "await mockaton.selectCookie('Normal User')",
351
+ "requestBody": {
352
+ "required": true,
353
+ "content": {
354
+ "application/json": {
355
+ "schema": {
356
+ "type": "string",
357
+ "description": "Cookie key",
358
+ "example": "Normal User"
359
+ }
360
+ }
361
+ }
362
+ },
363
+ "responses": {
364
+ "200": {
365
+ "description": "Available cookie labels",
366
+ "content": {
367
+ "application/json": {
368
+ "schema": {
369
+ "$ref": "#/components/schemas/CookieSelectionList"
370
+ }
371
+ }
372
+ }
373
+ },
374
+ "422": {
375
+ "description": "Cookie key not found"
376
+ }
377
+ }
378
+ }
379
+ },
380
+ "/mockaton/watch-mocks": {
381
+ "patch": {
382
+ "summary": "Enable or disable mock file watching",
383
+ "description": "Controls file watchers for mocksDir.",
384
+ "x-js-client-example": "await mockaton.setWatchMocks(true)",
385
+ "requestBody": {
386
+ "required": true,
387
+ "content": {
388
+ "application/json": {
389
+ "schema": {
390
+ "type": "boolean",
391
+ "description": "true to start watchers, false to stop them"
392
+ }
393
+ }
394
+ }
395
+ },
396
+ "responses": {
397
+ "200": {
398
+ "description": "OK"
399
+ },
400
+ "422": {
401
+ "description": "Invalid input - expected boolean"
402
+ }
403
+ }
404
+ }
405
+ },
406
+ "/mockaton/write-mock": {
407
+ "patch": {
408
+ "summary": "Write a mock file",
409
+ "description": "Writes a mock file to the mocks directory. Guarded by `readOnly` and `mocksDir`.",
410
+ "x-js-client-example": "await mockaton.writeMock('api/user/friends.GET.200.json', '{ \"friends\": [] }')",
411
+ "requestBody": {
412
+ "required": true,
413
+ "content": {
414
+ "application/json": {
415
+ "schema": {
416
+ "type": "array",
417
+ "minItems": 2,
418
+ "maxItems": 2,
419
+ "prefixItems": [
420
+ {
421
+ "$ref": "#/components/schemas/Filename"
422
+ },
423
+ {
424
+ "type": "string",
425
+ "description": "File content"
426
+ }
427
+ ],
428
+ "additionalItems": false
429
+ }
430
+ }
431
+ }
432
+ },
433
+ "responses": {
434
+ "200": {
435
+ "description": "OK"
436
+ },
437
+ "403": {
438
+ "description": "Forbidden (read-only mode or outside mocksDir)"
439
+ }
440
+ }
441
+ }
442
+ },
443
+ "/mockaton/delete-mock": {
444
+ "patch": {
445
+ "summary": "Delete a mock file",
446
+ "description": "Deletes a mock file from the mocks directory. Guarded by `readOnly` and `mocksDir`.",
447
+ "x-js-client-example": "await mockaton.deleteMock('api/user/friends.GET.200.json')",
448
+ "requestBody": {
449
+ "required": true,
450
+ "content": {
451
+ "application/json": {
452
+ "schema": {
453
+ "$ref": "#/components/schemas/Filename"
454
+ }
455
+ }
456
+ }
457
+ },
458
+ "responses": {
459
+ "200": {
460
+ "description": "OK"
461
+ },
462
+ "403": {
463
+ "description": "Forbidden (read-only mode or outside mocksDir)"
464
+ },
465
+ "422": {
466
+ "description": "Mock file does not exist"
467
+ }
468
+ }
469
+ }
470
+ },
471
+ "/mockaton/state": {
472
+ "get": {
473
+ "summary": "Get complete Mockaton state",
474
+ "x-js-client-example": "await mockaton.getState()",
475
+ "responses": {
476
+ "200": {
477
+ "description": "Mockaton state",
478
+ "content": {
479
+ "application/json": {
480
+ "schema": {
481
+ "$ref": "#/components/schemas/State"
482
+ }
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ },
489
+ "/mockaton/sync-version": {
490
+ "get": {
491
+ "summary": "Get sync version for long‑polling updates",
492
+ "description": "A counter that’s incremented when a new mock is added, removed, or renamed. Also, when the internal state changes, e.g., when changing the mock file for a route.",
493
+ "x-js-client-example": "await mockaton.getSyncVersion()",
494
+ "parameters": [
495
+ {
496
+ "name": "sync_version",
497
+ "in": "header",
498
+ "required": false,
499
+ "example": -1,
500
+ "schema": {
501
+ "type": "number"
502
+ },
503
+ "description": "When not present, or when the version mismatches it responds right away. Otherwise, it long polls. Times out in 8s."
504
+ }
505
+ ],
506
+ "responses": {
507
+ "200": {
508
+ "description": "Sync version value",
509
+ "content": {
510
+ "application/json": {
511
+ "schema": {
512
+ "type": "number",
513
+ "description": "Incremental integer"
514
+ }
515
+ }
516
+ }
517
+ }
518
+ }
519
+ }
520
+ }
521
+ },
522
+ "components": {
523
+ "responses": {
524
+ "Broker": {
525
+ "description": "Updated broker state",
526
+ "content": {
527
+ "application/json": {
528
+ "schema": {
529
+ "$ref": "#/components/schemas/ClientMockBroker"
530
+ }
531
+ }
532
+ }
533
+ }
534
+ },
535
+ "schemas": {
536
+ "Method": {
537
+ "type": "string",
538
+ "description": "HTTP Method of the mock",
539
+ "example": "GET"
540
+ },
541
+ "UrlMask": {
542
+ "type": "string",
543
+ "example": "/api/user/friends"
544
+ },
545
+ "Filename": {
546
+ "type": "string",
547
+ "description": "Mock filename. The convention is UrlMask.Method.StatusCode.Extension",
548
+ "example": "api/user/friends.GET.200.json"
549
+ },
550
+ "ClientMockBroker": {
551
+ "type": "object",
552
+ "properties": {
553
+ "mocks": {
554
+ "type": "array",
555
+ "items": {
556
+ "$ref": "#/components/schemas/Filename"
557
+ }
558
+ },
559
+ "file": {
560
+ "$ref": "#/components/schemas/Filename"
561
+ },
562
+ "status": {
563
+ "type": "number"
564
+ },
565
+ "autoStatus": {
566
+ "type": "number"
567
+ },
568
+ "isStatic": {
569
+ "type": "boolean"
570
+ },
571
+ "delayed": {
572
+ "type": "boolean"
573
+ },
574
+ "proxied": {
575
+ "type": "boolean"
576
+ }
577
+ },
578
+ "required": [
579
+ "mocks",
580
+ "file",
581
+ "status",
582
+ "autoStatus",
583
+ "isStatic",
584
+ "delayed",
585
+ "proxied"
586
+ ]
587
+ },
588
+ "State": {
589
+ "type": "object",
590
+ "properties": {
591
+ "brokersByMethod": {
592
+ "type": "object",
593
+ "additionalProperties": {
594
+ "type": "object",
595
+ "additionalProperties": {
596
+ "$ref": "#/components/schemas/ClientMockBroker"
597
+ }
598
+ }
599
+ },
600
+ "cookies": {
601
+ "$ref": "#/components/schemas/CookieSelectionList"
602
+ },
603
+ "comments": {
604
+ "type": "array",
605
+ "items": {
606
+ "type": "string"
607
+ }
608
+ },
609
+ "delay": {
610
+ "type": "number"
611
+ },
612
+ "delayJitter": {
613
+ "type": "number"
614
+ },
615
+ "collectProxied": {
616
+ "type": "boolean"
617
+ },
618
+ "proxyFallback": {
619
+ "type": "string"
620
+ },
621
+ "readOnly": {
622
+ "type": "boolean"
623
+ },
624
+ "corsAllowed": {
625
+ "type": "boolean"
626
+ }
627
+ },
628
+ "required": [
629
+ "brokersByMethod",
630
+ "cookies",
631
+ "comments",
632
+ "delay",
633
+ "delayJitter",
634
+ "collectProxied",
635
+ "proxyFallback",
636
+ "readOnly",
637
+ "corsAllowed"
638
+ ]
639
+ },
640
+ "CookieSelectionList": {
641
+ "type": "array",
642
+ "items": {
643
+ "type": "array",
644
+ "minItems": 2,
645
+ "maxItems": 2,
646
+ "prefixItems": [
647
+ {
648
+ "type": "string",
649
+ "description": "Label"
650
+ },
651
+ {
652
+ "type": "boolean",
653
+ "description": "Selected"
654
+ }
655
+ ],
656
+ "additionalItems": false
657
+ }
658
+ }
659
+ }
660
+ }
661
+ }