import { asClass, asFunction, asValue, createContainer } from 'awilix'; import { describe, expect, it, mock } from 'bun:test'; import { registerApplicationServices, registerCacheServices, registerDatabaseServices, } from '../src/registrations'; // Mock the queue module mock.module('@stock-bot/queue', () => ({ createServiceCache: mock(() => ({ get: mock(() => Promise.resolve(null)), set: mock(() => Promise.resolve()), del: mock(() => Promise.resolve()), })), })); describe('DI Registrations', () => { describe('registerCacheServices', () => { it('should register null cache when redis disabled', () => { const container = createContainer(); const config = { service: { name: 'test-service', type: 'WORKER' as const, }, redis: { enabled: false, host: 'localhost', port: 6379, }, } as any; registerCacheServices(container, config); const cache = container.resolve('cache'); expect(cache).toBeNull(); }); it('should register redis cache when redis config exists', () => { const container = createContainer(); // Register logger first as it's a dependency container.register({ logger: asValue({ info: () => {}, error: () => {}, warn: () => {}, debug: () => {}, }), }); const config = { service: { name: 'test-service', type: 'WORKER' as const, }, redis: { enabled: true, host: 'localhost', port: 6379, db: 1, }, } as any; registerCacheServices(container, config); const cache = container.resolve('cache'); expect(cache).toBeDefined(); }); it('should register both cache and globalCache', () => { const container = createContainer(); // Register logger dependency container.register({ logger: asValue({ info: () => {}, error: () => {}, warn: () => {}, debug: () => {}, }), }); const config = { service: { name: 'test-service', serviceName: 'test-service', }, redis: { enabled: true, host: 'localhost', port: 6379, db: 1, }, } as any; registerCacheServices(container, config); const cache = container.resolve('cache'); const globalCache = container.resolve('globalCache'); expect(cache).toBeDefined(); expect(globalCache).toBeDefined(); }); }); describe('registerDatabaseServices', () => { it('should register MongoDB when config exists', () => { const container = createContainer(); // Mock MongoDB client const mockMongoClient = { connect: mock(() => Promise.resolve()), disconnect: mock(() => Promise.resolve()), getDb: mock(() => ({})), }; // Mock the MongoDB factory mock.module('@stock-bot/mongodb', () => ({ MongoDBClient: class { constructor() { return mockMongoClient; } }, })); const config = { mongodb: { enabled: true, uri: 'mongodb://localhost', database: 'test-db', }, } as any; registerDatabaseServices(container, config); expect(container.hasRegistration('mongoClient')).toBe(true); }); it('should register PostgreSQL when config exists', () => { const container = createContainer(); // Mock Postgres client const mockPostgresClient = { connect: mock(() => Promise.resolve()), disconnect: mock(() => Promise.resolve()), query: mock(() => Promise.resolve({ rows: [] })), }; // Mock the Postgres factory mock.module('@stock-bot/postgres', () => ({ PostgresClient: class { constructor() { return mockPostgresClient; } }, })); const config = { postgres: { enabled: true, host: 'localhost', port: 5432, user: 'user', password: 'pass', database: 'test-db', }, } as any; registerDatabaseServices(container, config); expect(container.hasRegistration('postgresClient')).toBe(true); }); it('should register QuestDB when config exists', () => { const container = createContainer(); // Mock QuestDB client const mockQuestdbClient = { connect: mock(() => Promise.resolve()), disconnect: mock(() => Promise.resolve()), query: mock(() => Promise.resolve({ data: [] })), }; // Mock the QuestDB factory mock.module('@stock-bot/questdb', () => ({ QuestDBClient: class { constructor() { return mockQuestdbClient; } }, })); const config = { questdb: { enabled: true, host: 'localhost', httpPort: 9000, pgPort: 8812, influxPort: 9009, database: 'questdb', }, } as any; registerDatabaseServices(container, config); expect(container.hasRegistration('questdbClient')).toBe(true); }); it('should not register disabled databases', () => { const container = createContainer(); const config = { mongodb: { enabled: false }, postgres: { enabled: false }, questdb: undefined, } as any; registerDatabaseServices(container, config); // Services are registered but with null values when disabled expect(container.hasRegistration('mongoClient')).toBe(true); expect(container.hasRegistration('postgresClient')).toBe(true); expect(container.hasRegistration('questdbClient')).toBe(true); // Verify they resolve to null expect(container.resolve('mongoClient')).toBeNull(); expect(container.resolve('postgresClient')).toBeNull(); expect(container.resolve('questdbClient')).toBeNull(); }); }); describe('registerApplicationServices', () => { it('should register browser when config exists', () => { const container = createContainer(); // Mock browser factory const mockBrowser = { launch: mock(() => Promise.resolve()), close: mock(() => Promise.resolve()), }; mock.module('@stock-bot/browser', () => ({ createBrowser: () => mockBrowser, })); const config = { browser: { headless: true, timeout: 30000, }, } as any; registerApplicationServices(container, config); expect(container.hasRegistration('browser')).toBe(true); }); it('should register proxy when config exists', () => { const container = createContainer(); // Mock proxy factory const mockProxy = { getProxy: mock(() => 'http://proxy:8080'), }; mock.module('@stock-bot/proxy', () => ({ createProxyManager: () => mockProxy, })); const config = { proxy: { enabled: true, url: 'http://proxy:8080', }, } as any; registerApplicationServices(container, config); expect(container.hasRegistration('proxyManager')).toBe(true); }); it('should register queue manager when queue config exists', () => { const container = createContainer(); // Mock dependencies container.register({ cache: asValue({ get: mock(() => Promise.resolve(null)), set: mock(() => Promise.resolve()), }), handlerRegistry: asValue({ getHandler: mock(() => null), getAllHandlers: mock(() => []), }), logger: asValue({ info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), }), }); // Mock queue manager const mockQueueManager = { getQueue: mock(() => ({})), startAllWorkers: mock(() => {}), shutdown: mock(() => Promise.resolve()), }; mock.module('@stock-bot/queue', () => ({ QueueManager: class { constructor() { return mockQueueManager; } }, })); const config = { service: { name: 'test-service', serviceName: 'test-service', }, queue: { enabled: true, workers: 2, concurrency: 5, }, } as any; registerApplicationServices(container, config); expect(container.hasRegistration('queueManager')).toBe(true); }); it('should not register services when configs are missing', () => { const container = createContainer(); const config = {} as any; registerApplicationServices(container, config); expect(container.hasRegistration('browser')).toBe(true); expect(container.hasRegistration('proxyManager')).toBe(true); expect(container.hasRegistration('queueManager')).toBe(true); // They should be registered as null const browser = container.resolve('browser'); const proxyManager = container.resolve('proxyManager'); const queueManager = container.resolve('queueManager'); expect(browser).toBe(null); expect(proxyManager).toBe(null); expect(queueManager).toBe(null); }); }); describe('dependency resolution', () => { it('should properly resolve cache dependencies', () => { const container = createContainer(); const config = { service: { name: 'test-service', serviceName: 'test-service', }, redis: { enabled: true, host: 'localhost', port: 6379, db: 0, }, } as any; registerCacheServices(container, config); // Should have registered cache expect(container.hasRegistration('cache')).toBe(true); expect(container.hasRegistration('globalCache')).toBe(true); }); it('should handle circular dependencies gracefully', () => { const container = createContainer(); // Register services with potential circular deps container.register({ serviceA: asFunction(({ serviceB }) => ({ b: serviceB })).singleton(), serviceB: asFunction(({ serviceA }) => ({ a: serviceA })).singleton(), }); // This should throw or handle gracefully expect(() => container.resolve('serviceA')).toThrow(); }); }); describe('registration options', () => { it('should register services as singletons', () => { const container = createContainer(); const config = { browser: { headless: true, timeout: 30000, }, } as any; registerApplicationServices(container, config); // Check that browser was registered as singleton const registration = container.getRegistration('browser'); expect(registration).toBeDefined(); expect(registration?.lifetime).toBe('SINGLETON'); }); }); });