tests
This commit is contained in:
parent
3a7254708e
commit
b63e58784c
41 changed files with 5762 additions and 4477 deletions
337
libs/core/di/test/handler-scanner.test.ts
Normal file
337
libs/core/di/test/handler-scanner.test.ts
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue