import { describe, expect, it, beforeEach, mock } from 'bun:test'; import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler'; import type { IServiceContainer, ExecutionContext } from '@stock-bot/types'; describe('BaseHandler', () => { let mockServices: IServiceContainer; let mockContext: ExecutionContext; beforeEach(() => { const mockQueue = { add: mock(async () => ({ id: 'job-456' })), getName: mock(() => 'test-queue'), }; mockServices = { cache: { 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', } as any, globalCache: { 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', } as any, queueManager: { getQueue: mock(() => mockQueue), createQueue: mock(() => mockQueue), hasQueue: mock(() => true), sendToQueue: mock(async () => 'job-123'), } as any, proxy: { getProxy: mock(() => ({ host: 'proxy.example.com', port: 8080 })), } as any, browser: { scrape: mock(async () => ({ data: 'scraped' })), } as any, mongodb: { db: mock(() => ({ collection: mock(() => ({ find: mock(() => ({ toArray: mock(async () => []) })), insertOne: mock(async () => ({ insertedId: 'id-123' })), })), })), } as any, postgres: { query: mock(async () => ({ rows: [] })), } as any, questdb: null, logger: { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), } as any, queue: mockQueue as any, }; mockContext = { logger: mockServices.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]), }, } as any; // 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 any).mockClear(); } }); } }); }); class TestHandler extends BaseHandler { constructor() { super(mockServices, 'TestHandler'); } async testOperation(data: any) { 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(mockServices.cache.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(handler.queue.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.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 generate handler-specific cache keys', async () => { const handler = new TestHandler(); const key = await handler.cacheKey(mockContext, 'user', '123'); expect(key).toMatch(/TestHandler:user:123$/); }); it('should cache handler results', async () => { const handler = new TestHandler(); const operation = mock(async () => ({ result: 'data' })); // First call - executes operation const result1 = await handler.cacheHandler( mockContext, 'test-cache', operation, { ttl: 300 } ); expect(result1).toEqual({ result: 'data' }); expect(operation).toHaveBeenCalledTimes(1); expect(mockServices.cache.set).toHaveBeenCalled(); // Mock cache hit mockServices.cache.get = mock(async () => ({ result: 'data' })); // Second call - returns cached result const result2 = await handler.cacheHandler( mockContext, 'test-cache', operation, { ttl: 300 } ); expect(result2).toEqual({ result: 'data' }); expect(operation).toHaveBeenCalledTimes(1); // Not called again }); }); describe('scheduling', () => { it('should schedule operations', async () => { const handler = new TestHandler(); const jobId = await handler.scheduleOperation( mockContext, 'processData', { data: 'test' }, { delay: 5000 } ); expect(jobId).toBe('job-123'); expect(mockServices.queueManager.sendToQueue).toHaveBeenCalled(); }); }); describe('HTTP client', () => { it('should provide axios instance', () => { const handler = new TestHandler(); const http = handler.http(mockContext); expect(http).toBeDefined(); expect(http.get).toBeDefined(); expect(http.post).toBeDefined(); }); }); describe('handler metadata', () => { it('should extract handler metadata', () => { const handler = new TestHandler(); const metadata = handler.getHandlerMetadata(); expect(metadata).toBeDefined(); expect(metadata.name).toBe('TestHandler'); }); }); describe('lifecycle hooks', () => { class LifecycleHandler extends BaseHandler { onInitCalled = false; onStartCalled = false; onStopCalled = false; onDisposeCalled = false; constructor() { super(mockServices, 'LifecycleHandler'); } async onInit() { this.onInitCalled = true; } async onStart() { this.onStartCalled = true; } async onStop() { this.onStopCalled = true; } async onDispose() { 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 mockServices: IServiceContainer = { cache: { type: 'memory' } as any, globalCache: { type: 'memory' } as any, queueManager: { getQueue: () => null } as any, proxy: null, browser: null, mongodb: null, postgres: null, questdb: null, logger: null as any, queue: null as any, }; 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() { return { processed: 'daily' }; } async processHourlyData() { 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); }); });