mockaton 0.0.1
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/Api.js +108 -0
- package/ApiConstants.js +22 -0
- package/Config.js +41 -0
- package/Dashboard.css +206 -0
- package/Dashboard.html +12 -0
- package/Dashboard.js +355 -0
- package/LICENSE +21 -0
- package/MockBroker.js +107 -0
- package/MockDispatcher.js +72 -0
- package/Mockaton.js +39 -0
- package/README-dashboard-dropdown.png +0 -0
- package/README-dashboard.png +0 -0
- package/README-mocks-with-comments.png +0 -0
- package/README.md +211 -0
- package/Route.js +90 -0
- package/StaticDispatcher.js +29 -0
- package/Tests.js +367 -0
- package/_usage_example.js +14 -0
- package/cookie.js +29 -0
- package/index.d.ts +17 -0
- package/index.js +2 -0
- package/mockBrokersCollection.js +84 -0
- package/package.json +12 -0
- package/sample-mocks/api/user/.GET.200.json +1 -0
- package/sample-mocks/api/user/.GET.501.txt +7 -0
- package/sample-mocks/api/user/edit-name.PATCH.200.json +1 -0
- package/sample-mocks/api/user/edit-name.PATCH.200.md +12 -0
- package/sample-mocks/api/user/edit-name.PATCH.501.txt +0 -0
- package/sample-mocks/api/user/friends.GET.200.json +1 -0
- package/sample-mocks/api/user/friends.GET.204.json +4 -0
- package/sample-mocks/api/user/friends.GET.501.txt +0 -0
- package/sample-mocks/api/user/logout.POST.200.json +1 -0
- package/sample-mocks/api/user/logout.POST.501.txt +0 -0
- package/sample-mocks/api/user/videos(assorted).GET.200.json +13 -0
- package/sample-mocks/api/user/videos(entirely unverified).GET.200.json +13 -0
- package/sample-mocks/api/user/videos(entirely verified)(another comment).GET.200.json +13 -0
- package/sample-mocks/api/user/videos.GET.501.txt +0 -0
- package/sample-mocks/api/video/[id].GET.200.json +4 -0
- package/sample-mocks/api/video/[id].GET.501.txt +0 -0
- package/sample-mocks/api/video/list(concat newly uploaded).GET.200.mjs +8 -0
- package/sample-mocks/api/video/list.GET.200.json +11 -0
- package/sample-mocks/api/video/list.GET.501.txt +0 -0
- package/sample-mocks/api/video/stat/[stat-id]/[video-id].GET.200.json +1 -0
- package/sample-mocks/api/video/stat/[stat-id]/[video-id].GET.501.txt +0 -0
- package/sample-mocks/api/video/stat/[stat-id]/all-videos?limit=[limit].GET.200.json +4 -0
- package/sample-mocks/api/video/stat/[stat-id]/all-videos?limit=[limit].GET.501.txt +0 -0
- package/sample-mocks/api/video/upload(insert newly uploaded).POST.201.mjs +10 -0
- package/sample-mocks/api/video/upload.POST.201.json +3 -0
- package/sample-mocks/api/video/upload.POST.501.txt +0 -0
- package/sample-static/another-entry/index.html +12 -0
- package/sample-static/assets/app.js +1 -0
- package/sample-static/assets/video.mp4 +0 -0
- package/sample-static/index.html +13 -0
- package/utils/http-request.js +36 -0
- package/utils/http-response.js +60 -0
- package/utils/jwt.js +21 -0
- package/utils/mime.js +47 -0
- package/utils/validate.js +17 -0
package/Dashboard.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { Route } from '../Route.js'
|
|
2
|
+
import { DP, DF } from '../ApiConstants.js'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const Strings = {
|
|
6
|
+
bulk_select_by_comment: 'Bulk Select by Comment',
|
|
7
|
+
click_link_to_preview: 'Click a link to preview it',
|
|
8
|
+
cookie: 'Cookie',
|
|
9
|
+
delay: 'Delay',
|
|
10
|
+
empty_response_body: '/* Empty Response Body */',
|
|
11
|
+
fetching: '⌚ Fetching…',
|
|
12
|
+
mock: 'Mock',
|
|
13
|
+
none: 'None',
|
|
14
|
+
reset: 'Reset',
|
|
15
|
+
select_one: 'Select One',
|
|
16
|
+
title: 'Mockaton',
|
|
17
|
+
transforms: 'Transforms'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CSS = {
|
|
21
|
+
BulkSelectSection: 'BulkSelectSection',
|
|
22
|
+
CookieSelector: 'CookieSelector',
|
|
23
|
+
DelayCheckbox: 'DelayCheckbox',
|
|
24
|
+
Documentation: 'Documentation',
|
|
25
|
+
MockSelector: 'MockSelector',
|
|
26
|
+
PayloadViewer: 'PayloadViewer',
|
|
27
|
+
PreviewLink: 'PreviewLink',
|
|
28
|
+
TitleWrap: 'TitleWrap',
|
|
29
|
+
TransformSelector: 'TransformSelector',
|
|
30
|
+
TransformsSection: 'TransformsSection',
|
|
31
|
+
|
|
32
|
+
bold: 'bold',
|
|
33
|
+
chosen: 'chosen',
|
|
34
|
+
status4xx: 'status4xx',
|
|
35
|
+
status5xx: 'status5xx'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const r = createElement
|
|
39
|
+
const refDocumentation = useRef()
|
|
40
|
+
const refPayloadViewer = useRef()
|
|
41
|
+
const refPayloadFile = useRef()
|
|
42
|
+
|
|
43
|
+
function init() {
|
|
44
|
+
Promise.all([
|
|
45
|
+
DP.mocks,
|
|
46
|
+
DP.cookies,
|
|
47
|
+
DP.comments
|
|
48
|
+
].map(api => fetch(api).then(res => res.ok && res.json())))
|
|
49
|
+
.then(App)
|
|
50
|
+
.catch(console.error)
|
|
51
|
+
}
|
|
52
|
+
init()
|
|
53
|
+
|
|
54
|
+
function App([brokersByMethod, cookies, comments]) {
|
|
55
|
+
empty(document.body)
|
|
56
|
+
createRoot(document.body).render(
|
|
57
|
+
DevPanel(brokersByMethod, cookies, comments))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function DevPanel(brokersByMethod, cookies, comments) {
|
|
61
|
+
document.title = Strings.title
|
|
62
|
+
return (
|
|
63
|
+
r('div', null,
|
|
64
|
+
r('div', { className: CSS.TitleWrap },
|
|
65
|
+
r('h1', null, Strings.title),
|
|
66
|
+
r(ResetButton),
|
|
67
|
+
r(CookieSelector, { list: cookies })),
|
|
68
|
+
r('div', { className: CSS.BulkSelectSection },
|
|
69
|
+
r('h2', null, Strings.bulk_select_by_comment),
|
|
70
|
+
r(BulkSelector, { comments })),
|
|
71
|
+
r('main', null,
|
|
72
|
+
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
73
|
+
r(SectionByMethod, { method, brokers }))),
|
|
74
|
+
r('div', { className: CSS.PayloadViewer },
|
|
75
|
+
r('pre', { ref: refDocumentation, className: CSS.Documentation }),
|
|
76
|
+
r('h2', { ref: refPayloadFile }, Strings.mock),
|
|
77
|
+
r('pre', { ref: refPayloadViewer }, Strings.click_link_to_preview))),
|
|
78
|
+
r('div', { className: CSS.TransformsSection },
|
|
79
|
+
r('h2', null, Strings.transforms),
|
|
80
|
+
r(Transforms, { brokersByMethod }))))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
function ResetButton() {
|
|
85
|
+
return (
|
|
86
|
+
r('button', {
|
|
87
|
+
onClick() {
|
|
88
|
+
fetch(DP.reset, { method: 'PATCH' })
|
|
89
|
+
.then(init)
|
|
90
|
+
.catch(console.error)
|
|
91
|
+
}
|
|
92
|
+
}, Strings.reset)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function CookieSelector({ list }) {
|
|
97
|
+
return (
|
|
98
|
+
r('label', { className: CSS.CookieSelector },
|
|
99
|
+
Strings.cookie,
|
|
100
|
+
r('select', {
|
|
101
|
+
autocomplete: 'off',
|
|
102
|
+
disabled: list.length <= 1,
|
|
103
|
+
onChange() {
|
|
104
|
+
fetch(DP.cookies, {
|
|
105
|
+
method: 'PATCH',
|
|
106
|
+
body: JSON.stringify({ [DF.currentCookieKey]: this.value })
|
|
107
|
+
})
|
|
108
|
+
.then(init)
|
|
109
|
+
.catch(console.error)
|
|
110
|
+
}
|
|
111
|
+
}, list.map(([key, selected]) =>
|
|
112
|
+
r('option', {
|
|
113
|
+
value: key,
|
|
114
|
+
selected
|
|
115
|
+
}, key)))))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
function BulkSelector({ comments }) {
|
|
120
|
+
return (
|
|
121
|
+
r('select', {
|
|
122
|
+
autocomplete: 'off',
|
|
123
|
+
disabled: comments.length <= 1,
|
|
124
|
+
onChange() {
|
|
125
|
+
fetch(DP.bulkSelect, {
|
|
126
|
+
method: 'PATCH',
|
|
127
|
+
body: JSON.stringify({ [DF.comment]: this.value })
|
|
128
|
+
})
|
|
129
|
+
.then(init)
|
|
130
|
+
.catch(console.error)
|
|
131
|
+
}
|
|
132
|
+
}, [Strings.select_one].concat(comments).map(item =>
|
|
133
|
+
r('option', {
|
|
134
|
+
value: item
|
|
135
|
+
}, item))))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
function SectionByMethod({ method, brokers }) {
|
|
140
|
+
return (
|
|
141
|
+
r('tbody', null,
|
|
142
|
+
r('th', null, method),
|
|
143
|
+
Object.entries(brokers)
|
|
144
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
145
|
+
.filter(([, broker]) => broker.mocks.length) // handles Markdown doc or js transform without mocks
|
|
146
|
+
.map(([urlMask, broker]) =>
|
|
147
|
+
r('tr', null,
|
|
148
|
+
r('td', null, r(PreviewLink, { method, urlMask, documentation: broker.documentation })),
|
|
149
|
+
r('td', null, r(MockSelector, { items: broker.mocks, selected: broker.currentMock.file })),
|
|
150
|
+
r('td', null, r(DelayToggler, { name: broker.currentMock.file, checked: Boolean(broker.currentMock.delay) }))))))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function PreviewLink({ method, urlMask, documentation }) {
|
|
154
|
+
return (
|
|
155
|
+
r('a', {
|
|
156
|
+
className: CSS.PreviewLink,
|
|
157
|
+
href: urlMask,
|
|
158
|
+
'data-method': method,
|
|
159
|
+
async onClick(event) {
|
|
160
|
+
event.preventDefault()
|
|
161
|
+
try {
|
|
162
|
+
if (documentation) {
|
|
163
|
+
const r = await fetch(documentation)
|
|
164
|
+
refDocumentation.current.innerText = await r.text()
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
refDocumentation.current.innerText = ''
|
|
168
|
+
|
|
169
|
+
const spinner = setTimeout(() => refPayloadViewer.current.innerText = Strings.fetching, 180)
|
|
170
|
+
const res = await fetch(this.href, {
|
|
171
|
+
method: this.getAttribute('data-method'),
|
|
172
|
+
headers: { [DF.isForDashboard]: '1' }
|
|
173
|
+
})
|
|
174
|
+
document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
|
|
175
|
+
this.classList.add(CSS.chosen)
|
|
176
|
+
clearTimeout(spinner)
|
|
177
|
+
refPayloadViewer.current.innerText = await res.text() || Strings.empty_response_body
|
|
178
|
+
refPayloadFile.current.innerText = this.closest('tr').querySelector('select').value
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error(error)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}, urlMask))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function MockSelector({ items, selected }) {
|
|
188
|
+
const className = (defaultIsSelected, status) => cssClass(
|
|
189
|
+
CSS.MockSelector,
|
|
190
|
+
!defaultIsSelected && CSS.bold,
|
|
191
|
+
status >= 400 && status < 500 && CSS.status4xx,
|
|
192
|
+
status >= 500 && CSS.status5xx)
|
|
193
|
+
return (
|
|
194
|
+
r('select', {
|
|
195
|
+
className: className(selected === items[0], Route.parseFilename(selected).status),
|
|
196
|
+
autocomplete: 'off',
|
|
197
|
+
disabled: items.length <= 1,
|
|
198
|
+
onChange() {
|
|
199
|
+
const status = Route.parseFilename(this.value).status
|
|
200
|
+
this.style.fontWeight = this.value === this.options[0].value // default is selected
|
|
201
|
+
? 'normal'
|
|
202
|
+
: 'bold'
|
|
203
|
+
fetch(DP.edit, {
|
|
204
|
+
method: 'PATCH',
|
|
205
|
+
body: JSON.stringify({ [DF.file]: this.value })
|
|
206
|
+
}).then(() => {
|
|
207
|
+
this.closest('tr').querySelector('a').click()
|
|
208
|
+
this.className = className(this.value === this.options[0].value, this.value === status)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}, items.map(item =>
|
|
212
|
+
r('option', {
|
|
213
|
+
value: item,
|
|
214
|
+
selected: item === selected
|
|
215
|
+
}, item))))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function DelayToggler({ name, checked }) {
|
|
219
|
+
return (
|
|
220
|
+
r('label', {
|
|
221
|
+
className: CSS.DelayCheckbox,
|
|
222
|
+
title: Strings.delay
|
|
223
|
+
},
|
|
224
|
+
r('input', {
|
|
225
|
+
type: 'checkbox',
|
|
226
|
+
autocomplete: 'off',
|
|
227
|
+
name,
|
|
228
|
+
checked,
|
|
229
|
+
onChange(event) {
|
|
230
|
+
fetch(DP.edit, {
|
|
231
|
+
method: 'PATCH',
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
[DF.file]: this.name,
|
|
234
|
+
[DF.delayed]: event.currentTarget.checked
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}),
|
|
239
|
+
TimerIcon()))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function TimerIcon() {
|
|
243
|
+
return (
|
|
244
|
+
r('svg', { viewBox: '0 0 24 24' },
|
|
245
|
+
r('path', { d: 'M12 7H11v6l5 3.2.75-1.23-4.5-3z' })))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
function Transforms({ brokersByMethod }) {
|
|
250
|
+
const brokersWithTransforms = []
|
|
251
|
+
for (const brokers of Object.values(brokersByMethod))
|
|
252
|
+
for (const [urlMask, broker] of Object.entries(brokers))
|
|
253
|
+
if (broker.transforms.length)
|
|
254
|
+
brokersWithTransforms.push([urlMask, broker])
|
|
255
|
+
return (
|
|
256
|
+
r('table', null, brokersWithTransforms.map(([urlMask, broker]) =>
|
|
257
|
+
r('tr', null,
|
|
258
|
+
r('td', null, r(PreviewLink, { method: broker.method, urlMask })),
|
|
259
|
+
r('td', null, r(TransformSelector, {
|
|
260
|
+
urlMask,
|
|
261
|
+
method: broker.method,
|
|
262
|
+
items: ['', ...broker.transforms],
|
|
263
|
+
selected: broker.currentTransform
|
|
264
|
+
})))
|
|
265
|
+
)))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function TransformSelector({ method, urlMask, items, selected }) {
|
|
269
|
+
const className = defaultIsSelected => cssClass(
|
|
270
|
+
CSS.TransformSelector,
|
|
271
|
+
!defaultIsSelected && CSS.bold)
|
|
272
|
+
return (
|
|
273
|
+
r('select', {
|
|
274
|
+
className: className(selected === items[0]),
|
|
275
|
+
autocomplete: 'off',
|
|
276
|
+
'data-urlMask': urlMask,
|
|
277
|
+
'data-method': method,
|
|
278
|
+
onChange() {
|
|
279
|
+
fetch(DP.transform, {
|
|
280
|
+
method: 'PATCH',
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
[DF.file]: this.value,
|
|
283
|
+
[DF.urlMask]: this.getAttribute('data-urlMask'),
|
|
284
|
+
[DF.method]: this.getAttribute('data-method')
|
|
285
|
+
})
|
|
286
|
+
}).then(() => {
|
|
287
|
+
this.closest('tr').querySelector('a').click()
|
|
288
|
+
this.className = className(this.value === this.options[0].value)
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
}, items.map(item =>
|
|
292
|
+
r('option', {
|
|
293
|
+
value: item,
|
|
294
|
+
selected: item === selected
|
|
295
|
+
}, item || Strings.none))))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
/* === Utils === */
|
|
300
|
+
function cssClass(...args) {
|
|
301
|
+
return args.filter(a => a).join(' ')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function empty(node) {
|
|
305
|
+
while (node.firstChild)
|
|
306
|
+
node.removeChild(node.firstChild)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
// These are simplified React-compatible implementations.
|
|
311
|
+
// IOW, for switching to React, remove the `createRoot`, `createElement`, `useRef`
|
|
312
|
+
|
|
313
|
+
function createRoot(root) {
|
|
314
|
+
return {
|
|
315
|
+
render(app) {
|
|
316
|
+
root.appendChild(app)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function createElement(elem, props = null, ...children) {
|
|
322
|
+
if (typeof elem === 'function')
|
|
323
|
+
return elem(props)
|
|
324
|
+
|
|
325
|
+
if (['svg', 'path'].includes(elem)) // Incomplete list
|
|
326
|
+
return createSvgElement(elem, props, children)
|
|
327
|
+
|
|
328
|
+
const node = document.createElement(elem)
|
|
329
|
+
if (props)
|
|
330
|
+
for (const [key, value] of Object.entries(props))
|
|
331
|
+
if (key === 'ref')
|
|
332
|
+
value.current = node
|
|
333
|
+
else if (key.startsWith('on'))
|
|
334
|
+
node.addEventListener(key.replace(/^on/, '').toLowerCase(), value)
|
|
335
|
+
else if (key in node)
|
|
336
|
+
node[key] = value
|
|
337
|
+
else
|
|
338
|
+
node.setAttribute(key, value)
|
|
339
|
+
node.append(...children.flat())
|
|
340
|
+
return node
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function createSvgElement(tagName, props, ...children) {
|
|
344
|
+
const elem = document.createElementNS('http://www.w3.org/2000/svg', tagName)
|
|
345
|
+
for (const [key, value] of Object.entries(props))
|
|
346
|
+
elem.setAttribute(key, value)
|
|
347
|
+
elem.append(...children.flat())
|
|
348
|
+
return elem
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function useRef() {
|
|
352
|
+
return {
|
|
353
|
+
currentMock: null
|
|
354
|
+
}
|
|
355
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Eric Fortis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/MockBroker.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { existsSync, lstatSync, writeFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import { Route } from './Route.js'
|
|
5
|
+
import { Config } from './Config.js'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// MockBroker is a state for a particular route. It knows the available mock files
|
|
9
|
+
// that can be served for the route, the currently selected file, and its delay. Also,
|
|
10
|
+
// knows if the route has js preprocessors (transforms) and documentation (md).
|
|
11
|
+
export class MockBroker {
|
|
12
|
+
#route
|
|
13
|
+
|
|
14
|
+
constructor(file) {
|
|
15
|
+
this.#route = new Route(file)
|
|
16
|
+
this.method = this.#route.method
|
|
17
|
+
|
|
18
|
+
this.documentation = '' // .md
|
|
19
|
+
|
|
20
|
+
this.mocks = [] // *.json,txt
|
|
21
|
+
this.currentMock = {
|
|
22
|
+
file: '',
|
|
23
|
+
status: 200,
|
|
24
|
+
delay: 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.transforms = [] // *.js
|
|
28
|
+
this.currentTransform = ''
|
|
29
|
+
|
|
30
|
+
this.register(file)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
register(file) {
|
|
34
|
+
if (file.endsWith('.md'))
|
|
35
|
+
this.documentation = file
|
|
36
|
+
else if (file.endsWith('.mjs'))
|
|
37
|
+
this.transforms.push(file)
|
|
38
|
+
else {
|
|
39
|
+
if (!this.mocks.length) {
|
|
40
|
+
this.currentMock.file = file // The first mock file option for a particular route becomes the default
|
|
41
|
+
this.currentMock.status = Route.parseFilename(file).status
|
|
42
|
+
}
|
|
43
|
+
this.mocks.push(file)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
urlMaskMatches(url) { return this.#route.urlMaskMatches(url) }
|
|
48
|
+
|
|
49
|
+
get file() { return this.currentMock.file }
|
|
50
|
+
get status() { return this.currentMock.status }
|
|
51
|
+
get delay() { return this.currentMock.delay }
|
|
52
|
+
|
|
53
|
+
updateFile(filename) {
|
|
54
|
+
this.currentMock.file = filename
|
|
55
|
+
this.currentMock.status = Route.parseFilename(filename).status
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
updateDelay(delayed) {
|
|
59
|
+
this.currentMock.delay = Number(delayed) * Config.delay
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
updateTransform(filename) {
|
|
63
|
+
this.currentTransform = filename
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setByMatchingComment(comment) {
|
|
67
|
+
for (const file of this.mocks)
|
|
68
|
+
if (Route.hasInParentheses(file, comment)) {
|
|
69
|
+
this.updateFile(file)
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
for (const file of this.transforms)
|
|
73
|
+
if (Route.hasInParentheses(file, comment)) {
|
|
74
|
+
this.updateTransform(file)
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
extractComments() {
|
|
80
|
+
let comments = []
|
|
81
|
+
for (const file of [...this.mocks, ...this.transforms])
|
|
82
|
+
comments = comments.concat(Route.extractComments(file))
|
|
83
|
+
return comments
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ensureItHas501() {
|
|
87
|
+
if (!this.#has501())
|
|
88
|
+
this.#write501()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#has501() {
|
|
92
|
+
return this.mocks.some(mock =>
|
|
93
|
+
Route.parseFilename(mock).status === 501)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#write501() {
|
|
97
|
+
// TODO handle route with transforms but without mocks
|
|
98
|
+
const { urlMask, method } = Route.parseFilename(this.mocks[0])
|
|
99
|
+
let mask = urlMask
|
|
100
|
+
const t = join(Config.mocksDir, urlMask)
|
|
101
|
+
if (existsSync(t) && lstatSync(t).isDirectory())
|
|
102
|
+
mask = urlMask + '/'
|
|
103
|
+
const file = `${mask}.${method}.501.txt`
|
|
104
|
+
writeFileSync(join(Config.mocksDir, file), '')
|
|
105
|
+
this.register(file)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import { DF } from './ApiConstants.js'
|
|
5
|
+
import { cookie } from './cookie.js'
|
|
6
|
+
import { Config } from './Config.js'
|
|
7
|
+
import { mimeFor } from './utils/mime.js'
|
|
8
|
+
import * as MockBrokerCollection from './mockBrokersCollection.js'
|
|
9
|
+
import { parseJSON, JsonBodyParserError } from './utils/http-request.js'
|
|
10
|
+
import { sendInternalServerError, sendNotFound, sendFile, sendBadRequest } from './utils/http-response.js'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function serveDocumentation(req, response) {
|
|
14
|
+
sendFile(response, join(Config.mocksDir, decodeURIComponent(req.url)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function dispatchMock(req, response) {
|
|
18
|
+
if (req.method === 'GET' && req.url.endsWith('.md')) {
|
|
19
|
+
serveDocumentation(req, response)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mockBroker = MockBrokerCollection.findMatchingBroker(req.method, req.url)
|
|
24
|
+
if (!mockBroker)
|
|
25
|
+
sendNotFound(response)
|
|
26
|
+
else
|
|
27
|
+
try {
|
|
28
|
+
const { file, status, delay, currentTransform } = mockBroker
|
|
29
|
+
console.log(decodeURIComponent(req.url), '->', file)
|
|
30
|
+
|
|
31
|
+
response.statusCode = status
|
|
32
|
+
response.setHeader('content-type', mimeFor(file))
|
|
33
|
+
if (cookie.getCurrent())
|
|
34
|
+
response.setHeader('set-cookie', cookie.getCurrent())
|
|
35
|
+
|
|
36
|
+
let mockAsText = readMock(file)
|
|
37
|
+
if (mockBroker.currentTransform) {
|
|
38
|
+
const body = await requestBodyForTransform(req, mockAsText)
|
|
39
|
+
const transformFunc = await importTransformFunc(currentTransform)
|
|
40
|
+
mockAsText = transformFunc(mockAsText, body, Config)
|
|
41
|
+
}
|
|
42
|
+
setTimeout(() => response.end(mockAsText), delay)
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(error)
|
|
46
|
+
if (error instanceof JsonBodyParserError)
|
|
47
|
+
sendBadRequest(response)
|
|
48
|
+
else if (error.code === 'ENOENT')
|
|
49
|
+
sendNotFound(response) // file has been deleted
|
|
50
|
+
else
|
|
51
|
+
sendInternalServerError(response)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const nonSafeMethods = ['PATCH', 'POST', 'PUT', 'DELETE', 'CONNECT']
|
|
56
|
+
|
|
57
|
+
async function requestBodyForTransform(req, mockAsText) {
|
|
58
|
+
if (nonSafeMethods.includes(req.method))
|
|
59
|
+
return req.headers[DF.isForDashboard] // TESTME
|
|
60
|
+
? JSON.parse(mockAsText)
|
|
61
|
+
: await parseJSON(req)
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readMock(file) {
|
|
66
|
+
return readFileSync(join(Config.mocksDir, file), 'utf8')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function importTransformFunc(file) {
|
|
70
|
+
// The date param is just for cache busting
|
|
71
|
+
return (await import(join(Config.mocksDir, file) + '?' + Date.now())).default
|
|
72
|
+
}
|
package/Mockaton.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { exec } from 'node:child_process'
|
|
2
|
+
import { createServer } from 'node:http'
|
|
3
|
+
|
|
4
|
+
import { DP } from './ApiConstants.js'
|
|
5
|
+
import { Config, setup } from './Config.js'
|
|
6
|
+
import { dispatchMock } from './MockDispatcher.js'
|
|
7
|
+
import * as MockBrokerCollection from './mockBrokersCollection.js'
|
|
8
|
+
import { dispatchStatic, isStatic } from './StaticDispatcher.js'
|
|
9
|
+
import { apiPatchRequests, apiGetRequests } from './Api.js'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function Mockaton(options) {
|
|
13
|
+
setup(options)
|
|
14
|
+
MockBrokerCollection.init()
|
|
15
|
+
|
|
16
|
+
return createServer(async (req, response) => {
|
|
17
|
+
const { url, method } = req
|
|
18
|
+
if (method === 'GET' && apiGetRequests.has(url))
|
|
19
|
+
apiGetRequests.get(url)(req, response)
|
|
20
|
+
|
|
21
|
+
else if (method === 'PATCH' && apiPatchRequests.has(url))
|
|
22
|
+
await apiPatchRequests.get(url)(req, response)
|
|
23
|
+
|
|
24
|
+
else if (isStatic(req))
|
|
25
|
+
await dispatchStatic(req, response)
|
|
26
|
+
|
|
27
|
+
else
|
|
28
|
+
await dispatchMock(req, response)
|
|
29
|
+
})
|
|
30
|
+
.listen(Config.port, Config.host, function (error) {
|
|
31
|
+
const { address, port } = this.address()
|
|
32
|
+
const url = `http://${address}:${port}`
|
|
33
|
+
console.log('Listening on', url)
|
|
34
|
+
if (error)
|
|
35
|
+
console.error(error)
|
|
36
|
+
else if (!Config.skipOpen)
|
|
37
|
+
exec(`open ${url + DP.dashboard}`)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|