import { beforeEach, describe, expect, it } from 'bun:test'; import { HandlerRegistry } from '../src/registry'; import type { HandlerConfiguration, HandlerMetadata, OperationMetadata, ScheduleMetadata, } from '../src/types'; describe('HandlerRegistry Comprehensive Tests', () => { let registry: HandlerRegistry; beforeEach(() => { registry = new HandlerRegistry(); }); describe('registerMetadata', () => { it('should register handler metadata separately', () => { const metadata: HandlerMetadata = { name: 'TestHandler', service: 'test-service', operations: [ { name: 'processData', method: 'processData', }, { name: 'batchProcess', method: 'batchProcess', }, ], }; registry.registerMetadata(metadata); const retrieved = registry.getMetadata('TestHandler'); expect(retrieved).toEqual(metadata); }); it('should overwrite existing metadata', () => { const metadata1: HandlerMetadata = { name: 'TestHandler', service: 'service1', operations: [{ name: 'op1', method: 'op1' }], }; const metadata2: HandlerMetadata = { name: 'TestHandler', service: 'service2', operations: [{ name: 'op2', method: 'op2' }], }; registry.registerMetadata(metadata1); registry.registerMetadata(metadata2); const retrieved = registry.getMetadata('TestHandler'); expect(retrieved).toEqual(metadata2); }); }); describe('registerConfiguration', () => { it('should register handler configuration separately', () => { const config: HandlerConfiguration = { name: 'TestHandler', operations: { processData: async (data: unknown) => ({ processed: data }), batchProcess: async (items: unknown[]) => items.map(i => ({ processed: i })), }, }; registry.registerConfiguration(config); const retrieved = registry.getConfiguration('TestHandler'); expect(retrieved).toEqual(config); }); it('should handle async operations', async () => { const config: HandlerConfiguration = { name: 'AsyncHandler', operations: { asyncOp: async (data: unknown) => { await new Promise(resolve => setTimeout(resolve, 10)); return { result: data }; }, }, }; registry.registerConfiguration(config); const operation = registry.getOperation('AsyncHandler', 'asyncOp'); expect(operation).toBeDefined(); const result = await operation!({ value: 42 }); expect(result).toEqual({ result: { value: 42 } }); }); }); describe('getMetadata', () => { it('should return handler metadata', () => { const metadata: HandlerMetadata = { name: 'MetaHandler', service: 'meta-service', operations: [{ name: 'metaOp', method: 'metaOp' }], }; registry.registerMetadata(metadata); const retrieved = registry.getMetadata('MetaHandler'); expect(retrieved).toEqual(metadata); }); it('should return undefined for non-existent handler', () => { const metadata = registry.getMetadata('NonExistent'); expect(metadata).toBeUndefined(); }); }); describe('getServiceHandlers', () => { it('should return handlers for a specific service', () => { const metadata1: HandlerMetadata = { name: 'Handler1', service: 'service-a', operations: [], }; const config1: HandlerConfiguration = { name: 'Handler1', operations: {}, }; registry.register(metadata1, config1); const metadata2: HandlerMetadata = { name: 'Handler2', service: 'service-a', operations: [], }; const config2: HandlerConfiguration = { name: 'Handler2', operations: {}, }; registry.register(metadata2, config2); const metadata3: HandlerMetadata = { name: 'Handler3', service: 'service-b', operations: [], }; const config3: HandlerConfiguration = { name: 'Handler3', operations: {}, }; registry.register(metadata3, config3); const serviceAHandlers = registry.getServiceHandlers('service-a'); expect(serviceAHandlers).toHaveLength(2); expect(serviceAHandlers.map(h => h.name)).toContain('Handler1'); expect(serviceAHandlers.map(h => h.name)).toContain('Handler2'); const serviceBHandlers = registry.getServiceHandlers('service-b'); expect(serviceBHandlers).toHaveLength(1); expect(serviceBHandlers[0].name).toBe('Handler3'); }); it('should return empty array for non-existent service', () => { const handlers = registry.getServiceHandlers('non-existent-service'); expect(handlers).toEqual([]); }); }); describe('setHandlerService and getHandlerService', () => { it('should set and get handler service ownership', () => { const metadata: HandlerMetadata = { name: 'ServiceHandler', operations: [], }; const config: HandlerConfiguration = { name: 'ServiceHandler', operations: {}, }; registry.register(metadata, config); registry.setHandlerService('ServiceHandler', 'my-service'); const service = registry.getHandlerService('ServiceHandler'); expect(service).toBe('my-service'); }); it('should overwrite existing service ownership', () => { const metadata: HandlerMetadata = { name: 'ServiceHandler', service: 'initial-service', operations: [], }; const config: HandlerConfiguration = { name: 'ServiceHandler', operations: {}, }; registry.register(metadata, config); registry.setHandlerService('ServiceHandler', 'new-service'); const service = registry.getHandlerService('ServiceHandler'); expect(service).toBe('new-service'); }); it('should return undefined for non-existent handler', () => { const service = registry.getHandlerService('NonExistent'); expect(service).toBeUndefined(); }); }); describe('getScheduledJobs', () => { it('should return scheduled jobs for a handler', () => { const schedules: ScheduleMetadata[] = [ { operation: 'dailyJob', cronPattern: '0 0 * * *', priority: 1, }, { operation: 'hourlyJob', cronPattern: '0 * * * *', }, ]; const metadata: HandlerMetadata = { name: 'ScheduledHandler', operations: [ { name: 'dailyJob', method: 'dailyJob' }, { name: 'hourlyJob', method: 'hourlyJob' }, ], schedules, }; const config: HandlerConfiguration = { name: 'ScheduledHandler', operations: { dailyJob: async () => ({ result: 'daily' }), hourlyJob: async () => ({ result: 'hourly' }), }, scheduledJobs: [ { type: 'dailyJob', operation: 'dailyJob', cronPattern: '0 0 * * *', priority: 1, }, { type: 'hourlyJob', operation: 'hourlyJob', cronPattern: '0 * * * *', }, ], }; registry.register(metadata, config); const jobs = registry.getScheduledJobs('ScheduledHandler'); expect(jobs).toHaveLength(2); expect(jobs[0].type).toBe('dailyJob'); expect(jobs[1].type).toBe('hourlyJob'); }); it('should return empty array for handler without schedules', () => { const metadata: HandlerMetadata = { name: 'NoScheduleHandler', operations: [], }; const config: HandlerConfiguration = { name: 'NoScheduleHandler', operations: {}, }; registry.register(metadata, config); const jobs = registry.getScheduledJobs('NoScheduleHandler'); expect(jobs).toEqual([]); }); it('should return empty array for non-existent handler', () => { const jobs = registry.getScheduledJobs('NonExistent'); expect(jobs).toEqual([]); }); }); describe('getStats', () => { it('should return registry statistics', () => { // Register handlers with various configurations const metadata1: HandlerMetadata = { name: 'Handler1', service: 'service-a', operations: [ { name: 'op1', method: 'op1' }, { name: 'op2', method: 'op2' }, ], schedules: [{ operation: 'op1', cronPattern: '0 0 * * *' }], }; const config1: HandlerConfiguration = { name: 'Handler1', operations: { op1: async () => ({}), op2: async () => ({}), }, }; registry.register(metadata1, config1); const metadata2: HandlerMetadata = { name: 'Handler2', service: 'service-b', operations: [{ name: 'op3', method: 'op3' }], }; const config2: HandlerConfiguration = { name: 'Handler2', operations: { op3: async () => ({}), }, }; registry.register(metadata2, config2); const stats = registry.getStats(); expect(stats.handlers).toBe(2); expect(stats.operations).toBe(3); expect(stats.scheduledJobs).toBe(1); expect(stats.services).toBe(2); }); it('should return zero stats for empty registry', () => { const stats = registry.getStats(); expect(stats.handlers).toBe(0); expect(stats.operations).toBe(0); expect(stats.scheduledJobs).toBe(0); expect(stats.services).toBe(0); }); }); describe('clear', () => { it('should clear all registrations', () => { const metadata1: HandlerMetadata = { name: 'Handler1', operations: [], }; const config1: HandlerConfiguration = { name: 'Handler1', operations: {}, }; registry.register(metadata1, config1); const metadata2: HandlerMetadata = { name: 'Handler2', operations: [], }; const config2: HandlerConfiguration = { name: 'Handler2', operations: {}, }; registry.register(metadata2, config2); expect(registry.getHandlerNames()).toHaveLength(2); registry.clear(); expect(registry.getHandlerNames()).toHaveLength(0); expect(registry.getAllMetadata().size).toBe(0); expect(registry.getStats().handlers).toBe(0); }); }); describe('export and import', () => { it('should export and import registry data', () => { // Setup initial registry const metadata1: HandlerMetadata = { name: 'ExportHandler1', service: 'export-service', operations: [{ name: 'exportOp', method: 'exportOp' }], schedules: [{ operation: 'exportOp', cronPattern: '0 0 * * *' }], }; const config1: HandlerConfiguration = { name: 'ExportHandler1', operations: { exportOp: async () => ({ exported: true }), }, }; registry.register(metadata1, config1); const metadata2: HandlerMetadata = { name: 'ExportHandler2', operations: [{ name: 'anotherOp', method: 'anotherOp' }], }; const config2: HandlerConfiguration = { name: 'ExportHandler2', operations: { anotherOp: async () => ({ another: true }), }, }; registry.register(metadata2, config2); // Export data const exportedData = registry.export(); expect(exportedData.handlers).toHaveLength(2); expect(exportedData.configurations).toHaveLength(2); expect(exportedData.services).toHaveLength(1); // Only ExportHandler1 has a service // Clear and verify empty registry.clear(); expect(registry.getHandlerNames()).toHaveLength(0); // Import data registry.import(exportedData); // Verify restored expect(registry.getHandlerNames()).toHaveLength(2); expect(registry.hasHandler('ExportHandler1')).toBe(true); expect(registry.hasHandler('ExportHandler2')).toBe(true); const handler1 = registry.getMetadata('ExportHandler1'); expect(handler1?.service).toBe('export-service'); expect(handler1?.schedules).toHaveLength(1); const handler2 = registry.getMetadata('ExportHandler2'); expect(handler2?.operations).toHaveLength(1); expect(handler2?.operations[0].name).toBe('anotherOp'); }); it('should handle import with empty data', () => { const emptyData = { handlers: [], configurations: [], services: [], }; registry.import(emptyData); expect(registry.getHandlerNames()).toHaveLength(0); }); it('should preserve configurations during export/import', async () => { const testData = { value: 42 }; const metadata: HandlerMetadata = { name: 'ConfigHandler', operations: [{ name: 'configOp', method: 'configOp' }], }; const config: HandlerConfiguration = { name: 'ConfigHandler', operations: { configOp: async (data: any) => ({ processed: data.value * 2 }), }, }; registry.register(metadata, config); // Test operation before export const opBefore = registry.getOperation('ConfigHandler', 'configOp'); const resultBefore = await opBefore!(testData); expect(resultBefore).toEqual({ processed: 84 }); // Export and import const exported = registry.export(); registry.clear(); registry.import(exported); // Test operation after import - configurations are preserved const opAfter = registry.getOperation('ConfigHandler', 'configOp'); expect(opAfter).toBeDefined(); const resultAfter = await opAfter!(testData); expect(resultAfter).toEqual({ processed: 84 }); }); }); describe('edge cases', () => { it('should handle empty operations object', () => { const metadata: HandlerMetadata = { name: 'EmptyHandler', operations: [], }; const config: HandlerConfiguration = { name: 'EmptyHandler', operations: {}, }; registry.register(metadata, config); const retrieved = registry.getMetadata('EmptyHandler'); expect(retrieved?.operations).toEqual([]); const stats = registry.getStats(); expect(stats.operations).toBe(0); }); it('should handle handlers with many operations', () => { const operations: OperationMetadata[] = []; const operationHandlers: Record = {}; // Create 50 operations for (let i = 0; i < 50; i++) { const opName = `operation${i}`; operations.push({ name: opName, method: opName, }); operationHandlers[opName] = (async () => ({ index: i })) as JobHandler; } const metadata: HandlerMetadata = { name: 'ManyOpsHandler', operations, }; const config: HandlerConfiguration = { name: 'ManyOpsHandler', operations: operationHandlers, }; registry.register(metadata, config); const retrieved = registry.getMetadata('ManyOpsHandler'); expect(retrieved!.operations).toHaveLength(50); const stats = registry.getStats(); expect(stats.operations).toBe(50); }); it('should handle concurrent registrations', async () => { const promises = []; // Register 10 handlers concurrently for (let i = 0; i < 10; i++) { promises.push( Promise.resolve().then(() => { const metadata: HandlerMetadata = { name: `ConcurrentHandler${i}`, operations: [{ name: 'op', method: 'op' }], }; const config: HandlerConfiguration = { name: `ConcurrentHandler${i}`, operations: { op: async () => ({ handler: i }), }, }; registry.register(metadata, config); }) ); } await Promise.all(promises); expect(registry.getHandlerNames()).toHaveLength(10); // Verify all handlers registered correctly for (let i = 0; i < 10; i++) { expect(registry.hasHandler(`ConcurrentHandler${i}`)).toBe(true); } }); }); });