mockaton 11.2.6 → 11.3.0
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 +13 -841
- package/index.js +1 -1
- package/package.json +7 -1
- package/src/client/app-store.test.js +81 -0
- package/src/client/app.js +21 -7
- package/src/client/styles.css +6 -2
- package/src/server/Api.js +44 -47
- package/src/server/MockDispatcher.js +3 -4
- package/src/server/Mockaton.js +11 -13
- package/src/server/Mockaton.test.js +1190 -0
- package/src/server/ProxyRelay.js +3 -4
- package/src/server/StaticDispatcher.js +3 -4
- package/src/server/Watcher.js +2 -3
- package/src/server/WatcherDevClient.js +3 -4
- package/src/server/config.js +1 -1
- package/src/server/utils/{http-request.js → HttpIncomingMessage.js} +7 -1
- package/src/server/utils/HttpServerResponse.js +117 -0
- package/src/server/utils/http-cors.js +1 -1
- package/src/server/utils/http-cors.test.js +225 -0
- package/src/server/utils/logger.js +1 -1
- package/src/server/utils/mime.test.js +24 -0
- package/src/server/utils/validate.test.js +47 -0
- package/src/server/utils/http-response.js +0 -113
package/src/server/ProxyRelay.js
CHANGED
|
@@ -3,8 +3,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
3
3
|
|
|
4
4
|
import { extFor } from './utils/mime.js'
|
|
5
5
|
import { write, isFile } from './utils/fs.js'
|
|
6
|
-
import { readBody, BodyReaderError } from './utils/
|
|
7
|
-
import { sendUnprocessable, sendBadGateway } from './utils/http-response.js'
|
|
6
|
+
import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
|
|
8
7
|
|
|
9
8
|
import { config } from './config.js'
|
|
10
9
|
import { makeMockFilename } from './Filename.js'
|
|
@@ -23,9 +22,9 @@ export async function proxy(req, response, delay) {
|
|
|
23
22
|
}
|
|
24
23
|
catch (error) { // TESTME
|
|
25
24
|
if (error instanceof BodyReaderError)
|
|
26
|
-
|
|
25
|
+
response.unprocessable(error.name)
|
|
27
26
|
else
|
|
28
|
-
|
|
27
|
+
response.badGateway(error)
|
|
29
28
|
return
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -4,7 +4,6 @@ import { readFileSync } from 'node:fs'
|
|
|
4
4
|
import { isFile } from './utils/fs.js'
|
|
5
5
|
import { logger } from './utils/logger.js'
|
|
6
6
|
import { mimeFor } from './utils/mime.js'
|
|
7
|
-
import { sendMockNotFound, sendPartialContent } from './utils/http-response.js'
|
|
8
7
|
|
|
9
8
|
import { brokerByRoute } from './staticCollection.js'
|
|
10
9
|
import { config, calcDelay } from './config.js'
|
|
@@ -16,19 +15,19 @@ export async function dispatchStatic(req, response) {
|
|
|
16
15
|
|
|
17
16
|
setTimeout(async () => {
|
|
18
17
|
if (!broker || broker.status === 404) {
|
|
19
|
-
|
|
18
|
+
response.mockNotFound()
|
|
20
19
|
return
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
const file = join(config.staticDir, broker.route)
|
|
24
23
|
if (!isFile(file)) {
|
|
25
|
-
|
|
24
|
+
response.mockNotFound()
|
|
26
25
|
return
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
logger.accessMock(req.url, 'static200')
|
|
30
29
|
if (req.headers.range)
|
|
31
|
-
await
|
|
30
|
+
await response.partialContent(req.headers.range, file)
|
|
32
31
|
else {
|
|
33
32
|
response.setHeader('Content-Type', mimeFor(file))
|
|
34
33
|
response.end(readFileSync(file))
|
package/src/server/Watcher.js
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
} from './ApiConstants.js'
|
|
9
9
|
|
|
10
10
|
import { config } from './config.js'
|
|
11
|
-
import { sendJSON } from './utils/http-response.js'
|
|
12
11
|
import { isFile, isDirectory } from './utils/fs.js'
|
|
13
12
|
|
|
14
13
|
import * as staticCollection from './staticCollection.js'
|
|
@@ -101,12 +100,12 @@ export function longPollClientSyncVersion(req, response) {
|
|
|
101
100
|
const clientVersion = req.headers[HEADER_SYNC_VERSION]
|
|
102
101
|
if (clientVersion !== undefined && uiSyncVersion.version !== Number(clientVersion)) {
|
|
103
102
|
// e.g., tab was hidden while new mocks were added or removed
|
|
104
|
-
|
|
103
|
+
response.json(uiSyncVersion.version)
|
|
105
104
|
return
|
|
106
105
|
}
|
|
107
106
|
function onARR() {
|
|
108
107
|
uiSyncVersion.unsubscribe(onARR)
|
|
109
|
-
|
|
108
|
+
response.json(uiSyncVersion.version)
|
|
110
109
|
}
|
|
111
110
|
response.setTimeout(LONG_POLL_SERVER_TIMEOUT, onARR)
|
|
112
111
|
req.on('error', () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { EventEmitter } from 'node:events'
|
|
3
3
|
import { watch, readdirSync } from 'node:fs'
|
|
4
|
-
import { sendJSON, sendNotFound } from './utils/http-response.js'
|
|
5
4
|
import { LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
|
|
6
5
|
|
|
7
6
|
|
|
@@ -30,17 +29,17 @@ export function watchDevSPA() {
|
|
|
30
29
|
/** Realtime notify Dev UI changes */
|
|
31
30
|
export function longPollDevClientHotReload(req, response) {
|
|
32
31
|
if (!DEV) {
|
|
33
|
-
|
|
32
|
+
response.notFound()
|
|
34
33
|
return
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
function onDevChange(file) {
|
|
38
37
|
devClientWatcher.unsubscribe(onDevChange)
|
|
39
|
-
|
|
38
|
+
response.json(file)
|
|
40
39
|
}
|
|
41
40
|
response.setTimeout(LONG_POLL_SERVER_TIMEOUT, () => {
|
|
42
41
|
devClientWatcher.unsubscribe(onDevChange)
|
|
43
|
-
|
|
42
|
+
response.json('')
|
|
44
43
|
})
|
|
45
44
|
req.on('error', () => {
|
|
46
45
|
devClientWatcher.unsubscribe(onDevChange)
|
package/src/server/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import { logger } from './utils/logger.js'
|
|
|
4
4
|
import { isDirectory } from './utils/fs.js'
|
|
5
5
|
import { openInBrowser } from './utils/openInBrowser.js'
|
|
6
6
|
import { optional, is, validate } from './utils/validate.js'
|
|
7
|
-
import { SUPPORTED_METHODS } from './utils/
|
|
7
|
+
import { SUPPORTED_METHODS } from './utils/HttpIncomingMessage.js'
|
|
8
8
|
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
|
|
9
9
|
|
|
10
10
|
import { jsToJsonPlugin } from './MockDispatcher.js'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { METHODS } from 'node:http'
|
|
1
|
+
import http, { METHODS } from 'node:http'
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const SUPPORTED_METHODS = METHODS
|
|
@@ -12,6 +12,12 @@ export class BodyReaderError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export class IncomingMessage extends http.IncomingMessage {
|
|
16
|
+
json() {
|
|
17
|
+
return readBody(this, JSON.parse)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export const parseJSON = req => readBody(req, JSON.parse)
|
|
16
22
|
|
|
17
23
|
export function readBody(req, parser = a => a) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import fs, { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import { logger } from './logger.js'
|
|
5
|
+
import { mimeFor } from './mime.js'
|
|
6
|
+
import { HEADER_502 } from '../ApiConstants.js'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export class ServerResponse extends http.ServerResponse {
|
|
10
|
+
setHeaderList(headers) {
|
|
11
|
+
for (let i = 0; i < headers.length; i += 2)
|
|
12
|
+
this.setHeader(headers[i], headers[i + 1])
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ok() {
|
|
16
|
+
logger.access(this)
|
|
17
|
+
this.end()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
html(html, csp) {
|
|
21
|
+
logger.access(this)
|
|
22
|
+
this.setHeader('Content-Type', mimeFor('html'))
|
|
23
|
+
this.setHeader('Content-Security-Policy', csp)
|
|
24
|
+
this.end(html)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
json(payload) {
|
|
28
|
+
logger.access(this)
|
|
29
|
+
this.setHeader('Content-Type', 'application/json')
|
|
30
|
+
this.end(JSON.stringify(payload))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
file(file) {
|
|
34
|
+
logger.access(this)
|
|
35
|
+
this.setHeader('Content-Type', mimeFor(file))
|
|
36
|
+
this.end(readFileSync(file, 'utf8'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
noContent() {
|
|
40
|
+
this.statusCode = 204
|
|
41
|
+
logger.access(this)
|
|
42
|
+
this.end()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
badRequest() {
|
|
47
|
+
this.statusCode = 400
|
|
48
|
+
logger.access(this)
|
|
49
|
+
this.end()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
notFound() {
|
|
53
|
+
this.statusCode = 404
|
|
54
|
+
logger.access(this)
|
|
55
|
+
this.end()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
mockNotFound() {
|
|
59
|
+
this.statusCode = 404
|
|
60
|
+
logger.accessMock(this.req.url, '404')
|
|
61
|
+
this.end()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
uriTooLong() {
|
|
65
|
+
this.statusCode = 414
|
|
66
|
+
logger.access(this)
|
|
67
|
+
this.end()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
unprocessable(error) {
|
|
71
|
+
logger.access(this, error)
|
|
72
|
+
this.statusCode = 422
|
|
73
|
+
this.end(error)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
internalServerError(error) {
|
|
78
|
+
logger.error(500, this.req.url, error?.message || error, error?.stack || '')
|
|
79
|
+
this.statusCode = 500
|
|
80
|
+
this.end()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
badGateway(error) {
|
|
84
|
+
logger.warn('Fallback Proxy Error:', error.cause.message)
|
|
85
|
+
this.statusCode = 502
|
|
86
|
+
this.setHeader(HEADER_502, 1)
|
|
87
|
+
this.end()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async partialContent(range, file) {
|
|
92
|
+
const { size } = await fs.promises.lstat(file)
|
|
93
|
+
let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
94
|
+
if (isNaN(end)) end = size - 1
|
|
95
|
+
if (isNaN(start)) start = size - end
|
|
96
|
+
|
|
97
|
+
if (start < 0 || start > end || start >= size || end >= size) {
|
|
98
|
+
this.statusCode = 416 // Range Not Satisfiable
|
|
99
|
+
this.setHeader('Content-Range', `bytes */${size}`)
|
|
100
|
+
this.end()
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.statusCode = 206 // Partial Content
|
|
104
|
+
this.setHeader('Accept-Ranges', 'bytes')
|
|
105
|
+
this.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
106
|
+
this.setHeader('Content-Type', mimeFor(file))
|
|
107
|
+
const reader = fs.createReadStream(file, { start, end })
|
|
108
|
+
const response = this
|
|
109
|
+
reader.on('open', function () {
|
|
110
|
+
this.pipe(response)
|
|
111
|
+
})
|
|
112
|
+
reader.on('error', error => {
|
|
113
|
+
this.internalServerError(error)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { equal } from 'node:assert/strict'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
import { createServer } from 'node:http'
|
|
4
|
+
import { describe, test, after } from 'node:test'
|
|
5
|
+
import { isPreflight, setCorsHeaders, CorsHeader as CH } from './http-cors.js'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
function headerIs(response, header, value) {
|
|
9
|
+
equal(response.headers.get(header), value)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FooDotCom = 'http://foo.com'
|
|
13
|
+
const AllowedDotCom = 'http://allowed.com'
|
|
14
|
+
const NotAllowedDotCom = 'http://not-allowed.com'
|
|
15
|
+
|
|
16
|
+
await describe('CORS', async () => {
|
|
17
|
+
let corsConfig = {}
|
|
18
|
+
|
|
19
|
+
const server = createServer((req, response) => {
|
|
20
|
+
setCorsHeaders(req, response, corsConfig)
|
|
21
|
+
if (isPreflight(req)) {
|
|
22
|
+
response.statusCode = 204
|
|
23
|
+
response.end()
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
response.end('NON_PREFLIGHT')
|
|
27
|
+
})
|
|
28
|
+
await promisify(server.listen).bind(server, 0, '127.0.0.1')()
|
|
29
|
+
after(() => {
|
|
30
|
+
server.close()
|
|
31
|
+
})
|
|
32
|
+
function preflight(headers, method = 'OPTIONS') {
|
|
33
|
+
const { address, port } = server.address()
|
|
34
|
+
return fetch(`http://${address}:${port}/`, { method, headers })
|
|
35
|
+
}
|
|
36
|
+
function request(headers, method) {
|
|
37
|
+
const { address, port } = server.address()
|
|
38
|
+
return fetch(`http://${address}:${port}/`, { method, headers })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await describe('Identifies Preflight Requests', async () => {
|
|
42
|
+
const requiredRequestHeaders = {
|
|
43
|
+
[CH.Origin]: 'http://localhost:9999',
|
|
44
|
+
[CH.AcRequestMethod]: 'POST'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await test('Ignores non-OPTIONS requests', async () => {
|
|
48
|
+
const res = await request(requiredRequestHeaders, 'POST')
|
|
49
|
+
equal(await res.text(), 'NON_PREFLIGHT')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await test(`Ignores non-parseable req ${CH.Origin} header`, async () => {
|
|
53
|
+
const headers = {
|
|
54
|
+
...requiredRequestHeaders,
|
|
55
|
+
[CH.Origin]: 'non-url'
|
|
56
|
+
}
|
|
57
|
+
const res = await preflight(headers)
|
|
58
|
+
equal(await res.text(), 'NON_PREFLIGHT')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await test(`Ignores missing method in ${CH.AcRequestMethod} header`, async () => {
|
|
62
|
+
const headers = { ...requiredRequestHeaders }
|
|
63
|
+
delete headers[CH.AcRequestMethod]
|
|
64
|
+
const res = await preflight(headers)
|
|
65
|
+
equal(await res.text(), 'NON_PREFLIGHT')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await test(`Ignores non-standard method in ${CH.AcRequestMethod} header`, async () => {
|
|
69
|
+
const headers = {
|
|
70
|
+
...requiredRequestHeaders,
|
|
71
|
+
[CH.AcRequestMethod]: 'NON_STANDARD'
|
|
72
|
+
}
|
|
73
|
+
const res = await preflight(headers)
|
|
74
|
+
equal(await res.text(), 'NON_PREFLIGHT')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
await test('204 valid preflights', async () => {
|
|
78
|
+
const res = await preflight(requiredRequestHeaders)
|
|
79
|
+
equal(res.status, 204)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
await describe('Preflight Response Headers', async () => {
|
|
84
|
+
await test('no origins allowed', async () => {
|
|
85
|
+
corsConfig = {
|
|
86
|
+
corsOrigins: [],
|
|
87
|
+
corsMethods: ['GET']
|
|
88
|
+
}
|
|
89
|
+
const p = await preflight({
|
|
90
|
+
[CH.Origin]: FooDotCom,
|
|
91
|
+
[CH.AcRequestMethod]: 'GET'
|
|
92
|
+
})
|
|
93
|
+
headerIs(p, CH.AcAllowOrigin, null)
|
|
94
|
+
headerIs(p, CH.AcAllowMethods, null)
|
|
95
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
96
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
97
|
+
headerIs(p, CH.AcMaxAge, null)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await test('not in allowed origins', async () => {
|
|
101
|
+
corsConfig = {
|
|
102
|
+
corsOrigins: [AllowedDotCom],
|
|
103
|
+
corsMethods: ['GET']
|
|
104
|
+
}
|
|
105
|
+
const p = await preflight({
|
|
106
|
+
[CH.Origin]: NotAllowedDotCom,
|
|
107
|
+
[CH.AcRequestMethod]: 'GET'
|
|
108
|
+
})
|
|
109
|
+
headerIs(p, CH.AcAllowOrigin, null)
|
|
110
|
+
headerIs(p, CH.AcAllowMethods, null)
|
|
111
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
112
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await test('origin and method match', async () => {
|
|
116
|
+
corsConfig = {
|
|
117
|
+
corsOrigins: [AllowedDotCom],
|
|
118
|
+
corsMethods: ['GET']
|
|
119
|
+
}
|
|
120
|
+
const p = await preflight({
|
|
121
|
+
[CH.Origin]: AllowedDotCom,
|
|
122
|
+
[CH.AcRequestMethod]: 'GET'
|
|
123
|
+
})
|
|
124
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
|
|
125
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
126
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
127
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await test('origin matches from multiple', async () => {
|
|
131
|
+
corsConfig = {
|
|
132
|
+
corsOrigins: [AllowedDotCom, FooDotCom],
|
|
133
|
+
corsMethods: ['GET']
|
|
134
|
+
}
|
|
135
|
+
const p = await preflight({
|
|
136
|
+
[CH.Origin]: AllowedDotCom,
|
|
137
|
+
[CH.AcRequestMethod]: 'GET'
|
|
138
|
+
})
|
|
139
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
|
|
140
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
141
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
142
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
await test('wildcard origin', async () => {
|
|
146
|
+
corsConfig = {
|
|
147
|
+
corsOrigins: ['*'],
|
|
148
|
+
corsMethods: ['GET']
|
|
149
|
+
}
|
|
150
|
+
const p = await preflight({
|
|
151
|
+
[CH.Origin]: FooDotCom,
|
|
152
|
+
[CH.AcRequestMethod]: 'GET'
|
|
153
|
+
})
|
|
154
|
+
headerIs(p, CH.AcAllowOrigin, FooDotCom)
|
|
155
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
156
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
157
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await test(`wildcard and credentials`, async () => {
|
|
161
|
+
corsConfig = {
|
|
162
|
+
corsOrigins: ['*'],
|
|
163
|
+
corsMethods: ['GET'],
|
|
164
|
+
corsCredentials: true
|
|
165
|
+
}
|
|
166
|
+
const p = await preflight({
|
|
167
|
+
[CH.Origin]: FooDotCom,
|
|
168
|
+
[CH.AcRequestMethod]: 'GET'
|
|
169
|
+
})
|
|
170
|
+
headerIs(p, CH.AcAllowOrigin, FooDotCom)
|
|
171
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
172
|
+
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
173
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
await test(`wildcard, credentials, and headers`, async () => {
|
|
177
|
+
corsConfig = {
|
|
178
|
+
corsOrigins: ['*'],
|
|
179
|
+
corsMethods: ['GET'],
|
|
180
|
+
corsCredentials: true,
|
|
181
|
+
corsHeaders: ['content-type', 'my-header']
|
|
182
|
+
}
|
|
183
|
+
const p = await preflight({
|
|
184
|
+
[CH.Origin]: FooDotCom,
|
|
185
|
+
[CH.AcRequestMethod]: 'GET'
|
|
186
|
+
})
|
|
187
|
+
headerIs(p, CH.AcAllowOrigin, FooDotCom)
|
|
188
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
189
|
+
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
190
|
+
headerIs(p, CH.AcAllowHeaders, 'content-type,my-header')
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
await describe('Non-Preflight (Actual Response) Headers', async () => {
|
|
195
|
+
await test('no origins allowed', async () => {
|
|
196
|
+
corsConfig = {
|
|
197
|
+
corsOrigins: [],
|
|
198
|
+
corsMethods: ['GET']
|
|
199
|
+
}
|
|
200
|
+
const p = await request({
|
|
201
|
+
[CH.Origin]: NotAllowedDotCom
|
|
202
|
+
})
|
|
203
|
+
equal(p.status, 200)
|
|
204
|
+
headerIs(p, CH.AcAllowOrigin, null)
|
|
205
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
206
|
+
headerIs(p, CH.AcExposeHeaders, null)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
await test('origin allowed', async () => {
|
|
210
|
+
corsConfig = {
|
|
211
|
+
corsOrigins: [AllowedDotCom],
|
|
212
|
+
corsMethods: ['GET'],
|
|
213
|
+
corsCredentials: true,
|
|
214
|
+
corsExposedHeaders: ['x-h1', 'x-h2']
|
|
215
|
+
}
|
|
216
|
+
const p = await request({
|
|
217
|
+
[CH.Origin]: AllowedDotCom
|
|
218
|
+
})
|
|
219
|
+
equal(p.status, 200)
|
|
220
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
|
|
221
|
+
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
222
|
+
headerIs(p, CH.AcExposeHeaders, 'x-h1,x-h2')
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { equal } from 'node:assert/strict'
|
|
3
|
+
import { parseMime, extFor, mimeFor } from './mime.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
test('parseMime', () => [
|
|
7
|
+
'text/html',
|
|
8
|
+
'TEXT/html',
|
|
9
|
+
'text/html; charset=utf-8'
|
|
10
|
+
].map(input =>
|
|
11
|
+
equal(parseMime(input), 'text/html')))
|
|
12
|
+
|
|
13
|
+
test('extFor', () => [
|
|
14
|
+
'text/html',
|
|
15
|
+
'Text/html',
|
|
16
|
+
'text/Html; charset=UTF-16'
|
|
17
|
+
].map(input =>
|
|
18
|
+
equal(extFor(input), 'html')))
|
|
19
|
+
|
|
20
|
+
test('mimeFor', () => [
|
|
21
|
+
'file.html',
|
|
22
|
+
'file.HTmL'
|
|
23
|
+
].map(input =>
|
|
24
|
+
equal(mimeFor(input), 'text/html')))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import { doesNotThrow, throws } from 'node:assert/strict'
|
|
3
|
+
import { validate, is, optional } from './validate.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('validate', () => {
|
|
7
|
+
describe('optional', () => {
|
|
8
|
+
test('accepts undefined', () =>
|
|
9
|
+
doesNotThrow(() =>
|
|
10
|
+
validate({}, { field: optional(Number.isInteger) })))
|
|
11
|
+
|
|
12
|
+
test('accepts falsy value regardless of type', () =>
|
|
13
|
+
doesNotThrow(() =>
|
|
14
|
+
validate({ field: 0 }, { field: optional(Array.isArray) })))
|
|
15
|
+
|
|
16
|
+
test('accepts when tester func returns truthy', () =>
|
|
17
|
+
doesNotThrow(() =>
|
|
18
|
+
validate({ field: [] }, { field: optional(Array.isArray) })))
|
|
19
|
+
|
|
20
|
+
test('rejects when tester func returns falsy', () =>
|
|
21
|
+
throws(() =>
|
|
22
|
+
validate({ field: 1 }, { field: optional(Array.isArray) }),
|
|
23
|
+
/field=1 is invalid/))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('is', () => {
|
|
27
|
+
test('rejects mismatched type', () =>
|
|
28
|
+
throws(() =>
|
|
29
|
+
validate({ field: 1 }, { field: is(String) }),
|
|
30
|
+
/field=1 is invalid/))
|
|
31
|
+
|
|
32
|
+
test('accepts matched type', () =>
|
|
33
|
+
doesNotThrow(() =>
|
|
34
|
+
validate({ field: '' }, { field: is(String) })))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('custom tester func', () => {
|
|
38
|
+
test('rejects mismatched type', () =>
|
|
39
|
+
throws(() =>
|
|
40
|
+
validate({ field: 0.1 }, { field: Number.isInteger }),
|
|
41
|
+
/field=0.1 is invalid/))
|
|
42
|
+
|
|
43
|
+
test('accepts matched type', () =>
|
|
44
|
+
doesNotThrow(() =>
|
|
45
|
+
validate({ field: 1 }, { field: Number.isInteger })))
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import fs, { readFileSync } from 'node:fs'
|
|
2
|
-
|
|
3
|
-
import { logger } from './logger.js'
|
|
4
|
-
import { mimeFor } from './mime.js'
|
|
5
|
-
import { HEADER_502 } from '../ApiConstants.js'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export function setHeaders(response, headers) {
|
|
9
|
-
for (let i = 0; i < headers.length; i += 2)
|
|
10
|
-
response.setHeader(headers[i], headers[i + 1])
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function sendOK(response) {
|
|
14
|
-
logger.access(response)
|
|
15
|
-
response.end()
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function sendHTML(response, html, csp) {
|
|
19
|
-
logger.access(response)
|
|
20
|
-
response.setHeader('Content-Type', mimeFor('html'))
|
|
21
|
-
response.setHeader('Content-Security-Policy', csp)
|
|
22
|
-
response.end(html)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function sendJSON(response, payload) {
|
|
26
|
-
logger.access(response)
|
|
27
|
-
response.setHeader('Content-Type', 'application/json')
|
|
28
|
-
response.end(JSON.stringify(payload))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function sendFile(response, file) {
|
|
32
|
-
logger.access(response)
|
|
33
|
-
response.setHeader('Content-Type', mimeFor(file))
|
|
34
|
-
response.end(readFileSync(file, 'utf8'))
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function sendNoContent(response) {
|
|
38
|
-
response.statusCode = 204
|
|
39
|
-
logger.access(response)
|
|
40
|
-
response.end()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export function sendBadRequest(response) {
|
|
45
|
-
response.statusCode = 400
|
|
46
|
-
logger.access(response)
|
|
47
|
-
response.end()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function sendNotFound(response) {
|
|
51
|
-
response.statusCode = 404
|
|
52
|
-
logger.access(response)
|
|
53
|
-
response.end()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function sendMockNotFound(response) {
|
|
57
|
-
response.statusCode = 404
|
|
58
|
-
logger.accessMock(response.req.url, '404')
|
|
59
|
-
response.end()
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function sendTooLongURI(response) {
|
|
63
|
-
response.statusCode = 414
|
|
64
|
-
logger.access(response)
|
|
65
|
-
response.end()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function sendUnprocessable(response, error) {
|
|
69
|
-
logger.access(response, error)
|
|
70
|
-
response.statusCode = 422
|
|
71
|
-
response.end(error)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
export function sendInternalServerError(response, error) {
|
|
76
|
-
logger.error(500, response.req.url, error?.message || error, error?.stack || '')
|
|
77
|
-
response.statusCode = 500
|
|
78
|
-
response.end()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function sendBadGateway(response, error) {
|
|
82
|
-
logger.warn('Fallback Proxy Error:', error.cause.message)
|
|
83
|
-
response.statusCode = 502
|
|
84
|
-
response.setHeader(HEADER_502, 1)
|
|
85
|
-
response.end()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export async function sendPartialContent(response, range, file) {
|
|
90
|
-
const { size } = await fs.promises.lstat(file)
|
|
91
|
-
let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
92
|
-
if (isNaN(end)) end = size - 1
|
|
93
|
-
if (isNaN(start)) start = size - end
|
|
94
|
-
|
|
95
|
-
if (start < 0 || start > end || start >= size || end >= size) {
|
|
96
|
-
response.statusCode = 416 // Range Not Satisfiable
|
|
97
|
-
response.setHeader('Content-Range', `bytes */${size}`)
|
|
98
|
-
response.end()
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
response.statusCode = 206 // Partial Content
|
|
102
|
-
response.setHeader('Accept-Ranges', 'bytes')
|
|
103
|
-
response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
104
|
-
response.setHeader('Content-Type', mimeFor(file))
|
|
105
|
-
const reader = fs.createReadStream(file, { start, end })
|
|
106
|
-
reader.on('open', function () {
|
|
107
|
-
this.pipe(response)
|
|
108
|
-
})
|
|
109
|
-
reader.on('error', function (error) {
|
|
110
|
-
sendInternalServerError(response, error)
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
}
|