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

337 lines
10 KiB
TypeScript

import { asFunction, createContainer, type AwilixContainer } from 'awilix';
import { beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import type { HandlerRegistry } from '@stock-bot/handler-registry';
import * as logger from '@stock-bot/logger';
import type { ExecutionContext, IHandler } from '@stock-bot/types';
import { HandlerScanner } from '../src/scanner/handler-scanner';
// Mock handler class
class MockHandler implements IHandler {
static __handlerName = 'mockHandler';
static __operations = [
{ name: 'processData', method: 'processData' },
{ name: 'validateData', method: 'validateData' },
];
static __schedules = [
{
operation: 'processData',
cronPattern: '0 * * * *',
priority: 5,
immediately: false,
description: 'Process data every hour',
payload: { type: 'hourly' },
},
];
static __disabled = false;
constructor(private serviceContainer: any) {}
async execute(operation: string, payload: any, context: ExecutionContext): Promise<any> {
switch (operation) {
case 'processData':
return { processed: true, data: payload };
case 'validateData':
return { valid: true, data: payload };
default:
throw new Error(`Unknown operation: ${operation}`);
}
}
}
// Disabled handler for testing
class DisabledHandler extends MockHandler {
static __handlerName = 'disabledHandler';
static __disabled = true;
}
// Handler without metadata
class InvalidHandler {
constructor() {}
execute() {}
}
describe('HandlerScanner', () => {
let scanner: HandlerScanner;
let mockRegistry: HandlerRegistry;
let container: AwilixContainer;
let mockLogger: any;
beforeEach(() => {
// Create mock logger
mockLogger = {
info: mock(() => {}),
debug: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
};
// Mock getLogger to return our mock logger
spyOn(logger, 'getLogger').mockReturnValue(mockLogger);
// Create mock registry
mockRegistry = {
register: mock(() => {}),
getHandler: mock(() => null),
getHandlerMetadata: mock(() => null),
getAllHandlers: mock(() => []),
clear: mock(() => {}),
} as unknown as HandlerRegistry;
// Create container
container = createContainer();
// Create scanner
scanner = new HandlerScanner(mockRegistry, container, {
serviceName: 'test-service',
autoRegister: true,
});
});
describe('scanHandlers', () => {
it('should handle empty patterns gracefully', async () => {
await scanner.scanHandlers([]);
// Should complete without errors
expect(mockLogger.info).toHaveBeenCalledWith('Starting handler scan', { patterns: [] });
});
it('should handle file scan errors gracefully', async () => {
// We'll test that the scanner handles errors properly
// by calling internal methods directly
const filePath = '/non-existent-file.ts';
// This should not throw
await (scanner as any).scanFile(filePath);
expect(mockLogger.error).toHaveBeenCalled();
});
});
describe('registerHandlerClass', () => {
it('should register a handler class with registry and container', () => {
scanner.registerHandlerClass(MockHandler);
// Check registry registration
expect(mockRegistry.register).toHaveBeenCalledWith(
{
name: 'mockHandler',
service: 'test-service',
operations: [
{ name: 'processData', method: 'processData' },
{ name: 'validateData', method: 'validateData' },
],
schedules: [
{
operation: 'processData',
cronPattern: '0 * * * *',
priority: 5,
immediately: false,
description: 'Process data every hour',
payload: { type: 'hourly' },
},
],
},
expect.objectContaining({
name: 'mockHandler',
operations: expect.any(Object),
scheduledJobs: expect.arrayContaining([
expect.objectContaining({
type: 'mockHandler-processData',
operation: 'processData',
cronPattern: '0 * * * *',
priority: 5,
immediately: false,
description: 'Process data every hour',
payload: { type: 'hourly' },
}),
]),
})
);
// Check container registration
expect(container.hasRegistration('mockHandler')).toBe(true);
});
it('should skip disabled handlers', () => {
scanner.registerHandlerClass(DisabledHandler);
expect(mockRegistry.register).not.toHaveBeenCalled();
expect(container.hasRegistration('disabledHandler')).toBe(false);
});
it('should handle handlers without schedules', () => {
class NoScheduleHandler extends MockHandler {
static __handlerName = 'noScheduleHandler';
static __schedules = [];
}
scanner.registerHandlerClass(NoScheduleHandler);
expect(mockRegistry.register).toHaveBeenCalledWith(
expect.objectContaining({
schedules: [],
}),
expect.objectContaining({
scheduledJobs: [],
})
);
});
it('should use custom service name when provided', () => {
scanner.registerHandlerClass(MockHandler, { serviceName: 'custom-service' });
expect(mockRegistry.register).toHaveBeenCalledWith(
expect.objectContaining({
service: 'custom-service',
}),
expect.any(Object)
);
});
it('should not register with container when autoRegister is false', () => {
scanner = new HandlerScanner(mockRegistry, container, {
serviceName: 'test-service',
autoRegister: false,
});
scanner.registerHandlerClass(MockHandler);
expect(mockRegistry.register).toHaveBeenCalled();
expect(container.hasRegistration('mockHandler')).toBe(false);
});
});
describe('handler validation', () => {
it('should identify valid handlers', () => {
const isHandler = (scanner as any).isHandler;
expect(isHandler(MockHandler)).toBe(true);
expect(isHandler(InvalidHandler)).toBe(false);
expect(isHandler({})).toBe(false);
expect(isHandler('not a function')).toBe(false);
expect(isHandler(null)).toBe(false);
});
it('should handle handlers with batch configuration', () => {
class BatchHandler extends MockHandler {
static __handlerName = 'batchHandler';
static __schedules = [
{
operation: 'processBatch',
cronPattern: '*/5 * * * *',
priority: 10,
batch: {
size: 100,
window: 60000,
},
},
];
}
scanner.registerHandlerClass(BatchHandler);
expect(mockRegistry.register).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
scheduledJobs: expect.arrayContaining([
expect.objectContaining({
batch: {
size: 100,
window: 60000,
},
}),
]),
})
);
});
});
describe('getDiscoveredHandlers', () => {
it('should return all discovered handlers', () => {
scanner.registerHandlerClass(MockHandler);
const discovered = scanner.getDiscoveredHandlers();
expect(discovered.size).toBe(1);
expect(discovered.get('mockHandler')).toBe(MockHandler);
});
it('should return a copy of the map', () => {
scanner.registerHandlerClass(MockHandler);
const discovered1 = scanner.getDiscoveredHandlers();
const discovered2 = scanner.getDiscoveredHandlers();
expect(discovered1).not.toBe(discovered2);
expect(discovered1.get('mockHandler')).toBe(discovered2.get('mockHandler'));
});
});
describe('operation handler creation', () => {
it('should create job handlers for operations', () => {
scanner.registerHandlerClass(MockHandler);
const registrationCall = (mockRegistry.register as any).mock.calls[0];
const configuration = registrationCall[1];
expect(configuration.operations).toHaveProperty('processData');
expect(configuration.operations).toHaveProperty('validateData');
expect(typeof configuration.operations.processData).toBe('function');
});
it('should resolve handler from container when executing operations', async () => {
// Register handler with container
container.register({
serviceContainer: asFunction(() => ({})).singleton(),
});
scanner.registerHandlerClass(MockHandler);
// Create handler instance
const handlerInstance = container.resolve<IHandler>('mockHandler');
// Test execution
const context: ExecutionContext = {
type: 'queue',
metadata: { source: 'test', timestamp: Date.now() },
};
const result = await handlerInstance.execute('processData', { test: true }, context);
expect(result).toEqual({ processed: true, data: { test: true } });
});
});
describe('module scanning', () => {
it('should handle modules with multiple exports', () => {
const mockModule = {
Handler1: MockHandler,
Handler2: class SecondHandler extends MockHandler {
static __handlerName = 'secondHandler';
},
notAHandler: { some: 'object' },
helperFunction: () => {},
};
(scanner as any).registerHandlersFromModule(mockModule, 'test.ts');
expect(mockRegistry.register).toHaveBeenCalledTimes(2);
expect(mockRegistry.register).toHaveBeenCalledWith(
expect.objectContaining({ name: 'mockHandler' }),
expect.any(Object)
);
expect(mockRegistry.register).toHaveBeenCalledWith(
expect.objectContaining({ name: 'secondHandler' }),
expect.any(Object)
);
});
it('should handle empty modules', () => {
const mockModule = {};
(scanner as any).registerHandlersFromModule(mockModule, 'empty.ts');
expect(mockRegistry.register).not.toHaveBeenCalled();
});
});
});