const Container = require('../../../src/application/services/Container'); describe('Container - Dependency Injection', () => { let container; beforeEach(() => { container = new Container(); }); describe('constructor', () => { test('should initialize with empty Maps', () => { expect(container.services).toBeInstanceOf(Map); expect(container.factories).toBeInstanceOf(Map); expect(container.lifetimes).toBeInstanceOf(Map); expect(container.services.size).toBe(0); expect(container.factories.size).toBe(0); expect(container.lifetimes.size).toBe(0); }); }); describe('register', () => { test('should register a singleton service', () => { const factory = jest.fn(() => ({ id: 1, name: 'Test Service' })); container.register('TestService', factory, 'singleton'); expect(container.factories.has('TestService')).toBe(true); expect(container.lifetimes.get('TestService')).toBe('singleton'); expect(factory).toHaveBeenCalled(); // Singleton is created immediately expect(container.services.has('TestService')).toBe(true); }); test('should register a transient service', () => { const factory = jest.fn(() => ({ id: 2, name: 'Transient Service' })); container.register('TransientService', factory, 'transient'); expect(container.factories.has('TransientService')).toBe(true); expect(container.lifetimes.get('TransientService')).toBe('transient'); expect(factory).not.toHaveBeenCalled(); // Transient is NOT created immediately expect(container.services.has('TransientService')).toBe(false); }); test('should register a scoped service', () => { const factory = jest.fn(() => ({ id: 3, name: 'Scoped Service' })); container.register('ScopedService', factory, 'scoped'); expect(container.factories.has('ScopedService')).toBe(true); expect(container.lifetimes.get('ScopedService')).toBe('scoped'); expect(factory).not.toHaveBeenCalled(); // Scoped is NOT created immediately }); test('should default to singleton if lifetime not specified', () => { const factory = () => ({ id: 4 }); container.register('DefaultService', factory); expect(container.lifetimes.get('DefaultService')).toBe('singleton'); expect(container.services.has('DefaultService')).toBe(true); }); }); describe('resolve - singleton', () => { test('should return the same instance for singleton', () => { container.register('SingletonService', () => ({ id: Math.random() }), 'singleton'); const instance1 = container.resolve('SingletonService'); const instance2 = container.resolve('SingletonService'); expect(instance1).toBe(instance2); expect(instance1.id).toBe(instance2.id); }); test('should return the pre-created singleton instance', () => { const mockInstance = { id: 123, name: 'Mock' }; container.register('PreCreatedService', () => mockInstance, 'singleton'); const resolved = container.resolve('PreCreatedService'); expect(resolved).toBe(mockInstance); }); }); describe('resolve - transient', () => { test('should return different instances for transient', () => { container.register('TransientService', () => ({ id: Math.random() }), 'transient'); const instance1 = container.resolve('TransientService'); const instance2 = container.resolve('TransientService'); expect(instance1).not.toBe(instance2); expect(instance1.id).not.toBe(instance2.id); }); test('should call factory every time for transient', () => { const factory = jest.fn(() => ({ id: Math.random() })); container.register('TransientService', factory, 'transient'); container.resolve('TransientService'); container.resolve('TransientService'); container.resolve('TransientService'); expect(factory).toHaveBeenCalledTimes(3); }); }); describe('resolve - scoped', () => { test('should return same instance within the same scope', () => { container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); const scope = container.createScope(); const instance1 = scope.resolve('ScopedService'); const instance2 = scope.resolve('ScopedService'); expect(instance1).toBe(instance2); expect(instance1.id).toBe(instance2.id); }); test('should return different instances for different scopes', () => { container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); const scope1 = container.createScope(); const scope2 = container.createScope(); const instance1 = scope1.resolve('ScopedService'); const instance2 = scope2.resolve('ScopedService'); expect(instance1).not.toBe(instance2); expect(instance1.id).not.toBe(instance2.id); }); test('should resolve scoped service with scope parameter', () => { const scopeMap = new Map(); container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); const instance1 = container.resolve('ScopedService', scopeMap); const instance2 = container.resolve('ScopedService', scopeMap); expect(instance1).toBe(instance2); expect(scopeMap.has('ScopedService')).toBe(true); }); }); describe('resolve - error handling', () => { test('should throw error for unregistered service', () => { expect(() => container.resolve('NonExistentService')).toThrow( "Service 'NonExistentService' is not registered" ); }); test('should provide clear error message', () => { try { container.resolve('MissingService'); fail('Should have thrown error'); } catch (error) { expect(error.message).toContain('MissingService'); expect(error.message).toContain('not registered'); } }); }); describe('createScope', () => { test('should create a scope with resolve method', () => { const scope = container.createScope(); expect(scope).toHaveProperty('resolve'); expect(typeof scope.resolve).toBe('function'); }); test('should create independent scopes', () => { container.register('ScopedService', () => ({ id: Math.random() }), 'scoped'); const scope1 = container.createScope(); const scope2 = container.createScope(); const instance1 = scope1.resolve('ScopedService'); const instance2 = scope2.resolve('ScopedService'); expect(instance1.id).not.toBe(instance2.id); }); }); describe('real-world scenarios', () => { test('should handle database connection as singleton', () => { class DatabaseConnection { constructor() { this.id = Math.random(); this.connected = true; } } container.register('Database', () => new DatabaseConnection(), 'singleton'); const db1 = container.resolve('Database'); const db2 = container.resolve('Database'); expect(db1).toBe(db2); expect(db1.id).toBe(db2.id); expect(db1.connected).toBe(true); }); test('should handle logger as transient', () => { class Logger { constructor() { this.id = Math.random(); } log(msg) { return `[${this.id}] ${msg}`; } } container.register('Logger', () => new Logger(), 'transient'); const logger1 = container.resolve('Logger'); const logger2 = container.resolve('Logger'); expect(logger1).not.toBe(logger2); expect(logger1.id).not.toBe(logger2.id); }); test('should handle request context as scoped', () => { class RequestContext { constructor() { this.requestId = Math.random(); this.user = null; this.timestamp = Date.now(); } } container.register('RequestContext', () => new RequestContext(), 'scoped'); // Request 1 const request1Scope = container.createScope(); const ctx1a = request1Scope.resolve('RequestContext'); const ctx1b = request1Scope.resolve('RequestContext'); expect(ctx1a).toBe(ctx1b); // Request 2 const request2Scope = container.createScope(); const ctx2 = request2Scope.resolve('RequestContext'); expect(ctx1a).not.toBe(ctx2); expect(ctx1a.requestId).not.toBe(ctx2.requestId); }); test('should handle dependency chain', () => { class Repository { constructor(db) { this.db = db; } } class Service { constructor(repo) { this.repo = repo; } } const mockDb = { id: 1, connected: true }; container.register('Database', () => mockDb, 'singleton'); container.register('Repository', () => { return new Repository(container.resolve('Database')); }, 'singleton'); container.register('Service', () => { return new Service(container.resolve('Repository')); }, 'singleton'); const service = container.resolve('Service'); expect(service.repo).toBeDefined(); expect(service.repo.db).toBe(mockDb); }); }); describe('mixed lifecycle scenarios', () => { test('should handle mixed singleton and transient', () => { container.register('Config', () => ({ port: 3000 }), 'singleton'); container.register('Handler', () => ({ id: Math.random(), config: container.resolve('Config') }), 'transient'); const handler1 = container.resolve('Handler'); const handler2 = container.resolve('Handler'); // Different handlers expect(handler1).not.toBe(handler2); expect(handler1.id).not.toBe(handler2.id); // But same config expect(handler1.config).toBe(handler2.config); }); test('should handle mixed singleton and scoped', () => { container.register('Database', () => ({ id: 'db' }), 'singleton'); container.register('RequestData', () => ({ id: Math.random() }), 'scoped'); const scope1 = container.createScope(); const scope2 = container.createScope(); const data1a = scope1.resolve('RequestData'); const data1b = scope1.resolve('RequestData'); const data2 = scope2.resolve('RequestData'); expect(data1a).toBe(data1b); // Same within scope expect(data1a).not.toBe(data2); // Different across scopes }); }); });