From f6d0afb98a47c3e8ad54b1108f8e092fce9feadd Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Thu, 30 Oct 2025 03:16:04 -0400 Subject: [PATCH] Update bun.lock and remove DependencyManager tests - Added new dependency "@techniker-me/tools" and updated existing dependencies to specific versions in bun.lock - Removed the DependencyManager test file as part of codebase cleanup --- __test__/di/DependencyManager.test.ts | 365 ------------------- __test__/di/WhenManagingDependencies.test.ts | 58 +++ __test__/mocks/MockDatabase.ts | 15 + __test__/mocks/MockLogger.ts | 31 ++ __test__/mocks/MockUserServices.ts | 14 + __test__/mocks/index.ts | 3 + src/Defaults.ts | 11 + 7 files changed, 132 insertions(+), 365 deletions(-) delete mode 100644 __test__/di/DependencyManager.test.ts create mode 100644 __test__/di/WhenManagingDependencies.test.ts create mode 100644 __test__/mocks/MockDatabase.ts create mode 100644 __test__/mocks/MockLogger.ts create mode 100644 __test__/mocks/MockUserServices.ts create mode 100644 __test__/mocks/index.ts create mode 100644 src/Defaults.ts diff --git a/__test__/di/DependencyManager.test.ts b/__test__/di/DependencyManager.test.ts deleted file mode 100644 index d21c142..0000000 --- a/__test__/di/DependencyManager.test.ts +++ /dev/null @@ -1,365 +0,0 @@ -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); - }); - }); -}); diff --git a/__test__/di/WhenManagingDependencies.test.ts b/__test__/di/WhenManagingDependencies.test.ts new file mode 100644 index 0000000..0305940 --- /dev/null +++ b/__test__/di/WhenManagingDependencies.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, beforeEach } from 'bun:test'; +import { DependencyManager } from '../../src/di/DependencyManager'; +import { Logger, Database, UserService } from '../mocks'; + +describe('When Managing Dependencies', () => { + let dependencyManager: DependencyManager; + + beforeEach(() => { + dependencyManager = new DependencyManager(); + }); + + describe('GIVEN classes with no dependencies are registered with the dependency manager', () => { + beforeEach(() => { + dependencyManager.register('Logger', Logger, [], 'transient'); + dependencyManager.register('Database', Database, [], 'transient'); + }); + + it('resolves the class', () => { + const logger = dependencyManager.resolve('Logger'); + + expect(logger).toBeDefined(); + expect(logger).toBeInstanceOf(Logger); + }); + + describe('GIVEN the class is registered a second time', () => { + it('throws an error', () => { + expect(() => dependencyManager.register('Logger', Logger, [])).toThrow('Error: [Logger] is already registered'); + }); + }); + }); + + describe('GIVEN a singleton class is registered with the dependency manager', () => { + beforeEach(() => { + dependencyManager.register('Logger', Logger, [], 'singleton'); + }); + + it('resolves the class', () => { + const logger = dependencyManager.resolve('Logger'); + + expect(logger).toBeDefined(); + expect(logger).toBeInstanceOf(Logger); + }); + + describe('GIVEN a UserService with Logger dependency is registered', () => { + beforeEach(() => { + dependencyManager.register('UserService', UserService, ['Logger'], 'singleton'); + }); + + it('resolves the same instance when requested multiple times', () => { + const userService = dependencyManager.resolve('UserService'); + const userService2 = dependencyManager.resolve('UserService'); + + expect(userService).toBe(userService2); + expect(userService2).toBe(userService); + }); + }); + }); +}); diff --git a/__test__/mocks/MockDatabase.ts b/__test__/mocks/MockDatabase.ts new file mode 100644 index 0000000..a20f306 --- /dev/null +++ b/__test__/mocks/MockDatabase.ts @@ -0,0 +1,15 @@ +import type {Logger} from './MockLogger'; + +export class Database { + private _logger: Logger; + + constructor(logger: Logger) { + this._logger = logger; + } + + public query(sql: string) { + this._logger.debug(`Executing: ${sql}`); + + return 'result'; + } +} diff --git a/__test__/mocks/MockLogger.ts b/__test__/mocks/MockLogger.ts new file mode 100644 index 0000000..bee3591 --- /dev/null +++ b/__test__/mocks/MockLogger.ts @@ -0,0 +1,31 @@ +export class Logger { + private _category: string; + + constructor(category: string) { + this._category = category; + } + + public info(message: string, ...optionalParams: any[]) { + console.info(this.formatMessage(message), ...optionalParams); + } + + public debug(message: string, ...optionalParams: any[]) { + console.debug(this.formatMessage(message), ...optionalParams); + } + + public error(message: string, ...optionalParams: any[]) { + console.error(this.formatMessage(message), ...optionalParams); + } + + public warn(message: string, ...optionalParams: any[]) { + console.warn(this.formatMessage(message), ...optionalParams); + } + + public trace(message: string, ...optionalParams: any[]) { + console.trace(this.formatMessage(message), ...optionalParams); + } + + private formatMessage(message: string) { + return `${new Date().toISOString()} [${this._category}] ${message}`; + } +} diff --git a/__test__/mocks/MockUserServices.ts b/__test__/mocks/MockUserServices.ts new file mode 100644 index 0000000..888efe2 --- /dev/null +++ b/__test__/mocks/MockUserServices.ts @@ -0,0 +1,14 @@ +import type {Logger} from './MockLogger'; +import type {Database} from './MockDatabase'; + +export class UserService { + constructor( + private db: Database, + private logger: Logger + ) {} + + public getUser(id: number) { + this.logger.debug(`Getting user ${id}`); + return this.db.query(`SELECT * FROM users WHERE id = ${id}`); + } +} diff --git a/__test__/mocks/index.ts b/__test__/mocks/index.ts new file mode 100644 index 0000000..e7f0e8c --- /dev/null +++ b/__test__/mocks/index.ts @@ -0,0 +1,3 @@ +export * from './MockDatabase'; +export * from './MockLogger'; +export * from './MockUserServices'; diff --git a/src/Defaults.ts b/src/Defaults.ts new file mode 100644 index 0000000..f72ad75 --- /dev/null +++ b/src/Defaults.ts @@ -0,0 +1,11 @@ +import { LoggingLevel } from './logger/LoggingLevel'; + +export default class Defaults { + static get loggingLevel(): LoggingLevel { + return LoggingLevel.Info; + } + + private constructor() { + throw new Error('Defaults is a static class that may not be instantiated'); + } +}