mockaton 13.9.4 → 13.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client/app.css +6 -3
- package/src/client/dir/groupByFolder.test.js +2 -2
- package/src/client/utils/css.js +1 -0
- package/src/server/Api.js +12 -13
- package/src/server/MockDispatcher.js +1 -1
- package/src/server/Mockaton.js +3 -2
- package/src/server/utils/HttpServerResponse.js +17 -14
- package/src/server/utils/HttpServerResponse.test.js +84 -0
package/package.json
CHANGED
package/src/client/app.css
CHANGED
|
@@ -23,10 +23,12 @@
|
|
|
23
23
|
|
|
24
24
|
accent-color: var(--colorAccent);
|
|
25
25
|
--radius: 16px;
|
|
26
|
+
--subtoolbarHeight: 42px;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
html,
|
|
29
30
|
body {
|
|
31
|
+
overflow: hidden;
|
|
30
32
|
height: 100%;
|
|
31
33
|
font-size: 12px;
|
|
32
34
|
}
|
|
@@ -341,15 +343,15 @@ main {
|
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
.leftSide {
|
|
344
|
-
|
|
346
|
+
display: grid;
|
|
345
347
|
width: 50%;
|
|
346
348
|
border-top: 1px solid var(--colorBorder);
|
|
347
349
|
border-right: 1px solid var(--colorBorder);
|
|
350
|
+
grid-template-rows: var(--subtoolbarHeight) 1fr;
|
|
348
351
|
}
|
|
349
352
|
|
|
350
353
|
.rightSide {
|
|
351
354
|
position: relative;
|
|
352
|
-
overflow: hidden;
|
|
353
355
|
min-width: 100px;
|
|
354
356
|
min-height: 0;
|
|
355
357
|
flex: 1;
|
|
@@ -391,7 +393,6 @@ main {
|
|
|
391
393
|
|
|
392
394
|
.SubToolbar {
|
|
393
395
|
display: flex;
|
|
394
|
-
height: 42px;
|
|
395
396
|
align-items: center;
|
|
396
397
|
justify-content: space-between;
|
|
397
398
|
padding-right: 14px;
|
|
@@ -719,7 +720,9 @@ main {
|
|
|
719
720
|
|
|
720
721
|
|
|
721
722
|
.PayloadViewer {
|
|
723
|
+
display: grid;
|
|
722
724
|
height: 100%;
|
|
725
|
+
grid-template-rows: var(--subtoolbarHeight) 1fr;
|
|
723
726
|
|
|
724
727
|
> pre {
|
|
725
728
|
overflow: auto;
|
|
@@ -11,7 +11,7 @@ test('groupByFolder', () => {
|
|
|
11
11
|
PartialBrokerRowModel('GET', '/api/user/avatar'),
|
|
12
12
|
PartialBrokerRowModel('GET', '/api/video/[id]'),
|
|
13
13
|
PartialBrokerRowModel('GET', '/index.html'),
|
|
14
|
-
PartialBrokerRowModel('GET', '/media/file-a
|
|
14
|
+
PartialBrokerRowModel('GET', '/media/file-a'),
|
|
15
15
|
PartialBrokerRowModel('GET', '/media/file-b.txt'),
|
|
16
16
|
PartialBrokerRowModel('GET', '/media/sub/file-aa.txt'),
|
|
17
17
|
PartialBrokerRowModel('GET', '/media/sub/file-bb.txt'),
|
|
@@ -28,7 +28,7 @@ test('groupByFolder', () => {
|
|
|
28
28
|
PartialBrokerRowModel('PATCH', '/api/user')),
|
|
29
29
|
PartialBrokerRowModel('GET', '/api/video/[id]'),
|
|
30
30
|
PartialBrokerRowModel('GET', '/index.html'),
|
|
31
|
-
PartialBrokerRowModel('GET', '/media/file-a
|
|
31
|
+
PartialBrokerRowModel('GET', '/media/file-a',
|
|
32
32
|
PartialBrokerRowModel('GET', '/media/file-b.txt'),
|
|
33
33
|
PartialBrokerRowModel('GET', '/media/sub/file-aa.txt',
|
|
34
34
|
PartialBrokerRowModel('GET', '/media/sub/file-bb.txt')))
|
package/src/client/utils/css.js
CHANGED
|
@@ -6,6 +6,7 @@ export function classNames(...args) {
|
|
|
6
6
|
export function extractClassNames({ cssRules }) {
|
|
7
7
|
// Class names must begin with _ or a letter, then it can have numbers and hyphens
|
|
8
8
|
// TODO think about tag.className selectors
|
|
9
|
+
// TODO think about collisions with props on CSSStyleSheet (e.g. title, type, disabled, href, etc.)
|
|
9
10
|
const reClassName = /(?:^|[\s,{>])&?\s*\.([a-zA-Z_][\w-]*)/g
|
|
10
11
|
const cNames = {}
|
|
11
12
|
let match
|
package/src/server/Api.js
CHANGED
|
@@ -20,19 +20,22 @@ import { config, ConfigValidator } from './config.js'
|
|
|
20
20
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
export const
|
|
23
|
+
export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
|
|
24
24
|
|
|
25
25
|
export const apiGetReqs = new Map([
|
|
26
26
|
[API.dashboard, serveDashboard],
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
|
|
28
|
+
...listFilesRecursively(CLIENT_ASSETS).map(f => [
|
|
29
|
+
API.dashboard + '/' + f,
|
|
30
|
+
serveDashboardAsset(f)
|
|
31
|
+
]),
|
|
29
32
|
|
|
30
33
|
[API.state, getState],
|
|
31
34
|
[API.syncVersion, sseClientSyncVersion],
|
|
32
35
|
|
|
33
36
|
[API.watchHotReload, onDevWatch],
|
|
34
|
-
[API.
|
|
35
|
-
[API.
|
|
37
|
+
[API.openAPI, (_, response) => response.json(openapi)],
|
|
38
|
+
[API.throws, () => { throw new Error('Test500') }]
|
|
36
39
|
])
|
|
37
40
|
|
|
38
41
|
|
|
@@ -65,8 +68,10 @@ function serveDashboard(_, response) {
|
|
|
65
68
|
response.html(IndexHtml(config.hotReload, pkgJSON.version), CSP)
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
function
|
|
69
|
-
return (_, response) => {
|
|
71
|
+
function serveDashboardAsset(f) {
|
|
72
|
+
return (_, response) => {
|
|
73
|
+
response.file(join(CLIENT_ASSETS, f))
|
|
74
|
+
}
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
function getState(_, response) {
|
|
@@ -181,7 +186,6 @@ async function setCollectProxied(req, response) {
|
|
|
181
186
|
|
|
182
187
|
async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
183
188
|
const comment = await req.json()
|
|
184
|
-
|
|
185
189
|
mockBrokersCollection.setMocksMatchingComment(comment)
|
|
186
190
|
response.ok()
|
|
187
191
|
uiSyncVersion.increment()
|
|
@@ -190,7 +194,6 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
|
190
194
|
|
|
191
195
|
async function selectMock(req, response) {
|
|
192
196
|
const file = await req.json()
|
|
193
|
-
|
|
194
197
|
const broker = mockBrokersCollection.brokerByFilename(file)
|
|
195
198
|
if (!broker || !broker.hasMock(file))
|
|
196
199
|
response.unprocessable(`Missing Mock: ${file}`)
|
|
@@ -204,7 +207,6 @@ async function selectMock(req, response) {
|
|
|
204
207
|
|
|
205
208
|
async function toggleRouteStatus(req, response) {
|
|
206
209
|
const [method, urlMask, status] = await req.json()
|
|
207
|
-
|
|
208
210
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
209
211
|
if (!broker)
|
|
210
212
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
@@ -218,7 +220,6 @@ async function toggleRouteStatus(req, response) {
|
|
|
218
220
|
|
|
219
221
|
async function setRouteIsDelayed(req, response) {
|
|
220
222
|
const [method, urlMask, delayed] = await req.json()
|
|
221
|
-
|
|
222
223
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
223
224
|
if (!broker)
|
|
224
225
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
@@ -234,7 +235,6 @@ async function setRouteIsDelayed(req, response) {
|
|
|
234
235
|
|
|
235
236
|
async function setRouteIsProxied(req, response) {
|
|
236
237
|
const [method, urlMask, proxied] = await req.json()
|
|
237
|
-
|
|
238
238
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
239
239
|
if (!broker)
|
|
240
240
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
@@ -298,7 +298,6 @@ async function deleteMock(req, response) {
|
|
|
298
298
|
|
|
299
299
|
async function setWatchMocks(req, response) {
|
|
300
300
|
const enabled = await req.json()
|
|
301
|
-
|
|
302
301
|
if (typeof enabled !== 'boolean')
|
|
303
302
|
response.unprocessable(`Expected boolean for "watchMocks"`)
|
|
304
303
|
else {
|
|
@@ -39,7 +39,7 @@ export async function dispatchMock(req, response) {
|
|
|
39
39
|
|
|
40
40
|
if (isStatic && req.headers.range && !broker.autoStatus) {
|
|
41
41
|
setTimeout(async () => {
|
|
42
|
-
await response.partialContent(
|
|
42
|
+
await response.partialContent(join(config.mocksDir, broker.file))
|
|
43
43
|
}, Number(broker.delayed && calcDelay()))
|
|
44
44
|
return
|
|
45
45
|
}
|
package/src/server/Mockaton.js
CHANGED
|
@@ -11,7 +11,7 @@ import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpI
|
|
|
11
11
|
import { API } from '../client/ApiConstants.js'
|
|
12
12
|
import { cookie } from './cookie.js'
|
|
13
13
|
import { config, setup } from './config.js'
|
|
14
|
-
import { apiPatchReqs, apiGetReqs,
|
|
14
|
+
import { apiPatchReqs, apiGetReqs, CLIENT_ASSETS } from './Api.js'
|
|
15
15
|
|
|
16
16
|
import { dispatchMock } from './MockDispatcher.js'
|
|
17
17
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
@@ -27,6 +27,7 @@ export function Mockaton(options) {
|
|
|
27
27
|
mockBrokerCollection.init()
|
|
28
28
|
|
|
29
29
|
register('./resolverResolveExtensionless.js', import.meta.url)
|
|
30
|
+
|
|
30
31
|
if (config.bypassImportCache)
|
|
31
32
|
register('./resolverBypassImportCache.js', import.meta.url)
|
|
32
33
|
|
|
@@ -34,7 +35,7 @@ export function Mockaton(options) {
|
|
|
34
35
|
watchMocksDir()
|
|
35
36
|
|
|
36
37
|
if (config.hotReload)
|
|
37
|
-
watchDevSPA(
|
|
38
|
+
watchDevSPA(CLIENT_ASSETS)
|
|
38
39
|
|
|
39
40
|
const server = createServer({ IncomingMessage, ServerResponse }, onRequest)
|
|
40
41
|
server.on('error', reject)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import http from 'node:http'
|
|
2
1
|
import fs from 'node:fs'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import { pipeline } from 'node:stream/promises'
|
|
3
4
|
|
|
4
5
|
import { mimeFor } from './mime.js'
|
|
5
6
|
|
|
@@ -27,7 +28,7 @@ export class ServerResponse extends http.ServerResponse {
|
|
|
27
28
|
|
|
28
29
|
async file(file) {
|
|
29
30
|
this.setHeader('Content-Type', mimeFor(file))
|
|
30
|
-
|
|
31
|
+
await pipeline(fs.createReadStream(file), this)
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
noContent() {
|
|
@@ -73,13 +74,18 @@ export class ServerResponse extends http.ServerResponse {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
|
|
76
|
-
async partialContent(
|
|
77
|
+
async partialContent(file) {
|
|
77
78
|
const { size } = await fs.promises.lstat(file)
|
|
78
|
-
let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
79
|
-
|
|
80
|
-
if (isNaN(start))
|
|
79
|
+
let [start, end] = this.req.headers.range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
80
|
+
|
|
81
|
+
if (isNaN(start)) {
|
|
82
|
+
start = size - end
|
|
83
|
+
end = size - 1
|
|
84
|
+
}
|
|
85
|
+
else if (isNaN(end))
|
|
86
|
+
end = size - 1
|
|
81
87
|
|
|
82
|
-
if (start < 0 ||
|
|
88
|
+
if (start < 0 || end >= size || start > end) {
|
|
83
89
|
this.statusCode = 416 // Range Not Satisfiable
|
|
84
90
|
this.setHeader('Content-Range', `bytes */${size}`)
|
|
85
91
|
this.end()
|
|
@@ -89,14 +95,11 @@ export class ServerResponse extends http.ServerResponse {
|
|
|
89
95
|
this.statusCode = 206 // Partial Content
|
|
90
96
|
this.setHeader('Accept-Ranges', 'bytes')
|
|
91
97
|
this.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
98
|
+
this.setHeader('Content-Length', (end - start) + 1)
|
|
92
99
|
this.setHeader('Content-Type', mimeFor(file))
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
reader.on('error', reject)
|
|
98
|
-
reader.on('end', resolve)
|
|
99
|
-
reader.pipe(this)
|
|
100
|
-
})
|
|
101
|
+
const stream = fs.createReadStream(file, { start, end })
|
|
102
|
+
this.on('close', () => stream.destroy())
|
|
103
|
+
stream.pipe(this)
|
|
101
104
|
}
|
|
102
105
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test, before, after } from 'node:test'
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import http, { createServer } from 'node:http'
|
|
4
|
+
import { join, dirname } from 'node:path'
|
|
5
|
+
import { strictEqual } from 'node:assert'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { rm } from 'node:fs/promises'
|
|
8
|
+
|
|
9
|
+
import { ServerResponse } from './HttpServerResponse.js'
|
|
10
|
+
|
|
11
|
+
describe('ServerResponse.partialContent (real HTTP)', () => {
|
|
12
|
+
const FILE = '0123456789'
|
|
13
|
+
const FILE_SIZE = FILE.length
|
|
14
|
+
|
|
15
|
+
let tmpFile, server, baseUrl
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'response-'))
|
|
19
|
+
tmpFile = join(tmpDir, 'test.txt')
|
|
20
|
+
writeFileSync(tmpFile, FILE)
|
|
21
|
+
server = createServer({ ServerResponse }, async (_, response) => {
|
|
22
|
+
await response.partialContent(tmpFile)
|
|
23
|
+
})
|
|
24
|
+
await new Promise(resolve => server.listen(0, () => {
|
|
25
|
+
const { port } = server.address()
|
|
26
|
+
baseUrl = `http://127.0.0.1:${port}`
|
|
27
|
+
resolve()
|
|
28
|
+
}))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
after(async () => {
|
|
32
|
+
server?.close()
|
|
33
|
+
await rm(dirname(tmpFile), { recursive: true, force: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function request(range) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const req = http.get(baseUrl, { headers: { range } }, response => {
|
|
39
|
+
let data = ''
|
|
40
|
+
response.setEncoding('utf8')
|
|
41
|
+
response.on('data', chunk => data += chunk)
|
|
42
|
+
response.on('end', () => resolve({
|
|
43
|
+
statusCode: response.statusCode,
|
|
44
|
+
headers: response.headers,
|
|
45
|
+
data
|
|
46
|
+
}))
|
|
47
|
+
})
|
|
48
|
+
req.on('error', reject)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('416 - out of bounds', async () => {
|
|
53
|
+
for (const range of ['bytes=10-12', 'bytes=5-2', 'bytes=12-', 'bytes=-15']) {
|
|
54
|
+
const { statusCode, headers } = await request(range)
|
|
55
|
+
strictEqual(statusCode, 416)
|
|
56
|
+
strictEqual(headers['content-range'], `bytes */${FILE_SIZE}`)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('206 - normal range', async () => {
|
|
61
|
+
const { statusCode, headers, data } = await request('bytes=0-4')
|
|
62
|
+
strictEqual(statusCode, 206)
|
|
63
|
+
strictEqual(headers['content-range'], `bytes 0-4/${FILE_SIZE}`)
|
|
64
|
+
strictEqual(headers['content-length'], '5')
|
|
65
|
+
strictEqual(headers['content-type'], 'text/plain')
|
|
66
|
+
strictEqual(data, '01234')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('206 - suffix range', async () => {
|
|
70
|
+
const { statusCode, headers, data } = await request('bytes=-3')
|
|
71
|
+
strictEqual(statusCode, 206)
|
|
72
|
+
strictEqual(headers['content-range'], `bytes 7-9/${FILE_SIZE}`)
|
|
73
|
+
strictEqual(headers['content-length'], '3')
|
|
74
|
+
strictEqual(data, '789')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('206 - open ended range', async () => {
|
|
78
|
+
const { statusCode, headers, data } = await request('bytes=5-')
|
|
79
|
+
strictEqual(statusCode, 206)
|
|
80
|
+
strictEqual(headers['content-range'], `bytes 5-9/${FILE_SIZE}`)
|
|
81
|
+
strictEqual(headers['content-length'], '5')
|
|
82
|
+
strictEqual(data, '56789')
|
|
83
|
+
})
|
|
84
|
+
})
|