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.
Files changed (58) hide show
  1. package/Api.js +108 -0
  2. package/ApiConstants.js +22 -0
  3. package/Config.js +41 -0
  4. package/Dashboard.css +206 -0
  5. package/Dashboard.html +12 -0
  6. package/Dashboard.js +355 -0
  7. package/LICENSE +21 -0
  8. package/MockBroker.js +107 -0
  9. package/MockDispatcher.js +72 -0
  10. package/Mockaton.js +39 -0
  11. package/README-dashboard-dropdown.png +0 -0
  12. package/README-dashboard.png +0 -0
  13. package/README-mocks-with-comments.png +0 -0
  14. package/README.md +211 -0
  15. package/Route.js +90 -0
  16. package/StaticDispatcher.js +29 -0
  17. package/Tests.js +367 -0
  18. package/_usage_example.js +14 -0
  19. package/cookie.js +29 -0
  20. package/index.d.ts +17 -0
  21. package/index.js +2 -0
  22. package/mockBrokersCollection.js +84 -0
  23. package/package.json +12 -0
  24. package/sample-mocks/api/user/.GET.200.json +1 -0
  25. package/sample-mocks/api/user/.GET.501.txt +7 -0
  26. package/sample-mocks/api/user/edit-name.PATCH.200.json +1 -0
  27. package/sample-mocks/api/user/edit-name.PATCH.200.md +12 -0
  28. package/sample-mocks/api/user/edit-name.PATCH.501.txt +0 -0
  29. package/sample-mocks/api/user/friends.GET.200.json +1 -0
  30. package/sample-mocks/api/user/friends.GET.204.json +4 -0
  31. package/sample-mocks/api/user/friends.GET.501.txt +0 -0
  32. package/sample-mocks/api/user/logout.POST.200.json +1 -0
  33. package/sample-mocks/api/user/logout.POST.501.txt +0 -0
  34. package/sample-mocks/api/user/videos(assorted).GET.200.json +13 -0
  35. package/sample-mocks/api/user/videos(entirely unverified).GET.200.json +13 -0
  36. package/sample-mocks/api/user/videos(entirely verified)(another comment).GET.200.json +13 -0
  37. package/sample-mocks/api/user/videos.GET.501.txt +0 -0
  38. package/sample-mocks/api/video/[id].GET.200.json +4 -0
  39. package/sample-mocks/api/video/[id].GET.501.txt +0 -0
  40. package/sample-mocks/api/video/list(concat newly uploaded).GET.200.mjs +8 -0
  41. package/sample-mocks/api/video/list.GET.200.json +11 -0
  42. package/sample-mocks/api/video/list.GET.501.txt +0 -0
  43. package/sample-mocks/api/video/stat/[stat-id]/[video-id].GET.200.json +1 -0
  44. package/sample-mocks/api/video/stat/[stat-id]/[video-id].GET.501.txt +0 -0
  45. package/sample-mocks/api/video/stat/[stat-id]/all-videos?limit=[limit].GET.200.json +4 -0
  46. package/sample-mocks/api/video/stat/[stat-id]/all-videos?limit=[limit].GET.501.txt +0 -0
  47. package/sample-mocks/api/video/upload(insert newly uploaded).POST.201.mjs +10 -0
  48. package/sample-mocks/api/video/upload.POST.201.json +3 -0
  49. package/sample-mocks/api/video/upload.POST.501.txt +0 -0
  50. package/sample-static/another-entry/index.html +12 -0
  51. package/sample-static/assets/app.js +1 -0
  52. package/sample-static/assets/video.mp4 +0 -0
  53. package/sample-static/index.html +13 -0
  54. package/utils/http-request.js +36 -0
  55. package/utils/http-response.js +60 -0
  56. package/utils/jwt.js +21 -0
  57. package/utils/mime.js +47 -0
  58. 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