import { beforeEach, describe, expect, it, mock } from 'bun:test'; import { HandlerRegistry } from '../src/registry'; import type { HandlerConfiguration, HandlerMetadata, OperationMetadata, ScheduleMetadata, } from '../src/types'; import type { JobHandler, ScheduledJob } from '@stock-bot/types'; describe('HandlerRegistry Edge Cases', () => { let registry: HandlerRegistry; beforeEach(() => { registry = new HandlerRegistry(); }); describe('Metadata Edge Cases', () => { it('should handle metadata without service', () => { const metadata: HandlerMetadata = { name: 'NoServiceHandler', operations: [], }; registry.registerMetadata(metadata); expect(registry.getMetadata('NoServiceHandler')).toEqual(metadata); expect(registry.getHandlerService('NoServiceHandler')).toBeUndefined(); }); it('should handle metadata with optional fields', () => { const metadata: HandlerMetadata = { name: 'FullHandler', service: 'test-service', operations: [ { name: 'op1', method: 'method1', description: 'Operation 1', }, ], schedules: [ { operation: 'op1', cronPattern: '*/5 * * * *', priority: 10, immediately: true, description: 'Every 5 minutes', }, ], version: '1.0.0', description: 'Full handler with all fields', }; registry.registerMetadata(metadata); const retrieved = registry.getMetadata('FullHandler'); expect(retrieved).toEqual(metadata); expect(retrieved?.version).toBe('1.0.0'); expect(retrieved?.description).toBe('Full handler with all fields'); expect(retrieved?.schedules?.[0].immediately).toBe(true); }); it('should handle empty operations array', () => { const metadata: HandlerMetadata = { name: 'EmptyHandler', operations: [], }; registry.registerMetadata(metadata); const stats = registry.getStats(); expect(stats.handlers).toBe(1); expect(stats.operations).toBe(0); }); }); describe('Configuration Edge Cases', () => { it('should handle configuration without scheduled jobs', () => { const config: HandlerConfiguration = { name: 'SimpleHandler', operations: { process: mock(async () => {}) as JobHandler, }, }; registry.registerConfiguration(config); const scheduledJobs = registry.getScheduledJobs('SimpleHandler'); expect(scheduledJobs).toEqual([]); }); it('should handle empty operations object', () => { const config: HandlerConfiguration = { name: 'EmptyOpsHandler', operations: {}, }; registry.registerConfiguration(config); expect(registry.getOperation('EmptyOpsHandler', 'nonexistent')).toBeUndefined(); }); it('should handle configuration with empty scheduled jobs array', () => { const config: HandlerConfiguration = { name: 'NoScheduleHandler', operations: {}, scheduledJobs: [], }; registry.registerConfiguration(config); const scheduled = registry.getScheduledJobs('NoScheduleHandler'); expect(scheduled).toEqual([]); }); }); describe('Service Management Edge Cases', () => { it('should update metadata when setting handler service', () => { const metadata: HandlerMetadata = { name: 'UpdateableHandler', operations: [], service: 'old-service', }; registry.registerMetadata(metadata); registry.setHandlerService('UpdateableHandler', 'new-service'); const updated = registry.getMetadata('UpdateableHandler'); expect(updated?.service).toBe('new-service'); expect(registry.getHandlerService('UpdateableHandler')).toBe('new-service'); }); it('should set service for non-existent handler', () => { registry.setHandlerService('NonExistentHandler', 'some-service'); expect(registry.getHandlerService('NonExistentHandler')).toBe('some-service'); expect(registry.getMetadata('NonExistentHandler')).toBeUndefined(); }); it('should return empty array for service with no handlers', () => { const handlers = registry.getServiceHandlers('non-existent-service'); expect(handlers).toEqual([]); }); it('should handle multiple handlers for same service', () => { const metadata1: HandlerMetadata = { name: 'Handler1', service: 'shared-service', operations: [], }; const metadata2: HandlerMetadata = { name: 'Handler2', service: 'shared-service', operations: [], }; const metadata3: HandlerMetadata = { name: 'Handler3', service: 'other-service', operations: [], }; registry.registerMetadata(metadata1); registry.registerMetadata(metadata2); registry.registerMetadata(metadata3); const sharedHandlers = registry.getServiceHandlers('shared-service'); expect(sharedHandlers).toHaveLength(2); expect(sharedHandlers.map(h => h.name).sort()).toEqual(['Handler1', 'Handler2']); }); }); describe('Operation Access Edge Cases', () => { it('should return undefined for non-existent handler operation', () => { const op = registry.getOperation('NonExistent', 'operation'); expect(op).toBeUndefined(); }); it('should return undefined for non-existent operation name', () => { const config: HandlerConfiguration = { name: 'TestHandler', operations: { exists: mock(async () => {}) as JobHandler, }, }; registry.registerConfiguration(config); const op = registry.getOperation('TestHandler', 'notexists'); expect(op).toBeUndefined(); }); }); describe('getAllHandlersWithSchedule Edge Cases', () => { it('should handle mix of handlers with and without schedules', () => { const metadata1: HandlerMetadata = { name: 'WithSchedule', operations: [], }; const config1: HandlerConfiguration = { name: 'WithSchedule', operations: {}, scheduledJobs: [ { name: 'job1', handler: mock(async () => {}) as JobHandler, pattern: '* * * * *', } as ScheduledJob, ], }; const metadata2: HandlerMetadata = { name: 'WithoutSchedule', operations: [], }; const config2: HandlerConfiguration = { name: 'WithoutSchedule', operations: {}, }; registry.register(metadata1, config1); registry.register(metadata2, config2); const allWithSchedule = registry.getAllHandlersWithSchedule(); expect(allWithSchedule.size).toBe(2); const withSchedule = allWithSchedule.get('WithSchedule'); expect(withSchedule?.scheduledJobs).toHaveLength(1); const withoutSchedule = allWithSchedule.get('WithoutSchedule'); expect(withoutSchedule?.scheduledJobs).toEqual([]); }); it('should handle handler with metadata but no configuration', () => { const metadata: HandlerMetadata = { name: 'MetadataOnly', operations: [], }; registry.registerMetadata(metadata); const allWithSchedule = registry.getAllHandlersWithSchedule(); const handler = allWithSchedule.get('MetadataOnly'); expect(handler?.metadata).toEqual(metadata); expect(handler?.scheduledJobs).toEqual([]); }); }); describe('Import/Export Edge Cases', () => { it('should handle empty export', () => { const exported = registry.export(); expect(exported.handlers).toEqual([]); expect(exported.configurations).toEqual([]); expect(exported.services).toEqual([]); }); it('should handle empty import', () => { // Add some data first registry.registerMetadata({ name: 'ExistingHandler', operations: [], }); // Import empty data registry.import({ handlers: [], configurations: [], services: [], }); expect(registry.getHandlerNames()).toEqual([]); }); it('should preserve complex data through export/import cycle', () => { const metadata: HandlerMetadata = { name: 'ComplexHandler', service: 'complex-service', operations: [ { name: 'op1', method: 'method1' }, { name: 'op2', method: 'method2' }, ], schedules: [ { operation: 'op1', cronPattern: '0 * * * *', }, ], }; const handler = mock(async () => {}) as JobHandler; const config: HandlerConfiguration = { name: 'ComplexHandler', operations: { op1: handler, op2: handler, }, scheduledJobs: [ { name: 'scheduled1', handler, pattern: '0 * * * *', } as ScheduledJob, ], }; registry.register(metadata, config); registry.setHandlerService('ComplexHandler', 'overridden-service'); const exported = registry.export(); // Create new registry and import const newRegistry = new HandlerRegistry(); newRegistry.import(exported); expect(newRegistry.getMetadata('ComplexHandler')).toEqual(metadata); expect(newRegistry.getConfiguration('ComplexHandler')).toEqual(config); expect(newRegistry.getHandlerService('ComplexHandler')).toBe('overridden-service'); }); }); describe('Statistics Edge Cases', () => { it('should count schedules from metadata', () => { const metadata: HandlerMetadata = { name: 'ScheduledHandler', operations: [ { name: 'op1', method: 'method1' }, ], schedules: [ { operation: 'op1', cronPattern: '* * * * *' }, { operation: 'op1', cronPattern: '0 * * * *' }, ], }; registry.registerMetadata(metadata); const stats = registry.getStats(); expect(stats.handlers).toBe(1); expect(stats.operations).toBe(1); expect(stats.scheduledJobs).toBe(2); expect(stats.services).toBe(0); // No service specified }); it('should not double count services', () => { registry.registerMetadata({ name: 'Handler1', service: 'service1', operations: [], }); registry.registerMetadata({ name: 'Handler2', service: 'service1', // Same service operations: [], }); registry.registerMetadata({ name: 'Handler3', service: 'service2', operations: [], }); const stats = registry.getStats(); expect(stats.services).toBe(2); // Only 2 unique services }); }); describe('Error Scenarios', () => { it('should handle undefined values gracefully', () => { expect(registry.getMetadata(undefined as any)).toBeUndefined(); expect(registry.getConfiguration(undefined as any)).toBeUndefined(); expect(registry.getOperation(undefined as any, 'op')).toBeUndefined(); expect(registry.hasHandler(undefined as any)).toBe(false); }); it('should handle null service lookup', () => { const handlers = registry.getServiceHandlers(null as any); expect(handlers).toEqual([]); }); }); });