mockaton 8.7.7 → 8.8.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
@@ -3,8 +3,6 @@
3
3
  ![NPM Version](https://img.shields.io/npm/v/mockaton)
4
4
  ![NPM Version](https://img.shields.io/npm/l/mockaton)
5
5
 
6
- ## Mock your APIs, Enhance your Development Workflow
7
-
8
6
  Mockaton is an HTTP mock server with the goal of making
9
7
  your frontend development and testing easier—and a lot more fun.
10
8
 
@@ -17,11 +15,16 @@ For example, for `/api/user/1234` the mock filename would be:
17
15
  my-mocks-dir/api/user/[user-id].GET.200.json
18
16
  ```
19
17
 
20
- ## Scrapping Mocks from you Backend
18
+ ## No Need to Mock Everything
19
+ Mockaton can fallback to your real backend on routes you don’t have mocks
20
+ for. For that, type your backend address in the **Fallback Backend** field.
21
+
22
+ Similarly, if already have mocks for a route you can check the ☁️ **Cloud
23
+ checkbox** and Mockaton will request it from your backend.
21
24
 
22
- Mockaton can fallback to your real backend on routes you don’t have mocks for. For that,
23
- type your backend address in the **Fallback Backend** field. And if you
24
- check **Save Mocks**, it will collect those responses that hit your backend.
25
+
26
+ ## Scrapping Mocks from you Backend
27
+ If you check **Save Mocks**, Mockaton will collect the responses that hit your backend.
25
28
  Those mocks will be saved to your `config.mocksDir` following the filename convention.
26
29
 
27
30
 
@@ -31,7 +34,9 @@ Those mocks will be saved to your `config.mocksDir` following the filename conve
31
34
  Want to mock a locked-out user or an invalid login attempt?
32
35
  Add a comment to the filename in parentheses. For example:
33
36
 
34
- `api/login(locked out user).POST.423.json`
37
+ <pre>
38
+ api/login<b>(locked out user)</b>.POST.423.json
39
+ </pre>
35
40
 
36
41
  ### Different response status code
37
42
  For instance, you can have mocks with a `4xx` or `5xx` status code for triggering
@@ -295,9 +300,11 @@ checkbox, the delay amount is globally configurable.
295
300
 
296
301
 
297
302
  ### `proxyFallback?: string`
298
- Lets you specify a target server for serving routes you don’t have mocks for.
299
303
  For example, `config.proxyFallback = 'http://example.com'`
300
304
 
305
+ Lets you specify a target server for serving routes you don’t have mocks for,
306
+ or that you manually picked with the ☁️ **Cloud Checkbox**.
307
+
301
308
  ### `collectProxied?: boolean`
302
309
  Defaults to `false`. With this flag you can save mocks that hit
303
310
  your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
@@ -308,7 +315,10 @@ URL, the filename will have `[id]` in their place. For example,
308
315
  my-mocks-dir/api/user/[id]/likes.GET.200.json
309
316
  ```
310
317
 
311
- Your existing mocks won’t be overwritten (because they don’t hit the fallback server).
318
+ Your existing mocks won’t be overwritten. That is, the routes you manually
319
+ selected for using your backend with the ☁️ **Cloud Checkbox**, will have
320
+ a unique filename comment.
321
+
312
322
 
313
323
  <details>
314
324
  <summary>Extension Details</summary>
@@ -495,6 +505,11 @@ first mock in alphabetical order that matches.
495
505
  await mockaton.setRouteIsDelayed('GET', '/api/foo', true)
496
506
  ```
497
507
 
508
+ ### Set Route is Proxied
509
+ ```js
510
+ await mockaton.setRouteIsProxied('GET', '/api/foo', true)
511
+ ```
512
+
498
513
  ### Select a cookie
499
514
  In `config.cookies`, each key is the label used for selecting it.
500
515
  ```js
@@ -521,5 +536,5 @@ await mockaton.reset()
521
536
 
522
537
  <div style="display: flex; align-items: center; gap: 20px">
523
538
  <img src="fixtures-mocks/api/user/avatar.GET.200.png" width="170"/>
524
- <p style="font-size: 18px">“Use Mockaton”</p>
539
+ <p style="font-size: 18px">“Mockaton”</p>
525
540
  </div>
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.7.7",
5
+ "version": "8.8.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -38,6 +38,7 @@ export const apiGetRequests = new Map([
38
38
  export const apiPatchRequests = new Map([
39
39
  [API.select, selectMock],
40
40
  [API.delay, setRouteIsDelayed],
41
+ [API.proxied, setRouteIsProxied],
41
42
  [API.reset, reinitialize],
42
43
  [API.cookies, selectCookie],
43
44
  [API.fallback, updateProxyFallback],
@@ -118,13 +119,43 @@ async function selectMock(req, response) {
118
119
  async function setRouteIsDelayed(req, response) {
119
120
  try {
120
121
  const body = await parseJSON(req)
122
+ const delayed = body[DF.delayed]
121
123
  const broker = mockBrokersCollection.getBrokerForUrl(
122
124
  body[DF.routeMethod],
123
125
  body[DF.routeUrlMask])
126
+
127
+ if (!broker) // TESTME
128
+ sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`)
129
+ else if (typeof delayed !== 'boolean')
130
+ sendUnprocessableContent(response, `Expected a boolean for "delayed"`) // TESTME
131
+ else {
132
+ broker.updateDelay(body[DF.delayed])
133
+ sendOK(response)
134
+ }
135
+ }
136
+ catch (error) {
137
+ sendBadRequest(response, error)
138
+ }
139
+ }
140
+
141
+ async function setRouteIsProxied(req, response) { // TESTME
142
+ try {
143
+ const body = await parseJSON(req)
144
+ const proxied = body[DF.proxied]
145
+ const broker = mockBrokersCollection.getBrokerForUrl(
146
+ body[DF.routeMethod],
147
+ body[DF.routeUrlMask])
148
+
124
149
  if (!broker)
125
- throw `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`
126
- broker.updateDelay(body[DF.delayed])
127
- sendOK(response)
150
+ sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`)
151
+ else if (typeof proxied !== 'boolean')
152
+ sendUnprocessableContent(response, `Expected a boolean for "proxied"`)
153
+ else if (proxied && !config.proxyFallback)
154
+ sendUnprocessableContent(response, `There’s no proxy fallback`)
155
+ else {
156
+ broker.updateProxied(proxied)
157
+ sendOK(response)
158
+ }
128
159
  }
129
160
  catch (error) {
130
161
  sendBadRequest(response, error)
@@ -134,12 +165,14 @@ async function setRouteIsDelayed(req, response) {
134
165
  async function updateProxyFallback(req, response) {
135
166
  try {
136
167
  const fallback = await parseJSON(req)
137
- if (fallback && !URL.canParse(fallback))
168
+ if (fallback && !URL.canParse(fallback)) {
138
169
  sendUnprocessableContent(response)
139
- else {
140
- config.proxyFallback = fallback
141
- sendOK(response)
170
+ return
142
171
  }
172
+ if (!fallback) // TESTME
173
+ mockBrokersCollection.ensureAllRoutesHaveSelectedMock()
174
+ config.proxyFallback = fallback
175
+ sendOK(response)
143
176
  }
144
177
  catch (error) {
145
178
  sendBadRequest(response, error)
@@ -10,6 +10,7 @@ export const API = {
10
10
  cookies: MOUNT + '/cookies',
11
11
  fallback: MOUNT + '/fallback',
12
12
  collectProxied: MOUNT + '/collect-proxied',
13
+ proxied: MOUNT + '/proxied',
13
14
  cors: MOUNT + '/cors',
14
15
  static: MOUNT + '/static'
15
16
  }
@@ -17,7 +18,8 @@ export const API = {
17
18
  export const DF = { // Dashboard Fields (XHR)
18
19
  routeMethod: 'route_method',
19
20
  routeUrlMask: 'route_url_mask',
20
- delayed: 'delayed'
21
+ delayed: 'delayed',
22
+ proxied: 'proxied'
21
23
  }
22
24
 
23
25
  export const DEFAULT_500_COMMENT = '(Mockaton 500)'
package/src/Commander.js CHANGED
@@ -37,6 +37,14 @@ export class Commander {
37
37
  })
38
38
  }
39
39
 
40
+ setRouteIsProxied(routeMethod, routeUrlMask, proxied) {
41
+ return this.#patch(API.proxied, {
42
+ [DF.routeMethod]: routeMethod,
43
+ [DF.routeUrlMask]: routeUrlMask,
44
+ [DF.proxied]: proxied
45
+ })
46
+ }
47
+
40
48
  listCookies() {
41
49
  return this.#get(API.cookies)
42
50
  }
package/src/Dashboard.css CHANGED
@@ -1,5 +1,5 @@
1
1
  :root {
2
- --boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.1), 0 1px 1px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 0 rgba(0, 0, 0, 0.08);
2
+ --boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.15), 0 1px 1px 0 rgba(0, 0, 0, 0.15), 0 1px 3px 0 rgba(0, 0, 0, 0.1);
3
3
  --radius: 6px
4
4
  }
5
5
 
@@ -7,13 +7,14 @@
7
7
  :root {
8
8
  --color4xxBackground: #ffedd1;
9
9
  --colorAccent: #0075db;
10
- --colorAccentAlt: #008664;
10
+ --colorAccentAlt: #068564;
11
11
  --colorBackground: #fff;
12
12
  --colorComboBoxHeaderBackground: #fff;
13
13
  --colorComboBoxBackground: #f7f7f7;
14
14
  --colorHeaderBackground: #f3f3f3;
15
- --colorSecondaryButtonBackground: #f3f3f3;
16
- --colorDisabled: #444;
15
+ --colorSecondaryButtonBackground: transparent;
16
+ --colorSecondaryAction: #555;
17
+ --colorDisabledMockSelector: #444;
17
18
  --colorHover: #dfefff;
18
19
  --colorLabel: #444;
19
20
  --colorLightRed: #ffe4ee;
@@ -29,9 +30,10 @@
29
30
  --colorBackground: #161616;
30
31
  --colorHeaderBackground: #090909;
31
32
  --colorComboBoxBackground: #252525;
32
- --colorSecondaryButtonBackground: #444;
33
+ --colorSecondaryButtonBackground: #202020;
34
+ --colorSecondaryAction: #bbb;
33
35
  --colorComboBoxHeaderBackground: #222;
34
- --colorDisabled: #bbb;
36
+ --colorDisabledMockSelector: #b9b9b9;
35
37
  --colorHover: #023661;
36
38
  --colorLabel: #aaa;
37
39
  --colorLightRed: #ffe4ee;
@@ -101,11 +103,11 @@ select {
101
103
  display: flex;
102
104
  width: 100%;
103
105
  align-items: flex-end;
104
- padding: 15px 16px;
106
+ padding: 16px;
105
107
  border-bottom: 1px solid rgba(127, 127, 127, 0.1);
106
108
  background: var(--colorHeaderBackground);
107
109
  box-shadow: var(--boxShadow1);
108
- gap: 12px;
110
+ gap: 8px;
109
111
 
110
112
  img {
111
113
  width: 130px;
@@ -114,12 +116,14 @@ select {
114
116
  }
115
117
 
116
118
  .Field {
117
- min-width: 150px;
119
+ width: 120px;
118
120
 
119
121
  span {
120
- display: block;
122
+ display: flex;
123
+ align-items: center;
121
124
  color: var(--colorLabel);
122
125
  font-size: 11px;
126
+ gap: 4px;
123
127
  }
124
128
 
125
129
  input[type=url],
@@ -140,7 +144,14 @@ select {
140
144
 
141
145
  &.FallbackBackend {
142
146
  position: relative;
143
- width: 194px;
147
+ width: 210px;
148
+
149
+ svg {
150
+ width: 14px;
151
+ height: 14px;
152
+ fill: var(--colorLabel);
153
+ opacity: 0.6;
154
+ }
144
155
 
145
156
  input[type=url] {
146
157
  padding: 0 6px;
@@ -244,7 +255,7 @@ select {
244
255
  position: relative;
245
256
  left: -6px;
246
257
  display: inline-block;
247
- width: 280px;
258
+ width: 300px;
248
259
  padding: 8px 6px;
249
260
  border-radius: var(--radius);
250
261
  color: var(--colorAccent);
@@ -260,7 +271,7 @@ select {
260
271
  }
261
272
 
262
273
  .MockSelector {
263
- width: 300px;
274
+ width: 260px;
264
275
  height: 30px;
265
276
  border: 0;
266
277
  border-left: 3px solid transparent;
@@ -277,12 +288,13 @@ select {
277
288
  appearance: none;
278
289
  background: transparent;
279
290
  cursor: default;
280
- color: var(--colorDisabled);
291
+ color: var(--colorDisabledMockSelector);
281
292
  opacity: 1;
282
293
  }
283
294
  }
284
295
 
285
- .DelayToggler {
296
+ .DelayToggler,
297
+ .ProxyToggler {
286
298
  display: flex;
287
299
  margin-left: 8px;
288
300
  cursor: pointer;
@@ -297,25 +309,38 @@ select {
297
309
  }
298
310
  }
299
311
 
312
+ &:enabled:hover:not(:checked) ~ svg {
313
+ background: var(--colorHover);
314
+ fill: var(--colorText);
315
+ }
316
+
300
317
  &:checked ~ svg {
301
318
  background: var(--colorAccent);
302
319
  fill: white;
303
320
  }
321
+
322
+ &:disabled ~ svg {
323
+ opacity: .5;
324
+ cursor: not-allowed;
325
+ }
304
326
  }
305
327
 
306
328
  > svg {
307
329
  width: 20px;
308
330
  height: 20px;
309
331
  vertical-align: bottom;
310
- fill: var(--colorText);
332
+ fill: var(--colorSecondaryAction);
311
333
  border-radius: 50%;
312
334
  background: var(--colorSecondaryButtonBackground);
313
335
  box-shadow: var(--boxShadow1);
336
+ }
337
+ }
314
338
 
315
- &:hover {
316
- background: var(--colorHover);
317
- fill: var(--colorText);
318
- }
339
+ .ProxyToggler {
340
+ > svg {
341
+ width: 24px;
342
+ padding: 3px;
343
+ border-radius: 4px;
319
344
  }
320
345
  }
321
346
 
@@ -344,8 +369,8 @@ select {
344
369
  padding: 5px 4px;
345
370
  box-shadow: var(--boxShadow1);
346
371
  font-size: 10px;
347
- color: var(--colorText);
348
- border-radius: 2px;
372
+ color: var(--colorSecondaryAction);
373
+ border-radius: 4px;
349
374
  background: var(--colorSecondaryButtonBackground);
350
375
 
351
376
  &:hover {
package/src/Dashboard.js CHANGED
@@ -12,8 +12,8 @@ function syntaxHighlightJson(textBody) {
12
12
 
13
13
 
14
14
  const Strings = {
15
- bulk_select_by_comment: 'Bulk Select by Comment',
16
- bulk_select_by_comment_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
15
+ bulk_select: 'Bulk Select',
16
+ bulk_select_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
17
17
  click_link_to_preview: 'Click a link to preview it',
18
18
  cookie: 'Cookie',
19
19
  cookie_disabled_title: 'No cookies specified in Config.cookies',
@@ -24,7 +24,9 @@ const Strings = {
24
24
  internal_server_error: 'Internal Server Error',
25
25
  mock: 'Mock',
26
26
  no_mocks_found: 'No mocks found',
27
- pick: 'Pick…',
27
+ pick_comment: 'Pick Comment…',
28
+ proxied: 'Proxied',
29
+ proxy_toggler: 'Proxy Toggler',
28
30
  reset: 'Reset',
29
31
  save_proxied: 'Save Mocks',
30
32
  static_get: 'Static GET'
@@ -42,6 +44,7 @@ const CSS = {
42
44
  PayloadViewer: 'PayloadViewer',
43
45
  PreviewLink: 'PreviewLink',
44
46
  ProgressBar: 'ProgressBar',
47
+ ProxyToggler: 'ProxyToggler',
45
48
  ResetButton: 'ResetButton',
46
49
  SaveProxiedCheckbox: 'SaveProxiedCheckbox',
47
50
  StaticFilesList: 'StaticFilesList',
@@ -73,7 +76,7 @@ init()
73
76
  function App([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
74
77
  return [
75
78
  r(Header, { cookies, comments, fallbackAddress, collectProxied }),
76
- r(MockList, { brokersByMethod }),
79
+ r(MockList, { brokersByMethod, canProxy: Boolean(fallbackAddress) }),
77
80
  r(StaticFilesList, { staticFiles })
78
81
  ]
79
82
  }
@@ -121,7 +124,7 @@ function CookieSelector({ cookies }) {
121
124
  function BulkSelector({ comments }) {
122
125
  // UX wise this should be a menu instead of this `select`.
123
126
  // But this way is easier to implement, with a few hacks.
124
- const firstOption = Strings.pick
127
+ const firstOption = Strings.pick_comment
125
128
  function onChange() {
126
129
  const value = this.value
127
130
  this.value = firstOption // Hack
@@ -135,19 +138,19 @@ function BulkSelector({ comments }) {
135
138
  : [firstOption].concat(comments)
136
139
  return (
137
140
  r('label', { className: CSS.Field },
138
- r('span', null, Strings.bulk_select_by_comment),
141
+ r('span', null, Strings.bulk_select),
139
142
  r('select', {
140
143
  className: CSS.BulkSelector,
141
144
  'data-qaid': 'BulkSelector',
142
145
  autocomplete: 'off',
143
146
  disabled,
144
- title: disabled ? Strings.bulk_select_by_comment_disabled_title : '',
147
+ title: disabled ? Strings.bulk_select_disabled_title : '',
145
148
  onChange
146
149
  }, list.map(value =>
147
150
  r('option', { value }, value)))))
148
151
  }
149
152
 
150
- function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
153
+ function ProxyFallbackField({ fallbackAddress, collectProxied }) {
151
154
  function onChange() {
152
155
  const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
153
156
  saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
@@ -155,12 +158,16 @@ function ProxyFallbackField({ fallbackAddress = '', collectProxied }) {
155
158
  if (!this.validity.valid)
156
159
  this.reportValidity()
157
160
  else
158
- mockaton.setProxyFallback(this.value.trim()).catch(onError)
161
+ mockaton.setProxyFallback(this.value.trim())
162
+ .then(init)
163
+ .catch(onError)
159
164
  }
160
165
  return (
161
166
  r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
162
167
  r('label', null,
163
- r('span', null, Strings.fallback_server),
168
+ r('span', null,
169
+ r(CloudIcon),
170
+ Strings.fallback_server),
164
171
  r('input', {
165
172
  type: 'url',
166
173
  autocomplete: 'none',
@@ -205,7 +212,7 @@ function ResetButton() {
205
212
 
206
213
  // MockList ===============
207
214
 
208
- function MockList({ brokersByMethod }) {
215
+ function MockList({ brokersByMethod, canProxy }) {
209
216
  const hasMocks = Object.keys(brokersByMethod).length
210
217
  if (!hasMocks)
211
218
  return (
@@ -214,12 +221,12 @@ function MockList({ brokersByMethod }) {
214
221
  return (
215
222
  r('main', { className: CSS.MockList },
216
223
  r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
217
- r(SectionByMethod, { method, brokers }))),
224
+ r(SectionByMethod, { method, brokers, canProxy }))),
218
225
  r(PayloadViewer)))
219
226
  }
220
227
 
221
228
 
222
- function SectionByMethod({ method, brokers }) {
229
+ function SectionByMethod({ method, brokers, canProxy }) {
223
230
  return (
224
231
  r('tbody', null,
225
232
  r('th', null, method),
@@ -231,7 +238,8 @@ function SectionByMethod({ method, brokers }) {
231
238
  r('td', null, r(PreviewLink, { method, urlMask })),
232
239
  r('td', null, r(MockSelector, { broker })),
233
240
  r('td', null, r(DelayRouteToggler, { broker })),
234
- r('td', null, r(InternalServerErrorToggler, { broker }))))))
241
+ r('td', null, r(InternalServerErrorToggler, { broker })),
242
+ r('td', null, r(ProxyToggler, { broker, disabled: !canProxy }))))))
235
243
  }
236
244
 
237
245
 
@@ -257,38 +265,35 @@ function PreviewLink({ method, urlMask }) {
257
265
 
258
266
 
259
267
  function MockSelector({ broker }) {
260
- function className(defaultIsSelected, status) {
261
- return cssClass(
262
- CSS.MockSelector,
263
- !defaultIsSelected && CSS.bold,
264
- status >= 400 && status < 500 && CSS.status4xx)
265
- }
266
-
267
268
  function onChange() {
268
- const { status, urlMask, method } = parseFilename(this.value)
269
+ const { urlMask, method } = parseFilename(this.value)
269
270
  this.style.fontWeight = this.value === this.options[0].value // default is selected
270
271
  ? 'normal'
271
272
  : 'bold'
272
273
  mockaton.select(this.value)
273
- .then(() => {
274
- linkFor(method, urlMask)?.click()
275
- checkbox500For(method, urlMask).checked = status === 500
276
- this.className = className(this.value === this.options[0].value, status)
277
- })
274
+ .then(init)
275
+ .then(() => linkFor(method, urlMask)?.click())
278
276
  .catch(onError)
279
277
  }
280
278
 
281
- const selected = broker.currentMock.file
279
+ let selected = broker.currentMock.file
282
280
  const { status, urlMask } = parseFilename(selected)
283
281
  const files = broker.mocks.filter(item =>
284
282
  status === 500 ||
285
283
  !item.includes(DEFAULT_500_COMMENT))
284
+ if (!selected) {
285
+ selected = Strings.proxied
286
+ files.push(selected)
287
+ }
286
288
 
287
289
  return (
288
290
  r('select', {
289
291
  'data-qaid': urlMask,
290
292
  autocomplete: 'off',
291
- className: className(selected === files[0], status),
293
+ className: cssClass(
294
+ CSS.MockSelector,
295
+ selected !== files[0] && CSS.bold,
296
+ status >= 400 && status < 500 && CSS.status4xx),
292
297
  disabled: files.length <= 1,
293
298
  onChange
294
299
  }, files.map(file =>
@@ -317,12 +322,6 @@ function DelayRouteToggler({ broker }) {
317
322
  onChange
318
323
  }),
319
324
  TimerIcon()))
320
-
321
- function TimerIcon() {
322
- return (
323
- r('svg', { viewBox: '0 0 24 24' },
324
- r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
325
- }
326
325
  }
327
326
 
328
327
 
@@ -353,6 +352,30 @@ function InternalServerErrorToggler({ broker }) {
353
352
  )
354
353
  }
355
354
 
355
+ function ProxyToggler({ broker, disabled }) {
356
+ function onChange() {
357
+ const { urlMask, method } = parseFilename(this.name)
358
+ mockaton.setRouteIsProxied(method, urlMask, this.checked)
359
+ .then(init)
360
+ .then(() => linkFor(method, urlMask)?.click())
361
+ .catch(onError)
362
+ }
363
+ return (
364
+ r('label', {
365
+ className: CSS.ProxyToggler,
366
+ title: Strings.proxy_toggler
367
+ },
368
+ r('input', {
369
+ type: 'checkbox',
370
+ disabled,
371
+ name: broker.currentMock.file,
372
+ checked: !broker.currentMock.file,
373
+ onChange
374
+ }),
375
+ r(CloudIcon)))
376
+ }
377
+
378
+
356
379
 
357
380
  // Payload Preview ===============
358
381
 
@@ -458,6 +481,19 @@ function onError(error) {
458
481
  }
459
482
 
460
483
 
484
+ function TimerIcon() {
485
+ return (
486
+ r('svg', { viewBox: '0 0 24 24' },
487
+ r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
488
+ }
489
+
490
+ function CloudIcon() {
491
+ return (
492
+ r('svg', { viewBox: '0 0 24 24' },
493
+ r('path', { d: 'M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.61 5.64 5.36 8.04 2.35 8.36 0 10.9 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96M19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4h2c0-2.76-1.86-5.08-4.4-5.78C8.61 6.88 10.2 6 12 6c3.03 0 5.5 2.47 5.5 5.5v.5H19c1.65 0 3 1.35 3 3s-1.35 3-3 3' })))
494
+ }
495
+
496
+
461
497
 
462
498
  // Utils ============
463
499
 
package/src/MockBroker.js CHANGED
@@ -60,6 +60,7 @@ export class MockBroker {
60
60
 
61
61
  get file() { return this.currentMock.file }
62
62
  get delay() { return this.currentMock.delay }
63
+ get proxied() { return !this.currentMock.file }
63
64
  get status() { return parseFilename(this.file).status }
64
65
  get temp500IsSelected() { return this.#isTemp500(this.file) }
65
66
 
@@ -85,6 +86,13 @@ export class MockBroker {
85
86
  updateFile(filename) { this.currentMock.file = filename }
86
87
  updateDelay(delayed) { this.currentMock.delay = Number(delayed) * config.delay }
87
88
 
89
+ updateProxied(proxied) {
90
+ if (proxied)
91
+ this.updateFile('')
92
+ else
93
+ this.selectDefaultFile()
94
+ }
95
+
88
96
  setByMatchingComment(comment) {
89
97
  for (const file of this.mocks)
90
98
  if (includesComment(file, comment)) {
@@ -12,7 +12,7 @@ import { sendInternalServerError, sendNotFound, sendBadRequest } from './utils/h
12
12
  export async function dispatchMock(req, response) {
13
13
  try {
14
14
  const broker = mockBrokerCollection.getBrokerForUrl(req.method, req.url)
15
- if (!broker) {
15
+ if (!broker || broker.proxied) {
16
16
  if (config.proxyFallback)
17
17
  await proxy(req, response)
18
18
  else
@@ -174,7 +174,7 @@ for (const [file, body] of staticFiles)
174
174
  const server = Mockaton({
175
175
  mocksDir: tmpDir,
176
176
  staticDir: staticTmpDir,
177
- delay: 40,
177
+ delay: 80,
178
178
  onReady: () => {},
179
179
  cookies: {
180
180
  userA: 'CookieA',
package/src/ProxyRelay.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { join } from 'node:path'
2
+ import { existsSync } from 'node:fs'
3
+ import { randomUUID } from 'node:crypto'
2
4
  import { write } from './utils/fs.js'
3
5
  import { config } from './config.js'
4
6
  import { extFor } from './utils/mime.js'
@@ -23,7 +25,9 @@ export async function proxy(req, response) {
23
25
 
24
26
  if (config.collectProxied) {
25
27
  const ext = extFor(proxyResponse.headers.get('content-type'))
26
- const filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
28
+ let filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
29
+ if (existsSync(join(config.mocksDir, filename))) // TESTME
30
+ filename = makeMockFilename(req.url + `(${randomUUID()})`, req.method, proxyResponse.status, ext)
27
31
  write(join(config.mocksDir, filename), body)
28
32
  }
29
33
  }
@@ -1,10 +1,10 @@
1
- import { join, resolve } from 'node:path'
1
+ import { join } from 'node:path'
2
2
  import fs, { readFileSync, realpathSync } from 'node:fs'
3
3
 
4
4
  import { config } from './config.js'
5
5
  import { mimeFor } from './utils/mime.js'
6
6
  import { isDirectory, isFile } from './utils/fs.js'
7
- import { sendNotFound, sendInternalServerError } from './utils/http-response.js'
7
+ import { sendInternalServerError } from './utils/http-response.js'
8
8
 
9
9
 
10
10
  export function isStatic(req) {
@@ -16,34 +16,27 @@ export function isStatic(req) {
16
16
 
17
17
  export async function dispatchStatic(req, response) {
18
18
  const file = resolvedAllowedPath(req.url)
19
- if (!file)
20
- sendNotFound(response)
21
- else if (req.headers.range)
19
+ if (req.headers.range)
22
20
  await sendPartialContent(response, req.headers.range, file)
23
- else
24
- sendFile(response, file)
25
- }
26
-
27
- function resolvedAllowedPath(url) {
28
- let candidate = resolve(join(config.staticDir, url))
29
- if (isDirectory(candidate))
30
- candidate = join(candidate, 'index.html')
31
- if (!isFile(candidate))
32
- return false
33
- candidate = realpathSync(candidate)
34
- if (candidate.startsWith(config.staticDir))
35
- return candidate
36
- }
37
-
38
- function sendFile(response, file) {
39
- if (!isFile(file))
40
- sendNotFound(response)
41
21
  else {
42
22
  response.setHeader('Content-Type', mimeFor(file))
43
23
  response.end(readFileSync(file, 'utf8'))
44
24
  }
45
25
  }
46
26
 
27
+ function resolvedAllowedPath(url) {
28
+ try {
29
+ let candidate = realpathSync(join(config.staticDir, url))
30
+ if (!candidate.startsWith(config.staticDir))
31
+ return false
32
+ if (isDirectory(candidate))
33
+ candidate = join(candidate, 'index.html')
34
+ if (isFile(candidate))
35
+ return candidate
36
+ }
37
+ catch {}
38
+ }
39
+
47
40
  async function sendPartialContent(response, range, file) {
48
41
  const { size } = await fs.promises.lstat(file)
49
42
  let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
@@ -104,9 +104,16 @@ export function extractAllComments() {
104
104
  }
105
105
 
106
106
  export function setMocksMatchingComment(comment) {
107
- forEachBroker(broker => broker.setByMatchingComment(comment))
107
+ forEachBroker(broker =>
108
+ broker.setByMatchingComment(comment))
108
109
  }
109
110
 
111
+ export function ensureAllRoutesHaveSelectedMock() {
112
+ forEachBroker(broker => {
113
+ if (broker.proxied)
114
+ broker.selectDefaultFile()
115
+ })
116
+ }
110
117
 
111
118
  function forEachBroker(fn) {
112
119
  for (const brokers of Object.values(collection))
package/src/utils/fs.js CHANGED
@@ -14,6 +14,11 @@ export const listFilesRecursively = dir => {
14
14
  }
15
15
 
16
16
  export const write = (path, body) => {
17
- mkdirSync(dirname(path), { recursive: true })
18
- writeFileSync(path, body)
17
+ try {
18
+ mkdirSync(dirname(path), { recursive: true })
19
+ writeFileSync(path, body)
20
+ }
21
+ catch (err) {
22
+ console.error('Write access denied', err)
23
+ }
19
24
  }