mockaton 13.9.4 → 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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.9.4",
5
+ "version": "13.9.5",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -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.txt'),
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.txt',
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')))
@@ -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 CLIENT_DIR = join(import.meta.dirname, '../client')
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
- ...listFilesRecursively(CLIENT_DIR).map(f =>
28
- [`${API.dashboard}/${f}`, serveStatic(f)]),
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.throws, () => { throw new Error('Test500') }],
35
- [API.openAPI, (_, response) => response.json(openapi)]
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 serveStatic(f) {
69
- return (_, response) => { response.file(join(CLIENT_DIR, f)) }
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(req.headers.range, join(config.mocksDir, broker.file))
42
+ await response.partialContent(join(config.mocksDir, broker.file))
43
43
  }, Number(broker.delayed && calcDelay()))
44
44
  return
45
45
  }
@@ -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, CLIENT_DIR } from './Api.js'
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(CLIENT_DIR)
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
- this.end(await fs.promises.readFile(file))
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(range, file) {
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
- if (isNaN(end)) end = size - 1
80
- if (isNaN(start)) start = size - end
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 || start > end || start >= size || end >= size) {
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
- return new Promise((resolve, reject) => {
95
- const reader = fs.createReadStream(file, { start, end })
96
- this.on('error', reject)
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
+ })