import { beforeEach, describe, expect, it, mock } from 'bun:test'; import type { ExecutionContext, IServiceContainer } from '@stock-bot/types'; import { BaseHandler } from './base/BaseHandler'; import { Handler, Operation, QueueSchedule, ScheduledOperation } from './decorators/decorators'; import { createJobHandler } from './utils/create-job-handler'; // Mock service container const createMockServices = (): IServiceContainer => ({ logger: { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), } as any, cache: null, globalCache: null, queueManager: { getQueue: mock(() => ({ add: mock(() => Promise.resolve()), })), } as any, proxy: null, browser: null, mongodb: null, postgres: null, questdb: null, }); describe('BaseHandler', () => { let mockServices: IServiceContainer; beforeEach(() => { mockServices = createMockServices(); }); it('should initialize with services', () => { const handler = new BaseHandler(mockServices, 'test-handler'); expect(handler).toBeDefined(); expect(handler.logger).toBeDefined(); }); it('should execute operations', async () => { @Handler('test') class TestHandler extends BaseHandler { @Operation('testOp') async handleTestOp(payload: any) { return { result: 'success', payload }; } } const handler = new TestHandler(mockServices); const context: ExecutionContext = { type: 'queue', metadata: { source: 'test' }, }; const result = await handler.execute('testOp', { data: 'test' }, context); expect(result).toEqual({ result: 'success', payload: { data: 'test' } }); }); it('should throw for unknown operation', async () => { @Handler('test') class TestHandler extends BaseHandler {} const handler = new TestHandler(mockServices); const context: ExecutionContext = { type: 'queue', metadata: {}, }; await expect(handler.execute('unknown', {}, context)).rejects.toThrow( 'Unknown operation: unknown' ); }); it('should schedule operations', async () => { const mockQueue = { add: mock(() => Promise.resolve()), }; mockServices.queueManager = { getQueue: mock(() => mockQueue), } as any; const handler = new BaseHandler(mockServices, 'test-handler'); await handler.scheduleOperation('test-op', { data: 'test' }, { delay: 1000 }); expect(mockServices.queueManager.getQueue).toHaveBeenCalledWith('test-handler'); expect(mockQueue.add).toHaveBeenCalledWith( 'test-op', { handler: 'test-handler', operation: 'test-op', payload: { data: 'test' }, }, { delay: 1000 } ); }); describe('cache helpers', () => { it('should handle cache operations with namespace', async () => { const mockCache = { set: mock(() => Promise.resolve()), get: mock(() => Promise.resolve('cached-value')), del: mock(() => Promise.resolve()), }; mockServices.cache = mockCache as any; const handler = new BaseHandler(mockServices, 'my-handler'); await handler['cacheSet']('key', 'value', 3600); expect(mockCache.set).toHaveBeenCalledWith('my-handler:key', 'value', 3600); const result = await handler['cacheGet']('key'); expect(mockCache.get).toHaveBeenCalledWith('my-handler:key'); expect(result).toBe('cached-value'); await handler['cacheDel']('key'); expect(mockCache.del).toHaveBeenCalledWith('my-handler:key'); }); it('should handle null cache gracefully', async () => { const handler = new BaseHandler(mockServices, 'test'); await expect(handler['cacheSet']('key', 'value')).resolves.toBeUndefined(); await expect(handler['cacheGet']('key')).resolves.toBeNull(); await expect(handler['cacheDel']('key')).resolves.toBeUndefined(); }); }); describe('metadata extraction', () => { it('should extract metadata from decorated class', () => { @Handler('metadata-test') class MetadataHandler extends BaseHandler { @Operation('op1') async operation1() {} @Operation('op2') async operation2() {} @ScheduledOperation('scheduled-op', '* * * * *', { priority: 10 }) async scheduledOp() {} } const metadata = MetadataHandler.extractMetadata(); expect(metadata).toBeDefined(); expect(metadata!.name).toBe('metadata-test'); expect(metadata!.operations).toContain('op1'); expect(metadata!.operations).toContain('op2'); expect(metadata!.operations).toContain('scheduled-op'); expect(metadata!.scheduledJobs).toHaveLength(1); expect(metadata!.scheduledJobs![0]).toMatchObject({ operation: 'scheduled-op', cronPattern: '* * * * *', priority: 10, }); }); }); }); describe('Decorators', () => { it('should apply Handler decorator', () => { @Handler('test-handler') class TestClass {} expect((TestClass as any).__handlerName).toBe('test-handler'); }); it('should apply Operation decorator', () => { class TestClass { @Operation('my-operation') myMethod() {} } const operations = (TestClass as any).__operations; expect(operations).toBeDefined(); expect(operations).toHaveLength(1); expect(operations[0]).toMatchObject({ name: 'my-operation', method: 'myMethod', }); }); it('should apply ScheduledOperation decorator with options', () => { class TestClass { @ScheduledOperation('scheduled-task', '0 * * * *', { priority: 8, payload: { action: 'test' }, batch: { size: 100, delayInHours: 1 }, }) scheduledMethod() {} } const schedules = (TestClass as any).__schedules; expect(schedules).toBeDefined(); expect(schedules).toHaveLength(1); expect(schedules[0]).toMatchObject({ operation: 'scheduledMethod', cronPattern: '0 * * * *', priority: 8, payload: { action: 'test' }, batch: { size: 100, delayInHours: 1 }, }); }); it('should apply QueueSchedule decorator', () => { class TestClass { @QueueSchedule('15 * * * *', { priority: 3 }) queueMethod() {} } const schedules = (TestClass as any).__schedules; expect(schedules).toBeDefined(); expect(schedules[0]).toMatchObject({ operation: 'queueMethod', cronPattern: '15 * * * *', priority: 3, }); }); }); describe('createJobHandler', () => { it('should create a job handler', async () => { const handlerFn = mock(async (payload: any) => ({ success: true, payload })); const jobHandler = createJobHandler(handlerFn); const result = await jobHandler({ data: 'test' }); expect(handlerFn).toHaveBeenCalledWith({ data: 'test' }); expect(result).toEqual({ success: true, payload: { data: 'test' } }); }); it('should handle errors in job handler', async () => { const handlerFn = mock(async () => { throw new Error('Handler error'); }); const jobHandler = createJobHandler(handlerFn); await expect(jobHandler({})).rejects.toThrow('Handler error'); }); });