@flancer32/teq-web 0.3.1 → 0.4.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,225 +0,0 @@
1
- import { describe, it, beforeEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { EventEmitter } from 'node:events';
4
- import { buildTestContainer } from '../../../../common.js';
5
-
6
- const normalize = p => p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, '');
7
-
8
- class MockRes extends EventEmitter {
9
- constructor() {
10
- super();
11
- this.data = '';
12
- this.status = undefined;
13
- this.headers = undefined;
14
- this._hs = false;
15
- this._ended = false;
16
- }
17
- get headersSent() {
18
- return this._hs;
19
- }
20
- get writableEnded() {
21
- return this._ended;
22
- }
23
- writeHead(status, headers) {
24
- this.status = status;
25
- this.headers = headers;
26
- this._hs = true;
27
- }
28
- write(chunk) {
29
- this.data += chunk;
30
- }
31
- end(chunk) {
32
- if (chunk) this.write(chunk);
33
- this._ended = true;
34
- this.emit('finish');
35
- }
36
- }
37
-
38
- describe('Fl32_Web_Back_Handler_Static_A_FileService', () => {
39
- let storage, mockFs, mime, logger, logs, addFile, addDir;
40
-
41
- beforeEach(() => {
42
- storage = new Map();
43
-
44
- // mock fs.promises.stat and createReadStream
45
- mockFs = {
46
- promises: {
47
- stat: async p => {
48
- const key = normalize(p);
49
- if (!storage.has(key)) throw new Error('ENOENT');
50
- const entry = storage.get(key);
51
- return {
52
- isFile: entry.isFile,
53
- isDirectory: entry.isDirectory,
54
- size: entry.size,
55
- mtime: entry.mtime
56
- };
57
- }
58
- },
59
- createReadStream: p => ({
60
- pipe: res => {
61
- setImmediate(() => {
62
- const key = normalize(p);
63
- const entry = storage.get(key);
64
- if (entry && entry.content != null) res.write(entry.content);
65
- res.end();
66
- });
67
- }
68
- })
69
- };
70
-
71
- // simple mime helper
72
- mime = {
73
- getByExt: () => 'text/plain'
74
- };
75
-
76
- logs = [];
77
- // simple logger collecting calls
78
- logger = {
79
- info: (...args) => logs.push(['info', ...args]),
80
- warn: (...args) => logs.push(['warn', ...args]),
81
- exception: (...args) => logs.push(['exception', ...args])
82
- };
83
-
84
- // helpers to populate mock FS
85
- addFile = (p, content) => {
86
- const key = normalize(p);
87
- storage.set(key, {
88
- isFile: () => true,
89
- isDirectory: () => false,
90
- size: Buffer.byteLength(content),
91
- mtime: new Date(),
92
- content
93
- });
94
- };
95
- addDir = p => {
96
- const key = normalize(p);
97
- storage.set(key, {
98
- isFile: () => false,
99
- isDirectory: () => true,
100
- size: 0,
101
- mtime: new Date()
102
- });
103
- };
104
- });
105
-
106
- it('serves existing file', async () => {
107
- addFile('/root/a.txt', 'A');
108
-
109
- /** @type {{ root: string, prefix: string, defaults: string[] }} */
110
- const config = { root: '/root', prefix: '/p/', defaults: ['index.html'] };
111
- const res = new MockRes();
112
-
113
- const container = buildTestContainer();
114
- container.register('node:fs', mockFs);
115
- container.register('Fl32_Web_Back_Helper_Mime$', mime);
116
- container.register('Fl32_Web_Back_Logger$', logger);
117
-
118
- /** @type {Fl32_Web_Back_Handler_Static_A_FileService} */
119
- const service = await container.get('Fl32_Web_Back_Handler_Static_A_FileService$');
120
-
121
- const ok = await service.serve(config, 'a.txt', {}, res);
122
- await new Promise(r => res.on('finish', r));
123
-
124
- assert.ok(ok);
125
- assert.strictEqual(res.status, 200);
126
- assert.strictEqual(res.data, 'A');
127
- });
128
-
129
- it('returns false when file not found', async () => {
130
- addDir('/root');
131
-
132
- /** @type {{ root: string, prefix: string, defaults: string[] }} */
133
- const config = { root: '/root', prefix: '/p/', defaults: ['index.html'] };
134
- const res = new MockRes();
135
-
136
- const container = buildTestContainer();
137
- container.register('node:fs', mockFs);
138
- container.register('Fl32_Web_Back_Helper_Mime$', mime);
139
- container.register('Fl32_Web_Back_Logger$', logger);
140
-
141
- /** @type {Fl32_Web_Back_Handler_Static_A_FileService} */
142
- const service = await container.get('Fl32_Web_Back_Handler_Static_A_FileService$');
143
-
144
- const ok = await service.serve(config, 'missing.txt', {}, res);
145
- assert.strictEqual(ok, false);
146
- });
147
-
148
- it('logs info when file is missing during stat', async () => {
149
- mockFs.promises.stat = async () => {
150
- const err = new Error('ENOENT');
151
- err.code = 'ENOENT';
152
- throw err;
153
- };
154
- /** @type {{ root: string, prefix: string, defaults: string[] }} */
155
- const config = { root: '/root', prefix: '/p/', defaults: [] };
156
- const res = new MockRes();
157
-
158
- const container = buildTestContainer();
159
- container.register('node:fs', mockFs);
160
- container.register('Fl32_Web_Back_Helper_Mime$', mime);
161
- container.register('Fl32_Web_Back_Logger$', logger);
162
- container.register('Fl32_Web_Back_Handler_Static_A_Fallback$', { apply: async p => p });
163
-
164
- /** @type {Fl32_Web_Back_Handler_Static_A_FileService} */
165
- const service = await container.get('Fl32_Web_Back_Handler_Static_A_FileService$');
166
-
167
- const ok = await service.serve(config, 'missing.txt', {}, res);
168
-
169
- assert.strictEqual(ok, false);
170
- assert.strictEqual(logs.length, 1);
171
- assert.strictEqual(logs[0][0], 'info');
172
- });
173
-
174
- it('logs warn on access errors', async () => {
175
- /** force EACCES error */
176
- mockFs.promises.stat = async () => {
177
- const err = new Error('EACCES');
178
- err.code = 'EACCES';
179
- throw err;
180
- };
181
-
182
- /** @type {{ root: string, prefix: string, defaults: string[] }} */
183
- const config = { root: '/root', prefix: '/p/', defaults: [] };
184
- const res = new MockRes();
185
-
186
- const container = buildTestContainer();
187
- container.register('node:fs', mockFs);
188
- container.register('Fl32_Web_Back_Helper_Mime$', mime);
189
- container.register('Fl32_Web_Back_Logger$', logger);
190
- container.register('Fl32_Web_Back_Handler_Static_A_Fallback$', { apply: async p => p });
191
-
192
- /** @type {Fl32_Web_Back_Handler_Static_A_FileService} */
193
- const service = await container.get('Fl32_Web_Back_Handler_Static_A_FileService$');
194
-
195
- const ok = await service.serve(config, 'denied.txt', {}, res);
196
-
197
- assert.strictEqual(ok, false);
198
- assert.strictEqual(logs.length, 1);
199
- assert.strictEqual(logs[0][0], 'warn');
200
- });
201
-
202
- it('logs exception on unexpected errors', async () => {
203
- addFile('/root/x.txt', 'X');
204
- mockFs.createReadStream = () => { throw new Error('boom'); };
205
-
206
- /** @type {{ root: string, prefix: string, defaults: string[] }} */
207
- const config = { root: '/root', prefix: '/p/', defaults: [] };
208
- const res = new MockRes();
209
-
210
- const container = buildTestContainer();
211
- container.register('node:fs', mockFs);
212
- container.register('Fl32_Web_Back_Helper_Mime$', mime);
213
- container.register('Fl32_Web_Back_Logger$', logger);
214
- container.register('Fl32_Web_Back_Handler_Static_A_Fallback$', { apply: async p => p });
215
-
216
- /** @type {Fl32_Web_Back_Handler_Static_A_FileService} */
217
- const service = await container.get('Fl32_Web_Back_Handler_Static_A_FileService$');
218
-
219
- const ok = await service.serve(config, 'x.txt', {}, res);
220
-
221
- assert.strictEqual(ok, false);
222
- assert.strictEqual(logs.length, 1);
223
- assert.strictEqual(logs[0][0], 'exception');
224
- });
225
- });
@@ -1,83 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { buildTestContainer } from '../../../../common.js';
4
-
5
- /** Simple config factory mock */
6
- function getMockFactory() {
7
- return { create: dto => ({ root: dto.root, prefix: dto.prefix }) };
8
- }
9
-
10
- describe('Fl32_Web_Back_Handler_Static_A_Registry', () => {
11
- it('stores initial config', async () => {
12
- const container = buildTestContainer();
13
- container.register('Fl32_Web_Back_Handler_Static_A_Config$', getMockFactory());
14
- /** @type {Fl32_Web_Back_Handler_Static_A_Registry} */
15
- const registry = await container.get('Fl32_Web_Back_Handler_Static_A_Registry$');
16
-
17
- registry.addConfigs([{ root: '/a', prefix: '/p/' }]);
18
- const match = registry.find('/p/file.txt');
19
-
20
- assert.ok(match);
21
- assert.strictEqual(match.config.root, '/a');
22
- });
23
-
24
- it('adds config with new prefix', async () => {
25
- const container = buildTestContainer();
26
- container.register('Fl32_Web_Back_Handler_Static_A_Config$', getMockFactory());
27
- const registry = await container.get('Fl32_Web_Back_Handler_Static_A_Registry$');
28
-
29
- registry.addConfigs([{ root: '/a', prefix: '/p/' }]);
30
- registry.addConfigs([{ root: '/b', prefix: '/p/s/' }]);
31
-
32
- const match = registry.find('/p/s/file.txt');
33
- assert.ok(match);
34
- assert.strictEqual(match.config.root, '/b');
35
- });
36
-
37
- it('ignores config with existing prefix', async () => {
38
- const container = buildTestContainer();
39
- container.register('Fl32_Web_Back_Handler_Static_A_Config$', getMockFactory());
40
- const registry = await container.get('Fl32_Web_Back_Handler_Static_A_Registry$');
41
-
42
- registry.addConfigs([{ root: '/a', prefix: '/p/' }]);
43
- registry.addConfigs([{ root: '/b', prefix: '/p/s/' }]);
44
- registry.addConfigs([{ root: '/c', prefix: '/p/s/' }]);
45
-
46
- const match = registry.find('/p/s/test.txt');
47
- assert.ok(match);
48
- assert.strictEqual(match.config.root, '/b');
49
- });
50
-
51
- it('logs warning when prefix already exists', async () => {
52
- const log = [];
53
- const container = buildTestContainer();
54
- container.register('Fl32_Web_Back_Handler_Static_A_Config$', getMockFactory());
55
- container.register('Fl32_Web_Back_Logger$', {
56
- warn: (...args) => log.push(args)
57
- });
58
- const registry = await container.get('Fl32_Web_Back_Handler_Static_A_Registry$');
59
-
60
- registry.addConfigs([{ root: '/a', prefix: '/p/' }]);
61
- registry.addConfigs([{ root: '/b', prefix: '/p/' }]);
62
-
63
- assert.strictEqual(log.length, 1);
64
- assert.ok(log[0][0].includes('/p/'));
65
- });
66
-
67
- it('prefers longer prefix when matching', async () => {
68
- const container = buildTestContainer();
69
- container.register('Fl32_Web_Back_Handler_Static_A_Config$', getMockFactory());
70
- const registry = await container.get('Fl32_Web_Back_Handler_Static_A_Registry$');
71
-
72
- registry.addConfigs([
73
- { root: '/a', prefix: '/p/' },
74
- { root: '/b', prefix: '/p/s/' }
75
- ]);
76
-
77
- const match1 = registry.find('/p/s/x.txt');
78
- const match2 = registry.find('/p/y.txt');
79
-
80
- assert.strictEqual(match1.config.root, '/b');
81
- assert.strictEqual(match2.config.root, '/a');
82
- });
83
- });
@@ -1,73 +0,0 @@
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_Handler_Static_A_Resolver', () => {
6
- it('resolves allowed path', async () => {
7
- const container = buildTestContainer();
8
- /** @type {Fl32_Web_Back_Handler_Static_A_Resolver} */
9
- const resolver = await container.get('Fl32_Web_Back_Handler_Static_A_Resolver$');
10
-
11
- const { resolve } = await import('node:path');
12
- const config = {
13
- root: resolve('/root'),
14
- prefix: '/p/',
15
- allow: { pkg: ['a.txt'] }
16
- };
17
-
18
- const fsPath = resolver.resolve(config, 'pkg/a.txt');
19
- assert.strictEqual(fsPath, resolve('/root/pkg/a.txt'));
20
- });
21
-
22
- it('returns null for disallowed path', async () => {
23
- const container = buildTestContainer();
24
- /** @type {Fl32_Web_Back_Handler_Static_A_Resolver} */
25
- const resolver = await container.get('Fl32_Web_Back_Handler_Static_A_Resolver$');
26
-
27
- const { resolve } = await import('node:path');
28
- const config = {
29
- root: resolve('/root'),
30
- prefix: '/p/',
31
- allow: { pkg: ['a.txt'] }
32
- };
33
-
34
- const result = resolver.resolve(config, 'pkg/b.txt');
35
- assert.strictEqual(result, null);
36
- });
37
-
38
- it('throws on path traversal attempts', async () => {
39
- const container = buildTestContainer();
40
- /** @type {Fl32_Web_Back_Handler_Static_A_Resolver} */
41
- const resolver = await container.get('Fl32_Web_Back_Handler_Static_A_Resolver$');
42
-
43
- const { resolve } = await import('node:path');
44
- const config = {
45
- root: resolve('/root'),
46
- prefix: '/p/',
47
- allow: { pkg: ['a.txt'] }
48
- };
49
-
50
- assert.throws(
51
- () => resolver.resolve(config, '../x'),
52
- /Static access denied/
53
- );
54
- });
55
-
56
- it('throws on absolute rel paths', async () => {
57
- const container = buildTestContainer();
58
- /** @type {Fl32_Web_Back_Handler_Static_A_Resolver} */
59
- const resolver = await container.get('Fl32_Web_Back_Handler_Static_A_Resolver$');
60
-
61
- const { resolve } = await import('node:path');
62
- const config = {
63
- root: resolve('/root'),
64
- prefix: '/p/',
65
- allow: { pkg: ['a.txt'] }
66
- };
67
-
68
- assert.throws(
69
- () => resolver.resolve(config, '/etc/passwd'),
70
- /Static access denied/
71
- );
72
- });
73
- });
@@ -1,235 +0,0 @@
1
- import {describe, it, beforeEach} from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {EventEmitter} from 'node:events';
4
- import {buildTestContainer} from '../../../common.js';
5
-
6
- /** Simple HTTP/2 constants mock */
7
- const mockHttp2 = {
8
- constants: {
9
- HTTP2_HEADER_CONTENT_LENGTH: 'content-length',
10
- HTTP2_HEADER_CONTENT_TYPE: 'content-type',
11
- HTTP2_HEADER_LAST_MODIFIED: 'last-modified',
12
- HTTP_STATUS_OK: 200
13
- }
14
- };
15
-
16
- /** Minimal path mock for FS key normalization */
17
- const mockPath = {
18
- resolve: (...parts) => parts.join('/').replace(/\/+/g, '/'),
19
- join: (...parts) => parts.join('/').replace(/\/+/g, '/'),
20
- isAbsolute: p => p.startsWith('/'),
21
- extname: p => {
22
- const m = p.match(/(\.[^./]+)$/);
23
- return m ? m[1] : '';
24
- }
25
- };
26
-
27
- /** In-memory FS storage */
28
- let storage;
29
- let mockFs;
30
-
31
- function resetFs() {
32
- storage = new Map();
33
- }
34
-
35
- function addFile(p, content) {
36
- const key = mockPath.resolve(p);
37
- storage.set(key, {
38
- isFile: () => true,
39
- isDirectory: () => false,
40
- size: Buffer.byteLength(content),
41
- mtime: new Date(),
42
- content
43
- });
44
- }
45
-
46
- function addDir(p) {
47
- const key = mockPath.resolve(p);
48
- storage.set(key, {
49
- isFile: () => false,
50
- isDirectory: () => true,
51
- size: 0,
52
- mtime: new Date(),
53
- content: null
54
- });
55
- }
56
-
57
- beforeEach(() => {
58
- resetFs();
59
- mockFs = {
60
- promises: {
61
- stat: async p => {
62
- const key = mockPath.resolve(p);
63
- if (!storage.has(key)) throw new Error('ENOENT');
64
- const entry = storage.get(key);
65
- return {
66
- isFile: entry.isFile,
67
- isDirectory: entry.isDirectory,
68
- size: entry.size,
69
- mtime: entry.mtime
70
- };
71
- }
72
- },
73
- createReadStream: p => ({
74
- pipe: res => {
75
- setImmediate(() => {
76
- const entry = storage.get(mockPath.resolve(p));
77
- if (entry && entry.content != null) res.write(entry.content);
78
- res.end();
79
- });
80
- }
81
- })
82
- };
83
- });
84
-
85
- /** Mock response with writable stream semantics */
86
- class MockRes extends EventEmitter {
87
- constructor() {
88
- super();
89
- this.data = Buffer.alloc(0);
90
- this.statusCode = undefined;
91
- this.headers = undefined;
92
- this._sent = false;
93
- this._ended = false;
94
- }
95
-
96
- get headersSent() { return this._sent; }
97
-
98
- get writableEnded() { return this._ended; }
99
-
100
- writeHead(status, headers) {
101
- this.statusCode = status;
102
- this.headers = headers;
103
- this._sent = true;
104
- }
105
-
106
- write(chunk) {
107
- this.data = Buffer.concat([this.data, Buffer.from(chunk)]);
108
- }
109
-
110
- end(chunk) {
111
- if (chunk) this.write(chunk);
112
- this._ended = true;
113
- this.emit('finish');
114
- }
115
- }
116
-
117
- describe('Fl32_Web_Back_Handler_Static', () => {
118
- let container;
119
-
120
- beforeEach(() => {
121
- container = buildTestContainer();
122
- container.register('node:fs', mockFs);
123
- container.register('node:http2', mockHttp2);
124
- container.register('node:path', mockPath);
125
- container.register('Fl32_Web_Back_Logger$', {
126
- warn: () => {},
127
- exception: () => {}
128
- });
129
- });
130
-
131
- it('serves from the most specific source', async () => {
132
- addDir('/a');
133
- addFile('/a/test.txt', 'A');
134
- addDir('/b');
135
- addFile('/b/test.txt', 'B');
136
-
137
- /** @type {Fl32_Web_Back_Dto_Handler_Source} */
138
- const dtoSource = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
139
- /** @type {Fl32_Web_Back_Handler_Static} */
140
- const handler = await container.get('Fl32_Web_Back_Handler_Static$');
141
-
142
- await handler.init({
143
- sources: [
144
- dtoSource.create({prefix: '/files/', root: '/a', allow: {'.': ['.']}, defaults: []}),
145
- dtoSource.create({prefix: '/files/special/', root: '/b', allow: {'.': ['.']}, defaults: []})
146
- ]
147
- });
148
-
149
- const res = new MockRes();
150
- const ok = await handler.handle({url: '/files/special/test.txt'}, res);
151
- await new Promise(r => res.on('finish', r));
152
-
153
- assert.strictEqual(ok, true);
154
- assert.strictEqual(res.data.toString(), 'B');
155
- });
156
-
157
- it('enforces allow-list rules', async () => {
158
- addFile('src/Back/Server.js', 'class X {}');
159
- addFile('src/Back/Handler/Static.js', 'ignore');
160
-
161
- /** @type {Fl32_Web_Back_Dto_Handler_Source} */
162
- const dtoSource = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
163
- /** @type {Fl32_Web_Back_Handler_Static} */
164
- const handler = await container.get('Fl32_Web_Back_Handler_Static$');
165
-
166
- await handler.init({
167
- sources: [
168
- dtoSource.create({prefix: '/s/', root: 'src', allow: {Back: ['Server.js']}, defaults: []})
169
- ]
170
- });
171
-
172
- const okRes = new MockRes();
173
- const ok = await handler.handle({url: '/s/Back/Server.js'}, okRes);
174
- await new Promise(r => okRes.on('finish', r));
175
- assert.strictEqual(ok, true);
176
-
177
- const badRes = new MockRes();
178
- const bad = await handler.handle({url: '/s/Back/Handler/Static.js'}, badRes);
179
- assert.strictEqual(bad, false);
180
- assert.strictEqual(badRes.headersSent, false);
181
- });
182
-
183
- it('serves index files in directories', async () => {
184
- addDir('/dir');
185
- addDir('/dir/d');
186
- addFile('/dir/d/index.txt', 'INDEX');
187
-
188
- /** @type {Fl32_Web_Back_Dto_Handler_Source} */
189
- const dtoSource = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
190
- /** @type {Fl32_Web_Back_Handler_Static} */
191
- const handler = await container.get('Fl32_Web_Back_Handler_Static$');
192
-
193
- await handler.init({
194
- sources: [
195
- dtoSource.create({
196
- prefix: '/w/',
197
- root: '/dir',
198
- allow: {'.': ['.']},
199
- defaults: ['index.txt']
200
- })
201
- ]
202
- });
203
-
204
- const res = new MockRes();
205
- const ok = await handler.handle({url: '/w/d/'}, res);
206
- await new Promise(r => res.on('finish', r));
207
-
208
- assert.strictEqual(ok, true);
209
- assert.strictEqual(res.data.toString(), 'INDEX');
210
- });
211
-
212
- it('rejects traversal and unmatched prefixes', async () => {
213
- addFile('/safe/file.txt', 'ok');
214
-
215
- /** @type {Fl32_Web_Back_Dto_Handler_Source} */
216
- const dtoSource = await container.get('Fl32_Web_Back_Dto_Handler_Source$');
217
- /** @type {Fl32_Web_Back_Handler_Static} */
218
- const handler = await container.get('Fl32_Web_Back_Handler_Static$');
219
-
220
- await handler.init({
221
- sources: [
222
- dtoSource.create({prefix: '/p/', root: '/safe', allow: {'.': ['.']}, defaults: []})
223
- ]
224
- });
225
-
226
- const res1 = new MockRes();
227
- const bad1 = await handler.handle({url: '/p/../file.txt'}, res1);
228
- assert.strictEqual(bad1, false);
229
-
230
- const res2 = new MockRes();
231
- const bad2 = await handler.handle({url: '/x/file.txt'}, res2);
232
- assert.strictEqual(bad2, false);
233
- assert.strictEqual(res2.headersSent, false);
234
- });
235
- });
@@ -1,59 +0,0 @@
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
- });