import { describe, it, expect, beforeEach, mock } from 'bun:test'; import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler'; import type { IServiceContainer, ExecutionContext } from '@stock-bot/types'; // Test handler implementation class TestHandler extends BaseHandler { testMethod(input: any, context: ExecutionContext) { return { result: 'test', input, context }; } async onInit() { // Lifecycle hook } protected getScheduledJobPayload(operation: string) { return { scheduled: true, operation }; } } // Handler with no operations class EmptyHandler extends BaseHandler {} // Handler with missing method class BrokenHandler extends BaseHandler { constructor(services: IServiceContainer) { super(services); const ctor = this.constructor as any; ctor.__operations = [{ name: 'missing', method: 'nonExistentMethod' }]; } } describe('BaseHandler Edge Cases', () => { let mockServices: IServiceContainer; beforeEach(() => { mockServices = { cache: { get: mock(async () => null), set: mock(async () => {}), del: mock(async () => {}), has: mock(async () => false), clear: mock(async () => {}), keys: mock(async () => []), mget: mock(async () => []), mset: mock(async () => {}), mdel: mock(async () => {}), ttl: mock(async () => -1), expire: mock(async () => true), getClientType: () => 'redis', isConnected: () => true, }, globalCache: null, queueManager: { getQueue: mock(() => ({ add: mock(async () => ({})), addBulk: mock(async () => []), pause: mock(async () => {}), resume: mock(async () => {}), clean: mock(async () => []), drain: mock(async () => {}), obliterate: mock(async () => {}), close: mock(async () => {}), isReady: mock(async () => true), isClosed: () => false, name: 'test-queue', })), }, proxy: null, browser: null, mongodb: null, postgres: null, questdb: null, } as any; }); describe('Constructor Edge Cases', () => { it('should handle handler without decorator metadata', () => { const handler = new TestHandler(mockServices); expect(handler).toBeInstanceOf(BaseHandler); }); it('should use provided handler name', () => { const handler = new TestHandler(mockServices, 'custom-handler'); expect(handler).toBeInstanceOf(BaseHandler); }); it('should handle null queue manager', () => { const servicesWithoutQueue = { ...mockServices, queueManager: null }; const handler = new TestHandler(servicesWithoutQueue); expect(handler.queue).toBeUndefined(); }); }); describe('Execute Method Edge Cases', () => { it('should throw for unknown operation', async () => { const handler = new TestHandler(mockServices); const context: ExecutionContext = { type: 'queue', metadata: {} }; await expect(handler.execute('unknownOp', {}, context)).rejects.toThrow('Unknown operation: unknownOp'); }); it('should handle operation with no operations metadata', async () => { const handler = new EmptyHandler(mockServices); const context: ExecutionContext = { type: 'queue', metadata: {} }; await expect(handler.execute('anyOp', {}, context)).rejects.toThrow('Unknown operation: anyOp'); }); it('should throw when method is not a function', async () => { const handler = new BrokenHandler(mockServices); const context: ExecutionContext = { type: 'queue', metadata: {} }; await expect(handler.execute('missing', {}, context)).rejects.toThrow( "Operation method 'nonExistentMethod' not found on handler" ); }); it('should execute operation with proper context', async () => { const handler = new TestHandler(mockServices); const ctor = handler.constructor as any; ctor.__operations = [{ name: 'test', method: 'testMethod' }]; const context: ExecutionContext = { type: 'queue', metadata: { source: 'test' } }; const result = await handler.execute('test', { data: 'test' }, context); expect(result).toEqual({ result: 'test', input: { data: 'test' }, context, }); }); }); describe('Service Helper Methods Edge Cases', () => { it('should handle missing cache service', async () => { const servicesWithoutCache = { ...mockServices, cache: null }; const handler = new TestHandler(servicesWithoutCache); // Should not throw, just return gracefully await handler['cacheSet']('key', 'value'); const value = await handler['cacheGet']('key'); expect(value).toBeNull(); await handler['cacheDel']('key'); }); it('should handle missing global cache service', async () => { const handler = new TestHandler(mockServices); // globalCache is already null await handler['globalCacheSet']('key', 'value'); const value = await handler['globalCacheGet']('key'); expect(value).toBeNull(); await handler['globalCacheDel']('key'); }); it('should handle missing MongoDB service', () => { const handler = new TestHandler(mockServices); expect(() => handler['collection']('test')).toThrow('MongoDB service is not available'); }); it('should schedule operation without queue', async () => { const servicesWithoutQueue = { ...mockServices, queueManager: null }; const handler = new TestHandler(servicesWithoutQueue); await expect(handler.scheduleOperation('test', {})).rejects.toThrow( 'Queue service is not available for this handler' ); }); }); describe('Execution Context Creation', () => { it('should create execution context with metadata', () => { const handler = new TestHandler(mockServices); const context = handler['createExecutionContext']('http', { custom: 'data' }); expect(context.type).toBe('http'); expect(context.metadata.custom).toBe('data'); expect(context.metadata.timestamp).toBeDefined(); expect(context.metadata.traceId).toBeDefined(); expect(context.metadata.traceId).toContain('TestHandler'); }); it('should create execution context without metadata', () => { const handler = new TestHandler(mockServices); const context = handler['createExecutionContext']('queue'); expect(context.type).toBe('queue'); expect(context.metadata.timestamp).toBeDefined(); expect(context.metadata.traceId).toBeDefined(); }); }); describe('HTTP Helper Edge Cases', () => { it('should provide HTTP methods', () => { const handler = new TestHandler(mockServices); const http = handler['http']; expect(http.get).toBeDefined(); expect(http.post).toBeDefined(); expect(http.put).toBeDefined(); expect(http.delete).toBeDefined(); // All should be functions expect(typeof http.get).toBe('function'); expect(typeof http.post).toBe('function'); expect(typeof http.put).toBe('function'); expect(typeof http.delete).toBe('function'); }); }); describe('Static Methods Edge Cases', () => { it('should return null for handler without metadata', () => { const metadata = TestHandler.extractMetadata(); expect(metadata).toBeNull(); }); it('should extract metadata with all fields', () => { const HandlerWithMeta = class extends BaseHandler { static __handlerName = 'meta-handler'; static __operations = [ { name: 'op1', method: 'method1' }, { name: 'op2', method: 'method2' }, ]; static __schedules = [ { operation: 'method1', cronPattern: '* * * * *', priority: 10, immediately: true, description: 'Test schedule', payload: { test: true }, batch: { size: 10 }, }, ]; static __description = 'Test handler description'; }; const metadata = HandlerWithMeta.extractMetadata(); expect(metadata).toBeDefined(); expect(metadata?.name).toBe('meta-handler'); expect(metadata?.operations).toEqual(['op1', 'op2']); expect(metadata?.description).toBe('Test handler description'); expect(metadata?.scheduledJobs).toHaveLength(1); const job = metadata?.scheduledJobs[0]; expect(job?.type).toBe('meta-handler-method1'); expect(job?.operation).toBe('op1'); expect(job?.cronPattern).toBe('* * * * *'); expect(job?.priority).toBe(10); expect(job?.immediately).toBe(true); expect(job?.payload).toEqual({ test: true }); expect(job?.batch).toEqual({ size: 10 }); }); }); describe('Handler Configuration Creation', () => { it('should throw when no metadata found', () => { const handler = new TestHandler(mockServices); expect(() => handler.createHandlerConfig()).toThrow('Handler metadata not found'); }); it('should create handler config with operations', () => { const HandlerWithMeta = class extends BaseHandler { static __handlerName = 'config-handler'; static __operations = [ { name: 'process', method: 'processData' }, ]; static __schedules = []; }; const handler = new HandlerWithMeta(mockServices); const config = handler.createHandlerConfig(); expect(config.name).toBe('config-handler'); expect(config.operations.process).toBeDefined(); expect(typeof config.operations.process).toBe('function'); expect(config.scheduledJobs).toEqual([]); }); }); describe('Service Availability Check', () => { it('should correctly identify available services', () => { const handler = new TestHandler(mockServices); expect(handler['hasService']('cache')).toBe(true); expect(handler['hasService']('queueManager')).toBe(true); expect(handler['hasService']('globalCache')).toBe(false); expect(handler['hasService']('mongodb')).toBe(false); }); }); describe('Scheduled Handler Edge Cases', () => { it('should be instance of BaseHandler', () => { const handler = new ScheduledHandler(mockServices); expect(handler).toBeInstanceOf(BaseHandler); expect(handler).toBeInstanceOf(ScheduledHandler); }); }); describe('Cache Helpers with Namespacing', () => { it('should create namespaced cache', () => { const handler = new TestHandler(mockServices); const nsCache = handler['createNamespacedCache']('api'); expect(nsCache).toBeDefined(); }); it('should prefix cache keys with handler name', async () => { const TestHandlerWithName = class extends BaseHandler { static __handlerName = 'test-handler'; }; const handler = new TestHandlerWithName(mockServices); await handler['cacheSet']('mykey', 'value', 3600); expect(mockServices.cache?.set).toHaveBeenCalledWith('test-handler:mykey', 'value', 3600); }); }); describe('Schedule Helper Methods', () => { it('should schedule with delay in seconds', async () => { const handler = new TestHandler(mockServices); // The queue is already set in the handler constructor const mockAdd = handler.queue?.add; await handler['scheduleIn']('test-op', { data: 'test' }, 30, { priority: 10 }); expect(mockAdd).toHaveBeenCalledWith( 'test-op', { handler: 'testhandler', operation: 'test-op', payload: { data: 'test' }, }, { delay: 30000, priority: 10 } ); }); }); describe('Logging Helper', () => { it('should log with handler context', () => { const handler = new TestHandler(mockServices); // The log method should exist expect(typeof handler['log']).toBe('function'); // It should be callable without errors expect(() => { handler['log']('info', 'Test message', { extra: 'data' }); }).not.toThrow(); }); }); });