@tachybase/di 1.3.51 → 1.3.53

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,178 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { CannotInjectValueError, Container, Inject, InjectMany, Service, Token } from '../index';
4
+
5
+ describe('Decorators', () => {
6
+ beforeEach(() => {
7
+ Container.reset({ strategy: 'resetServices' });
8
+ });
9
+
10
+ afterEach(() => {
11
+ Container.reset({ strategy: 'resetServices' });
12
+ });
13
+
14
+ describe('@Service Decorator', () => {
15
+ it('should register service with default options', () => {
16
+ @Service()
17
+ class TestService {
18
+ public name = 'test';
19
+ }
20
+
21
+ expect(Container.has(TestService)).toBe(true);
22
+ const instance = Container.get(TestService);
23
+ expect(instance).toBeInstanceOf(TestService);
24
+ expect(instance.name).toBe('test');
25
+ });
26
+
27
+ it('should register service with custom id', () => {
28
+ @Service({ id: 'custom-service' })
29
+ class TestService {
30
+ public name = 'test';
31
+ }
32
+
33
+ expect(Container.has('custom-service')).toBe(true);
34
+ const instance = Container.get('custom-service');
35
+ expect(instance).toBeInstanceOf(TestService);
36
+ });
37
+
38
+ it('should register service with singleton scope', () => {
39
+ @Service({ scope: 'singleton' })
40
+ class TestService {
41
+ public id = Math.random();
42
+ }
43
+
44
+ const instance1 = Container.get(TestService);
45
+ const instance2 = Container.get(TestService);
46
+
47
+ expect(instance1).toBe(instance2);
48
+ });
49
+
50
+ it('should register service with transient scope', () => {
51
+ @Service({ scope: 'transient' })
52
+ class TestService {
53
+ public id = Math.random();
54
+ }
55
+
56
+ const instance1 = Container.get(TestService);
57
+ const instance2 = Container.get(TestService);
58
+
59
+ expect(instance1).not.toBe(instance2);
60
+ });
61
+
62
+ it('should register service with multiple flag', () => {
63
+ @Service({ multiple: true })
64
+ class TestService {
65
+ public name = 'test';
66
+ }
67
+
68
+ expect(Container.has(TestService)).toBe(true);
69
+ const services = Container.getMany(TestService);
70
+ expect(services).toHaveLength(1);
71
+ });
72
+
73
+ it('should register service with eager flag', () => {
74
+ let instantiated = false;
75
+
76
+ @Service({ eager: true })
77
+ class TestService {
78
+ constructor() {
79
+ instantiated = true;
80
+ }
81
+ }
82
+
83
+ expect(instantiated).toBe(true);
84
+ });
85
+
86
+ it('should register service with factory', () => {
87
+ const factory = () => ({ name: 'factory-created' });
88
+
89
+ @Service({ factory })
90
+ class TestService {}
91
+
92
+ const instance = Container.get(TestService);
93
+ expect(instance).toEqual({ name: 'factory-created' });
94
+ });
95
+ });
96
+
97
+ describe('@Inject Decorator', () => {
98
+ it('should inject service by string identifier', () => {
99
+ @Service({ id: 'dep-service' })
100
+ class DependencyService {
101
+ public name = 'dependency';
102
+ }
103
+
104
+ @Service()
105
+ class TestService {
106
+ @Inject('dep-service')
107
+ public dependency!: DependencyService;
108
+ }
109
+
110
+ const instance = Container.get(TestService);
111
+ expect(instance.dependency).toBeInstanceOf(DependencyService);
112
+ });
113
+
114
+ it('should inject service by token', () => {
115
+ const token = new Token<DependencyService>('dep-token');
116
+
117
+ @Service({ id: token })
118
+ class DependencyService {
119
+ public name = 'dependency';
120
+ }
121
+
122
+ @Service()
123
+ class TestService {
124
+ @Inject(token)
125
+ public dependency!: DependencyService;
126
+ }
127
+
128
+ const instance = Container.get(TestService);
129
+ expect(instance.dependency).toBeInstanceOf(DependencyService);
130
+ });
131
+ });
132
+
133
+ describe('@InjectMany Decorator', () => {
134
+ it('should inject multiple services by string identifier', () => {
135
+ @Service({ id: 'dep-service', multiple: true })
136
+ class DependencyService {
137
+ public name = 'dependency';
138
+ }
139
+
140
+ @Service({ id: 'dep-service', multiple: true })
141
+ class AnotherDependencyService {
142
+ public name = 'another-dependency';
143
+ }
144
+
145
+ @Service()
146
+ class TestService {
147
+ @InjectMany('dep-service')
148
+ public dependencies!: DependencyService[];
149
+ }
150
+
151
+ const instance = Container.get(TestService);
152
+ expect(instance.dependencies).toHaveLength(2);
153
+ });
154
+
155
+ it('should inject multiple services by token', () => {
156
+ const token = new Token<DependencyService>('dep-token');
157
+
158
+ @Service({ id: token, multiple: true })
159
+ class DependencyService {
160
+ public name = 'dependency';
161
+ }
162
+
163
+ @Service({ id: token, multiple: true })
164
+ class AnotherDependencyService {
165
+ public name = 'another-dependency';
166
+ }
167
+
168
+ @Service()
169
+ class TestService {
170
+ @InjectMany(token)
171
+ public dependencies!: DependencyService[];
172
+ }
173
+
174
+ const instance = Container.get(TestService);
175
+ expect(instance.dependencies).toHaveLength(2);
176
+ });
177
+ });
178
+ });
@@ -0,0 +1,359 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ CannotInjectValueError,
5
+ CannotInstantiateValueError,
6
+ Container,
7
+ ContainerInstance,
8
+ ServiceNotFoundError,
9
+ Token,
10
+ } from '../index';
11
+
12
+ describe('Error Handling', () => {
13
+ let container: ContainerInstance;
14
+
15
+ beforeEach(() => {
16
+ container = new ContainerInstance(`test-container-${Math.random()}`);
17
+ Container.reset({ strategy: 'resetServices' });
18
+ });
19
+
20
+ afterEach(() => {
21
+ container.reset({ strategy: 'resetServices' });
22
+ Container.reset({ strategy: 'resetServices' });
23
+ });
24
+
25
+ describe('ServiceNotFoundError', () => {
26
+ it('should throw ServiceNotFoundError for non-existent string service', () => {
27
+ expect(() => container.get('non-existent-service')).toThrow(ServiceNotFoundError);
28
+ });
29
+
30
+ it('should throw ServiceNotFoundError for non-existent class service', () => {
31
+ class NonExistentService {}
32
+
33
+ expect(() => container.get(NonExistentService)).toThrow(ServiceNotFoundError);
34
+ });
35
+
36
+ it('should throw ServiceNotFoundError for non-existent token service', () => {
37
+ const token = new Token('non-existent-token');
38
+
39
+ expect(() => container.get(token)).toThrow(ServiceNotFoundError);
40
+ });
41
+
42
+ it('should have correct error message for string identifier', () => {
43
+ try {
44
+ container.get('test-service');
45
+ } catch (error) {
46
+ expect(error).toBeInstanceOf(ServiceNotFoundError);
47
+ expect(error.message).toContain('Service with "test-service" identifier was not found');
48
+ expect(error.message).toContain('Register it before usage');
49
+ }
50
+ });
51
+
52
+ it('should have correct error message for class identifier', () => {
53
+ class TestService {}
54
+
55
+ try {
56
+ container.get(TestService);
57
+ } catch (error) {
58
+ expect(error).toBeInstanceOf(ServiceNotFoundError);
59
+ expect(error.message).toContain('MaybeConstructable<TestService>');
60
+ expect(error.message).toContain('Register it before usage');
61
+ }
62
+ });
63
+
64
+ it('should have correct error message for token identifier', () => {
65
+ const token = new Token('test-token');
66
+
67
+ try {
68
+ container.get(token);
69
+ } catch (error) {
70
+ expect(error).toBeInstanceOf(ServiceNotFoundError);
71
+ expect(error.message).toContain('Token<test-token>');
72
+ expect(error.message).toContain('Register it before usage');
73
+ }
74
+ });
75
+
76
+ it('should have correct error message for token without name', () => {
77
+ const token = new Token();
78
+
79
+ try {
80
+ container.get(token);
81
+ } catch (error) {
82
+ expect(error).toBeInstanceOf(ServiceNotFoundError);
83
+ expect(error.message).toContain('Token<UNSET_NAME>');
84
+ expect(error.message).toContain('Register it before usage');
85
+ }
86
+ });
87
+
88
+ it('should throw ServiceNotFoundError for getMany with non-existent service', () => {
89
+ expect(() => container.getMany('non-existent-service')).toThrow(ServiceNotFoundError);
90
+ });
91
+ });
92
+
93
+ describe('CannotInstantiateValueError', () => {
94
+ it('should throw CannotInstantiateValueError when no type or factory provided', () => {
95
+ container.set({ id: 'invalid-service' });
96
+
97
+ expect(() => container.get('invalid-service')).toThrow(CannotInstantiateValueError);
98
+ });
99
+
100
+ it('should have correct error message', () => {
101
+ container.set({ id: 'invalid-service' });
102
+
103
+ try {
104
+ container.get('invalid-service');
105
+ } catch (error) {
106
+ expect(error).toBeInstanceOf(CannotInstantiateValueError);
107
+ expect(error.message).toContain('Cannot instantiate the requested value for the "invalid-service" identifier');
108
+ expect(error.message).toContain("doesn't contain a factory or a type to instantiate");
109
+ }
110
+ });
111
+
112
+ it('should throw error for class identifier', () => {
113
+ class TestService {}
114
+ container.set({ id: TestService });
115
+
116
+ expect(() => container.get(TestService)).toThrow(CannotInstantiateValueError);
117
+ });
118
+
119
+ it('should throw error for token identifier', () => {
120
+ const token = new Token('test-token');
121
+ container.set({ id: token });
122
+
123
+ expect(() => container.get(token)).toThrow(CannotInstantiateValueError);
124
+ });
125
+
126
+ it('should handle factory returning undefined', () => {
127
+ const factory = () => undefined;
128
+ container.set({ id: 'undefined-factory', factory });
129
+
130
+ // Factory returning undefined should not throw, but the value should be undefined
131
+ const result = container.get('undefined-factory');
132
+ expect(result).toBeUndefined();
133
+ });
134
+
135
+ it('should handle factory returning null', () => {
136
+ const factory = () => null;
137
+ container.set({ id: 'null-factory', factory });
138
+
139
+ // Factory returning null should not throw, but the value should be null
140
+ const result = container.get('null-factory');
141
+ expect(result).toBeNull();
142
+ });
143
+ });
144
+
145
+ describe('CannotInjectValueError', () => {
146
+ it('should throw CannotInjectValueError when injecting unknown type', () => {
147
+ expect(() => {
148
+ // This would be caught during decoration, but we can't test it directly
149
+ // since the decorator runs at class definition time
150
+ class TestService {
151
+ // @Inject() // This would throw CannotInjectValueError
152
+ public unknown!: any;
153
+ }
154
+ }).not.toThrow(); // The error would be thrown during decoration, not here
155
+ });
156
+
157
+ it('should have correct error message format', () => {
158
+ class TestService {}
159
+ const propertyName = 'testProperty';
160
+
161
+ const error = new CannotInjectValueError(TestService, propertyName);
162
+
163
+ expect(error.message).toContain('Cannot inject value into "TestService.testProperty"');
164
+ expect(error.message).toContain('setup reflect-metadata properly');
165
+ expect(error.message).toContain('interfaces without service tokens');
166
+ });
167
+ });
168
+
169
+ describe('Container Disposal Errors', () => {
170
+ it('should throw error when using disposed container', async () => {
171
+ const testContainer = new ContainerInstance(`dispose-test-${Math.random()}`);
172
+ await testContainer.dispose();
173
+
174
+ expect(() => testContainer.get('test')).toThrow('Cannot use container after it has been disposed.');
175
+ });
176
+
177
+ it('should throw error when setting service in disposed container', async () => {
178
+ const testContainer = new ContainerInstance(`dispose-test-${Math.random()}`);
179
+ await testContainer.dispose();
180
+
181
+ expect(() => testContainer.set({ id: 'test', value: 'test' })).toThrow(
182
+ 'Cannot use container after it has been disposed.',
183
+ );
184
+ });
185
+
186
+ it('should throw error when checking service in disposed container', async () => {
187
+ const testContainer = new ContainerInstance(`dispose-test-${Math.random()}`);
188
+ await testContainer.dispose();
189
+
190
+ expect(() => testContainer.has('test')).toThrow('Cannot use container after it has been disposed.');
191
+ });
192
+
193
+ it('should throw error when removing service in disposed container', async () => {
194
+ const testContainer = new ContainerInstance(`dispose-test-${Math.random()}`);
195
+ await testContainer.dispose();
196
+
197
+ expect(() => testContainer.remove('test')).toThrow('Cannot use container after it has been disposed.');
198
+ });
199
+
200
+ it('should throw error when resetting disposed container', async () => {
201
+ const testContainer = new ContainerInstance(`dispose-test-${Math.random()}`);
202
+ await testContainer.dispose();
203
+
204
+ expect(() => testContainer.reset()).toThrow('Cannot use container after it has been disposed.');
205
+ });
206
+ });
207
+
208
+ describe('Factory Errors', () => {
209
+ it('should propagate factory errors', () => {
210
+ const factory = () => {
211
+ throw new Error('Factory error');
212
+ };
213
+
214
+ container.set({ id: 'error-factory', factory });
215
+
216
+ expect(() => container.get('error-factory')).toThrow('Factory error');
217
+ });
218
+
219
+ it('should handle factory class not found in container', () => {
220
+ class Factory {
221
+ create() {
222
+ return 'created';
223
+ }
224
+ }
225
+
226
+ container.set({ id: 'factory-service', factory: [Factory, 'create'] });
227
+
228
+ // Should create factory instance directly since it's not in container
229
+ const result = container.get('factory-service');
230
+ expect(result).toBe('created');
231
+ });
232
+
233
+ it('should handle factory class found in container', () => {
234
+ class Factory {
235
+ create() {
236
+ return 'created-from-container';
237
+ }
238
+ }
239
+
240
+ container.set({ type: Factory });
241
+ container.set({ id: 'factory-service', factory: [Factory, 'create'] });
242
+
243
+ const result = container.get('factory-service');
244
+ expect(result).toBe('created-from-container');
245
+ });
246
+ });
247
+
248
+ describe('Multiple Service Errors', () => {
249
+ it('should throw error when getting single service with multiple flag', () => {
250
+ class TestService {
251
+ public name = 'test';
252
+ }
253
+
254
+ container.set({ id: 'test-services', type: TestService, multiple: true });
255
+
256
+ expect(() => container.get('test-services')).toThrow(
257
+ 'Service with "test-services" identifier was not found in the container',
258
+ );
259
+ });
260
+
261
+ it('should throw error when getting single service with multiple flag using class', () => {
262
+ class TestService {
263
+ public name = 'test';
264
+ }
265
+
266
+ container.set({ type: TestService, multiple: true });
267
+
268
+ expect(() => container.get(TestService)).toThrow(
269
+ 'Service with "MaybeConstructable<TestService>" identifier was not found in the container',
270
+ );
271
+ });
272
+ });
273
+
274
+ describe('Circular Dependency Errors', () => {
275
+ it('should handle circular dependencies gracefully', () => {
276
+ class ServiceA {
277
+ public serviceB: ServiceB;
278
+ constructor() {
279
+ this.serviceB = container.get(ServiceB);
280
+ }
281
+ }
282
+
283
+ class ServiceB {
284
+ public serviceA: ServiceA;
285
+ constructor() {
286
+ this.serviceA = container.get(ServiceA);
287
+ }
288
+ }
289
+
290
+ container.set({ type: ServiceA });
291
+ container.set({ type: ServiceB });
292
+
293
+ // Circular dependencies will cause stack overflow
294
+ expect(() => container.get(ServiceA)).toThrow('Maximum call stack size exceeded');
295
+ });
296
+ });
297
+
298
+ describe('Invalid Reset Strategy', () => {
299
+ it('should throw error for invalid reset strategy', () => {
300
+ expect(() => container.reset({ strategy: 'invalid' as any })).toThrow('Received invalid reset strategy.');
301
+ });
302
+ });
303
+
304
+ describe('Service Disposal Errors', () => {
305
+ it('should handle disposal errors gracefully', () => {
306
+ class TestService {
307
+ dispose() {
308
+ throw new Error('Disposal error');
309
+ }
310
+ }
311
+
312
+ container.set({ type: TestService });
313
+ const instance = container.get(TestService);
314
+
315
+ // Should not throw error even if disposal fails
316
+ expect(() => container.remove(TestService)).not.toThrow();
317
+ });
318
+
319
+ it('should handle disposal of service without dispose method', () => {
320
+ class TestService {
321
+ public name = 'test';
322
+ }
323
+
324
+ container.set({ type: TestService });
325
+ const instance = container.get(TestService);
326
+
327
+ // Should not throw error
328
+ expect(() => container.remove(TestService)).not.toThrow();
329
+ });
330
+ });
331
+
332
+ describe('Edge Cases', () => {
333
+ it('should handle undefined service identifier', () => {
334
+ expect(() => container.get(undefined as any)).toThrow();
335
+ });
336
+
337
+ it('should handle null service identifier', () => {
338
+ expect(() => container.get(null as any)).toThrow();
339
+ });
340
+
341
+ it('should handle empty string service identifier', () => {
342
+ expect(() => container.get('')).toThrow(ServiceNotFoundError);
343
+ });
344
+
345
+ it('should handle service with empty value', () => {
346
+ container.set({ id: 'empty-service', value: null });
347
+
348
+ const result = container.get('empty-service');
349
+ expect(result).toBeNull();
350
+ });
351
+
352
+ it('should handle service with undefined value', () => {
353
+ container.set({ id: 'undefined-service', value: undefined });
354
+
355
+ const result = container.get('undefined-service');
356
+ expect(result).toBeUndefined();
357
+ });
358
+ });
359
+ });
@@ -0,0 +1,123 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { Container, ContainerInstance, Inject, InjectMany, Service, Token } from '../index';
4
+
5
+ describe('Integration Tests', () => {
6
+ let container: ContainerInstance;
7
+
8
+ beforeEach(() => {
9
+ container = new ContainerInstance(`integration-test-${Math.random()}`);
10
+ Container.reset({ strategy: 'resetServices' });
11
+ });
12
+
13
+ afterEach(() => {
14
+ container.reset({ strategy: 'resetServices' });
15
+ Container.reset({ strategy: 'resetServices' });
16
+ });
17
+
18
+ describe('Real-world Application Scenarios', () => {
19
+ it('should handle plugin system with multiple implementations', () => {
20
+ // Plugin interface
21
+ const PluginToken = new Token<Plugin>('plugin');
22
+
23
+ interface Plugin {
24
+ name: string;
25
+ execute(): string;
26
+ }
27
+
28
+ // Plugin implementations
29
+ @Service({ id: PluginToken, multiple: true })
30
+ class LoggingPlugin implements Plugin {
31
+ public name = 'logging';
32
+
33
+ execute() {
34
+ return 'logging executed';
35
+ }
36
+ }
37
+
38
+ @Service({ id: PluginToken, multiple: true })
39
+ class CachingPlugin implements Plugin {
40
+ public name = 'caching';
41
+
42
+ execute() {
43
+ return 'caching executed';
44
+ }
45
+ }
46
+
47
+ // Plugin manager
48
+ @Service()
49
+ class PluginManager {
50
+ @InjectMany(PluginToken)
51
+ public plugins!: Plugin[];
52
+
53
+ public executeAll() {
54
+ return this.plugins.map((plugin) => plugin.execute());
55
+ }
56
+ }
57
+
58
+ const manager = Container.get(PluginManager);
59
+ const results = manager.executeAll();
60
+
61
+ expect(results).toHaveLength(2);
62
+ expect(results).toContain('logging executed');
63
+ expect(results).toContain('caching executed');
64
+ });
65
+ });
66
+
67
+ describe('Performance and Memory', () => {
68
+ it('should handle large number of services efficiently', () => {
69
+ const services: any[] = [];
70
+
71
+ // Create many services
72
+ for (let i = 0; i < 100; i++) {
73
+ class TestService {
74
+ public id = i;
75
+ }
76
+
77
+ container.set({ id: `service-${i}`, type: TestService });
78
+ services.push(container.get(`service-${i}`));
79
+ }
80
+
81
+ // Verify all services are accessible
82
+ for (let i = 0; i < 100; i++) {
83
+ expect(container.has(`service-${i}`)).toBe(true);
84
+ const service = container.get(`service-${i}`);
85
+ expect(service.id).toBe(i);
86
+ }
87
+ });
88
+
89
+ it('should handle rapid service creation and destruction', () => {
90
+ class TestService {
91
+ public id = Math.random();
92
+ }
93
+
94
+ // Rapidly create and destroy services
95
+ for (let i = 0; i < 50; i++) {
96
+ container.set({ id: `temp-service-${i}`, type: TestService });
97
+ const service = container.get(`temp-service-${i}`);
98
+ expect(service).toBeInstanceOf(TestService);
99
+ container.remove(`temp-service-${i}`);
100
+ expect(container.has(`temp-service-${i}`)).toBe(false);
101
+ }
102
+ });
103
+ });
104
+
105
+ describe('Cross-Container Scenarios', () => {
106
+ it('should handle singleton services across containers', () => {
107
+ const container1 = new ContainerInstance('container1');
108
+ const container2 = new ContainerInstance('container2');
109
+
110
+ @Service({ scope: 'singleton' })
111
+ class SingletonService {
112
+ public id = Math.random();
113
+ }
114
+
115
+ const service1 = container1.get(SingletonService);
116
+ const service2 = container2.get(SingletonService);
117
+
118
+ // Singleton should be shared across containers
119
+ expect(service1).toBe(service2);
120
+ expect(service1.id).toBe(service2.id);
121
+ });
122
+ });
123
+ });