import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test'; import type { Collection, Db, MongoClient } from 'mongodb'; import type { Pool, QueryResult } from 'pg'; import type { SimpleBrowser } from '@stock-bot/browser'; import type { CacheProvider } from '@stock-bot/cache'; import type { Logger } from '@stock-bot/logger'; import type { SimpleProxyManager } from '@stock-bot/proxy'; import type { Queue, QueueManager } from '@stock-bot/queue'; import type { ExecutionContext, IServiceContainer, ServiceTypes } from '@stock-bot/types'; import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler'; import { Handler, Operation } from '../src/decorators/decorators'; type MockQueue = { add: Mock<(name: string, data: any) => Promise<{ id: string }>>; getName: Mock<() => string>; }; type MockQueueManager = { getQueue: Mock<(name: string) => MockQueue | null>; createQueue: Mock<(name: string) => MockQueue>; hasQueue: Mock<(name: string) => boolean>; sendToQueue: Mock<(service: string, handler: string, data: any) => Promise>; }; type MockCache = { get: Mock<(key: string) => Promise>; set: Mock<(key: string, value: any, ttl?: number) => Promise>; del: Mock<(key: string) => Promise>; clear: Mock<() => Promise>; has: Mock<(key: string) => Promise>; keys: Mock<(pattern?: string) => Promise>; ttl: Mock<(key: string) => Promise>; type: 'memory'; }; type MockLogger = { info: Mock<(message: string, meta?: any) => void>; error: Mock<(message: string, meta?: any) => void>; warn: Mock<(message: string, meta?: any) => void>; debug: Mock<(message: string, meta?: any) => void>; }; type MockBrowser = { scrape: Mock<(url: string) => Promise<{ data: string }>>; }; type MockProxy = { getProxy: Mock<() => { host: string; port: number }>; }; type MockPostgres = { query: Mock<(text: string, values?: any[]) => Promise>; }; type MockMongoDB = { db: Mock< (name?: string) => { collection: Mock< (name: string) => { find: Mock<(filter: any) => { toArray: Mock<() => Promise> }>; insertOne: Mock<(doc: any) => Promise<{ insertedId: string }>>; } >; } >; }; describe('BaseHandler', () => { let mockServices: IServiceContainer; let mockContext: ExecutionContext; let mockQueue: MockQueue; let mockQueueManager: MockQueueManager; let mockCache: MockCache; let mockLogger: MockLogger; beforeEach(() => { mockQueue = { add: mock(async () => ({ id: 'job-456' })), getName: mock(() => 'test-queue'), }; mockQueueManager = { getQueue: mock(() => mockQueue), createQueue: mock(() => mockQueue), hasQueue: mock(() => true), sendToQueue: mock(async () => 'job-123'), }; mockCache = { get: mock(async () => null), set: mock(async () => {}), del: mock(async () => {}), clear: mock(async () => {}), has: mock(async () => false), keys: mock(async () => []), ttl: mock(async () => -1), type: 'memory', }; mockLogger = { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), }; const mockBrowser: MockBrowser = { scrape: mock(async () => ({ data: 'scraped' })), }; const mockProxy: MockProxy = { getProxy: mock(() => ({ host: 'proxy.example.com', port: 8080 })), }; const mockPostgres: MockPostgres = { query: mock(async () => ({ rows: [], rowCount: 0 }) as QueryResult), }; const mockMongoDB: MockMongoDB = { db: mock(() => ({ collection: mock(() => ({ find: mock(() => ({ toArray: mock(async () => []) })), insertOne: mock(async () => ({ insertedId: 'id-123' })), })), })), }; mockServices = { cache: mockCache as unknown as ServiceTypes['cache'], globalCache: { ...mockCache } as unknown as ServiceTypes['globalCache'], queueManager: mockQueueManager as unknown as ServiceTypes['queueManager'], proxy: mockProxy as unknown as ServiceTypes['proxy'], browser: mockBrowser as unknown as ServiceTypes['browser'], mongodb: mockMongoDB as unknown as ServiceTypes['mongodb'], postgres: mockPostgres as unknown as ServiceTypes['postgres'], questdb: null, logger: mockLogger as unknown as ServiceTypes['logger'], queue: mockQueue as unknown as ServiceTypes['queue'], }; mockContext = { logger: mockLogger as unknown as Logger, traceId: 'trace-123', handlerName: 'TestHandler', operationName: 'testOperation', metadata: {}, startTime: new Date(), container: { resolve: mock((name: string) => mockServices[name as keyof IServiceContainer]), resolveAsync: mock(async (name: string) => mockServices[name as keyof IServiceContainer]), }, }; // Reset all mocks Object.values(mockServices).forEach(service => { if (service && typeof service === 'object') { Object.values(service).forEach(method => { if (typeof method === 'function' && 'mockClear' in method) { (method as unknown as Mock).mockClear(); } }); } }); }); class TestHandler extends BaseHandler { constructor() { super(mockServices, 'TestHandler'); } async testOperation(data: unknown): Promise<{ processed: unknown }> { return { processed: data }; } } describe('service access', () => { it('should provide access to cache service', async () => { const handler = new TestHandler(); await handler.cache.set('key', 'value'); expect(mockCache.set).toHaveBeenCalledWith('key', 'value'); }); it('should have logger initialized', () => { const handler = new TestHandler(); expect(handler.logger).toBeDefined(); // Logger is created by getLogger, not from mockServices }); it('should provide access to queue service', () => { const handler = new TestHandler(); expect(handler.queue).toBeDefined(); expect(mockQueue.getName()).toBe('test-queue'); }); it('should provide access to mongodb', () => { const handler = new TestHandler(); expect(handler.mongodb).toBe(mockServices.mongodb); }); it('should provide access to postgres', async () => { const handler = new TestHandler(); const result = await handler.postgres.query('SELECT 1'); expect(result.rows).toEqual([]); expect(mockServices.postgres.query).toHaveBeenCalledWith('SELECT 1'); }); it('should provide access to browser', async () => { const handler = new TestHandler(); const result = await handler.browser.scrape('https://example.com'); expect(result).toEqual({ data: 'scraped' }); expect((mockServices.browser as unknown as MockBrowser).scrape).toHaveBeenCalledWith( 'https://example.com' ); }); it('should provide access to proxy manager', () => { const handler = new TestHandler(); const proxy = handler.proxy.getProxy(); expect(proxy).toEqual({ host: 'proxy.example.com', port: 8080 }); }); }); describe('caching utilities', () => { it('should set and get cache values with handler namespace', async () => { const handler = new TestHandler(); mockCache.set.mockClear(); mockCache.get.mockClear(); // Test cacheSet await handler['cacheSet']('testKey', 'testValue', 3600); expect(mockCache.set).toHaveBeenCalledWith('TestHandler:testKey', 'testValue', 3600); // Test cacheGet mockCache.get.mockImplementation(async () => 'cachedValue'); const result = await handler['cacheGet']('testKey'); expect(mockCache.get).toHaveBeenCalledWith('TestHandler:testKey'); expect(result).toBe('cachedValue'); }); it('should delete cache values with handler namespace', async () => { const handler = new TestHandler(); mockCache.del.mockClear(); await handler['cacheDel']('testKey'); expect(mockCache.del).toHaveBeenCalledWith('TestHandler:testKey'); }); it('should handle null cache gracefully', async () => { mockServices.cache = null; const handler = new TestHandler(); // Should not throw when cache is null await expect(handler['cacheSet']('key', 'value')).resolves.toBeUndefined(); await expect(handler['cacheGet']('key')).resolves.toBeNull(); await expect(handler['cacheDel']('key')).resolves.toBeUndefined(); }); }); describe('scheduling', () => { it('should schedule operations', async () => { const handler = new TestHandler(); mockQueueManager.hasQueue.mockClear(); mockQueue.add.mockClear(); await handler.scheduleOperation('processData', { data: 'test' }, { delay: 5000 }); expect(mockQueueManager.getQueue).toHaveBeenCalledWith('TestHandler'); expect(mockQueue.add).toHaveBeenCalledWith( 'processData', { handler: 'TestHandler', operation: 'processData', payload: { data: 'test' }, }, { delay: 5000 } ); }); }); describe('HTTP client', () => { it('should provide http methods', () => { const handler = new TestHandler(); const http = handler['http']; expect(http).toBeDefined(); expect(http.get).toBeDefined(); expect(http.post).toBeDefined(); expect(http.put).toBeDefined(); expect(http.delete).toBeDefined(); }); }); describe('handler metadata', () => { it('should extract handler metadata', () => { // For metadata extraction, we need a decorated handler @Handler('MetadataTestHandler') class MetadataTestHandler extends BaseHandler { @Operation('testOp') async handleTestOp() { return { result: 'success' }; } } const metadata = MetadataTestHandler.extractMetadata(); expect(metadata).toBeDefined(); expect(metadata!.name).toBe('MetadataTestHandler'); expect(metadata!.operations).toContain('testOp'); }); }); describe('lifecycle hooks', () => { class LifecycleHandler extends BaseHandler { onInitCalled = false; onStartCalled = false; onStopCalled = false; onDisposeCalled = false; constructor() { super(mockServices, 'LifecycleHandler'); } async onInit(): Promise { this.onInitCalled = true; } async onStart(): Promise { this.onStartCalled = true; } async onStop(): Promise { this.onStopCalled = true; } async onDispose(): Promise { this.onDisposeCalled = true; } } it('should call lifecycle hooks', async () => { const handler = new LifecycleHandler(); await handler.onInit(); expect(handler.onInitCalled).toBe(true); await handler.onStart(); expect(handler.onStartCalled).toBe(true); await handler.onStop(); expect(handler.onStopCalled).toBe(true); await handler.onDispose(); expect(handler.onDisposeCalled).toBe(true); }); }); }); describe('ScheduledHandler', () => { const mockQueue: MockQueue = { add: mock(async () => ({ id: 'job-456' })), getName: mock(() => 'test-queue'), }; const mockServices: IServiceContainer = { cache: { type: 'memory' } as unknown as ServiceTypes['cache'], globalCache: { type: 'memory' } as unknown as ServiceTypes['globalCache'], queueManager: { getQueue: () => mockQueue, } as unknown as ServiceTypes['queueManager'], proxy: null as unknown as ServiceTypes['proxy'], browser: null as unknown as ServiceTypes['browser'], mongodb: null as unknown as ServiceTypes['mongodb'], postgres: null as unknown as ServiceTypes['postgres'], questdb: null, logger: null as unknown as ServiceTypes['logger'], queue: mockQueue as unknown as ServiceTypes['queue'], }; class TestScheduledHandler extends ScheduledHandler { constructor() { super(mockServices, 'TestScheduledHandler'); } getScheduledJobs() { return [ { name: 'dailyJob', schedule: '0 0 * * *', handler: 'processDailyData', }, { name: 'hourlyJob', schedule: '0 * * * *', handler: 'processHourlyData', options: { timezone: 'UTC', }, }, ]; } async processDailyData(): Promise<{ processed: string }> { return { processed: 'daily' }; } async processHourlyData(): Promise<{ processed: string }> { return { processed: 'hourly' }; } } it('should define scheduled jobs', () => { const handler = new TestScheduledHandler(); const jobs = handler.getScheduledJobs(); expect(jobs).toHaveLength(2); expect(jobs[0]).toEqual({ name: 'dailyJob', schedule: '0 0 * * *', handler: 'processDailyData', }); expect(jobs[1]).toEqual({ name: 'hourlyJob', schedule: '0 * * * *', handler: 'processHourlyData', options: { timezone: 'UTC', }, }); }); it('should be a BaseHandler', () => { const handler = new TestScheduledHandler(); expect(handler).toBeInstanceOf(BaseHandler); expect(handler).toBeInstanceOf(ScheduledHandler); }); });