mockaton 13.9.3 → 13.9.5
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 +4 -4
- package/package.json +1 -1
- package/src/client/ApiCommander.js +0 -6
- package/src/client/ApiConstants.js +0 -1
- package/src/client/app.css +8 -7
- package/src/client/dir/groupByFolder.test.js +2 -2
- package/src/client/utils/css.js +1 -0
- package/src/server/Api.js +11 -17
- package/src/server/MockDispatcher.js +1 -1
- package/src/server/Mockaton.js +3 -2
- package/src/server/Mockaton.test.js +1 -1
- package/src/server/resolverBypassImportCache.js +0 -1
- package/src/server/resolverResolveExtensionless.js +1 -0
- package/src/server/utils/HttpServerResponse.js +17 -14
- package/src/server/utils/HttpServerResponse.test.js +84 -0
- package/src/server/utils/fs.js +2 -2
- package/src/server/utils/fs.test.js +31 -0
package/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|

|
|
3
3
|
[](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
|
|
4
4
|
[](https://codecov.io/github/ericfortis/mockaton)
|
|
5
|
-
[](https://opensource.org/licenses/MIT)
|
|
6
5
|
|
|
7
6
|
## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog) | [Skills](skills/mockaton/SKILL.md)
|
|
8
7
|
|
|
@@ -38,9 +37,10 @@ Dashboard: [localhost:2020/mockaton](http://localhost:2020/mockaton)
|
|
|
38
37
|
npx mockaton --port 2020 my-mocks-dir/
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
Mockaton will serve the files on the given directory. It's a file-system
|
|
42
|
-
|
|
43
|
-
Also, each route can have different mock
|
|
40
|
+
Mockaton will serve the files on the given directory. It's a file-system based router, so filenames can have dynamic
|
|
41
|
+
parameters.
|
|
42
|
+
Also, filenames can have comments, which are anything within parentheses, this way each route can have different mock
|
|
43
|
+
file variants.
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
| Route | Filename | Description |
|
package/package.json
CHANGED
|
@@ -17,15 +17,10 @@ export class Commander {
|
|
|
17
17
|
reset = () => this.#patch(API.reset)
|
|
18
18
|
|
|
19
19
|
setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
|
|
20
|
-
|
|
21
20
|
setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
|
|
22
|
-
|
|
23
21
|
setCorsAllowed = value => this.#patch(API.cors, value)
|
|
24
|
-
|
|
25
22
|
setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
|
|
26
|
-
|
|
27
23
|
setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
|
|
28
|
-
|
|
29
24
|
setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
|
|
30
25
|
|
|
31
26
|
/** @returns {JsonPromise<State.cookies>} */
|
|
@@ -50,7 +45,6 @@ export class Commander {
|
|
|
50
45
|
|
|
51
46
|
|
|
52
47
|
writeMock = (file, content) => this.#patch(API.writeMock, [file, content])
|
|
53
|
-
|
|
54
48
|
deleteMock = file => this.#patch(API.deleteMock, file)
|
|
55
49
|
|
|
56
50
|
|
package/src/client/app.css
CHANGED
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
|
|
28
28
|
html,
|
|
29
29
|
body {
|
|
30
|
-
overflow: hidden;
|
|
31
30
|
height: 100%;
|
|
32
31
|
font-size: 12px;
|
|
33
32
|
}
|
|
@@ -45,7 +44,7 @@ body {
|
|
|
45
44
|
border: 0;
|
|
46
45
|
margin: 0;
|
|
47
46
|
letter-spacing: -0.374px;
|
|
48
|
-
line-height:
|
|
47
|
+
line-height: 14px;
|
|
49
48
|
font-family: inherit;
|
|
50
49
|
font-size: 100%;
|
|
51
50
|
scrollbar-width: thin;
|
|
@@ -185,13 +184,13 @@ header {
|
|
|
185
184
|
}
|
|
186
185
|
|
|
187
186
|
.HelpLink {
|
|
188
|
-
opacity: 0.8;
|
|
189
187
|
width: 22px;
|
|
190
188
|
height: 22px;
|
|
191
189
|
flex-shrink: 0;
|
|
192
190
|
align-self: end;
|
|
193
191
|
margin-bottom: 3px;
|
|
194
192
|
margin-left: auto;
|
|
193
|
+
opacity: 0.8;
|
|
195
194
|
border-radius: 50%;
|
|
196
195
|
fill: var(--colorBgHeader);
|
|
197
196
|
background: var(--colorLabel);
|
|
@@ -342,6 +341,7 @@ main {
|
|
|
342
341
|
}
|
|
343
342
|
|
|
344
343
|
.leftSide {
|
|
344
|
+
overflow: hidden;
|
|
345
345
|
width: 50%;
|
|
346
346
|
border-top: 1px solid var(--colorBorder);
|
|
347
347
|
border-right: 1px solid var(--colorBorder);
|
|
@@ -349,6 +349,7 @@ main {
|
|
|
349
349
|
|
|
350
350
|
.rightSide {
|
|
351
351
|
position: relative;
|
|
352
|
+
overflow: hidden;
|
|
352
353
|
min-width: 100px;
|
|
353
354
|
min-height: 0;
|
|
354
355
|
flex: 1;
|
|
@@ -399,8 +400,7 @@ main {
|
|
|
399
400
|
background: var(--colorBgHeader);
|
|
400
401
|
}
|
|
401
402
|
|
|
402
|
-
.GroupByMethod
|
|
403
|
-
.ViewSourceCheckbox {
|
|
403
|
+
.GroupByMethod {
|
|
404
404
|
display: flex;
|
|
405
405
|
align-items: center;
|
|
406
406
|
gap: 6px;
|
|
@@ -426,7 +426,7 @@ main {
|
|
|
426
426
|
height: 100%;
|
|
427
427
|
padding: 16px;
|
|
428
428
|
padding-bottom: 64px;
|
|
429
|
-
padding-left:
|
|
429
|
+
padding-left: 15px;
|
|
430
430
|
user-select: none;
|
|
431
431
|
overflow-y: auto;
|
|
432
432
|
|
|
@@ -482,7 +482,8 @@ main {
|
|
|
482
482
|
cursor: grabbing;
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
&::-webkit-details-marker
|
|
485
|
+
&::-webkit-details-marker,
|
|
486
|
+
&::marker {
|
|
486
487
|
display: none;
|
|
487
488
|
}
|
|
488
489
|
|
|
@@ -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
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
|
-
import {
|
|
8
|
-
import { write, rm, isFile, resolveIn } from './utils/fs.js'
|
|
7
|
+
import { write, rm, isFile, resolveIn, listFilesRecursively } from './utils/fs.js'
|
|
9
8
|
|
|
10
9
|
import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
|
|
11
10
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
@@ -21,24 +20,25 @@ import { config, ConfigValidator } from './config.js'
|
|
|
21
20
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
export const
|
|
25
|
-
const DASHBOARD_ASSETS = readdirSync(CLIENT_DIR, { recursive: true })
|
|
26
|
-
|
|
23
|
+
export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
|
|
27
24
|
|
|
28
25
|
export const apiGetReqs = new Map([
|
|
29
26
|
[API.dashboard, serveDashboard],
|
|
30
|
-
|
|
27
|
+
|
|
28
|
+
...listFilesRecursively(CLIENT_ASSETS).map(f => [
|
|
29
|
+
API.dashboard + '/' + f,
|
|
30
|
+
serveDashboardAsset(f)
|
|
31
|
+
]),
|
|
31
32
|
|
|
32
33
|
[API.state, getState],
|
|
33
34
|
[API.syncVersion, sseClientSyncVersion],
|
|
34
35
|
|
|
35
36
|
[API.watchHotReload, onDevWatch],
|
|
36
|
-
[API.
|
|
37
|
-
[API.
|
|
37
|
+
[API.openAPI, (_, response) => response.json(openapi)],
|
|
38
|
+
[API.throws, () => { throw new Error('Test500') }]
|
|
38
39
|
])
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
|
|
42
42
|
export const apiPatchReqs = new Map([
|
|
43
43
|
[API.cors, setCorsAllowed],
|
|
44
44
|
[API.reset, reset],
|
|
@@ -68,9 +68,9 @@ function serveDashboard(_, response) {
|
|
|
68
68
|
response.html(IndexHtml(config.hotReload, pkgJSON.version), CSP)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function
|
|
71
|
+
function serveDashboardAsset(f) {
|
|
72
72
|
return (_, response) => {
|
|
73
|
-
response.file(join(
|
|
73
|
+
response.file(join(CLIENT_ASSETS, f))
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -186,7 +186,6 @@ async function setCollectProxied(req, response) {
|
|
|
186
186
|
|
|
187
187
|
async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
188
188
|
const comment = await req.json()
|
|
189
|
-
|
|
190
189
|
mockBrokersCollection.setMocksMatchingComment(comment)
|
|
191
190
|
response.ok()
|
|
192
191
|
uiSyncVersion.increment()
|
|
@@ -195,7 +194,6 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
|
195
194
|
|
|
196
195
|
async function selectMock(req, response) {
|
|
197
196
|
const file = await req.json()
|
|
198
|
-
|
|
199
197
|
const broker = mockBrokersCollection.brokerByFilename(file)
|
|
200
198
|
if (!broker || !broker.hasMock(file))
|
|
201
199
|
response.unprocessable(`Missing Mock: ${file}`)
|
|
@@ -209,7 +207,6 @@ async function selectMock(req, response) {
|
|
|
209
207
|
|
|
210
208
|
async function toggleRouteStatus(req, response) {
|
|
211
209
|
const [method, urlMask, status] = await req.json()
|
|
212
|
-
|
|
213
210
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
214
211
|
if (!broker)
|
|
215
212
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
@@ -223,7 +220,6 @@ async function toggleRouteStatus(req, response) {
|
|
|
223
220
|
|
|
224
221
|
async function setRouteIsDelayed(req, response) {
|
|
225
222
|
const [method, urlMask, delayed] = await req.json()
|
|
226
|
-
|
|
227
223
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
228
224
|
if (!broker)
|
|
229
225
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
@@ -239,7 +235,6 @@ async function setRouteIsDelayed(req, response) {
|
|
|
239
235
|
|
|
240
236
|
async function setRouteIsProxied(req, response) {
|
|
241
237
|
const [method, urlMask, proxied] = await req.json()
|
|
242
|
-
|
|
243
238
|
const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
|
|
244
239
|
if (!broker)
|
|
245
240
|
response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
|
|
@@ -303,7 +298,6 @@ async function deleteMock(req, response) {
|
|
|
303
298
|
|
|
304
299
|
async function setWatchMocks(req, response) {
|
|
305
300
|
const enabled = await req.json()
|
|
306
|
-
|
|
307
301
|
if (typeof enabled !== 'boolean')
|
|
308
302
|
response.unprocessable(`Expected boolean for "watchMocks"`)
|
|
309
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)
|
|
@@ -1047,7 +1047,7 @@ describe('Write and Delete Mock', () => {
|
|
|
1047
1047
|
})
|
|
1048
1048
|
|
|
1049
1049
|
|
|
1050
|
-
describe('import
|
|
1050
|
+
describe('import resolvers', () => {
|
|
1051
1051
|
test('resolves extensionless ts', async () => {
|
|
1052
1052
|
await api.writeMock('_scores.ts', 'export default [1,2,3]')
|
|
1053
1053
|
await api.writeMock('user-scores.GET.200.ts',
|
|
@@ -2,7 +2,6 @@ import { resolve as _resolve } from 'node:path'
|
|
|
2
2
|
|
|
3
3
|
const mockatonSrcRoot = `file://${_resolve(import.meta.dirname, '..')}`
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
// We register this hook at runtime so it doesn’t interfere with non-dynamic imports.
|
|
7
6
|
// Cache bust by appending timestamp query param
|
|
8
7
|
export async function resolve(specifier, context, nextResolve) {
|
|
@@ -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
|
+
})
|
package/src/server/utils/fs.js
CHANGED
|
@@ -34,8 +34,8 @@ export async function rm(path) {
|
|
|
34
34
|
export async function resolveIn(baseDir, file) {
|
|
35
35
|
try {
|
|
36
36
|
const parent = await realpath(baseDir)
|
|
37
|
-
const child = resolve(parent, file)
|
|
38
|
-
return child.startsWith(parent
|
|
37
|
+
const child = resolve(join(parent, file))
|
|
38
|
+
return child.startsWith(join(parent, sep))
|
|
39
39
|
? child
|
|
40
40
|
: null
|
|
41
41
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { equal } from 'node:assert/strict'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { after, describe, test } from 'node:test'
|
|
5
|
+
import { mkdtempSync, rmSync, realpathSync } from 'node:fs'
|
|
6
|
+
|
|
7
|
+
import { resolveIn } from './fs.js'
|
|
8
|
+
|
|
9
|
+
const isNull = v => equal(v, null)
|
|
10
|
+
|
|
11
|
+
describe('resolveIn', () => {
|
|
12
|
+
const baseDir = mkdtempSync(join(tmpdir(), '_resolveIn'))
|
|
13
|
+
const baseParentDir = join(baseDir, '..')
|
|
14
|
+
after(() => rmSync(baseDir, { recursive: true, force: true }))
|
|
15
|
+
|
|
16
|
+
test('null when baseDir does not exist', async () =>
|
|
17
|
+
isNull(await resolveIn(join(baseParentDir, 'missing'), 'file.json')))
|
|
18
|
+
|
|
19
|
+
test('null when relative path escapes baseDir', async () =>
|
|
20
|
+
isNull(await resolveIn(baseDir, '../outside.json')))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const realBaseDir = realpathSync(baseDir)
|
|
24
|
+
const onReal = f => join(realBaseDir, f)
|
|
25
|
+
|
|
26
|
+
test('resolves a relative file within baseDir', async () =>
|
|
27
|
+
equal(await resolveIn(baseDir, 'file.json'), onReal('file.json')))
|
|
28
|
+
|
|
29
|
+
test('resolves file starting with /', async () =>
|
|
30
|
+
equal(await resolveIn(baseDir, '/file.json'), onReal('file.json')))
|
|
31
|
+
})
|