@flancer32/teq-web 0.1.0 → 0.2.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/.github/workflows/npm-publish.yml +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +33 -0
- package/package.json +4 -2
- package/src/Back/Handler/Npm.js +161 -0
- package/test/accept/ExternalServer.test.mjs +66 -0
- package/test/accept/Server.test.mjs +46 -1
- package/test/dev/app/Plugin/Start.js +4 -0
- package/test/unit/Back/Handler/Npm.test.mjs +69 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0] - 2025-06-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Integration test covering NPM handler with the built-in server.
|
|
7
|
+
- Built-in NPM handler for serving files from `node_modules`.
|
|
8
|
+
- JSDoc examples for NPM handler initialization.
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2025-06-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release of `@flancer32/teq-web`, a TeqFW plugin for centralized HTTP(S) request handling.
|
|
14
|
+
- Dispatcher with three-stage lifecycle: `pre`, `process`, and `post`, each with isolated execution logic.
|
|
15
|
+
- Middleware registration with support for execution order via `before`/`after` dependencies.
|
|
16
|
+
- Support for custom adapters to integrate with various web servers (e.g., Express, Fastify, Node.js `http`).
|
|
17
|
+
- Basic Node.js HTTP server implementation for standalone use cases.
|
|
18
|
+
- Unified interfaces for registering request handlers from other teq-plugins.
|
|
19
|
+
- Modular architecture compatible with Tequila Framework philosophy.
|
|
20
|
+
|
package/README.md
CHANGED
|
@@ -81,12 +81,20 @@ resolver.addNamespaceRoot('Fl32_Web_', './node_modules/@flancer32/teq-web/src');
|
|
|
81
81
|
|
|
82
82
|
// Get and configure built-in handlers
|
|
83
83
|
const logHandler = await container.get('Fl32_Web_Back_Handler_Pre_Log$');
|
|
84
|
+
const npmHandler = await container.get('Fl32_Web_Back_Handler_Npm$');
|
|
84
85
|
const staticHandler = await container.get('Fl32_Web_Back_Handler_Static$');
|
|
86
|
+
await npmHandler.init({
|
|
87
|
+
allow: {
|
|
88
|
+
vue: ['dist/vue.global.prod.js'],
|
|
89
|
+
'@teqfw/di': ['src/Container.js'],
|
|
90
|
+
}
|
|
91
|
+
});
|
|
85
92
|
await staticHandler.init({rootPath: webRoot});
|
|
86
93
|
|
|
87
94
|
// Register handlers
|
|
88
95
|
const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
|
|
89
96
|
dispatcher.addHandler(logHandler);
|
|
97
|
+
dispatcher.addHandler(npmHandler);
|
|
90
98
|
dispatcher.addHandler(staticHandler);
|
|
91
99
|
|
|
92
100
|
// Create and start the server
|
|
@@ -105,10 +113,35 @@ await server.start({
|
|
|
105
113
|
This will start an HTTPS server on port `3443` with:
|
|
106
114
|
|
|
107
115
|
* `Fl32_Web_Back_Handler_Pre_Log` logging each request method and URL;
|
|
116
|
+
* `Fl32_Web_Back_Handler_Npm` serving allowed files from `node_modules`;
|
|
108
117
|
* `Fl32_Web_Back_Handler_Static` serving files from the `/web` folder.
|
|
109
118
|
|
|
110
119
|
---
|
|
111
120
|
|
|
121
|
+
### Using with Express or Fastify
|
|
122
|
+
|
|
123
|
+
The dispatcher can be connected to external web frameworks instead of the built-in server.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
// Express
|
|
127
|
+
const app = Express();
|
|
128
|
+
app.use(async (req, res) => {
|
|
129
|
+
await dispatcher.onEventRequest(req, res);
|
|
130
|
+
});
|
|
131
|
+
app.listen(3000);
|
|
132
|
+
|
|
133
|
+
// Fastify
|
|
134
|
+
const fastify = Fastify();
|
|
135
|
+
fastify.all('*', async (request, reply) => {
|
|
136
|
+
const req = request.raw;
|
|
137
|
+
const res = reply.raw;
|
|
138
|
+
await dispatcher.onEventRequest(req, res);
|
|
139
|
+
});
|
|
140
|
+
await fastify.listen({port: 3000});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
112
145
|
## Installation
|
|
113
146
|
|
|
114
147
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flancer32/teq-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Node.js web plugin supporting Express and Fastify integration, with built-in server for standalone operation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@eslint/js": "^9.25.0",
|
|
36
36
|
"eslint": "^9.25.0",
|
|
37
|
-
"eslint-plugin-jsdoc": "^50.6.8"
|
|
37
|
+
"eslint-plugin-jsdoc": "^50.6.8",
|
|
38
|
+
"express": "^4.18.2",
|
|
39
|
+
"fastify": "^4.27.2"
|
|
38
40
|
},
|
|
39
41
|
"engines": {
|
|
40
42
|
"node": ">=20"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serves whitelisted files from ./node_modules directory.
|
|
3
|
+
* @implements Fl32_Web_Back_Api_Handler
|
|
4
|
+
*/
|
|
5
|
+
export default class Fl32_Web_Back_Handler_Npm {
|
|
6
|
+
/* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
|
|
7
|
+
/**
|
|
8
|
+
* @param {typeof import('node:fs')} fs
|
|
9
|
+
* @param {typeof import('node:http2')} http2
|
|
10
|
+
* @param {typeof import('node:path')} path
|
|
11
|
+
* @param {Fl32_Web_Back_Logger} logger
|
|
12
|
+
* @param {Fl32_Web_Back_Helper_Mime} helpMime
|
|
13
|
+
* @param {Fl32_Web_Back_Helper_Respond} respond
|
|
14
|
+
* @param {Fl32_Web_Back_Dto_Handler_Info} dtoInfo
|
|
15
|
+
* @param {typeof Fl32_Web_Back_Enum_Stage} STAGE
|
|
16
|
+
*/
|
|
17
|
+
constructor(
|
|
18
|
+
{
|
|
19
|
+
'node:fs': fs,
|
|
20
|
+
'node:http2': http2,
|
|
21
|
+
'node:path': path,
|
|
22
|
+
Fl32_Web_Back_Logger$: logger,
|
|
23
|
+
Fl32_Web_Back_Helper_Mime$: helpMime,
|
|
24
|
+
Fl32_Web_Back_Helper_Respond$: respond,
|
|
25
|
+
Fl32_Web_Back_Dto_Handler_Info$: dtoInfo,
|
|
26
|
+
Fl32_Web_Back_Enum_Stage$: STAGE,
|
|
27
|
+
}
|
|
28
|
+
) {
|
|
29
|
+
/* eslint-enable jsdoc/check-param-names */
|
|
30
|
+
// VARS
|
|
31
|
+
const {promises: fsp} = fs;
|
|
32
|
+
const {constants: H2} = http2;
|
|
33
|
+
const {
|
|
34
|
+
HTTP2_HEADER_CONTENT_LENGTH,
|
|
35
|
+
HTTP2_HEADER_CONTENT_TYPE,
|
|
36
|
+
HTTP2_HEADER_LAST_MODIFIED,
|
|
37
|
+
HTTP_STATUS_OK,
|
|
38
|
+
} = H2;
|
|
39
|
+
const _root = path.resolve('node_modules');
|
|
40
|
+
|
|
41
|
+
/** @type {Fl32_Web_Back_Dto_Handler_Info.Dto} */
|
|
42
|
+
const _info = dtoInfo.create();
|
|
43
|
+
_info.name = this.constructor.name;
|
|
44
|
+
_info.stage = STAGE.PROCESS;
|
|
45
|
+
Object.freeze(_info);
|
|
46
|
+
|
|
47
|
+
/** @type {{[key: string]: string[]}} */
|
|
48
|
+
let _allow = {};
|
|
49
|
+
|
|
50
|
+
// MAIN
|
|
51
|
+
/**
|
|
52
|
+
* Handles request to serve allowed files from node_modules.
|
|
53
|
+
*
|
|
54
|
+
* @param {module:http.IncomingMessage|module:http2.Http2ServerRequest} req
|
|
55
|
+
* @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res
|
|
56
|
+
* @returns {Promise<boolean>}
|
|
57
|
+
*/
|
|
58
|
+
this.handle = async function (req, res) {
|
|
59
|
+
if (!respond.isWritable(res)) return false;
|
|
60
|
+
|
|
61
|
+
const urlPath = decodeURIComponent(req.url.split('?')[0]);
|
|
62
|
+
const prefix = '/node_modules/';
|
|
63
|
+
if (!urlPath.startsWith(prefix)) return false;
|
|
64
|
+
|
|
65
|
+
const rel = urlPath.slice(prefix.length);
|
|
66
|
+
if (rel.includes('..') || path.isAbsolute(rel)) {
|
|
67
|
+
logger.warn(`NPM static access denied: ${rel}`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let pkg; let subPath;
|
|
72
|
+
for (const key of Object.keys(_allow)) {
|
|
73
|
+
if (rel === key || rel.startsWith(`${key}/`)) {
|
|
74
|
+
pkg = key;
|
|
75
|
+
subPath = rel.slice(key.length);
|
|
76
|
+
if (subPath.startsWith('/')) subPath = subPath.slice(1);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!pkg) return false;
|
|
81
|
+
|
|
82
|
+
const rules = _allow[pkg] || [];
|
|
83
|
+
let allowed = false;
|
|
84
|
+
if (rules.includes('.')) {
|
|
85
|
+
allowed = true;
|
|
86
|
+
} else {
|
|
87
|
+
for (const p of rules) {
|
|
88
|
+
if (subPath === p || subPath.startsWith(`${p}/`)) {
|
|
89
|
+
allowed = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!allowed) {
|
|
96
|
+
logger.warn(`NPM static access denied: ${rel}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const fsPath = path.resolve(_root, rel);
|
|
101
|
+
if (!fsPath.startsWith(_root)) {
|
|
102
|
+
logger.warn(`NPM static access denied: ${rel}`);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let stat;
|
|
107
|
+
try {
|
|
108
|
+
stat = await fsp.stat(fsPath);
|
|
109
|
+
} catch {
|
|
110
|
+
logger.warn(`NPM static file not found: ${rel}`);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (!stat.isFile()) {
|
|
114
|
+
logger.warn(`NPM static file not found: ${rel}`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const stream = fs.createReadStream(fsPath);
|
|
119
|
+
const ext = path.extname(fsPath).toLowerCase();
|
|
120
|
+
const headers = {
|
|
121
|
+
[HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
|
|
122
|
+
[HTTP2_HEADER_CONTENT_TYPE]: helpMime.getByExt(ext),
|
|
123
|
+
[HTTP2_HEADER_LAST_MODIFIED]: stat.mtime.toUTCString(),
|
|
124
|
+
};
|
|
125
|
+
res.writeHead(HTTP_STATUS_OK, headers);
|
|
126
|
+
stream.pipe(res);
|
|
127
|
+
return true;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Initialize handler with allow list.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} params
|
|
134
|
+
* @param {{[key: string]: string[]}} params.allow - Map of packages to paths
|
|
135
|
+
* that can be served from `node_modules`.
|
|
136
|
+
* @returns {Promise<void>}
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // Allow a single file from a package
|
|
140
|
+
* await npmHandler.init({
|
|
141
|
+
* allow: {
|
|
142
|
+
* vue: ['dist/vue.global.prod.js'],
|
|
143
|
+
* },
|
|
144
|
+
* });
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* // Allow all files from a subfolder
|
|
148
|
+
* await npmHandler.init({
|
|
149
|
+
* allow: {
|
|
150
|
+
* '@teqfw/di/src': ['.'],
|
|
151
|
+
* },
|
|
152
|
+
* });
|
|
153
|
+
*/
|
|
154
|
+
this.init = async function ({allow}) {
|
|
155
|
+
_allow = allow || {};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** @returns {Fl32_Web_Back_Dto_Handler_Info.Dto} */
|
|
159
|
+
this.getRegistrationInfo = () => _info;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {describe, it} from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import {buildTestContainer} from '../unit/common.js';
|
|
5
|
+
import Express from 'express';
|
|
6
|
+
import Fastify from 'fastify';
|
|
7
|
+
|
|
8
|
+
async function startExpress(port, dispatcher) {
|
|
9
|
+
const app = Express();
|
|
10
|
+
app.use(async (req, res) => {
|
|
11
|
+
await dispatcher.onEventRequest(req, res);
|
|
12
|
+
});
|
|
13
|
+
const server = await new Promise((resolve, reject) => {
|
|
14
|
+
const srv = app.listen(port, err => err ? reject(err) : resolve(srv));
|
|
15
|
+
});
|
|
16
|
+
return server;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function startFastify(port, dispatcher) {
|
|
20
|
+
const fastify = Fastify();
|
|
21
|
+
fastify.all('*', async (request, reply) => {
|
|
22
|
+
const req = request.raw;
|
|
23
|
+
const res = reply.raw;
|
|
24
|
+
await dispatcher.onEventRequest(req, res);
|
|
25
|
+
});
|
|
26
|
+
await fastify.listen({port});
|
|
27
|
+
return fastify;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('Dispatcher integration with external servers', () => {
|
|
31
|
+
it('should respond via express', async () => {
|
|
32
|
+
const container = buildTestContainer();
|
|
33
|
+
const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
|
|
34
|
+
const port = 3054;
|
|
35
|
+
const server = await startExpress(port, dispatcher);
|
|
36
|
+
|
|
37
|
+
const status = await new Promise((resolve, reject) => {
|
|
38
|
+
http.get(`http://localhost:${port}`, res => {
|
|
39
|
+
const {statusCode} = res;
|
|
40
|
+
res.resume();
|
|
41
|
+
res.on('end', () => resolve(statusCode));
|
|
42
|
+
}).on('error', reject);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.strictEqual(status, 404);
|
|
46
|
+
await new Promise(resolve => server.close(resolve));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should respond via fastify', async () => {
|
|
50
|
+
const container = buildTestContainer();
|
|
51
|
+
const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
|
|
52
|
+
const port = 3055;
|
|
53
|
+
const fastify = await startFastify(port, dispatcher);
|
|
54
|
+
|
|
55
|
+
const status = await new Promise((resolve, reject) => {
|
|
56
|
+
http.get(`http://localhost:${port}`, res => {
|
|
57
|
+
const {statusCode} = res;
|
|
58
|
+
res.resume();
|
|
59
|
+
res.on('end', () => resolve(statusCode));
|
|
60
|
+
}).on('error', reject);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.strictEqual(status, 404);
|
|
64
|
+
await fastify.close();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -11,7 +11,7 @@ async function waitListening(server) {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
describe('Fl32_Web_Back_Server
|
|
14
|
+
describe('Fl32_Web_Back_Server', () => {
|
|
15
15
|
it('should start and respond in HTTP/1 mode', async () => {
|
|
16
16
|
const container = buildTestContainer();
|
|
17
17
|
const server = await container.get('Fl32_Web_Back_Server$');
|
|
@@ -103,3 +103,48 @@ describe('Fl32_Web_Back_Server (real)', () => {
|
|
|
103
103
|
assert.strictEqual(server.getInstance(), undefined);
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
describe('Fl32_Web_Back_Api_Handler', () => {
|
|
108
|
+
it('should serve allowed NPM file', async () => {
|
|
109
|
+
const container = buildTestContainer();
|
|
110
|
+
const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
|
|
111
|
+
const handler = await container.get('Fl32_Web_Back_Handler_Npm$');
|
|
112
|
+
await handler.init({
|
|
113
|
+
allow: {
|
|
114
|
+
'@teqfw/di/src': ['.'],
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
dispatcher.addHandler(handler);
|
|
118
|
+
dispatcher.orderHandlers();
|
|
119
|
+
|
|
120
|
+
const server = await container.get('Fl32_Web_Back_Server$');
|
|
121
|
+
const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
|
|
122
|
+
const Config = await container.get('Fl32_Web_Back_Server_Config$');
|
|
123
|
+
const http = await container.get('node:http');
|
|
124
|
+
|
|
125
|
+
const cfg = Config.create({ port: 3056, type: SERVER_TYPE.HTTP });
|
|
126
|
+
await server.start(cfg);
|
|
127
|
+
await waitListening(server);
|
|
128
|
+
|
|
129
|
+
const result = await new Promise((resolve, reject) => {
|
|
130
|
+
const req = http.get({
|
|
131
|
+
hostname: 'localhost',
|
|
132
|
+
port: cfg.port,
|
|
133
|
+
path: '/node_modules/@teqfw/di/src/Api/Container/Parser/Chunk.js',
|
|
134
|
+
}, res => {
|
|
135
|
+
const chunks = [];
|
|
136
|
+
res.on('data', ch => chunks.push(ch));
|
|
137
|
+
res.on('end', () => {
|
|
138
|
+
resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() });
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
req.on('error', reject);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
assert.strictEqual(result.status, 200);
|
|
145
|
+
assert.match(result.body, /class TeqFw_Di_Api_Container_Parser_Chunk/);
|
|
146
|
+
|
|
147
|
+
await server.stop();
|
|
148
|
+
assert.strictEqual(server.getInstance(), undefined);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -4,6 +4,7 @@ export default class App_Plugin_Start {
|
|
|
4
4
|
* @param {typeof import('node:url')} url
|
|
5
5
|
* @param {Fl32_Web_Back_Dispatcher} dispatcher
|
|
6
6
|
* @param {Fl32_Web_Back_Handler_Pre_Log} hndlRequestLog
|
|
7
|
+
* @param {Fl32_Web_Back_Handler_Npm} hndlNpm
|
|
7
8
|
* @param {Fl32_Web_Back_Handler_Static} hndlStatic
|
|
8
9
|
*/
|
|
9
10
|
constructor(
|
|
@@ -12,6 +13,7 @@ export default class App_Plugin_Start {
|
|
|
12
13
|
'node:url': url,
|
|
13
14
|
Fl32_Web_Back_Dispatcher$: dispatcher,
|
|
14
15
|
Fl32_Web_Back_Handler_Pre_Log$: hndlRequestLog,
|
|
16
|
+
Fl32_Web_Back_Handler_Npm$: hndlNpm,
|
|
15
17
|
Fl32_Web_Back_Handler_Static$: hndlStatic,
|
|
16
18
|
}
|
|
17
19
|
) {
|
|
@@ -29,9 +31,11 @@ export default class App_Plugin_Start {
|
|
|
29
31
|
|
|
30
32
|
return async function () {
|
|
31
33
|
// Set up handlers
|
|
34
|
+
await hndlNpm.init({allow: {'@teqfw/di': ['src/Container.js']}});
|
|
32
35
|
await hndlStatic.init({rootPath: webRoot});
|
|
33
36
|
// Register handlers
|
|
34
37
|
dispatcher.addHandler(hndlRequestLog);
|
|
38
|
+
dispatcher.addHandler(hndlNpm);
|
|
35
39
|
dispatcher.addHandler(hndlStatic);
|
|
36
40
|
};
|
|
37
41
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {describe, it, beforeEach} from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {Writable} from 'node:stream';
|
|
4
|
+
import {buildTestContainer} from '../../common.js';
|
|
5
|
+
|
|
6
|
+
class MockRes extends Writable {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.data = Buffer.alloc(0);
|
|
10
|
+
this.statusCode = undefined;
|
|
11
|
+
this.headers = undefined;
|
|
12
|
+
this._headersSent = false;
|
|
13
|
+
this._ended = false;
|
|
14
|
+
}
|
|
15
|
+
get headersSent() { return this._headersSent; }
|
|
16
|
+
get writableEnded() { return this._ended; }
|
|
17
|
+
writeHead(status, headers) {
|
|
18
|
+
this.statusCode = status;
|
|
19
|
+
this.headers = headers;
|
|
20
|
+
this._headersSent = true;
|
|
21
|
+
}
|
|
22
|
+
_write(chunk, enc, cb) {
|
|
23
|
+
this.data = Buffer.concat([this.data, chunk]);
|
|
24
|
+
cb();
|
|
25
|
+
}
|
|
26
|
+
end(chunk) {
|
|
27
|
+
if (chunk) this.data = Buffer.concat([this.data, Buffer.from(chunk)]);
|
|
28
|
+
this._ended = true;
|
|
29
|
+
super.end();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('Fl32_Web_Back_Handler_Npm', () => {
|
|
34
|
+
let container;
|
|
35
|
+
const log = [];
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
container = buildTestContainer();
|
|
39
|
+
container.register('Fl32_Web_Back_Logger$', {
|
|
40
|
+
warn: (...args) => log.push(['warn', ...args]),
|
|
41
|
+
exception: (...args) => log.push(['exception', ...args]),
|
|
42
|
+
});
|
|
43
|
+
log.length = 0;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should serve allowed file', async () => {
|
|
47
|
+
const handler = await container.get('Fl32_Web_Back_Handler_Npm$');
|
|
48
|
+
await handler.init({allow: {'@teqfw/di': ['package.json']}});
|
|
49
|
+
const req = {url: '/node_modules/@teqfw/di/package.json'};
|
|
50
|
+
const res = new MockRes();
|
|
51
|
+
const ok = await handler.handle(req, res);
|
|
52
|
+
await new Promise(resolve => res.on('finish', resolve));
|
|
53
|
+
assert.strictEqual(ok, true);
|
|
54
|
+
assert.strictEqual(res.statusCode, 200);
|
|
55
|
+
assert.match(res.data.toString(), /@teqfw\/di/);
|
|
56
|
+
assert.strictEqual(log.length, 0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should deny disallowed path', async () => {
|
|
60
|
+
const handler = await container.get('Fl32_Web_Back_Handler_Npm$');
|
|
61
|
+
await handler.init({allow: {'@teqfw/di': ['package.json']}});
|
|
62
|
+
const req = {url: '/node_modules/@teqfw/di/secret.js'};
|
|
63
|
+
const res = new MockRes();
|
|
64
|
+
const ok = await handler.handle(req, res);
|
|
65
|
+
assert.strictEqual(ok, false);
|
|
66
|
+
assert.strictEqual(res.headersSent, false);
|
|
67
|
+
assert.ok(log[0][0] === 'warn');
|
|
68
|
+
});
|
|
69
|
+
});
|