import { asFunction, createContainer, type AwilixContainer } from 'awilix'; import { beforeEach, describe, expect, it, mock, spyOn } from 'bun:test'; import type { HandlerRegistry } from '@stock-bot/handler-registry'; import * as logger from '@stock-bot/logger'; import type { ExecutionContext, IHandler } from '@stock-bot/types'; import { HandlerScanner } from '../src/scanner/handler-scanner'; // Mock handler class class MockHandler implements IHandler { static __handlerName = 'mockHandler'; static __operations = [ { name: 'processData', method: 'processData' }, { name: 'validateData', method: 'validateData' }, ]; static __schedules = [ { operation: 'processData', cronPattern: '0 * * * *', priority: 5, immediately: false, description: 'Process data every hour', payload: { type: 'hourly' }, }, ]; static __disabled = false; constructor(private serviceContainer: any) {} async execute(operation: string, payload: any, context: ExecutionContext): Promise { switch (operation) { case 'processData': return { processed: true, data: payload }; case 'validateData': return { valid: true, data: payload }; default: throw new Error(`Unknown operation: ${operation}`); } } } // Disabled handler for testing class DisabledHandler extends MockHandler { static __handlerName = 'disabledHandler'; static __disabled = true; } // Handler without metadata class InvalidHandler { constructor() {} execute() {} } describe('HandlerScanner', () => { let scanner: HandlerScanner; let mockRegistry: HandlerRegistry; let container: AwilixContainer; let mockLogger: any; beforeEach(() => { // Create mock logger mockLogger = { info: mock(() => {}), debug: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), }; // Mock getLogger to return our mock logger spyOn(logger, 'getLogger').mockReturnValue(mockLogger); // Create mock registry mockRegistry = { register: mock(() => {}), getHandler: mock(() => null), getHandlerMetadata: mock(() => null), getAllHandlers: mock(() => []), clear: mock(() => {}), } as unknown as HandlerRegistry; // Create container container = createContainer(); // Create scanner scanner = new HandlerScanner(mockRegistry, container, { serviceName: 'test-service', autoRegister: true, }); }); describe('scanHandlers', () => { it('should handle empty patterns gracefully', async () => { await scanner.scanHandlers([]); // Should complete without errors expect(mockLogger.info).toHaveBeenCalledWith('Starting handler scan', { patterns: [] }); }); it('should handle file scan errors gracefully', async () => { // We'll test that the scanner handles errors properly // by calling internal methods directly const filePath = '/non-existent-file.ts'; // This should not throw await (scanner as any).scanFile(filePath); expect(mockLogger.error).toHaveBeenCalled(); }); }); describe('registerHandlerClass', () => { it('should register a handler class with registry and container', () => { scanner.registerHandlerClass(MockHandler); // Check registry registration expect(mockRegistry.register).toHaveBeenCalledWith( { name: 'mockHandler', service: 'test-service', operations: [ { name: 'processData', method: 'processData' }, { name: 'validateData', method: 'validateData' }, ], schedules: [ { operation: 'processData', cronPattern: '0 * * * *', priority: 5, immediately: false, description: 'Process data every hour', payload: { type: 'hourly' }, }, ], }, expect.objectContaining({ name: 'mockHandler', operations: expect.any(Object), scheduledJobs: expect.arrayContaining([ expect.objectContaining({ type: 'mockHandler-processData', operation: 'processData', cronPattern: '0 * * * *', priority: 5, immediately: false, description: 'Process data every hour', payload: { type: 'hourly' }, }), ]), }) ); // Check container registration expect(container.hasRegistration('mockHandler')).toBe(true); }); it('should skip disabled handlers', () => { scanner.registerHandlerClass(DisabledHandler); expect(mockRegistry.register).not.toHaveBeenCalled(); expect(container.hasRegistration('disabledHandler')).toBe(false); }); it('should handle handlers without schedules', () => { class NoScheduleHandler extends MockHandler { static __handlerName = 'noScheduleHandler'; static __schedules = []; } scanner.registerHandlerClass(NoScheduleHandler); expect(mockRegistry.register).toHaveBeenCalledWith( expect.objectContaining({ schedules: [], }), expect.objectContaining({ scheduledJobs: [], }) ); }); it('should use custom service name when provided', () => { scanner.registerHandlerClass(MockHandler, { serviceName: 'custom-service' }); expect(mockRegistry.register).toHaveBeenCalledWith( expect.objectContaining({ service: 'custom-service', }), expect.any(Object) ); }); it('should not register with container when autoRegister is false', () => { scanner = new HandlerScanner(mockRegistry, container, { serviceName: 'test-service', autoRegister: false, }); scanner.registerHandlerClass(MockHandler); expect(mockRegistry.register).toHaveBeenCalled(); expect(container.hasRegistration('mockHandler')).toBe(false); }); }); describe('handler validation', () => { it('should identify valid handlers', () => { const isHandler = (scanner as any).isHandler; expect(isHandler(MockHandler)).toBe(true); expect(isHandler(InvalidHandler)).toBe(false); expect(isHandler({})).toBe(false); expect(isHandler('not a function')).toBe(false); expect(isHandler(null)).toBe(false); }); it('should handle handlers with batch configuration', () => { class BatchHandler extends MockHandler { static __handlerName = 'batchHandler'; static __schedules = [ { operation: 'processBatch', cronPattern: '*/5 * * * *', priority: 10, batch: { size: 100, window: 60000, }, }, ]; } scanner.registerHandlerClass(BatchHandler); expect(mockRegistry.register).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ scheduledJobs: expect.arrayContaining([ expect.objectContaining({ batch: { size: 100, window: 60000, }, }), ]), }) ); }); }); describe('getDiscoveredHandlers', () => { it('should return all discovered handlers', () => { scanner.registerHandlerClass(MockHandler); const discovered = scanner.getDiscoveredHandlers(); expect(discovered.size).toBe(1); expect(discovered.get('mockHandler')).toBe(MockHandler); }); it('should return a copy of the map', () => { scanner.registerHandlerClass(MockHandler); const discovered1 = scanner.getDiscoveredHandlers(); const discovered2 = scanner.getDiscoveredHandlers(); expect(discovered1).not.toBe(discovered2); expect(discovered1.get('mockHandler')).toBe(discovered2.get('mockHandler')); }); }); describe('operation handler creation', () => { it('should create job handlers for operations', () => { scanner.registerHandlerClass(MockHandler); const registrationCall = (mockRegistry.register as any).mock.calls[0]; const configuration = registrationCall[1]; expect(configuration.operations).toHaveProperty('processData'); expect(configuration.operations).toHaveProperty('validateData'); expect(typeof configuration.operations.processData).toBe('function'); }); it('should resolve handler from container when executing operations', async () => { // Register handler with container container.register({ serviceContainer: asFunction(() => ({})).singleton(), }); scanner.registerHandlerClass(MockHandler); // Create handler instance const handlerInstance = container.resolve('mockHandler'); // Test execution const context: ExecutionContext = { type: 'queue', metadata: { source: 'test', timestamp: Date.now() }, }; const result = await handlerInstance.execute('processData', { test: true }, context); expect(result).toEqual({ processed: true, data: { test: true } }); }); }); describe('module scanning', () => { it('should handle modules with multiple exports', () => { const mockModule = { Handler1: MockHandler, Handler2: class SecondHandler extends MockHandler { static __handlerName = 'secondHandler'; }, notAHandler: { some: 'object' }, helperFunction: () => {}, }; (scanner as any).registerHandlersFromModule(mockModule, 'test.ts'); expect(mockRegistry.register).toHaveBeenCalledTimes(2); expect(mockRegistry.register).toHaveBeenCalledWith( expect.objectContaining({ name: 'mockHandler' }), expect.any(Object) ); expect(mockRegistry.register).toHaveBeenCalledWith( expect.objectContaining({ name: 'secondHandler' }), expect.any(Object) ); }); it('should handle empty modules', () => { const mockModule = {}; (scanner as any).registerHandlersFromModule(mockModule, 'empty.ts'); expect(mockRegistry.register).not.toHaveBeenCalled(); }); }); });