import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test'; import type { CacheProvider } from '@stock-bot/cache'; import type { Logger } from '@stock-bot/logger'; import type { Queue, QueueManager } from '@stock-bot/queue'; import type { ExecutionContext, IServiceContainer, ServiceTypes } from '@stock-bot/types'; import { BaseHandler } from '../src/base/BaseHandler'; import { Handler, Operation, QueueSchedule, ScheduledOperation, } from '../src/decorators/decorators'; import { createJobHandler } from '../src/utils/create-job-handler'; 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 MockQueue = { add: Mock<(name: string, data: any, options?: any) => Promise>; }; type MockQueueManager = { getQueue: Mock<(name: string) => MockQueue>; }; type MockCache = { get: Mock<(key: string) => Promise>; set: Mock<(key: string, value: any, ttl?: number) => Promise>; del: Mock<(key: string) => Promise>; }; // Mock service container const createMockServices = (): IServiceContainer => { const mockLogger: MockLogger = { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), }; const mockQueue: MockQueue = { add: mock(() => Promise.resolve()), }; const mockQueueManager: MockQueueManager = { getQueue: mock(() => mockQueue), }; return { logger: mockLogger as unknown as ServiceTypes['logger'], cache: null, globalCache: null, queueManager: mockQueueManager as unknown as ServiceTypes['queueManager'], proxy: null, browser: null, mongodb: null, postgres: null, questdb: null, queue: mockQueue as unknown as ServiceTypes['queue'], }; }; 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: unknown) { 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: MockQueue = { add: mock(() => Promise.resolve()), }; const mockQueueManager: MockQueueManager = { getQueue: mock(() => mockQueue), }; mockServices.queueManager = mockQueueManager as unknown as ServiceTypes['queueManager']; const handler = new BaseHandler(mockServices, 'test-handler'); await handler.scheduleOperation('test-op', { data: 'test' }, { delay: 1000 }); expect(mockQueueManager.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: MockCache = { set: mock(() => Promise.resolve()), get: mock(() => Promise.resolve('cached-value')), del: mock(() => Promise.resolve()), }; mockServices.cache = mockCache as unknown as ServiceTypes['cache']; 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 {} const decoratedClass = TestClass as typeof TestClass & { __handlerName: string }; expect(decoratedClass.__handlerName).toBe('test-handler'); }); it('should apply Operation decorator', () => { class TestClass { @Operation('my-operation') myMethod() {} } const decoratedClass = TestClass as typeof TestClass & { __operations: Array<{ name: string; method: string }>; }; const operations = decoratedClass.__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 decoratedClass = TestClass as typeof TestClass & { __schedules: Array<{ operation: string; cronPattern: string; priority: number; payload: any; batch: { size: number; delayInHours: number }; }>; }; const schedules = decoratedClass.__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 decoratedClass = TestClass as typeof TestClass & { __schedules: Array<{ operation: string; cronPattern: string; priority: number; }>; }; const schedules = decoratedClass.__schedules; expect(schedules).toBeDefined(); expect(schedules[0]).toMatchObject({ operation: 'queueMethod', cronPattern: '15 * * * *', priority: 3, }); }); }); describe('createJobHandler', () => { it('should create a job handler', async () => { type TestPayload = { data: string }; type TestResult = { success: boolean; payload: TestPayload }; const handlerFn = mock( async (payload: TestPayload): Promise => ({ 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'); }); });