@flancer32/teq-web 0.2.0 → 0.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.
@@ -1,137 +1,61 @@
1
1
  /**
2
- * Serves static files from a configured root directory.
3
- * Prevents directory traversal and streams file content to the response.
2
+ * Universal file handler serving files from multiple sources using helper modules.
4
3
  *
5
4
  * @implements Fl32_Web_Back_Api_Handler
6
5
  */
7
6
  export default class Fl32_Web_Back_Handler_Static {
8
7
  /* eslint-disable jsdoc/require-param-description,jsdoc/check-param-names */
9
8
  /**
10
- * @param {typeof import('node:fs')} fs - Node.js file system module.
11
- * @param {typeof import('node:http2')} http2
12
- * @param {typeof import('node:path')} path - Node.js path module.
13
- * @param {Fl32_Web_Back_Logger} logger - Logger instance.
14
- * @param {Fl32_Web_Back_Helper_Mime} helpMime - MIME helper for content type resolution.
15
- * @param {Fl32_Web_Back_Helper_Respond} respond - Response helper with status utilities.
16
- * @param {Fl32_Web_Back_Dto_Handler_Info} dtoInfo - DTO factory for handler registration.
17
- * @param {typeof Fl32_Web_Back_Enum_Stage} STAGE - Enum of handler stages.
9
+ * @param {Fl32_Web_Back_Handler_Static_A_Registry} registry
10
+ * @param {Fl32_Web_Back_Handler_Static_A_FileService} fileService
11
+ * @param {Fl32_Web_Back_Helper_Respond} respond
12
+ * @param {Fl32_Web_Back_Logger} logger
13
+ * @param {Fl32_Web_Back_Dto_Handler_Info} dtoInfo
14
+ * @param {typeof Fl32_Web_Back_Enum_Stage} STAGE
18
15
  */
19
16
  constructor(
20
17
  {
21
- 'node:fs': fs,
22
- 'node:http2': http2,
23
- 'node:path': path,
24
- Fl32_Web_Back_Logger$: logger,
25
- Fl32_Web_Back_Helper_Mime$: helpMime,
18
+ Fl32_Web_Back_Handler_Static_A_Registry$: registry,
19
+ Fl32_Web_Back_Handler_Static_A_FileService$: fileService,
26
20
  Fl32_Web_Back_Helper_Respond$: respond,
21
+ Fl32_Web_Back_Logger$: logger,
27
22
  Fl32_Web_Back_Dto_Handler_Info$: dtoInfo,
28
23
  Fl32_Web_Back_Enum_Stage$: STAGE,
29
24
  }
30
25
  ) {
31
26
  /* eslint-enable jsdoc/check-param-names */
32
- // VARS
33
- const {promises: fsp} = fs;
34
- const {constants: H2} = http2;
35
- const {
36
- HTTP2_HEADER_CONTENT_LENGTH,
37
- HTTP2_HEADER_CONTENT_TYPE,
38
- HTTP2_HEADER_LAST_MODIFIED,
39
- HTTP_STATUS_OK,
40
- } = H2;
41
27
 
42
- /**
43
- * Handler registration info.
44
- * @type {Fl32_Web_Back_Dto_Handler_Info.Dto}
45
- */
46
28
  const _info = dtoInfo.create();
47
29
  _info.name = this.constructor.name;
48
30
  _info.stage = STAGE.PROCESS;
49
31
  Object.freeze(_info);
50
32
 
51
33
  /**
52
- * Root directory for static files.
53
- * @type {string}
54
- */
55
- let _root;
56
-
57
- /**
58
- * Default filenames to try when a path is a directory.
59
- * @type {string[]}
60
- */
61
- const _defaultFiles = ['index.html', 'index.htm', 'index.txt'];
62
-
63
-
64
- /**
65
- * Handles the incoming request by attempting to serve a static file.
34
+ * Initialize registry with provided sources.
66
35
  *
67
- * @param {module:http.IncomingMessage|module:http2.Http2ServerRequest} req - HTTP request object.
68
- * @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res - HTTP response object.
69
- * @returns {Promise<boolean>} - True if the file was served, false otherwise.
36
+ * @param {{sources: Fl32_Web_Back_Dto_Handler_Source.Dto[]}} params
37
+ * @returns {Promise<void>}
70
38
  */
71
- this.handle = async function (req, res) {
72
- if (!respond.isWritable(res)) return false;
73
-
74
- try {
75
- const urlPath = decodeURIComponent(req.url.split('?')[0]);
76
- let fsPath = path.resolve(_root, '.' + urlPath);
77
-
78
- if (!fsPath.startsWith(_root)) return false;
79
-
80
- let stat;
81
- try {
82
- stat = await fsp.stat(fsPath);
83
- } catch {
84
- return false;
85
- }
86
-
87
- // If a path is a directory — try default files
88
- if (stat.isDirectory()) {
89
- for (const file of _defaultFiles) {
90
- const candidate = path.join(fsPath, file);
91
- try {
92
- const s = await fsp.stat(candidate);
93
- if (s.isFile()) {
94
- fsPath = candidate;
95
- stat = s;
96
- break;
97
- }
98
- } catch {
99
- // ignore and continue
100
- }
101
- }
102
- if (!stat.isFile()) return false;
103
- }
104
-
105
- const stream = fs.createReadStream(fsPath);
106
- const ext = path.extname(fsPath).toLowerCase();
107
- const headers = {
108
- [HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
109
- [HTTP2_HEADER_CONTENT_TYPE]: helpMime.getByExt(ext),
110
- [HTTP2_HEADER_LAST_MODIFIED]: stat.mtime.toUTCString(),
111
- };
112
- res.writeHead(HTTP_STATUS_OK, headers);
113
- stream.pipe(res);
114
- return true;
115
- } catch (e) {
116
- logger.exception(e);
117
- return false;
118
- }
39
+ this.init = async ({sources = []} = {}) => {
40
+ registry.addConfigs(sources);
119
41
  };
120
42
 
121
43
  /**
122
- * Initializes the handler with the root directory.
44
+ * Attempt to handle incoming request.
123
45
  *
124
- * @param {object} params
125
- * @param {string} params.rootPath - Absolute or relative path to the static root directory.
126
- * @returns {Promise<void>}
46
+ * @param {module:http.IncomingMessage|module:http2.Http2ServerRequest} req
47
+ * @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res
48
+ * @returns {Promise<boolean>} True if file served
127
49
  */
128
- this.init = async function ({rootPath}) {
129
- _root = path.resolve(rootPath);
130
- logger.info(`Static files root: ${_root}`);
50
+ this.handle = async (req, res) => {
51
+ if (!respond.isWritable(res)) return false;
52
+ const urlPath = decodeURIComponent(req.url.split('?')[0]);
53
+ const match = registry.find(urlPath);
54
+ if (!match) return false;
55
+ return fileService.serve(match.config, match.rel, req, res);
131
56
  };
132
57
 
133
58
  /**
134
- * Returns the handler registration info.
135
59
  * @returns {Fl32_Web_Back_Dto_Handler_Info.Dto}
136
60
  */
137
61
  this.getRegistrationInfo = () => _info;
@@ -83,4 +83,32 @@ export default class Fl32_Web_Back_Helper_Cast {
83
83
  }
84
84
  return undefined;
85
85
  }
86
+
87
+ /**
88
+ * Cast an object to a map with string keys and array-of-string values.
89
+ * Throws error on invalid structure or values.
90
+ *
91
+ * @param {*} data - Raw input to cast.
92
+ * @returns {Record<string, string[]>}
93
+ */
94
+ stringArrayMap(data) {
95
+ if (data === undefined) return {};
96
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
97
+ throw new Error('Invalid value for allow');
98
+ }
99
+ const res = {};
100
+ for (const [key, arr] of Object.entries(data)) {
101
+ if (!Array.isArray(arr)) throw new Error(`Invalid allow list for ${key}`);
102
+ const k = this.string(key);
103
+ if (!k) throw new Error('Invalid allow key');
104
+ const items = [];
105
+ for (const item of arr) {
106
+ const val = this.string(item);
107
+ if (!val) throw new Error(`Invalid allow list for ${k}`);
108
+ items.push(val);
109
+ }
110
+ res[k] = items;
111
+ }
112
+ return res;
113
+ }
86
114
  }
@@ -56,16 +56,93 @@ export default class Fl32_Web_Back_Helper_Respond {
56
56
  return send({res, headers, body}, HTTP_STATUS_OK);
57
57
  };
58
58
 
59
+ /** @see send */
60
+ this.code201_Created = function ({res, headers = {}, body = ''}) {
61
+ return send({res, headers, body}, HTTP_STATUS_CREATED);
62
+ };
63
+
64
+ /** @see send */
65
+ this.code204_NoContent = function ({res, headers = {}}) {
66
+ return send({res, headers}, HTTP_STATUS_NO_CONTENT);
67
+ };
68
+
69
+ /** @see send */
70
+ this.code301_MovedPermanently = function ({res, headers = {}, body = ''}) {
71
+ return send({res, headers, body}, HTTP_STATUS_MOVED_PERMANENTLY);
72
+ };
73
+
74
+ /** @see send */
75
+ this.code302_Found = function ({res, headers = {}, body = ''}) {
76
+ return send({res, headers, body}, HTTP_STATUS_FOUND);
77
+ };
78
+
79
+ /** @see send */
80
+ this.code303_SeeOther = function ({res, headers = {}, body = ''}) {
81
+ return send({res, headers, body}, HTTP_STATUS_SEE_OTHER);
82
+ };
83
+
84
+ /** @see send */
85
+ this.code304_NotModified = function ({res, headers = {}, body = ''}) {
86
+ return send({res, headers, body}, HTTP_STATUS_NOT_MODIFIED);
87
+ };
88
+
89
+ /** @see send */
90
+ this.code400_BadRequest = function ({res, headers = {}, body = ''}) {
91
+ return send({res, headers, body}, HTTP_STATUS_BAD_REQUEST);
92
+ };
93
+
94
+ /** @see send */
95
+ this.code401_Unauthorized = function ({res, headers = {}, body = ''}) {
96
+ return send({res, headers, body}, HTTP_STATUS_UNAUTHORIZED);
97
+ };
98
+
99
+ /** @see send */
100
+ this.code402_PaymentRequired = function ({res, headers = {}, body = ''}) {
101
+ return send({res, headers, body}, HTTP_STATUS_PAYMENT_REQUIRED);
102
+ };
103
+
104
+ /** @see send */
105
+ this.code403_Forbidden = function ({res, headers = {}, body = ''}) {
106
+ return send({res, headers, body}, HTTP_STATUS_FORBIDDEN);
107
+ };
108
+
59
109
  /** @see send */
60
110
  this.code404_NotFound = function ({res, headers = {}, body = ''}) {
61
111
  return send({res, headers, body}, HTTP_STATUS_NOT_FOUND);
62
112
  };
63
113
 
114
+ /** @see send */
115
+ this.code405_MethodNotAllowed = function ({res, headers = {}, body = '', allowed = 'HEAD, GET, POST'}) {
116
+ return send(
117
+ {
118
+ res,
119
+ headers: {...headers, [HTTP2_HEADER_ALLOW]: allowed},
120
+ body,
121
+ },
122
+ HTTP_STATUS_METHOD_NOT_ALLOWED
123
+ );
124
+ };
125
+
126
+ /** @see send */
127
+ this.code409_Conflict = function ({res, headers = {}, body = ''}) {
128
+ return send({res, headers, body}, HTTP_STATUS_CONFLICT);
129
+ };
130
+
64
131
  /** @see send */
65
132
  this.code500_InternalServerError = function ({res, headers = {}, body = 'Internal Server Error'}) {
66
133
  return send({res, headers, body}, HTTP_STATUS_INTERNAL_SERVER_ERROR);
67
134
  };
68
135
 
136
+ /** @see send */
137
+ this.code502_BadGateway = function ({res, headers = {}, body = ''}) {
138
+ return send({res, headers, body}, HTTP_STATUS_BAD_GATEWAY);
139
+ };
140
+
141
+ /** @see send */
142
+ this.code503_ServiceUnavailable = function ({res, headers = {}, body = ''}) {
143
+ return send({res, headers, body}, HTTP_STATUS_SERVICE_UNAVAILABLE);
144
+ };
145
+
69
146
  /**
70
147
  * Checks if the response is writable and not yet sent.
71
148
  * @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res
@@ -108,11 +108,16 @@ describe('Fl32_Web_Back_Api_Handler', () => {
108
108
  it('should serve allowed NPM file', async () => {
109
109
  const container = buildTestContainer();
110
110
  const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
111
- const handler = await container.get('Fl32_Web_Back_Handler_Npm$');
111
+ const handler = await container.get('Fl32_Web_Back_Handler_Static$');
112
+ const Cfg = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
112
113
  await handler.init({
113
- allow: {
114
- '@teqfw/di/src': ['.'],
115
- },
114
+ sources: [Cfg.create({
115
+ root: 'node_modules',
116
+ prefix: '/npm/',
117
+ allow: {
118
+ '@teqfw/di/src': ['.'],
119
+ },
120
+ })],
116
121
  });
117
122
  dispatcher.addHandler(handler);
118
123
  dispatcher.orderHandlers();
@@ -130,7 +135,7 @@ describe('Fl32_Web_Back_Api_Handler', () => {
130
135
  const req = http.get({
131
136
  hostname: 'localhost',
132
137
  port: cfg.port,
133
- path: '/node_modules/@teqfw/di/src/Api/Container/Parser/Chunk.js',
138
+ path: '/npm/@teqfw/di/src/Api/Container/Parser/Chunk.js',
134
139
  }, res => {
135
140
  const chunks = [];
136
141
  res.on('data', ch => chunks.push(ch));
@@ -147,4 +152,52 @@ describe('Fl32_Web_Back_Api_Handler', () => {
147
152
  await server.stop();
148
153
  assert.strictEqual(server.getInstance(), undefined);
149
154
  });
155
+
156
+ it('should serve allowed source file', async () => {
157
+ const container = buildTestContainer();
158
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
159
+ const handler = await container.get('Fl32_Web_Back_Handler_Static$');
160
+ const Cfg = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
161
+ await handler.init({
162
+ sources: [Cfg.create({
163
+ root: 'src',
164
+ prefix: '/sources/',
165
+ allow: {
166
+ Back: ['Server.js'],
167
+ },
168
+ })],
169
+ });
170
+ dispatcher.addHandler(handler);
171
+ dispatcher.orderHandlers();
172
+
173
+ const server = await container.get('Fl32_Web_Back_Server$');
174
+ const SERVER_TYPE = await container.get('Fl32_Web_Back_Enum_Server_Type$');
175
+ const Config = await container.get('Fl32_Web_Back_Server_Config$');
176
+ const http = await container.get('node:http');
177
+
178
+ const cfg = Config.create({ port: 3057, type: SERVER_TYPE.HTTP });
179
+ await server.start(cfg);
180
+ await waitListening(server);
181
+
182
+ const result = await new Promise((resolve, reject) => {
183
+ const req = http.get({
184
+ hostname: 'localhost',
185
+ port: cfg.port,
186
+ path: '/sources/Back/Server.js',
187
+ }, res => {
188
+ const chunks = [];
189
+ res.on('data', ch => chunks.push(ch));
190
+ res.on('end', () => {
191
+ resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() });
192
+ });
193
+ });
194
+ req.on('error', reject);
195
+ });
196
+
197
+ assert.strictEqual(result.status, 200);
198
+ assert.match(result.body, /class Fl32_Web_Back_Server/);
199
+
200
+ await server.stop();
201
+ assert.strictEqual(server.getInstance(), undefined);
202
+ });
150
203
  });
@@ -4,8 +4,8 @@ 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
8
7
  * @param {Fl32_Web_Back_Handler_Static} hndlStatic
8
+ * @param {Fl32_Web_Back_Dto_Handler_Source} dtoCfg
9
9
  */
10
10
  constructor(
11
11
  {
@@ -13,8 +13,8 @@ export default class App_Plugin_Start {
13
13
  'node:url': url,
14
14
  Fl32_Web_Back_Dispatcher$: dispatcher,
15
15
  Fl32_Web_Back_Handler_Pre_Log$: hndlRequestLog,
16
- Fl32_Web_Back_Handler_Npm$: hndlNpm,
17
16
  Fl32_Web_Back_Handler_Static$: hndlStatic,
17
+ Fl32_Web_Back_Dto_Handler_Source$: dtoCfg,
18
18
  }
19
19
  ) {
20
20
  // VARS
@@ -30,12 +30,10 @@ export default class App_Plugin_Start {
30
30
  const webRoot = join(root, 'web');
31
31
 
32
32
  return async function () {
33
- // Set up handlers
34
- await hndlNpm.init({allow: {'@teqfw/di': ['src/Container.js']}});
35
- await hndlStatic.init({rootPath: webRoot});
36
- // Register handlers
33
+ const srcNpm = dtoCfg.create({root: 'node_modules', prefix: '/npm/', allow: {'@teqfw/di': ['src/Container.js']}});
34
+ const srcWeb = dtoCfg.create({root: webRoot, prefix: '/'});
35
+ await hndlStatic.init({sources: [srcNpm, srcWeb]});
37
36
  dispatcher.addHandler(hndlRequestLog);
38
- dispatcher.addHandler(hndlNpm);
39
37
  dispatcher.addHandler(hndlStatic);
40
38
  };
41
39
  }
@@ -0,0 +1,106 @@
1
+ # AI Agent Unit Testing Instructions for @flancer32/teq-web
2
+
3
+ This document outlines project-specific guidelines for writing and maintaining unit tests in the `@flancer32/teq-web` plugin. Follow these rules to ensure consistency and reliability.
4
+
5
+ ---
6
+
7
+ ## 1. Test File Location & Naming
8
+
9
+ * Mirror `src/` structure under `test/unit/`, using the same path and filenames.
10
+ * Use `.test.mjs` suffix for test files, e.g. `Config.test.mjs`, `Kahn.test.mjs`.
11
+ * Top-level `describe` must reference the full DI key:
12
+
13
+ ```js
14
+ describe('Fl32_Web_Back_Handler_Static_A_Config', () => { /* ... */ });
15
+ ```
16
+
17
+ ## 2. Dependency Injection via DI Container
18
+
19
+ * Always call `buildTestContainer()` from `test/unit/common.js` and then:
20
+
21
+ ```js
22
+ /** @type {Fl32_Web_Back_Handler_Static_A_Config} */
23
+ const config = await container.get('Fl32_Web_Back_Handler_Static_A_Config$');
24
+ ```
25
+ * **Do not** import production modules (`node:path`, etc.) directly—register or mock everything through the container.
26
+ * Use `container.register(depId, mock)` to override dependencies in test mode.
27
+
28
+ ## 3. Asynchronous Subject Retrieval
29
+
30
+ * Declare your `it` or `beforeEach` callbacks as `async` if you call `await container.get(...)`.
31
+ * Always `await container.get('Your_Service_Key$')` to obtain the actual instance before invoking its methods.
32
+
33
+ ## 4. Testing Patterns
34
+
35
+ * Use Node’s built-in test runner and assertion library:
36
+
37
+ ```js
38
+ import { describe, it } from 'node:test';
39
+ import assert from 'node:assert/strict';
40
+ ```
41
+ * **Success cases**:
42
+
43
+ * `assert.strictEqual(actual, expected)` for primitives.
44
+ * `assert.deepStrictEqual(actual, expected)` for objects/arrays.
45
+ * **Error cases**:
46
+
47
+ * `assert.throws(() => fn(), /message/)` matching a key fragment of the error.
48
+
49
+ ## 5. DTO Shape & Defaults
50
+
51
+ * Pass a full DTO to `factory.create(dto)`.
52
+ * To test fallback/default logic, supply `[]` for optional arrays.
53
+
54
+ ## 6. Comments & Documentation
55
+
56
+ * All inline comments must be in **English**.
57
+ * Comment only non-trivial logic; don’t restate obvious assertions.
58
+
59
+ ## 7. Mocks & Helpers
60
+
61
+ * Use plain JS objects or small factory functions for mocks.
62
+ * No external mocking libraries—rely on `@teqfw/di` test mode.
63
+
64
+ ## 8. Maintenance
65
+
66
+ * One behavior per `it` block—keep tests focused and concise.
67
+ * Update tests when API or default constants change.
68
+ * Ensure CI runs all tests automatically.
69
+
70
+ ---
71
+
72
+ ## Test Mode Support in `@teqfw/di`
73
+
74
+ When you enable test mode, you can inject or override any dependency—built-in or custom—without touching production code:
75
+
76
+ ```js
77
+ const container = buildTestContainer();
78
+ container.enableTestMode();
79
+
80
+ // override a service or Node builtin
81
+ container.register('Fl32_Web_Back_Logger$', mockLogger);
82
+ container.register('node:fs', { /* mock fs.promises.stat… */});
83
+ ```
84
+
85
+ * **register** vs. **registerInstance**
86
+ Use `container.register(depId, instanceOrFactory)` to bind mocks. Avoid `registerInstance`, which is not part of the public test-mode API.
87
+
88
+ ### Mocking Node.js Built-ins
89
+
90
+ ```js
91
+ // simulate filesystem behavior
92
+ container.register('node:fs', {
93
+ promises: {
94
+ stat: async (p) => { /* … */ }
95
+ },
96
+ createReadStream: (p) => { /* … */ }
97
+ });
98
+
99
+ // adjust path logic
100
+ container.register('node:path', {
101
+ join: (...parts) => parts.join('/'),
102
+ resolve: (p) => `/abs/${p}`
103
+ });
104
+ ```
105
+
106
+ These overrides are injected into every module that asks for `node:fs` or `node:path`, enabling isolated, deterministic tests without side effects.
@@ -0,0 +1,150 @@
1
+ import {describe, it, beforeEach} from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {buildTestContainer} from '../common.js';
4
+
5
+ /** Collects execution order */
6
+ let log;
7
+ /** DI container for each test */
8
+ let container;
9
+ /** Dispatcher stages enum */
10
+ let STAGE;
11
+ /** Respond helper mock */
12
+ let respond;
13
+
14
+ beforeEach(async () => {
15
+ log = [];
16
+ container = buildTestContainer();
17
+
18
+ // Mock logger to keep tests quiet
19
+ container.register('Fl32_Web_Back_Logger$', {
20
+ info: () => {},
21
+ error: () => {},
22
+ exception: () => {},
23
+ });
24
+ // Keep handler order as provided
25
+ container.register('Fl32_Web_Back_Helper_Order_Kahn$', {sort: arr => arr});
26
+
27
+ // Respond helper stub
28
+ respond = {
29
+ isWritable: res => !res.headersSent && !res.writableEnded,
30
+ code404_NotFound: ({res}) => { res.code = 404; res.headersSent = true; },
31
+ code500_InternalServerError: ({res}) => { res.code = 500; res.headersSent = true; },
32
+ };
33
+ container.register('Fl32_Web_Back_Helper_Respond$', respond);
34
+
35
+ STAGE = await container.get('Fl32_Web_Back_Enum_Stage$');
36
+ });
37
+
38
+ function pre(name) {
39
+ return {
40
+ getRegistrationInfo: () => ({name, stage: STAGE.PRE}),
41
+ handle: async () => { log.push(name); },
42
+ };
43
+ }
44
+
45
+ function proc(name, opts = {}) {
46
+ const {handled = true, throwErr = false, send = true} = opts;
47
+ return {
48
+ getRegistrationInfo: () => ({name, stage: STAGE.PROCESS}),
49
+ handle: async (req, res) => {
50
+ log.push(name);
51
+ if (throwErr) throw new Error('boom');
52
+ if (send) res.headersSent = true;
53
+ return handled;
54
+ },
55
+ };
56
+ }
57
+
58
+ function post(name) {
59
+ return {
60
+ getRegistrationInfo: () => ({name, stage: STAGE.POST}),
61
+ handle: async () => { log.push(name); },
62
+ };
63
+ }
64
+
65
+ describe('Fl32_Web_Back_Dispatcher', () => {
66
+ it('calls pre-handlers even when a process handler fails', async () => {
67
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
68
+ dispatcher.addHandler(pre('pre'));
69
+ dispatcher.addHandler(proc('proc', {throwErr: true, send: false}));
70
+ dispatcher.orderHandlers();
71
+
72
+ const req = {url: '/'}; const res = {};
73
+ await dispatcher.onEventRequest(req, res);
74
+
75
+ assert.strictEqual(log[0], 'pre');
76
+ });
77
+
78
+ it('executes post-handlers after a successful process handler', async () => {
79
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
80
+ dispatcher.addHandler(pre('pre'));
81
+ dispatcher.addHandler(proc('proc'));
82
+ dispatcher.addHandler(post('post'));
83
+ dispatcher.orderHandlers();
84
+
85
+ const req = {url: '/'}; const res = {};
86
+ await dispatcher.onEventRequest(req, res);
87
+
88
+ assert.deepStrictEqual(log, ['pre', 'proc', 'post']);
89
+ });
90
+
91
+ it('returns 500 if a process handler throws', async () => {
92
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
93
+ dispatcher.addHandler(proc('proc', {throwErr: true, send: false}));
94
+ dispatcher.orderHandlers();
95
+
96
+ const req = {url: '/'}; const res = {};
97
+ await dispatcher.onEventRequest(req, res);
98
+
99
+ assert.strictEqual(res.code, 500);
100
+ });
101
+
102
+ it('returns 404 if no process handler handles the request', async () => {
103
+ const dispatcher = await container.get('Fl32_Web_Back_Dispatcher$');
104
+ dispatcher.addHandler(pre('pre'));
105
+ dispatcher.addHandler(proc('p1', {handled: false, send: false}));
106
+ dispatcher.addHandler(post('post'));
107
+ dispatcher.orderHandlers();
108
+
109
+ const req = {url: '/missing'}; const res = {};
110
+ await dispatcher.onEventRequest(req, res);
111
+
112
+ assert.strictEqual(res.code, 404);
113
+ assert.deepStrictEqual(log, ['pre', 'p1', 'post']);
114
+ });
115
+
116
+ it('orders handlers according to before/after dependencies', async () => {
117
+ const localLog = [];
118
+ const container2 = buildTestContainer();
119
+
120
+ container2.register('Fl32_Web_Back_Logger$', {
121
+ info: () => {},
122
+ error: () => {},
123
+ exception: () => {},
124
+ });
125
+
126
+ const respond2 = {
127
+ isWritable: res => !res.headersSent && !res.writableEnded,
128
+ code404_NotFound: ({res}) => { res.code = 404; res.headersSent = true; },
129
+ code500_InternalServerError: ({res}) => { res.code = 500; res.headersSent = true; },
130
+ };
131
+ container2.register('Fl32_Web_Back_Helper_Respond$', respond2);
132
+
133
+ const STAGE2 = await container2.get('Fl32_Web_Back_Enum_Stage$');
134
+ const mk = (name, after = []) => ({
135
+ getRegistrationInfo: () => ({name, stage: STAGE2.PRE, after}),
136
+ handle: async () => { localLog.push(name); },
137
+ });
138
+
139
+ const dispatcher = await container2.get('Fl32_Web_Back_Dispatcher$');
140
+ dispatcher.addHandler(mk('a', ['c']));
141
+ dispatcher.addHandler(mk('b', ['a']));
142
+ dispatcher.addHandler(mk('c'));
143
+ dispatcher.orderHandlers();
144
+
145
+ await dispatcher.onEventRequest({}, {});
146
+
147
+ assert.deepStrictEqual(localLog, ['c', 'a', 'b']);
148
+ });
149
+ });
150
+