stock-bot/libs/core/handler-registry/test/registry-comprehensive.test.ts
2025-06-25 11:38:23 -04:00

550 lines
16 KiB
TypeScript

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<string, JobHandler> = {};
// 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);
}
});
});
});