import {describe, test, expect, beforeEach} from 'bun:test'; import {DependencyManager} from '../../src/di/DependencyManager'; // Test classes for dependency injection class Logger { log(message: string) { return `LOG: ${message}`; } } class Database { constructor(private logger: Logger) {} query(sql: string) { this.logger.log(`Executing: ${sql}`); return 'result'; } getLogger() { return this.logger; } } class UserService { constructor( private db: Database, private logger: Logger ) {} getUser(id: number) { this.logger.log(`Getting user ${id}`); return this.db.query(`SELECT * FROM users WHERE id = ${id}`); } getDb() { return this.db; } getLogger() { return this.logger; } } class CircularA { constructor(public b: CircularB) {} } class CircularB { constructor(public a: CircularA) {} } describe('DependencyManager', () => { let dm: DependencyManager; beforeEach(() => { dm = new DependencyManager(); }); describe('register', () => { test('should register a class without dependencies', () => { expect(() => { dm.register('Logger', Logger, []); }).not.toThrow(); }); test('should register a class with dependencies', () => { dm.register('Logger', Logger, []); expect(() => { dm.register('Database', Database, ['Logger']); }).not.toThrow(); }); test('should throw error when registering duplicate name', () => { dm.register('Logger', Logger, []); expect(() => { dm.register('Logger', Logger, []); }).toThrow('Error: [Logger] is already registered'); }); test('should throw error for invalid lifecycle', () => { expect(() => { dm.register('Logger', Logger, [], 'invalid' as any); }).toThrow('Invalid lifecycle [invalid]'); }); test('should accept valid lifecycle types', () => { expect(() => { dm.register('Logger1', Logger, [], 'singleton'); }).not.toThrow(); expect(() => { dm.register('Logger2', Logger, [], 'instance'); }).not.toThrow(); expect(() => { dm.register('Logger3', Logger, [], 'service'); }).not.toThrow(); }); }); describe('resolve', () => { test('should resolve a simple class without dependencies', () => { dm.register('Logger', Logger, []); const logger = dm.resolve('Logger'); expect(logger).toBeInstanceOf(Logger); expect(logger.log('test')).toBe('LOG: test'); }); test('should throw error when resolving unregistered dependency', () => { expect(() => { dm.resolve('NonExistent'); }).toThrow('Error: Unable to resolve unregistered [NonExistent]'); }); test('should resolve class with dependencies', () => { dm.register('Logger', Logger, []); dm.register('Database', Database, ['Logger']); const db = dm.resolve('Database'); expect(db).toBeInstanceOf(Database); expect(db.getLogger()).toBeInstanceOf(Logger); }); test('should resolve nested dependencies', () => { dm.register('Logger', Logger, []); dm.register('Database', Database, ['Logger']); dm.register('UserService', UserService, ['Database', 'Logger']); const userService = dm.resolve('UserService'); expect(userService).toBeInstanceOf(UserService); expect(userService.getDb()).toBeInstanceOf(Database); expect(userService.getLogger()).toBeInstanceOf(Logger); }); }); describe('singleton lifecycle', () => { test('should return same instance for singleton lifecycle', () => { dm.register('Logger', Logger, [], 'singleton'); const logger1 = dm.resolve('Logger'); const logger2 = dm.resolve('Logger'); expect(logger1).toBe(logger2); // Same instance }); test('should inject same singleton instance into multiple dependents', () => { dm.register('Logger', Logger, [], 'singleton'); dm.register('Database', Database, ['Logger']); dm.register('UserService', UserService, ['Database', 'Logger']); const userService = dm.resolve('UserService'); const db = userService.getDb(); const loggerFromUserService = userService.getLogger(); const loggerFromDatabase = db.getLogger(); expect(loggerFromUserService).toBe(loggerFromDatabase); // Same singleton instance }); test('should use singleton by default', () => { dm.register('Logger', Logger, []); // No lifecycle specified const logger1 = dm.resolve('Logger'); const logger2 = dm.resolve('Logger'); expect(logger1).toBe(logger2); }); }); describe('instance/service lifecycle (transient)', () => { test('should return new instance each time for instance lifecycle', () => { dm.register('Logger', Logger, [], 'instance'); const logger1 = dm.resolve('Logger'); const logger2 = dm.resolve('Logger'); expect(logger1).not.toBe(logger2); // Different instances expect(logger1).toBeInstanceOf(Logger); expect(logger2).toBeInstanceOf(Logger); }); test('should return new instance each time for service lifecycle', () => { dm.register('Logger', Logger, [], 'service'); const logger1 = dm.resolve('Logger'); const logger2 = dm.resolve('Logger'); expect(logger1).not.toBe(logger2); // Different instances }); test('should inject new instances into each dependent', () => { dm.register('Logger', Logger, [], 'instance'); dm.register('Database', Database, ['Logger']); dm.register('UserService', UserService, ['Database', 'Logger']); const userService = dm.resolve('UserService'); const loggerFromUserService = userService.getLogger(); const loggerFromDatabase = userService.getDb().getLogger(); expect(loggerFromUserService).not.toBe(loggerFromDatabase); // Different instances }); }); describe('circular dependency detection', () => { test('should detect direct circular dependency', () => { dm.register('CircularA', CircularA, ['CircularB']); dm.register('CircularB', CircularB, ['CircularA']); expect(() => { dm.resolve('CircularA'); }).toThrow('Circular dependency detected'); }); test('should detect self-referencing circular dependency', () => { dm.register('SelfRef', Logger, ['SelfRef']); expect(() => { dm.resolve('SelfRef'); }).toThrow('Circular dependency detected: [SelfRef]'); }); test('should detect indirect circular dependency chain', () => { class A { constructor(public b: any) {} } class B { constructor(public c: any) {} } class C { constructor(public a: any) {} } dm.register('A', A, ['B']); dm.register('B', B, ['C']); dm.register('C', C, ['A']); expect(() => { dm.resolve('A'); }).toThrow('Circular dependency detected'); }); }); describe('has', () => { test('should return true for registered dependency', () => { dm.register('Logger', Logger, []); expect(dm.has('Logger')).toBe(true); }); test('should return false for unregistered dependency', () => { expect(dm.has('NonExistent')).toBe(false); }); test('should not throw error for unregistered dependency', () => { expect(() => { dm.has('NonExistent'); }).not.toThrow(); }); }); describe('clearSingletons', () => { test('should clear cached singleton instances', () => { dm.register('Logger', Logger, [], 'singleton'); const logger1 = dm.resolve('Logger'); dm.clearSingletons(); const logger2 = dm.resolve('Logger'); expect(logger1).not.toBe(logger2); // Different instances after clearing }); test('should not affect registrations', () => { dm.register('Logger', Logger, []); dm.clearSingletons(); expect(dm.has('Logger')).toBe(true); expect(() => { dm.resolve('Logger'); }).not.toThrow(); }); test('should work with multiple singletons', () => { dm.register('Logger', Logger, [], 'singleton'); dm.register('Database', Database, ['Logger'], 'singleton'); const logger1 = dm.resolve('Logger'); const db1 = dm.resolve('Database'); dm.clearSingletons(); const logger2 = dm.resolve('Logger'); const db2 = dm.resolve('Database'); expect(logger1).not.toBe(logger2); expect(db1).not.toBe(db2); }); }); describe('complex scenarios', () => { test('should handle mixed lifecycles', () => { dm.register('Logger', Logger, [], 'instance'); // Transient dm.register('Database', Database, ['Logger'], 'singleton'); dm.register('UserService', UserService, ['Database', 'Logger']); const userService1 = dm.resolve('UserService'); const userService2 = dm.resolve('UserService'); // UserService is singleton by default expect(userService1).toBe(userService2); // Database should be same singleton expect(userService1.getDb()).toBe(userService2.getDb()); }); test('should handle multiple dependencies of same type', () => { class MultiDependent { constructor( public logger1: Logger, public logger2: Logger ) {} } dm.register('Logger', Logger, [], 'instance'); dm.register('MultiDependent', MultiDependent, ['Logger', 'Logger']); const multiDep = dm.resolve('MultiDependent'); // Should be different instances since Logger is transient expect(multiDep.logger1).not.toBe(multiDep.logger2); }); test('should handle no dependencies (empty array)', () => { dm.register('Logger', Logger, []); const logger = dm.resolve('Logger'); expect(logger).toBeInstanceOf(Logger); }); }); describe('type safety', () => { test('should support generic type parameter', () => { dm.register('Logger', Logger, []); // TypeScript should infer the correct type const logger = dm.resolve('Logger'); // This should compile and work expect(logger.log('test')).toBe('LOG: test'); }); test('should work without explicit type parameter', () => { dm.register('Logger', Logger, []); const logger = dm.resolve('Logger'); expect(logger).toBeInstanceOf(Logger); }); }); });