@flancer32/teq-web 0.1.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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Factory for server configuration DTO.
3
+ * Supports HTTP, HTTPS and HTTP2 server types with TLS configuration.
4
+ */
5
+ export default class Fl32_Web_Back_Server_Config {
6
+ /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
7
+ /**
8
+ * @param {Fl32_Web_Back_Helper_Cast} cast
9
+ * @param {typeof Fl32_Web_Back_Enum_Server_Type} SERVER_TYPE
10
+ * @param {Fl32_Web_Back_Server_Config_Tls} tlsFactory
11
+ */
12
+ constructor(
13
+ {
14
+ Fl32_Web_Back_Helper_Cast$: cast,
15
+ Fl32_Web_Back_Enum_Server_Type$: SERVER_TYPE,
16
+ Fl32_Web_Back_Server_Config_Tls$: tlsFactory,
17
+ }
18
+ ) {
19
+ /* eslint-enable jsdoc/require-param-description,jsdoc/check-param-names */
20
+ // INSTANCE METHODS
21
+ /**
22
+ * Creates a new DTO instance with properly casted attributes.
23
+ * Ensures valid values for enums and numerical fields.
24
+ * Validates TLS configuration when type is HTTPS.
25
+ *
26
+ * @param {Fl32_Web_Back_Server_Config.Dto|object} [data] - Raw input data for the DTO.
27
+ * @returns {Dto} - A properly structured DTO instance.
28
+ * @throws {Error} When HTTPS type is specified without TLS configuration.
29
+ */
30
+ this.create = function (data) {
31
+ const res = Object.assign(new Dto(), data);
32
+ if (data) {
33
+ res.port = cast.int(data.port);
34
+ res.type = cast.enum(data.type, SERVER_TYPE, {lower: true});
35
+
36
+ if (data.tls) {
37
+ res.tls = tlsFactory.create(data.tls);
38
+ }
39
+
40
+ if (res.type === SERVER_TYPE.HTTPS && !res.tls) {
41
+ throw new Error('TLS configuration is required for HTTPS server type');
42
+ }
43
+ }
44
+ return res;
45
+ };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @memberOf Fl32_Web_Back_Server_Config
51
+ */
52
+ class Dto {
53
+ /**
54
+ * Port to listening (3000).
55
+ *
56
+ * @type {number}
57
+ */
58
+ port;
59
+ /**
60
+ * @type {string}
61
+ * @see Fl32_Web_Back_Enum_Server_Type
62
+ */
63
+ type;
64
+ /**
65
+ * TLS configuration for HTTPS server.
66
+ * @type {Fl32_Web_Back_Server_Config_Tls.Dto|undefined}
67
+ */
68
+ tls;
69
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Web server implementation supporting HTTP/1 and HTTP/2 protocols.
3
+ * Handles incoming requests and delegates them to the dispatcher.
4
+ *
5
+ * @property {function(): module:http.Server} getInstance - Returns the server instance.
6
+ * @property {function(Fl32_Web_Back_Server_Config.Dto): Promise<void>} start - Starts the server with given configuration.
7
+ * @property {function(): Promise<void>} stop - Stops the server.
8
+ */
9
+ export default class Fl32_Web_Back_Server {
10
+ /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
11
+ /**
12
+ * @param {typeof import('node:http')} http
13
+ * @param {typeof import('node:http2')} http2
14
+ * @param {Fl32_Web_Back_Defaults} DEF
15
+ * @param {Fl32_Web_Back_Logger} logger
16
+ * @param {Fl32_Web_Back_Dispatcher} dispatcher
17
+ * @param {typeof Fl32_Web_Back_Enum_Server_Type} SERVER_TYPE
18
+ */
19
+ constructor(
20
+ {
21
+ 'node:http': http,
22
+ 'node:http2': http2,
23
+ Fl32_Web_Back_Defaults$: DEF,
24
+ Fl32_Web_Back_Logger$: logger,
25
+ Fl32_Web_Back_Dispatcher$: dispatcher,
26
+ Fl32_Web_Back_Enum_Server_Type$: SERVER_TYPE,
27
+ }
28
+ ) {
29
+ /* eslint-enable jsdoc/require-param-description,jsdoc/check-param-names */
30
+ // VARS
31
+ const {createServer} = http;
32
+ const {createServer: createServerH2, createSecureServer} = http2;
33
+ /** @type {module:http.Server} */
34
+ let _instance;
35
+
36
+ // MAIN
37
+ /**
38
+ * @returns {module:http.Server}
39
+ */
40
+ this.getInstance = () => _instance;
41
+
42
+ /**
43
+ * Starts the server with optional configuration.
44
+ * @param {Fl32_Web_Back_Server_Config.Dto} [cfg] - Server configuration
45
+ * @returns {Promise<void>}
46
+ */
47
+ this.start = async function (cfg) {
48
+ // order handlers in the dispatcher
49
+ dispatcher.orderHandlers();
50
+ // create server
51
+ const port = cfg?.port ?? DEF.PORT;
52
+ const type = cfg?.type ?? SERVER_TYPE.HTTP;
53
+
54
+ if (type === SERVER_TYPE.HTTP2) {
55
+ _instance = createServerH2();
56
+ logger.info(`Starting server in HTTP/2 mode on port ${port}...`);
57
+ } else if (type === SERVER_TYPE.HTTP) {
58
+ _instance = createServer({});
59
+ logger.info(`Starting server in HTTP/1 mode on port ${port}...`);
60
+ } else if (type === SERVER_TYPE.HTTPS) {
61
+ if (!cfg.tls?.key || !cfg.tls?.cert) {
62
+ logger.error('HTTPS server requires TLS key and certificate');
63
+ throw new Error('TLS key and certificate are required for HTTPS server');
64
+ }
65
+ _instance = createSecureServer(cfg.tls);
66
+ logger.info(`Starting server in HTTPS (HTTP/2 + TLS) mode on port ${port}...`);
67
+ } else {
68
+ logger.error(`Unsupported server type: ${type}`);
69
+ throw new Error(`Server type '${type}' is not supported`);
70
+ }
71
+
72
+ _instance.on('request', dispatcher.onEventRequest);
73
+ _instance.listen(port);
74
+ };
75
+
76
+ /**
77
+ * Stops the server.
78
+ * @returns {Promise<void>}
79
+ */
80
+ this.stop = async function () {
81
+ if (_instance) {
82
+ await new Promise((resolve, reject) => {
83
+ _instance.close(err => err ? reject(err) : resolve());
84
+ });
85
+ logger.info('Server stopped');
86
+ _instance = undefined;
87
+ } else {
88
+ logger.warn('Server is not running');
89
+ }
90
+ };
91
+ }
92
+ }
@@ -0,0 +1,105 @@
1
+ import {describe, it} from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {readFileSync} from 'node:fs';
4
+ import {join, dirname} from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {buildTestContainer} from '../unit/common.js';
7
+
8
+ async function waitListening(server) {
9
+ if (!server.getInstance().listening) {
10
+ await new Promise(res => server.getInstance().once('listening', res));
11
+ }
12
+ }
13
+
14
+ describe('Fl32_Web_Back_Server (real)', () => {
15
+ it('should start and respond in HTTP/1 mode', async () => {
16
+ const container = buildTestContainer();
17
+ const server = await container.get('Fl32_Web_Back_Server$');
18
+ const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
19
+ const Config = await container.get('Fl32_Web_Back_Server_Config$');
20
+ const http = await container.get('node:http');
21
+
22
+ const cfg = Config.create({ port: 3051, type: SERVER_TYPE.HTTP });
23
+ await server.start(cfg);
24
+ await waitListening(server);
25
+
26
+ const status = await new Promise((resolve, reject) => {
27
+ http.get(`http://localhost:${cfg.port}`, res => {
28
+ const {statusCode} = res;
29
+ res.resume();
30
+ res.on('end', () => resolve(statusCode));
31
+ }).on('error', reject);
32
+ });
33
+
34
+ assert.strictEqual(status, 404);
35
+ await server.stop();
36
+ assert.strictEqual(server.getInstance(), undefined);
37
+ });
38
+
39
+ it('should start and respond in HTTP/2 mode', async () => {
40
+ const container = buildTestContainer();
41
+ const server = await container.get('Fl32_Web_Back_Server$');
42
+ const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
43
+ const Config = await container.get('Fl32_Web_Back_Server_Config$');
44
+ const http2 = await container.get('node:http2');
45
+
46
+ const cfg = Config.create({ port: 3052, type: SERVER_TYPE.HTTP2 });
47
+ await server.start(cfg);
48
+ await waitListening(server);
49
+
50
+ const status = await new Promise((resolve, reject) => {
51
+ const client = http2.connect(`http://localhost:${cfg.port}`);
52
+ client.on('error', reject);
53
+ const req = client.request({ ':path': '/' });
54
+ req.on('response', headers => {
55
+ resolve(headers[':status']);
56
+ });
57
+ req.on('end', () => client.close());
58
+ req.end();
59
+ });
60
+
61
+ assert.strictEqual(status, 404);
62
+ await server.stop();
63
+ assert.strictEqual(server.getInstance(), undefined);
64
+ });
65
+
66
+ it('should start and respond in HTTPS mode', async () => {
67
+ const container = buildTestContainer();
68
+ const server = await container.get('Fl32_Web_Back_Server$');
69
+ const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
70
+ const Config = await container.get('Fl32_Web_Back_Server_Config$');
71
+ const http2 = await container.get('node:http2');
72
+
73
+ const dir = dirname(fileURLToPath(import.meta.url));
74
+ const certDir = join(dir, '..', 'certs');
75
+ const key = readFileSync(join(certDir, 'key.pem'), 'utf8');
76
+ const cert = readFileSync(join(certDir, 'cert.pem'), 'utf8');
77
+ let ca;
78
+ try { ca = readFileSync(join(certDir, 'ca.pem'), 'utf8'); } catch {}
79
+
80
+ const cfg = Config.create({
81
+ port: 3053,
82
+ type: SERVER_TYPE.HTTPS,
83
+ tls: {key, cert, ca}
84
+ });
85
+ await server.start(cfg);
86
+ await waitListening(server);
87
+
88
+ const status = await new Promise((resolve, reject) => {
89
+ const client = http2.connect(`https://localhost:${cfg.port}`, {
90
+ rejectUnauthorized: false,
91
+ });
92
+ client.on('error', reject);
93
+ const req = client.request({ ':path': '/' });
94
+ req.on('response', headers => {
95
+ resolve(headers[':status']);
96
+ });
97
+ req.on('end', () => client.close());
98
+ req.end();
99
+ });
100
+
101
+ assert.strictEqual(status, 404);
102
+ await server.stop();
103
+ assert.strictEqual(server.getInstance(), undefined);
104
+ });
105
+ });
@@ -0,0 +1,19 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDCTCCAfGgAwIBAgIUBpZ2v9fA8XfqVF83Nw5byUa0ylswDQYJKoZIhvcNAQEL
3
+ BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYwODA5MDA1N1oXDTI2MDYw
4
+ ODA5MDA1N1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
5
+ AAOCAQ8AMIIBCgKCAQEAymikxJnbPFs3H/rRzetPjUdOPVuMOTfkz5oBDcP7s/2W
6
+ O7135UPbDGzClYyYJvwAZm9i1Vivzm6KrNDZR0lawqYVf6COGJSAtMrkzx+bFSh7
7
+ HWhuk4ByAdoZr6kFzpYBrxcfjSLU2oqaCyzb5vfksfrk1Yyr/i8DK6sw640OUlzP
8
+ 1rtKTuRCpYm8We/wrxEvrcdzk7FVFYLCzQfGOZLPmZpoDI3pdUJ/wkY4tjPAPFLn
9
+ 143L+07ZLN7Zuf0mpRAGpKve+MWzfId4xXoJ2XpGolLEkhJhfDNI9DCBjEZ3cYmJ
10
+ LYSkpevQuTJNQm/89ivPk3FV0p3H4KjKKgtvtUaOZQIDAQABo1MwUTAdBgNVHQ4E
11
+ FgQUjriVSWVYIJ/a21utkNLU2Cvjl1swHwYDVR0jBBgwFoAUjriVSWVYIJ/a21ut
12
+ kNLU2Cvjl1swDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAkcHr
13
+ TPlqzehyG1KGGb7VAGwCoqhUJfz0Pd3z2p+UOoKq/EmC8b1FRgKDZxSQHk0hhQWE
14
+ M4f51tTbv2LgskfOF/S/tDoKBGnOwYS0oFmVqEstHuC2uto8ukj3FNvsZW58x4Q1
15
+ DOGLkUqlPj5vAh8Zhw8GWokorF11dZlnKdPTC3NCdqyPTWNQPeSqd2qGsFgMBB2i
16
+ q49QsUCNqiVc5ZfpbTBq+Jk1qHd1Pyf9Dlxm3Wi6RBguBcpHooZTAAYleSk+uhSa
17
+ xH9f8v9E2aeYAk/zsZQt2f3x9sZQCAfjHPDOzXydiv568FasLxACY12/GkMNGdvy
18
+ xlgkz6c0z3TS8kFxrQ==
19
+ -----END CERTIFICATE-----
@@ -0,0 +1,19 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDCTCCAfGgAwIBAgIUBpZ2v9fA8XfqVF83Nw5byUa0ylswDQYJKoZIhvcNAQEL
3
+ BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYwODA5MDA1N1oXDTI2MDYw
4
+ ODA5MDA1N1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
5
+ AAOCAQ8AMIIBCgKCAQEAymikxJnbPFs3H/rRzetPjUdOPVuMOTfkz5oBDcP7s/2W
6
+ O7135UPbDGzClYyYJvwAZm9i1Vivzm6KrNDZR0lawqYVf6COGJSAtMrkzx+bFSh7
7
+ HWhuk4ByAdoZr6kFzpYBrxcfjSLU2oqaCyzb5vfksfrk1Yyr/i8DK6sw640OUlzP
8
+ 1rtKTuRCpYm8We/wrxEvrcdzk7FVFYLCzQfGOZLPmZpoDI3pdUJ/wkY4tjPAPFLn
9
+ 143L+07ZLN7Zuf0mpRAGpKve+MWzfId4xXoJ2XpGolLEkhJhfDNI9DCBjEZ3cYmJ
10
+ LYSkpevQuTJNQm/89ivPk3FV0p3H4KjKKgtvtUaOZQIDAQABo1MwUTAdBgNVHQ4E
11
+ FgQUjriVSWVYIJ/a21utkNLU2Cvjl1swHwYDVR0jBBgwFoAUjriVSWVYIJ/a21ut
12
+ kNLU2Cvjl1swDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAkcHr
13
+ TPlqzehyG1KGGb7VAGwCoqhUJfz0Pd3z2p+UOoKq/EmC8b1FRgKDZxSQHk0hhQWE
14
+ M4f51tTbv2LgskfOF/S/tDoKBGnOwYS0oFmVqEstHuC2uto8ukj3FNvsZW58x4Q1
15
+ DOGLkUqlPj5vAh8Zhw8GWokorF11dZlnKdPTC3NCdqyPTWNQPeSqd2qGsFgMBB2i
16
+ q49QsUCNqiVc5ZfpbTBq+Jk1qHd1Pyf9Dlxm3Wi6RBguBcpHooZTAAYleSk+uhSa
17
+ xH9f8v9E2aeYAk/zsZQt2f3x9sZQCAfjHPDOzXydiv568FasLxACY12/GkMNGdvy
18
+ xlgkz6c0z3TS8kFxrQ==
19
+ -----END CERTIFICATE-----
@@ -0,0 +1,28 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKaKTEmds8Wzcf
3
+ +tHN60+NR049W4w5N+TPmgENw/uz/ZY7vXflQ9sMbMKVjJgm/ABmb2LVWK/Oboqs
4
+ 0NlHSVrCphV/oI4YlIC0yuTPH5sVKHsdaG6TgHIB2hmvqQXOlgGvFx+NItTaipoL
5
+ LNvm9+Sx+uTVjKv+LwMrqzDrjQ5SXM/Wu0pO5EKlibxZ7/CvES+tx3OTsVUVgsLN
6
+ B8Y5ks+ZmmgMjel1Qn/CRji2M8A8UufXjcv7Ttks3tm5/SalEAakq974xbN8h3jF
7
+ egnZekaiUsSSEmF8M0j0MIGMRndxiYkthKSl69C5Mk1Cb/z2K8+TcVXSncfgqMoq
8
+ C2+1Ro5lAgMBAAECggEAEva0lUI6/aUoKC62Ox4++6QWIFPe8o9vjcRxMSp5qcvq
9
+ Uu+LTPzLXZclBfZAXSACzkDFAusbvEdeu8FCJ4EnvTvqoLoDW38yWH3367CbiuMa
10
+ NyT30zRXZMqxLw7ddJUDqbViEeA/uWKp+xO5Xfg/bNjDrtROsEe++szqY5n5dl2B
11
+ aJLPEtg2OQW7GWKF7PX9YyZmbZds+L7WFhTE+42zfO6MMQm5jaTr8er74u6FnEA2
12
+ ewrJmB7i9CS1DfYhCnJCwqOIxr9rPp9WMvRNfmL8wwqAr+4f1TidI7aKpA3vt2d0
13
+ HOrnNws+08tlcqQYHHyasLzBzsYGAZRTvCoCxjVd8wKBgQDkWjNxp9TmNxQXxvhB
14
+ J42lmo+9xBYzKUzmMMSvS7RoPFG8myf1s82qPTiwZZzGlpR6XIXfSZKQ45YhYz7S
15
+ 8i8VbhjVtGyOmExBRYryI3y8pC1aVHk6bPQl8rATIkqsIGN8VHSiwCMeV+j6VWUP
16
+ ZYjb0iqS2zKhhXD9CVcXIT/SfwKBgQDi6lG64nI/ECjakXfrXa+J1T5NJE+OEP3J
17
+ AR9nqRiR/VcwAFMBiJwFfm07dv4WHLbfI1qdx+OU4aSsBohyLg3X9WO0Y8iGw2w4
18
+ V6GGiwGK009OfzdrwTM9QjVHhk23SlFeQFPib4uRGcRnjimMNY6M1E3qvXHpyVXV
19
+ rpvxyGElGwKBgGjcXwlPJ738BvcQQIoy7qHggyeCdytRSOXf+UICQrsnD+XLXiM/
20
+ SS9m47RlRQQQu+ggur0ZnPt590Qnvf7ChgqSP0dLjhpBJ6tFkxO0ZiB+R/FWH0FM
21
+ LSWL930h3yaBzQ2X/uOJ1damSe9C7aCPYLSJI1HC5NI1Y/hepKaTdypjAoGAEfkr
22
+ ZhkfoX0fL0jMbdkq2UkJuUSCBKe14mDzYtuS9aVSbZvo9zsh2JGOB2LCd2/o0D3V
23
+ pJ+7mARTbcjKr/iT4iIutpAcxwfdn4zZX3XNNnjMVFRhSGiyLUz8OWEa8MSzMzr3
24
+ Kf1Z2bFnzCgHhHKNivwZ+9jrl+/5m4ZMFdegUjcCgYEAu/cKxIBc3Lb4gVZx6nHp
25
+ bPr9flDbdCKQk20kpVPvNxq67FuVyKy3n0svRX+fZ9uPhuv20IWkTBue3yCGKpjD
26
+ kUJ2A9Y9FZGSwhU3LSo6grdEv3pIW1YLTl95nUO20bRWCWcwsoV5/GEhFkEZWq06
27
+ wZCQXFhIY78gzd4gk+qJQpg=
28
+ -----END PRIVATE KEY-----
@@ -0,0 +1,38 @@
1
+ export default class App_Plugin_Start {
2
+ /**
3
+ * @param {typeof import('node:path')} path
4
+ * @param {typeof import('node:url')} url
5
+ * @param {Fl32_Web_Back_Dispatcher} dispatcher
6
+ * @param {Fl32_Web_Back_Handler_Pre_Log} hndlRequestLog
7
+ * @param {Fl32_Web_Back_Handler_Static} hndlStatic
8
+ */
9
+ constructor(
10
+ {
11
+ 'node:path': path,
12
+ 'node:url': url,
13
+ Fl32_Web_Back_Dispatcher$: dispatcher,
14
+ Fl32_Web_Back_Handler_Pre_Log$: hndlRequestLog,
15
+ Fl32_Web_Back_Handler_Static$: hndlStatic,
16
+ }
17
+ ) {
18
+ // VARS
19
+ const {dirname, join} = path;
20
+ const {fileURLToPath} = url;
21
+
22
+ // MAIN
23
+ /* Resolve a path to the root folder. */
24
+ const metaUrl = new URL(import.meta.url);
25
+ const script = fileURLToPath(metaUrl);
26
+ const cur = dirname(script);
27
+ const root = join(cur, '..', '..');
28
+ const webRoot = join(root, 'web');
29
+
30
+ return async function () {
31
+ // Set up handlers
32
+ await hndlStatic.init({rootPath: webRoot});
33
+ // Register handlers
34
+ dispatcher.addHandler(hndlRequestLog);
35
+ dispatcher.addHandler(hndlStatic);
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * A test script to emulate an app that uses the web server.
5
+ */
6
+ import {dirname, join} from 'node:path';
7
+ import {fileURLToPath} from 'node:url';
8
+ import {readFileSync} from 'node:fs';
9
+ import Container from '@teqfw/di';
10
+
11
+ // VARS
12
+ /* Resolve a path to the root folder. */
13
+ const url = new URL(import.meta.url);
14
+ const script = fileURLToPath(url);
15
+ const cur = dirname(script);
16
+ const root = join(cur, '..', '..');
17
+
18
+ // Create a new instance of the container
19
+ const container = new Container();
20
+
21
+ // Get the resolver from the container
22
+ const resolver = container.getResolver();
23
+ resolver.addNamespaceRoot('Fl32_Web_', join(root, 'src'));
24
+ resolver.addNamespaceRoot('App_', join(cur, 'app'));
25
+
26
+ // init the app (add the handlers to the Dispatcher)
27
+ /** @type {function} */
28
+ const appStart = await container.get('App_Plugin_Start$');
29
+ await appStart();
30
+
31
+ // order handlers in the Dispatcher
32
+ /** @type {Fl32_Web_Back_Dispatcher} */
33
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
34
+ dispatcher.orderHandlers();
35
+
36
+ // configure and run the server
37
+ /** @type {Fl32_Web_Back_Server} */
38
+ const server = await container.get('Fl32_Web_Back_Server$');
39
+ /** @type {typeof Fl32_Web_Back_Enum_Server_Type} */
40
+ const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
41
+ /** @type {Fl32_Web_Back_Server_Config} */
42
+ const factConfig = await container.get('Fl32_Web_Back_Server_Config$');
43
+
44
+ // Read TLS certificates
45
+ const certsDir = join(cur, '..', 'certs');
46
+ const key = readFileSync(join(certsDir, 'key.pem'), 'utf8');
47
+ const cert = readFileSync(join(certsDir, 'cert.pem'), 'utf8');
48
+ let ca;
49
+ try {
50
+ ca = readFileSync(join(certsDir, 'ca.pem'), 'utf8');
51
+ } catch (e) {
52
+ // CA certificate is optional
53
+ }
54
+
55
+ console.log('Starting HTTPS server on port 3443...');
56
+ const cfg = factConfig.create({
57
+ port: 3443,
58
+ type: SERVER_TYPE.HTTPS,
59
+ tls: {
60
+ key,
61
+ cert,
62
+ ca
63
+ }
64
+ });
65
+ await server.start(cfg);
Binary file
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
+ <title>@flancer32/teq-web</title>
7
+ </head>
8
+ <body>
9
+ <p>Hello, from `@flancer32/teq-web`!</p>
10
+ </body>
11
+ </html>
@@ -0,0 +1,59 @@
1
+ import {describe, it} from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {buildTestContainer} from '../../../common.js';
4
+
5
+ describe('Fl32_Web_Back_Helper_Order_Kahn', () => {
6
+
7
+ describe('sort', () => {
8
+ it('should sort handlers respecting after/before constraints', async () => {
9
+ const container = buildTestContainer();
10
+ const sorter = await container.get('Fl32_Web_Back_Helper_Order_Kahn$');
11
+
12
+ // Mock handlers with dependencies
13
+ const mk = (name, after = [], before = []) => ({
14
+ getRegistrationInfo: () => ({name, after, before})
15
+ });
16
+
17
+ const a = mk('a'); // no deps
18
+ const b = mk('b', ['a']); // after a
19
+ const c = mk('c', [], ['b']); // before b (== b after c)
20
+ const d = mk('d', ['b', 'c']); // after b and c
21
+
22
+ const result = sorter.sort([a, b, c, d]);
23
+ const names = result.map(h => h.getRegistrationInfo().name);
24
+ assert.deepStrictEqual(names, ['a', 'c', 'b', 'd']);
25
+ });
26
+
27
+ it('should detect circular dependency', async () => {
28
+ const container = buildTestContainer();
29
+ const sorter = await container.get('Fl32_Web_Back_Helper_Order_Kahn$');
30
+
31
+ const mk = (name, after = [], before = []) => ({
32
+ getRegistrationInfo: () => ({name, after, before})
33
+ });
34
+
35
+ const x = mk('x', ['y']);
36
+ const y = mk('y', ['x']);
37
+
38
+ assert.throws(() => {
39
+ sorter.sort([x, y]);
40
+ }, /Circular dependency detected/);
41
+ });
42
+
43
+ it('should ignore references to unknown handlers', async () => {
44
+ const container = buildTestContainer();
45
+ const sorter = await container.get('Fl32_Web_Back_Helper_Order_Kahn$');
46
+
47
+ const mk = (name, after = [], before = []) => ({
48
+ getRegistrationInfo: () => ({name, after, before})
49
+ });
50
+
51
+ const a = mk('a', ['ghost']); // ghost does not exist
52
+ const b = mk('b');
53
+
54
+ const result = sorter.sort([a, b]);
55
+ const names = result.map(h => h.getRegistrationInfo().name);
56
+ assert.deepStrictEqual(names.sort(), ['a', 'b']); // Order can vary, but no crash
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,112 @@
1
+ import {describe, it, beforeEach} from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {buildTestContainer} from '../common.js';
4
+
5
+ describe('Fl32_Web_Back_Server (mocked)', () => {
6
+
7
+ /** @type {import('@teqfw/di').Container} */
8
+ let container;
9
+ /** @type {Array<*>} */
10
+ const log = [];
11
+
12
+ // Mocks for HTTP/1 and HTTP/2 servers
13
+ const mockHttp = {
14
+ createServer: () => ({
15
+ listen: () => { log.push('http.listen'); },
16
+ on: () => { log.push('http.on'); },
17
+ close: (cb) => { log.push('http.close'); cb && cb(); },
18
+ }),
19
+ };
20
+
21
+ const mockHttp2 = {
22
+ createServer: () => ({
23
+ listen: () => { log.push('http2.listen'); },
24
+ on: () => { log.push('http2.on'); },
25
+ close: (cb) => { log.push('http2.close'); cb && cb(); },
26
+ }),
27
+ createSecureServer: (tlsOpts) => ({
28
+ listen: () => { log.push('http2s.listen'); },
29
+ on: () => { log.push('http2s.on'); },
30
+ close: (cb) => { log.push('http2s.close'); cb && cb(); },
31
+ })
32
+ };
33
+
34
+ beforeEach(() => {
35
+ log.length = 0;
36
+ container = buildTestContainer();
37
+
38
+ container.register('node:http', mockHttp);
39
+ container.register('node:http2', mockHttp2);
40
+
41
+ container.register('Fl32_Web_Back_Logger$', {
42
+ info: (...args) => log.push(['info', ...args]),
43
+ error: (...args) => log.push(['error', ...args]),
44
+ });
45
+
46
+ container.register('Fl32_Web_Back_Dispatcher$', {
47
+ orderHandlers: () => log.push('dispatcher.orderHandlers'),
48
+ onEventRequest: () => {},
49
+ });
50
+ });
51
+
52
+ it('should start in HTTP/1 mode by default', async () => {
53
+ const server = await container.get('Fl32_Web_Back_Server$');
54
+ await server.start(); // default mode is HTTP/1
55
+ assert.deepStrictEqual(log, [
56
+ 'dispatcher.orderHandlers',
57
+ ['info', 'Starting server in HTTP/1 mode on port 3000...'],
58
+ 'http.on',
59
+ 'http.listen',
60
+ ]);
61
+ });
62
+
63
+ it('should start in HTTP/2 mode if specified', async () => {
64
+ const server = await container.get('Fl32_Web_Back_Server$');
65
+ await server.start({type: 'http2', port: 8080});
66
+ assert.deepStrictEqual(log, [
67
+ 'dispatcher.orderHandlers',
68
+ ['info', 'Starting server in HTTP/2 mode on port 8080...'],
69
+ 'http2.on',
70
+ 'http2.listen',
71
+ ]);
72
+ });
73
+
74
+ it('should start in HTTPS/2 mode with TLS config', async () => {
75
+ const server = await container.get('Fl32_Web_Back_Server$');
76
+ await server.start({type: 'https', port: 8443, tls: {key: 'a', cert: 'b'}});
77
+ assert.deepStrictEqual(log, [
78
+ 'dispatcher.orderHandlers',
79
+ ['info', 'Starting server in HTTPS (HTTP/2 + TLS) mode on port 8443...'],
80
+ 'http2s.on',
81
+ 'http2s.listen',
82
+ ]);
83
+ });
84
+
85
+ it('should throw error if TLS config is missing in HTTPS mode', async () => {
86
+ const server = await container.get('Fl32_Web_Back_Server$');
87
+ await assert.rejects(
88
+ () => server.start({type: 'https', port: 1234}),
89
+ /TLS key and certificate are required/
90
+ );
91
+ assert.deepStrictEqual(log.at(-1), ['error', 'HTTPS server requires TLS key and certificate']);
92
+ });
93
+
94
+ it('should throw error on unsupported server type', async () => {
95
+ const server = await container.get('Fl32_Web_Back_Server$');
96
+ await assert.rejects(
97
+ () => server.start({type: 'ftp', port: 21}),
98
+ /not supported/
99
+ );
100
+ assert.deepStrictEqual(log.at(-1), ['error', 'Unsupported server type: ftp']);
101
+ });
102
+
103
+ it('should stop the server', async () => {
104
+ const server = await container.get('Fl32_Web_Back_Server$');
105
+ await server.start();
106
+ await server.stop();
107
+ assert.deepStrictEqual(log.slice(-2), [
108
+ 'http.close',
109
+ ['info', 'Server stopped'],
110
+ ]);
111
+ });
112
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Provides a utility to create a configured TeqFW DI container for unit testing.
3
+ */
4
+ import path from 'node:path';
5
+ import Container from '@teqfw/di';
6
+
7
+ // Resolve the plugin source path relative to this script
8
+ const SRC = path.resolve(import.meta.dirname, '../../src');
9
+
10
+ /**
11
+ * Builds a test DI container for unit tests.
12
+ * Registers plugin namespace and enables test mode.
13
+ *
14
+ * @returns {TeqFw_Di_Container} Test container instance.
15
+ */
16
+ export function buildTestContainer() {
17
+ const container = new Container();
18
+ const resolver = container.getResolver();
19
+ resolver.addNamespaceRoot('Fl32_Web_', SRC);
20
+ container.enableTestMode();
21
+ return container;
22
+ }