mockaton 8.8.1 → 8.9.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 +3 -3
- package/TODO.md +3 -1
- package/package.json +1 -1
- package/src/Api.js +86 -114
- package/src/ApiConstants.js +4 -1
- package/src/Commander.js +7 -1
- package/src/Dashboard.css +40 -28
- package/src/Dashboard.js +61 -41
- package/src/MockBroker.js +2 -3
- package/src/MockDispatcher.js +3 -3
- package/src/Mockaton.js +35 -36
- package/src/Mockaton.test.js +1 -1
- package/src/StaticDispatcher.js +9 -14
- package/src/Watcher.js +48 -0
- package/src/mockBrokersCollection.js +9 -5
- package/src/mockaton-logo.svg +1 -1
- package/src/utils/http-request.js +1 -1
- package/src/utils/http-response.js +0 -6
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ other features such as delaying responses, or triggering an autogenerated
|
|
|
23
23
|
which is handy for setting up tests (see **Commander API** below).
|
|
24
24
|
|
|
25
25
|
<picture>
|
|
26
|
-
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.
|
|
27
|
-
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.
|
|
28
|
-
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.
|
|
26
|
+
<source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp830x800.light.gold.png">
|
|
27
|
+
<source media="(prefers-color-scheme: dark)" srcset="./pixaton-tests/pic-for-readme.vp830x800.dark.gold.png">
|
|
28
|
+
<img alt="Mockaton Dashboard" src="./pixaton-tests/pic-for-readme.vp830x800.light.gold.png">
|
|
29
29
|
</picture>
|
|
30
30
|
|
|
31
31
|
|
package/TODO.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# TODO
|
|
2
2
|
|
|
3
3
|
- Refactor tests
|
|
4
|
-
-
|
|
4
|
+
- Preserve focus when refreshing dashboard `init()`
|
|
5
|
+
- More real-time updates. Currently, it's only for add/remove mock but not for
|
|
6
|
+
static files and changes from another client (Browser, or Commander).
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { cookie } from './cookie.js'
|
|
8
8
|
import { config } from './config.js'
|
|
9
|
-
import { DF, API } from './ApiConstants.js'
|
|
10
9
|
import { parseJSON } from './utils/http-request.js'
|
|
11
10
|
import { listFilesRecursively } from './utils/fs.js'
|
|
12
11
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
13
|
-
import {
|
|
12
|
+
import { DF, API, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
|
|
13
|
+
import { countAR_Events, subscribeAR_EventListener, unsubscribeAR_EventListener } from './Watcher.js'
|
|
14
|
+
import { sendOK, sendJSON, sendUnprocessableContent, sendDashboardFile, sendForbidden } from './utils/http-response.js'
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
const dashboardAssets = [
|
|
@@ -26,25 +27,26 @@ export const apiGetRequests = new Map([
|
|
|
26
27
|
[API.dashboard, serveDashboard],
|
|
27
28
|
...dashboardAssets.map(f =>
|
|
28
29
|
[API.dashboard + f, serveDashboardAsset]),
|
|
30
|
+
[API.cors, getIsCorsAllowed],
|
|
31
|
+
[API.static, listStaticFiles],
|
|
29
32
|
[API.mocks, listMockBrokers],
|
|
30
33
|
[API.cookies, listCookies],
|
|
31
|
-
[API.comments, listComments],
|
|
32
34
|
[API.fallback, getProxyFallback],
|
|
33
|
-
[API.
|
|
34
|
-
[API.
|
|
35
|
-
[API.
|
|
35
|
+
[API.arEvents, longPollAR_Events],
|
|
36
|
+
[API.comments, listComments],
|
|
37
|
+
[API.collectProxied, getCollectProxied]
|
|
36
38
|
])
|
|
37
39
|
|
|
38
40
|
export const apiPatchRequests = new Map([
|
|
39
|
-
[API.
|
|
41
|
+
[API.cors, setCorsAllowed],
|
|
40
42
|
[API.delay, setRouteIsDelayed],
|
|
41
|
-
[API.proxied, setRouteIsProxied],
|
|
42
43
|
[API.reset, reinitialize],
|
|
44
|
+
[API.select, selectMock],
|
|
45
|
+
[API.proxied, setRouteIsProxied],
|
|
43
46
|
[API.cookies, selectCookie],
|
|
44
47
|
[API.fallback, updateProxyFallback],
|
|
45
|
-
[API.collectProxied, setCollectProxied],
|
|
46
48
|
[API.bulkSelect, bulkUpdateBrokersByCommentTag],
|
|
47
|
-
[API.
|
|
49
|
+
[API.collectProxied, setCollectProxied]
|
|
48
50
|
])
|
|
49
51
|
|
|
50
52
|
/* === GET === */
|
|
@@ -52,6 +54,7 @@ export const apiPatchRequests = new Map([
|
|
|
52
54
|
function serveDashboard(_, response) {
|
|
53
55
|
sendDashboardFile(response, join(import.meta.dirname, 'Dashboard.html'))
|
|
54
56
|
}
|
|
57
|
+
|
|
55
58
|
function serveDashboardAsset(req, response) {
|
|
56
59
|
const f = req.url.replace(API.dashboard, '')
|
|
57
60
|
if (dashboardAssets.includes(f))
|
|
@@ -68,18 +71,27 @@ function getIsCorsAllowed(_, response) { sendJSON(response, config.corsAllowed)
|
|
|
68
71
|
function getCollectProxied(_, response) { sendJSON(response, config.collectProxied) }
|
|
69
72
|
|
|
70
73
|
function listStaticFiles(req, response) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
const files = config.staticDir
|
|
75
|
+
? listFilesRecursively(config.staticDir).filter(f => !config.ignore.test(f))
|
|
76
|
+
: []
|
|
77
|
+
sendJSON(response, files)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function longPollAR_Events(req, response) {
|
|
81
|
+
function onAddOrRemoveMock() {
|
|
82
|
+
unsubscribeAR_EventListener(onAddOrRemoveMock)
|
|
83
|
+
sendJSON(response, countAR_Events())
|
|
84
|
+
}
|
|
85
|
+
response.setTimeout(LONG_POLL_SERVER_TIMEOUT, onAddOrRemoveMock)
|
|
86
|
+
req.on('error', () => {
|
|
87
|
+
unsubscribeAR_EventListener(onAddOrRemoveMock)
|
|
88
|
+
response.destroy()
|
|
89
|
+
})
|
|
90
|
+
subscribeAR_EventListener(onAddOrRemoveMock)
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
|
|
94
|
+
|
|
83
95
|
/* === PATCH === */
|
|
84
96
|
|
|
85
97
|
function reinitialize(_, response) {
|
|
@@ -88,124 +100,84 @@ function reinitialize(_, response) {
|
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
async function selectCookie(req, response) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
sendOK(response)
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
sendBadRequest(response, error)
|
|
100
|
-
}
|
|
103
|
+
const error = cookie.setCurrent(await parseJSON(req))
|
|
104
|
+
if (error)
|
|
105
|
+
sendUnprocessableContent(response, error)
|
|
106
|
+
else
|
|
107
|
+
sendOK(response)
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
async function selectMock(req, response) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
sendOK(response)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
sendBadRequest(response, error)
|
|
111
|
+
const file = await parseJSON(req)
|
|
112
|
+
const broker = mockBrokersCollection.getBrokerByFilename(file)
|
|
113
|
+
if (!broker || !broker.hasMock(file))
|
|
114
|
+
sendUnprocessableContent(response, `Missing Mock: ${file}`)
|
|
115
|
+
else {
|
|
116
|
+
broker.updateFile(file)
|
|
117
|
+
sendOK(response)
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
async function setRouteIsDelayed(req, response) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
sendOK(response)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
sendBadRequest(response, error)
|
|
122
|
+
const body = await parseJSON(req)
|
|
123
|
+
const delayed = body[DF.delayed]
|
|
124
|
+
const broker = mockBrokersCollection.getBrokerForUrl(
|
|
125
|
+
body[DF.routeMethod],
|
|
126
|
+
body[DF.routeUrlMask])
|
|
127
|
+
|
|
128
|
+
if (!broker) // TESTME
|
|
129
|
+
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`)
|
|
130
|
+
else if (typeof delayed !== 'boolean')
|
|
131
|
+
sendUnprocessableContent(response, `Expected a boolean for "delayed"`) // TESTME
|
|
132
|
+
else {
|
|
133
|
+
broker.updateDelay(body[DF.delayed])
|
|
134
|
+
sendOK(response)
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
async function setRouteIsProxied(req, response) { // TESTME
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
sendOK(response)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
sendBadRequest(response, error)
|
|
139
|
+
const body = await parseJSON(req)
|
|
140
|
+
const proxied = body[DF.proxied]
|
|
141
|
+
const broker = mockBrokersCollection.getBrokerForUrl(
|
|
142
|
+
body[DF.routeMethod],
|
|
143
|
+
body[DF.routeUrlMask])
|
|
144
|
+
|
|
145
|
+
if (!broker)
|
|
146
|
+
sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`)
|
|
147
|
+
else if (typeof proxied !== 'boolean')
|
|
148
|
+
sendUnprocessableContent(response, `Expected a boolean for "proxied"`)
|
|
149
|
+
else if (proxied && !config.proxyFallback)
|
|
150
|
+
sendUnprocessableContent(response, `There’s no proxy fallback`)
|
|
151
|
+
else {
|
|
152
|
+
broker.updateProxied(proxied)
|
|
153
|
+
sendOK(response)
|
|
162
154
|
}
|
|
163
155
|
}
|
|
164
156
|
|
|
165
157
|
async function updateProxyFallback(req, response) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
sendOK(response)
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
sendBadRequest(response, error)
|
|
179
|
-
}
|
|
158
|
+
const fallback = await parseJSON(req)
|
|
159
|
+
if (fallback && !URL.canParse(fallback)) {
|
|
160
|
+
sendUnprocessableContent(response)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (!fallback) // TESTME
|
|
164
|
+
mockBrokersCollection.ensureAllRoutesHaveSelectedMock()
|
|
165
|
+
config.proxyFallback = fallback
|
|
166
|
+
sendOK(response)
|
|
180
167
|
}
|
|
181
168
|
|
|
182
169
|
async function setCollectProxied(req, response) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
sendOK(response)
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
sendBadRequest(response, error)
|
|
189
|
-
}
|
|
170
|
+
config.collectProxied = await parseJSON(req)
|
|
171
|
+
sendOK(response)
|
|
190
172
|
}
|
|
191
173
|
|
|
192
174
|
async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
sendOK(response)
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
sendBadRequest(response, error)
|
|
199
|
-
}
|
|
175
|
+
mockBrokersCollection.setMocksMatchingComment(await parseJSON(req))
|
|
176
|
+
sendOK(response)
|
|
200
177
|
}
|
|
201
178
|
|
|
202
179
|
async function setCorsAllowed(req, response) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
sendOK(response)
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
sendBadRequest(response, error)
|
|
209
|
-
}
|
|
180
|
+
config.corsAllowed = await parseJSON(req)
|
|
181
|
+
sendOK(response)
|
|
210
182
|
}
|
|
211
183
|
|
package/src/ApiConstants.js
CHANGED
|
@@ -12,7 +12,8 @@ export const API = {
|
|
|
12
12
|
collectProxied: MOUNT + '/collect-proxied',
|
|
13
13
|
proxied: MOUNT + '/proxied',
|
|
14
14
|
cors: MOUNT + '/cors',
|
|
15
|
-
static: MOUNT + '/static'
|
|
15
|
+
static: MOUNT + '/static',
|
|
16
|
+
arEvents: MOUNT + '/ar_events'
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const DF = { // Dashboard Fields (XHR)
|
|
@@ -25,3 +26,5 @@ export const DF = { // Dashboard Fields (XHR)
|
|
|
25
26
|
export const DEFAULT_500_COMMENT = '(Mockaton 500)'
|
|
26
27
|
export const DEFAULT_MOCK_COMMENT = '(default)'
|
|
27
28
|
export const EXT_FOR_UNKNOWN_MIME = 'unknown'
|
|
29
|
+
|
|
30
|
+
export const LONG_POLL_SERVER_TIMEOUT = 8_000
|
package/src/Commander.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { API, DF } from './ApiConstants.js'
|
|
1
|
+
import { API, DF, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
// Client for controlling Mockaton via its HTTP API
|
|
@@ -84,4 +84,10 @@ export class Commander {
|
|
|
84
84
|
reset() {
|
|
85
85
|
return this.#patch(API.reset)
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
getAR_EventsCount() {
|
|
89
|
+
return fetch(API.arEvents, {
|
|
90
|
+
signal: AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
87
93
|
}
|
package/src/Dashboard.css
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
:root {
|
|
2
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
|
-
--radius:
|
|
3
|
+
--radius: 4px;
|
|
4
|
+
--radiusSmall: 2px;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
@media (prefers-color-scheme: light) {
|
|
7
8
|
:root {
|
|
8
9
|
--color4xxBackground: #ffedd1;
|
|
9
10
|
--colorAccent: #0075db;
|
|
10
|
-
--colorAccentAlt: #
|
|
11
|
+
--colorAccentAlt: #068185;
|
|
11
12
|
--colorBackground: #fff;
|
|
12
13
|
--colorComboBoxHeaderBackground: #fff;
|
|
13
14
|
--colorComboBoxBackground: #f7f7f7;
|
|
14
15
|
--colorHeaderBackground: #f3f3f3;
|
|
15
|
-
--colorSecondaryButtonBackground:
|
|
16
|
+
--colorSecondaryButtonBackground: #f3f3f3;
|
|
16
17
|
--colorSecondaryAction: #555;
|
|
17
18
|
--colorDisabledMockSelector: #444;
|
|
18
19
|
--colorHover: #dfefff;
|
|
@@ -77,18 +78,22 @@ a, button, input[type=checkbox] {
|
|
|
77
78
|
|
|
78
79
|
select {
|
|
79
80
|
font-size: 100%;
|
|
80
|
-
background: var(--colorComboBoxBackground);
|
|
81
81
|
color: var(--colorText);
|
|
82
82
|
cursor: pointer;
|
|
83
83
|
outline: 0;
|
|
84
84
|
border-radius: var(--radius);
|
|
85
|
+
appearance: none;
|
|
86
|
+
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888888'><path d='M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z'/></svg>") no-repeat;
|
|
87
|
+
background-color: var(--colorComboBoxBackground);
|
|
88
|
+
background-size: 16px;
|
|
89
|
+
background-position: 100% center;
|
|
85
90
|
|
|
86
91
|
&:enabled {
|
|
87
92
|
box-shadow: var(--boxShadow1);
|
|
88
93
|
}
|
|
89
94
|
&:enabled:hover {
|
|
90
95
|
cursor: pointer;
|
|
91
|
-
background: var(--colorHover);
|
|
96
|
+
background-color: var(--colorHover);
|
|
92
97
|
}
|
|
93
98
|
&:disabled {
|
|
94
99
|
cursor: not-allowed;
|
|
@@ -112,11 +117,11 @@ select {
|
|
|
112
117
|
img {
|
|
113
118
|
width: 130px;
|
|
114
119
|
align-self: center;
|
|
115
|
-
margin-right:
|
|
120
|
+
margin-right: 20px;
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
.Field {
|
|
119
|
-
width:
|
|
124
|
+
width: 132px;
|
|
120
125
|
|
|
121
126
|
span {
|
|
122
127
|
display: flex;
|
|
@@ -130,11 +135,11 @@ select {
|
|
|
130
135
|
select {
|
|
131
136
|
width: 100%;
|
|
132
137
|
height: 28px;
|
|
133
|
-
padding: 4px
|
|
138
|
+
padding: 4px 8px;
|
|
134
139
|
border-right: 3px solid transparent;
|
|
135
140
|
margin-top: 4px;
|
|
136
141
|
font-size: 11px;
|
|
137
|
-
background: var(--colorComboBoxHeaderBackground);
|
|
142
|
+
background-color: var(--colorComboBoxHeaderBackground);
|
|
138
143
|
border-radius: var(--radius);
|
|
139
144
|
}
|
|
140
145
|
|
|
@@ -171,25 +176,26 @@ select {
|
|
|
171
176
|
gap: 4px;
|
|
172
177
|
|
|
173
178
|
input:disabled + span {
|
|
174
|
-
opacity: 0.
|
|
179
|
+
opacity: 0.8;
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
.BulkSelector {
|
|
181
|
-
|
|
182
|
-
text-align: center;
|
|
186
|
+
background-image: none;
|
|
187
|
+
text-align-last: center;
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
.ResetButton {
|
|
186
|
-
padding:
|
|
191
|
+
padding: 6px 12px;
|
|
187
192
|
border: 1px solid var(--colorRed);
|
|
188
|
-
margin-
|
|
193
|
+
margin-left: 4px;
|
|
189
194
|
background: transparent;
|
|
190
195
|
color: var(--colorRed);
|
|
191
196
|
border-radius: 50px;
|
|
192
197
|
|
|
198
|
+
|
|
193
199
|
&:hover {
|
|
194
200
|
background: var(--colorRed);
|
|
195
201
|
color: white;
|
|
@@ -255,7 +261,7 @@ select {
|
|
|
255
261
|
position: relative;
|
|
256
262
|
left: -6px;
|
|
257
263
|
display: inline-block;
|
|
258
|
-
width:
|
|
264
|
+
width: 278px;
|
|
259
265
|
padding: 8px 6px;
|
|
260
266
|
border-radius: var(--radius);
|
|
261
267
|
color: var(--colorAccent);
|
|
@@ -274,12 +280,17 @@ select {
|
|
|
274
280
|
width: 260px;
|
|
275
281
|
height: 30px;
|
|
276
282
|
border: 0;
|
|
277
|
-
|
|
283
|
+
padding-right: 5px;
|
|
278
284
|
text-align: right;
|
|
279
285
|
direction: rtl;
|
|
280
286
|
text-overflow: ellipsis;
|
|
281
287
|
font-size: 12px;
|
|
288
|
+
background-position: 2px center;
|
|
282
289
|
|
|
290
|
+
&.nonDefault {
|
|
291
|
+
font-weight: bold;
|
|
292
|
+
font-size: 0.92rem;
|
|
293
|
+
}
|
|
283
294
|
&.status4xx {
|
|
284
295
|
background: var(--color4xxBackground);
|
|
285
296
|
}
|
|
@@ -315,32 +326,36 @@ select {
|
|
|
315
326
|
}
|
|
316
327
|
|
|
317
328
|
&:checked ~ svg {
|
|
329
|
+
border-color: transparent;
|
|
318
330
|
background: var(--colorAccent);
|
|
319
331
|
fill: white;
|
|
332
|
+
box-shadow: var(--boxShadow1);
|
|
320
333
|
}
|
|
321
334
|
|
|
322
335
|
&:disabled ~ svg {
|
|
323
336
|
opacity: .5;
|
|
324
337
|
cursor: not-allowed;
|
|
338
|
+
box-shadow: none;
|
|
325
339
|
}
|
|
326
340
|
}
|
|
327
341
|
|
|
328
342
|
> svg {
|
|
329
|
-
width:
|
|
330
|
-
height:
|
|
343
|
+
width: 18px;
|
|
344
|
+
height: 18px;
|
|
331
345
|
vertical-align: bottom;
|
|
332
346
|
fill: var(--colorSecondaryAction);
|
|
333
347
|
border-radius: 50%;
|
|
334
348
|
background: var(--colorSecondaryButtonBackground);
|
|
335
|
-
box-shadow: var(--boxShadow1);
|
|
336
349
|
}
|
|
337
350
|
}
|
|
338
351
|
|
|
339
352
|
.ProxyToggler {
|
|
353
|
+
margin-left: 4px;
|
|
340
354
|
> svg {
|
|
341
|
-
width:
|
|
342
|
-
padding:
|
|
343
|
-
border-
|
|
355
|
+
width: 22px;
|
|
356
|
+
padding: 1px;
|
|
357
|
+
border-color: transparent;
|
|
358
|
+
border-radius: var(--radiusSmall);
|
|
344
359
|
}
|
|
345
360
|
}
|
|
346
361
|
|
|
@@ -362,18 +377,19 @@ select {
|
|
|
362
377
|
&:checked ~ span {
|
|
363
378
|
color: white;
|
|
364
379
|
background: var(--colorRed);
|
|
380
|
+
box-shadow: var(--boxShadow1);
|
|
365
381
|
}
|
|
366
382
|
}
|
|
367
383
|
|
|
368
384
|
> span {
|
|
369
385
|
padding: 5px 4px;
|
|
370
|
-
box-shadow: var(--boxShadow1);
|
|
371
386
|
font-size: 10px;
|
|
372
387
|
color: var(--colorSecondaryAction);
|
|
373
|
-
border-radius:
|
|
388
|
+
border-radius: var(--radiusSmall);
|
|
374
389
|
background: var(--colorSecondaryButtonBackground);
|
|
375
390
|
|
|
376
391
|
&:hover {
|
|
392
|
+
box-shadow: var(--boxShadow1);
|
|
377
393
|
background: var(--colorLightRed);
|
|
378
394
|
color: var(--colorRed);
|
|
379
395
|
}
|
|
@@ -406,10 +422,6 @@ select {
|
|
|
406
422
|
}
|
|
407
423
|
}
|
|
408
424
|
|
|
409
|
-
.bold {
|
|
410
|
-
font-weight: bold;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
425
|
.StaticFilesList {
|
|
414
426
|
margin-top: 20px;
|
|
415
427
|
|
package/src/Dashboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { DEFAULT_500_COMMENT } from './ApiConstants.js'
|
|
1
2
|
import { parseFilename } from './Filename.js'
|
|
2
3
|
import { Commander } from './Commander.js'
|
|
3
|
-
import { DEFAULT_500_COMMENT } from './ApiConstants.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
function syntaxHighlightJson(textBody) {
|
|
@@ -10,13 +10,12 @@ function syntaxHighlightJson(textBody) {
|
|
|
10
10
|
: false
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
const Strings = {
|
|
15
14
|
bulk_select: 'Bulk Select',
|
|
16
15
|
bulk_select_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
|
|
17
16
|
click_link_to_preview: 'Click a link to preview it',
|
|
18
17
|
cookie: 'Cookie',
|
|
19
|
-
cookie_disabled_title: 'No cookies specified in
|
|
18
|
+
cookie_disabled_title: 'No cookies specified in config.cookies',
|
|
20
19
|
delay: 'Delay',
|
|
21
20
|
empty_response_body: '/* Empty Response Body */',
|
|
22
21
|
fallback_server: 'Fallback Backend',
|
|
@@ -50,16 +49,22 @@ const CSS = {
|
|
|
50
49
|
SaveProxiedCheckbox: 'SaveProxiedCheckbox',
|
|
51
50
|
StaticFilesList: 'StaticFilesList',
|
|
52
51
|
|
|
53
|
-
bold: 'bold',
|
|
54
52
|
empty: 'empty',
|
|
55
53
|
chosen: 'chosen',
|
|
56
|
-
status4xx: 'status4xx'
|
|
54
|
+
status4xx: 'status4xx',
|
|
55
|
+
nonDefault: 'nonDefault'
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
const r = createElement
|
|
60
|
-
|
|
61
59
|
const mockaton = new Commander(window.location.origin)
|
|
62
60
|
|
|
61
|
+
init()
|
|
62
|
+
pollAR_Events() // Add or Remove Mocks from File System
|
|
63
|
+
document.addEventListener('visibilitychange', () => {
|
|
64
|
+
if (!document.hidden)
|
|
65
|
+
pollAR_Events()
|
|
66
|
+
})
|
|
67
|
+
|
|
63
68
|
function init() {
|
|
64
69
|
return Promise.all([
|
|
65
70
|
mockaton.listMocks(),
|
|
@@ -69,20 +74,18 @@ function init() {
|
|
|
69
74
|
mockaton.getProxyFallback(),
|
|
70
75
|
mockaton.listStaticFiles()
|
|
71
76
|
].map(api => api.then(response => response.ok && response.json())))
|
|
72
|
-
.then(data => document.body.replaceChildren(
|
|
77
|
+
.then(data => document.body.replaceChildren(App(data)))
|
|
73
78
|
.catch(onError)
|
|
74
79
|
}
|
|
75
|
-
init()
|
|
76
80
|
|
|
77
81
|
function App([brokersByMethod, cookies, comments, collectProxied, fallbackAddress, staticFiles]) {
|
|
78
|
-
return
|
|
79
|
-
r(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
return (
|
|
83
|
+
r('div', null,
|
|
84
|
+
r(Header, { cookies, comments, fallbackAddress, collectProxied }),
|
|
85
|
+
r(MockList, { brokersByMethod, canProxy: Boolean(fallbackAddress) }),
|
|
86
|
+
r(StaticFilesList, { staticFiles })))
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
|
|
86
89
|
// Header ===============
|
|
87
90
|
|
|
88
91
|
function Header({ cookies, comments, fallbackAddress, collectProxied }) {
|
|
@@ -106,8 +109,7 @@ function Logo() {
|
|
|
106
109
|
|
|
107
110
|
function CookieSelector({ cookies }) {
|
|
108
111
|
function onChange() {
|
|
109
|
-
mockaton.selectCookie(this.value)
|
|
110
|
-
.catch(onError)
|
|
112
|
+
mockaton.selectCookie(this.value).catch(onError)
|
|
111
113
|
}
|
|
112
114
|
const disabled = cookies.length <= 1
|
|
113
115
|
return (
|
|
@@ -166,9 +168,7 @@ function ProxyFallbackField({ fallbackAddress, collectProxied }) {
|
|
|
166
168
|
return (
|
|
167
169
|
r('div', { className: cssClass(CSS.Field, CSS.FallbackBackend) },
|
|
168
170
|
r('label', null,
|
|
169
|
-
r('span', null,
|
|
170
|
-
r(CloudIcon),
|
|
171
|
-
Strings.fallback_server),
|
|
171
|
+
r('span', null, r(CloudIcon), Strings.fallback_server),
|
|
172
172
|
r('input', {
|
|
173
173
|
type: 'url',
|
|
174
174
|
autocomplete: 'none',
|
|
@@ -268,9 +268,6 @@ function PreviewLink({ method, urlMask }) {
|
|
|
268
268
|
function MockSelector({ broker }) {
|
|
269
269
|
function onChange() {
|
|
270
270
|
const { urlMask, method } = parseFilename(this.value)
|
|
271
|
-
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
272
|
-
? 'normal'
|
|
273
|
-
: 'bold'
|
|
274
271
|
mockaton.select(this.value)
|
|
275
272
|
.then(init)
|
|
276
273
|
.then(() => linkFor(method, urlMask)?.click())
|
|
@@ -289,14 +286,14 @@ function MockSelector({ broker }) {
|
|
|
289
286
|
|
|
290
287
|
return (
|
|
291
288
|
r('select', {
|
|
292
|
-
|
|
289
|
+
onChange,
|
|
293
290
|
autocomplete: 'off',
|
|
291
|
+
'data-qaid': urlMask,
|
|
292
|
+
disabled: files.length <= 1,
|
|
294
293
|
className: cssClass(
|
|
295
294
|
CSS.MockSelector,
|
|
296
|
-
selected !== files[0] && CSS.
|
|
297
|
-
status >= 400 && status < 500 && CSS.status4xx)
|
|
298
|
-
disabled: files.length <= 1,
|
|
299
|
-
onChange
|
|
295
|
+
selected !== files[0] && CSS.nonDefault,
|
|
296
|
+
status >= 400 && status < 500 && CSS.status4xx)
|
|
300
297
|
}, files.map(file =>
|
|
301
298
|
r('option', {
|
|
302
299
|
value: file,
|
|
@@ -304,12 +301,10 @@ function MockSelector({ broker }) {
|
|
|
304
301
|
}, file))))
|
|
305
302
|
}
|
|
306
303
|
|
|
307
|
-
|
|
308
304
|
function DelayRouteToggler({ broker }) {
|
|
309
305
|
function onChange() {
|
|
310
|
-
const { method, urlMask } = parseFilename(
|
|
311
|
-
mockaton.setRouteIsDelayed(method, urlMask, this.checked)
|
|
312
|
-
.catch(onError)
|
|
306
|
+
const { method, urlMask } = parseFilename(broker.mocks[0])
|
|
307
|
+
mockaton.setRouteIsDelayed(method, urlMask, this.checked).catch(onError)
|
|
313
308
|
}
|
|
314
309
|
return (
|
|
315
310
|
r('label', {
|
|
@@ -318,7 +313,6 @@ function DelayRouteToggler({ broker }) {
|
|
|
318
313
|
},
|
|
319
314
|
r('input', {
|
|
320
315
|
type: 'checkbox',
|
|
321
|
-
name: broker.currentMock.file,
|
|
322
316
|
checked: Boolean(broker.currentMock.delay),
|
|
323
317
|
onChange
|
|
324
318
|
}),
|
|
@@ -348,14 +342,13 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
348
342
|
checked: parseFilename(broker.currentMock.file).status === 500,
|
|
349
343
|
onChange
|
|
350
344
|
}),
|
|
351
|
-
r('span', null, '500')
|
|
352
|
-
)
|
|
353
|
-
)
|
|
345
|
+
r('span', null, '500')))
|
|
354
346
|
}
|
|
355
347
|
|
|
348
|
+
|
|
356
349
|
function ProxyToggler({ broker, disabled }) {
|
|
357
350
|
function onChange() {
|
|
358
|
-
const { urlMask, method } = parseFilename(
|
|
351
|
+
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
359
352
|
mockaton.setRouteIsProxied(method, urlMask, this.checked)
|
|
360
353
|
.then(init)
|
|
361
354
|
.then(() => linkFor(method, urlMask)?.click())
|
|
@@ -369,7 +362,6 @@ function ProxyToggler({ broker, disabled }) {
|
|
|
369
362
|
r('input', {
|
|
370
363
|
type: 'checkbox',
|
|
371
364
|
disabled,
|
|
372
|
-
name: broker.currentMock.file,
|
|
373
365
|
checked: !broker.currentMock.file,
|
|
374
366
|
onChange
|
|
375
367
|
}),
|
|
@@ -490,6 +482,7 @@ function StaticFilesList({ staticFiles }) {
|
|
|
490
482
|
}
|
|
491
483
|
|
|
492
484
|
|
|
485
|
+
// Misc ===============
|
|
493
486
|
|
|
494
487
|
function onError(error) {
|
|
495
488
|
if (error?.message === 'Failed to fetch')
|
|
@@ -497,7 +490,6 @@ function onError(error) {
|
|
|
497
490
|
console.error(error)
|
|
498
491
|
}
|
|
499
492
|
|
|
500
|
-
|
|
501
493
|
function TimerIcon() {
|
|
502
494
|
return (
|
|
503
495
|
r('svg', { viewBox: '0 0 24 24' },
|
|
@@ -511,11 +503,39 @@ function CloudIcon() {
|
|
|
511
503
|
}
|
|
512
504
|
|
|
513
505
|
|
|
506
|
+
// AR Events (Add or Remove mock) ============
|
|
507
|
+
|
|
508
|
+
pollAR_Events.isPolling = false
|
|
509
|
+
pollAR_Events.oldAR_EventsCount = 0
|
|
510
|
+
async function pollAR_Events() {
|
|
511
|
+
if (pollAR_Events.isPolling || document.hidden)
|
|
512
|
+
return
|
|
513
|
+
try {
|
|
514
|
+
pollAR_Events.isPolling = true
|
|
515
|
+
const response = await mockaton.getAR_EventsCount()
|
|
516
|
+
if (response.ok) {
|
|
517
|
+
const nAR_Events = await response.json()
|
|
518
|
+
if (pollAR_Events.oldAR_EventsCount !== nAR_Events) { // because it could be < or >
|
|
519
|
+
pollAR_Events.oldAR_EventsCount = nAR_Events
|
|
520
|
+
await init()
|
|
521
|
+
}
|
|
522
|
+
pollAR_Events.isPolling = false
|
|
523
|
+
pollAR_Events()
|
|
524
|
+
}
|
|
525
|
+
else
|
|
526
|
+
throw response.status
|
|
527
|
+
}
|
|
528
|
+
catch (_) {
|
|
529
|
+
pollAR_Events.isPolling = false
|
|
530
|
+
setTimeout(pollAR_Events, 5000)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
514
534
|
|
|
515
535
|
// Utils ============
|
|
516
536
|
|
|
517
537
|
function cssClass(...args) {
|
|
518
|
-
return args.filter(
|
|
538
|
+
return args.filter(Boolean).join(' ')
|
|
519
539
|
}
|
|
520
540
|
|
|
521
541
|
|
|
@@ -542,7 +562,7 @@ function createElement(elem, props = null, ...children) {
|
|
|
542
562
|
node[key] = value
|
|
543
563
|
else
|
|
544
564
|
node.setAttribute(key, value)
|
|
545
|
-
node.append(...children.flat().filter(
|
|
565
|
+
node.append(...children.flat().filter(Boolean))
|
|
546
566
|
return node
|
|
547
567
|
}
|
|
548
568
|
|
|
@@ -550,7 +570,7 @@ function createSvgElement(tagName, props, ...children) {
|
|
|
550
570
|
const elem = document.createElementNS('http://www.w3.org/2000/svg', tagName)
|
|
551
571
|
for (const [key, value] of Object.entries(props))
|
|
552
572
|
elem.setAttribute(key, value)
|
|
553
|
-
elem.append(...children.flat().filter(
|
|
573
|
+
elem.append(...children.flat().filter(Boolean))
|
|
554
574
|
return elem
|
|
555
575
|
}
|
|
556
576
|
|
package/src/MockBroker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { config } from './config.js'
|
|
2
|
-
import { DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
3
2
|
import { includesComment, extractComments, parseFilename } from './Filename.js'
|
|
3
|
+
import { DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// MockBroker is a state for a particular route. It knows the available mock files
|
|
@@ -70,10 +70,9 @@ export class MockBroker {
|
|
|
70
70
|
this.mocks.sort()
|
|
71
71
|
const defaults = this.mocks.filter(file => includesComment(file, DEFAULT_MOCK_COMMENT))
|
|
72
72
|
const temp500 = this.mocks.filter(file => includesComment(file, DEFAULT_500_COMMENT))
|
|
73
|
-
this.mocks = this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file))
|
|
74
73
|
this.mocks = [
|
|
75
74
|
...defaults,
|
|
76
|
-
...this.mocks,
|
|
75
|
+
...this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file)),
|
|
77
76
|
...temp500
|
|
78
77
|
]
|
|
79
78
|
}
|
package/src/MockDispatcher.js
CHANGED
|
@@ -4,9 +4,9 @@ import { proxy } from './ProxyRelay.js'
|
|
|
4
4
|
import { cookie } from './cookie.js'
|
|
5
5
|
import { config } from './config.js'
|
|
6
6
|
import { applyPlugins } from './MockDispatcherPlugins.js'
|
|
7
|
-
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
8
7
|
import { BodyReaderError } from './utils/http-request.js'
|
|
9
|
-
import
|
|
8
|
+
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
9
|
+
import { sendInternalServerError, sendNotFound, sendUnprocessableContent } from './utils/http-response.js'
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
export async function dispatchMock(req, response) {
|
|
@@ -38,7 +38,7 @@ export async function dispatchMock(req, response) {
|
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
40
|
if (error instanceof BodyReaderError)
|
|
41
|
-
|
|
41
|
+
sendUnprocessableContent(response, error.name)
|
|
42
42
|
else if (error.code === 'ENOENT') // mock-file has been deleted
|
|
43
43
|
sendNotFound(response)
|
|
44
44
|
else if (error.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
|
package/src/Mockaton.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
1
|
import { createServer } from 'node:http'
|
|
3
|
-
import { watch, existsSync } from 'node:fs'
|
|
4
2
|
|
|
5
3
|
import { API } from './ApiConstants.js'
|
|
6
|
-
import { dispatchMock } from './MockDispatcher.js'
|
|
7
4
|
import { config, setup } from './config.js'
|
|
8
|
-
import {
|
|
5
|
+
import { dispatchMock } from './MockDispatcher.js'
|
|
6
|
+
import { watchMocksDir } from './Watcher.js'
|
|
7
|
+
import { BodyReaderError } from './utils/http-request.js'
|
|
9
8
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
10
9
|
import { dispatchStatic, isStatic } from './StaticDispatcher.js'
|
|
11
10
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
12
11
|
import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
12
|
+
import { sendNoContent, sendInternalServerError, sendUnprocessableContent } from './utils/http-response.js'
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
process.on('unhandledRejection', error => { throw error })
|
|
@@ -17,16 +17,7 @@ process.on('unhandledRejection', error => { throw error })
|
|
|
17
17
|
export function Mockaton(options) {
|
|
18
18
|
setup(options)
|
|
19
19
|
mockBrokerCollection.init()
|
|
20
|
-
|
|
21
|
-
watch(config.mocksDir, { recursive: true, persistent: false },
|
|
22
|
-
function handleAddedOrDeletedMocks(_, file) {
|
|
23
|
-
if (!file)
|
|
24
|
-
return
|
|
25
|
-
if (existsSync(join(config.mocksDir, file)))
|
|
26
|
-
mockBrokerCollection.registerMock(file, 'isFromWatcher')
|
|
27
|
-
else
|
|
28
|
-
mockBrokerCollection.unregisterMock(file)
|
|
29
|
-
})
|
|
20
|
+
watchMocksDir()
|
|
30
21
|
|
|
31
22
|
return createServer(onRequest).listen(config.port, config.host, function (error) {
|
|
32
23
|
const { address, port } = this.address()
|
|
@@ -41,34 +32,42 @@ export function Mockaton(options) {
|
|
|
41
32
|
}
|
|
42
33
|
|
|
43
34
|
async function onRequest(req, response) {
|
|
44
|
-
req.on('error', console.error)
|
|
45
35
|
response.on('error', console.error)
|
|
46
|
-
response.setHeader('Server', 'Mockaton')
|
|
47
36
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
origins: config.corsOrigins,
|
|
51
|
-
headers: config.corsHeaders,
|
|
52
|
-
methods: config.corsMethods,
|
|
53
|
-
maxAge: config.corsMaxAge,
|
|
54
|
-
credentials: config.corsCredentials,
|
|
55
|
-
exposedHeaders: config.corsExposedHeaders
|
|
56
|
-
})
|
|
37
|
+
try {
|
|
38
|
+
response.setHeader('Server', 'Mockaton')
|
|
57
39
|
|
|
58
|
-
|
|
40
|
+
if (config.corsAllowed)
|
|
41
|
+
setCorsHeaders(req, response, {
|
|
42
|
+
origins: config.corsOrigins,
|
|
43
|
+
headers: config.corsHeaders,
|
|
44
|
+
methods: config.corsMethods,
|
|
45
|
+
maxAge: config.corsMaxAge,
|
|
46
|
+
credentials: config.corsCredentials,
|
|
47
|
+
exposedHeaders: config.corsExposedHeaders
|
|
48
|
+
})
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
sendNoContent(response)
|
|
50
|
+
const { url, method } = req
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
if (isPreflight(req))
|
|
53
|
+
sendNoContent(response)
|
|
65
54
|
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
else if (method === 'PATCH' && apiPatchRequests.has(url))
|
|
56
|
+
await apiPatchRequests.get(url)(req, response)
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
else if (method === 'GET' && apiGetRequests.has(url))
|
|
59
|
+
apiGetRequests.get(url)(req, response)
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
else if (method === 'GET' && isStatic(req))
|
|
62
|
+
await dispatchStatic(req, response)
|
|
63
|
+
|
|
64
|
+
else
|
|
65
|
+
await dispatchMock(req, response)
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof BodyReaderError)
|
|
69
|
+
sendUnprocessableContent(response, error.name)
|
|
70
|
+
else
|
|
71
|
+
sendInternalServerError(response, error)
|
|
72
|
+
}
|
|
74
73
|
}
|
package/src/Mockaton.test.js
CHANGED
|
@@ -501,7 +501,7 @@ export default function (req, response) {
|
|
|
501
501
|
async function testStaticFileServing() {
|
|
502
502
|
await describe('Static File Serving', () => {
|
|
503
503
|
it('404 path traversal', async () => {
|
|
504
|
-
const res = await request('
|
|
504
|
+
const res = await request('/../../../../../../../../../../../%2E%2E/etc/passwd')
|
|
505
505
|
equal(res.status, 404)
|
|
506
506
|
})
|
|
507
507
|
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import fs, { readFileSync
|
|
2
|
+
import fs, { readFileSync } from 'node:fs'
|
|
3
3
|
|
|
4
4
|
import { config } from './config.js'
|
|
5
5
|
import { mimeFor } from './utils/mime.js'
|
|
@@ -10,12 +10,12 @@ import { sendInternalServerError } from './utils/http-response.js'
|
|
|
10
10
|
export function isStatic(req) {
|
|
11
11
|
if (!config.staticDir)
|
|
12
12
|
return false
|
|
13
|
-
const f =
|
|
13
|
+
const f = resolvePath(req.url)
|
|
14
14
|
return f && !config.ignore.test(f)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export async function dispatchStatic(req, response) {
|
|
18
|
-
const file =
|
|
18
|
+
const file = resolvePath(req.url)
|
|
19
19
|
if (req.headers.range)
|
|
20
20
|
await sendPartialContent(response, req.headers.range, file)
|
|
21
21
|
else {
|
|
@@ -24,17 +24,12 @@ export async function dispatchStatic(req, response) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
candidate = join(candidate, 'index.html')
|
|
34
|
-
if (isFile(candidate))
|
|
35
|
-
return candidate
|
|
36
|
-
}
|
|
37
|
-
catch {}
|
|
27
|
+
function resolvePath(url) { // url is absolute e.g. /home/../.. => /
|
|
28
|
+
let candidate = join(config.staticDir, url)
|
|
29
|
+
if (isDirectory(candidate))
|
|
30
|
+
candidate = join(candidate, 'index.html')
|
|
31
|
+
if (isFile(candidate))
|
|
32
|
+
return candidate
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
async function sendPartialContent(response, range, file) {
|
package/src/Watcher.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { watch } from 'node:fs'
|
|
3
|
+
import { EventEmitter } from 'node:events'
|
|
4
|
+
|
|
5
|
+
import { config } from './config.js'
|
|
6
|
+
import { isFile } from './utils/fs.js'
|
|
7
|
+
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
let nAR_Events = 0 // AR = Add or Remove Mock
|
|
11
|
+
|
|
12
|
+
export function countAR_Events() {
|
|
13
|
+
return nAR_Events
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const emitter = new EventEmitter()
|
|
18
|
+
|
|
19
|
+
export function subscribeAR_EventListener(callback) {
|
|
20
|
+
emitter.on('AR', callback)
|
|
21
|
+
}
|
|
22
|
+
export function unsubscribeAR_EventListener(callback) {
|
|
23
|
+
emitter.removeListener('AR', callback)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function emitAddOrRemoveMock() {
|
|
27
|
+
nAR_Events++
|
|
28
|
+
emitter.emit('AR')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function watchMocksDir() {
|
|
32
|
+
const dir = config.mocksDir
|
|
33
|
+
watch(dir, { recursive: true, persistent: false }, (_, file) => {
|
|
34
|
+
if (!file)
|
|
35
|
+
return
|
|
36
|
+
if (isFile(join(dir, file))) {
|
|
37
|
+
if (mockBrokerCollection.registerMock(file, 'isFromWatcher'))
|
|
38
|
+
emitAddOrRemoveMock()
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
mockBrokerCollection.unregisterMock(file)
|
|
42
|
+
emitAddOrRemoveMock()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// TODO staticDir, config changes
|
|
48
|
+
// TODO think about throttling e.g. bulk deletes/remove files
|
|
@@ -35,24 +35,28 @@ export function init() {
|
|
|
35
35
|
})
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** @returns {boolean} registered */
|
|
38
39
|
export function registerMock(file, isFromWatcher) {
|
|
39
40
|
if (getBrokerByFilename(file)?.hasMock(file)
|
|
40
41
|
|| config.ignore.test(file)
|
|
41
42
|
|| !filenameIsValid(file))
|
|
42
|
-
return
|
|
43
|
+
return false
|
|
43
44
|
|
|
44
45
|
const { method, urlMask } = parseFilename(file)
|
|
45
46
|
collection[method] ??= {}
|
|
47
|
+
|
|
46
48
|
if (!collection[method][urlMask])
|
|
47
49
|
collection[method][urlMask] = new MockBroker(file)
|
|
48
50
|
else
|
|
49
51
|
collection[method][urlMask].register(file)
|
|
50
52
|
|
|
51
|
-
if (isFromWatcher)
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
if (isFromWatcher && !this.file)
|
|
54
|
+
collection[method][urlMask].selectDefaultFile()
|
|
55
|
+
|
|
56
|
+
if (isFromWatcher)
|
|
54
57
|
collection[method][urlMask].ensureItHas500()
|
|
55
|
-
|
|
58
|
+
|
|
59
|
+
return true
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export function unregisterMock(file) {
|
package/src/mockaton-logo.svg
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<svg version="1.1" viewBox="0 0 570 100" xmlns="http://www.w3.org/2000/svg">
|
|
3
3
|
<style>
|
|
4
4
|
:root { --color: #000000; }
|
|
5
|
-
@media (prefers-color-scheme: light) { :root { --color: #
|
|
5
|
+
@media (prefers-color-scheme: light) { :root { --color: #444 } }
|
|
6
6
|
@media (prefers-color-scheme: dark) { :root { --color: #eee } }
|
|
7
7
|
path { fill: var(--color) }
|
|
8
8
|
</style>
|
|
@@ -26,12 +26,6 @@ export function sendDashboardFile(response, file) {
|
|
|
26
26
|
response.end(readFileSync(file, 'utf8'))
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function sendBadRequest(response, error) {
|
|
30
|
-
console.error(error)
|
|
31
|
-
response.statusCode = 400
|
|
32
|
-
response.end()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
29
|
export function sendNotFound(response) {
|
|
36
30
|
response.statusCode = 404
|
|
37
31
|
response.end()
|