import { describe, expect, it, beforeEach } from 'bun:test'; import { HandlerRegistry } from '../src/registry'; import type { HandlerMetadata, HandlerConfiguration, 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: { processData: { name: 'processData', batch: false, }, batchProcess: { name: 'batchProcess', batch: true, batchSize: 10, }, }, }; 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: { op1: { name: 'op1', batch: false }, }, }; const metadata2: HandlerMetadata = { name: 'TestHandler', service: 'service2', operations: { op2: { name: 'op2', batch: false }, }, }; 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 = { processData: async (data: any) => ({ processed: data }), batchProcess: async (items: any[]) => items.map(i => ({ processed: i })), }; registry.registerConfiguration('TestHandler', config); const retrieved = registry.getConfiguration('TestHandler'); expect(retrieved).toEqual(config); }); it('should handle async operations', async () => { const config: HandlerConfiguration = { asyncOp: async (data: any) => { await new Promise(resolve => setTimeout(resolve, 10)); return { result: data }; }, }; registry.registerConfiguration('AsyncHandler', 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: { metaOp: { name: 'metaOp', batch: false }, }, }; 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', () => { registry.register({ metadata: { name: 'Handler1', service: 'service-a', operations: {}, }, configuration: {}, }); registry.register({ metadata: { name: 'Handler2', service: 'service-a', operations: {}, }, configuration: {}, }); registry.register({ metadata: { name: 'Handler3', service: 'service-b', operations: {}, }, configuration: {}, }); 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', () => { registry.register({ metadata: { name: 'ServiceHandler', operations: {}, }, configuration: {}, }); registry.setHandlerService('ServiceHandler', 'my-service'); const service = registry.getHandlerService('ServiceHandler'); expect(service).toBe('my-service'); }); it('should overwrite existing service ownership', () => { registry.register({ metadata: { name: 'ServiceHandler', service: 'initial-service', operations: {}, }, configuration: {}, }); 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[] = [ { operationName: 'dailyJob', schedule: '0 0 * * *', options: { timezone: 'UTC' }, }, { operationName: 'hourlyJob', schedule: '0 * * * *', }, ]; registry.register({ metadata: { name: 'ScheduledHandler', operations: { dailyJob: { name: 'dailyJob', batch: false }, hourlyJob: { name: 'hourlyJob', batch: false }, }, schedules, }, configuration: { dailyJob: async () => ({ result: 'daily' }), hourlyJob: async () => ({ result: 'hourly' }), }, }); const jobs = registry.getScheduledJobs('ScheduledHandler'); expect(jobs).toHaveLength(2); expect(jobs).toEqual(schedules); }); it('should return empty array for handler without schedules', () => { registry.register({ metadata: { name: 'NoScheduleHandler', operations: {}, }, configuration: {}, }); 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 registry.register({ metadata: { name: 'Handler1', service: 'service-a', operations: { op1: { name: 'op1', batch: false }, op2: { name: 'op2', batch: true, batchSize: 5 }, }, schedules: [ { operationName: 'op1', schedule: '0 0 * * *' }, ], }, configuration: { op1: async () => ({}), op2: async () => ({}), }, }); registry.register({ metadata: { name: 'Handler2', service: 'service-b', operations: { op3: { name: 'op3', batch: false }, }, }, configuration: { op3: async () => ({}), }, }); const stats = registry.getStats(); expect(stats.totalHandlers).toBe(2); expect(stats.totalOperations).toBe(3); expect(stats.batchOperations).toBe(1); expect(stats.scheduledOperations).toBe(1); expect(stats.handlersByService).toEqual({ 'service-a': 1, 'service-b': 1, }); }); it('should return zero stats for empty registry', () => { const stats = registry.getStats(); expect(stats.totalHandlers).toBe(0); expect(stats.totalOperations).toBe(0); expect(stats.batchOperations).toBe(0); expect(stats.scheduledOperations).toBe(0); expect(stats.handlersByService).toEqual({}); }); }); describe('clear', () => { it('should clear all registrations', () => { registry.register({ metadata: { name: 'Handler1', operations: {}, }, configuration: {}, }); registry.register({ metadata: { name: 'Handler2', operations: {}, }, configuration: {}, }); expect(registry.getHandlerNames()).toHaveLength(2); registry.clear(); expect(registry.getHandlerNames()).toHaveLength(0); expect(registry.getAllMetadata()).toEqual([]); expect(registry.getStats().totalHandlers).toBe(0); }); }); describe('export and import', () => { it('should export and import registry data', () => { // Setup initial registry registry.register({ metadata: { name: 'ExportHandler1', service: 'export-service', operations: { exportOp: { name: 'exportOp', batch: false }, }, schedules: [ { operationName: 'exportOp', schedule: '0 0 * * *' }, ], }, configuration: { exportOp: async () => ({ exported: true }), }, }); registry.register({ metadata: { name: 'ExportHandler2', operations: { anotherOp: { name: 'anotherOp', batch: true, batchSize: 10 }, }, }, configuration: { anotherOp: async () => ({ another: true }), }, }); // Export data const exportedData = registry.export(); expect(exportedData.handlers).toHaveLength(2); expect(exportedData.version).toBe('1.0'); expect(exportedData.exportedAt).toBeInstanceOf(Date); // 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.anotherOp.batch).toBe(true); expect(handler2?.operations.anotherOp.batchSize).toBe(10); }); it('should handle import with empty data', () => { const emptyData = { version: '1.0', exportedAt: new Date(), handlers: [], }; registry.import(emptyData); expect(registry.getHandlerNames()).toHaveLength(0); }); it('should preserve configurations during export/import', async () => { const testData = { value: 42 }; registry.register({ metadata: { name: 'ConfigHandler', operations: { configOp: { name: 'configOp', batch: false }, }, }, configuration: { configOp: async (data: any) => ({ processed: data.value * 2 }), }, }); // 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 lost in export const opAfter = registry.getOperation('ConfigHandler', 'configOp'); expect(opAfter).toBeUndefined(); // Configurations don't persist }); }); describe('edge cases', () => { it('should handle empty operations object', () => { registry.register({ metadata: { name: 'EmptyHandler', operations: {}, }, configuration: {}, }); const metadata = registry.getMetadata('EmptyHandler'); expect(metadata?.operations).toEqual({}); const stats = registry.getStats(); expect(stats.totalOperations).toBe(0); }); it('should handle handlers with many operations', () => { const operations: Record = {}; const configuration: HandlerConfiguration = {}; // Create 50 operations for (let i = 0; i < 50; i++) { const opName = `operation${i}`; operations[opName] = { name: opName, batch: i % 2 === 0, batchSize: i % 2 === 0 ? i * 2 : undefined, }; configuration[opName] = async () => ({ index: i }); } registry.register({ metadata: { name: 'ManyOpsHandler', operations, }, configuration, }); const metadata = registry.getMetadata('ManyOpsHandler'); expect(Object.keys(metadata!.operations)).toHaveLength(50); const stats = registry.getStats(); expect(stats.totalOperations).toBe(50); expect(stats.batchOperations).toBe(25); // Half are batch operations }); it('should handle concurrent registrations', async () => { const promises = []; // Register 10 handlers concurrently for (let i = 0; i < 10; i++) { promises.push( Promise.resolve().then(() => { registry.register({ metadata: { name: `ConcurrentHandler${i}`, operations: { op: { name: 'op', batch: false }, }, }, configuration: { op: async () => ({ handler: i }), }, }); }) ); } 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); } }); }); });