stock-bot/libs/core/handlers/src/handlers.test.ts

242 lines
7.1 KiB
TypeScript

import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
import { BaseHandler } from './base/BaseHandler';
import { Handler, Operation, QueueSchedule, ScheduledOperation } from './decorators/decorators';
import { createJobHandler } from './utils/create-job-handler';
// Mock service container
const createMockServices = (): IServiceContainer => ({
logger: {
info: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
debug: mock(() => {}),
} as any,
cache: null,
globalCache: null,
queueManager: {
getQueue: mock(() => ({
add: mock(() => Promise.resolve()),
})),
} as any,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
});
describe('BaseHandler', () => {
let mockServices: IServiceContainer;
beforeEach(() => {
mockServices = createMockServices();
});
it('should initialize with services', () => {
const handler = new BaseHandler(mockServices, 'test-handler');
expect(handler).toBeDefined();
expect(handler.logger).toBeDefined();
});
it('should execute operations', async () => {
@Handler('test')
class TestHandler extends BaseHandler {
@Operation('testOp')
async handleTestOp(payload: any) {
return { result: 'success', payload };
}
}
const handler = new TestHandler(mockServices);
const context: ExecutionContext = {
type: 'queue',
metadata: { source: 'test' },
};
const result = await handler.execute('testOp', { data: 'test' }, context);
expect(result).toEqual({ result: 'success', payload: { data: 'test' } });
});
it('should throw for unknown operation', async () => {
@Handler('test')
class TestHandler extends BaseHandler {}
const handler = new TestHandler(mockServices);
const context: ExecutionContext = {
type: 'queue',
metadata: {},
};
await expect(handler.execute('unknown', {}, context)).rejects.toThrow(
'Unknown operation: unknown'
);
});
it('should schedule operations', async () => {
const mockQueue = {
add: mock(() => Promise.resolve()),
};
mockServices.queueManager = {
getQueue: mock(() => mockQueue),
} as any;
const handler = new BaseHandler(mockServices, 'test-handler');
await handler.scheduleOperation('test-op', { data: 'test' }, { delay: 1000 });
expect(mockServices.queueManager.getQueue).toHaveBeenCalledWith('test-handler');
expect(mockQueue.add).toHaveBeenCalledWith(
'test-op',
{
handler: 'test-handler',
operation: 'test-op',
payload: { data: 'test' },
},
{ delay: 1000 }
);
});
describe('cache helpers', () => {
it('should handle cache operations with namespace', async () => {
const mockCache = {
set: mock(() => Promise.resolve()),
get: mock(() => Promise.resolve('cached-value')),
del: mock(() => Promise.resolve()),
};
mockServices.cache = mockCache as any;
const handler = new BaseHandler(mockServices, 'my-handler');
await handler['cacheSet']('key', 'value', 3600);
expect(mockCache.set).toHaveBeenCalledWith('my-handler:key', 'value', 3600);
const result = await handler['cacheGet']('key');
expect(mockCache.get).toHaveBeenCalledWith('my-handler:key');
expect(result).toBe('cached-value');
await handler['cacheDel']('key');
expect(mockCache.del).toHaveBeenCalledWith('my-handler:key');
});
it('should handle null cache gracefully', async () => {
const handler = new BaseHandler(mockServices, 'test');
await expect(handler['cacheSet']('key', 'value')).resolves.toBeUndefined();
await expect(handler['cacheGet']('key')).resolves.toBeNull();
await expect(handler['cacheDel']('key')).resolves.toBeUndefined();
});
});
describe('metadata extraction', () => {
it('should extract metadata from decorated class', () => {
@Handler('metadata-test')
class MetadataHandler extends BaseHandler {
@Operation('op1')
async operation1() {}
@Operation('op2')
async operation2() {}
@ScheduledOperation('scheduled-op', '* * * * *', { priority: 10 })
async scheduledOp() {}
}
const metadata = MetadataHandler.extractMetadata();
expect(metadata).toBeDefined();
expect(metadata!.name).toBe('metadata-test');
expect(metadata!.operations).toContain('op1');
expect(metadata!.operations).toContain('op2');
expect(metadata!.operations).toContain('scheduled-op');
expect(metadata!.scheduledJobs).toHaveLength(1);
expect(metadata!.scheduledJobs![0]).toMatchObject({
operation: 'scheduled-op',
cronPattern: '* * * * *',
priority: 10,
});
});
});
});
describe('Decorators', () => {
it('should apply Handler decorator', () => {
@Handler('test-handler')
class TestClass {}
expect((TestClass as any).__handlerName).toBe('test-handler');
});
it('should apply Operation decorator', () => {
class TestClass {
@Operation('my-operation')
myMethod() {}
}
const operations = (TestClass as any).__operations;
expect(operations).toBeDefined();
expect(operations).toHaveLength(1);
expect(operations[0]).toMatchObject({
name: 'my-operation',
method: 'myMethod',
});
});
it('should apply ScheduledOperation decorator with options', () => {
class TestClass {
@ScheduledOperation('scheduled-task', '0 * * * *', {
priority: 8,
payload: { action: 'test' },
batch: { size: 100, delayInHours: 1 },
})
scheduledMethod() {}
}
const schedules = (TestClass as any).__schedules;
expect(schedules).toBeDefined();
expect(schedules).toHaveLength(1);
expect(schedules[0]).toMatchObject({
operation: 'scheduledMethod',
cronPattern: '0 * * * *',
priority: 8,
payload: { action: 'test' },
batch: { size: 100, delayInHours: 1 },
});
});
it('should apply QueueSchedule decorator', () => {
class TestClass {
@QueueSchedule('15 * * * *', { priority: 3 })
queueMethod() {}
}
const schedules = (TestClass as any).__schedules;
expect(schedules).toBeDefined();
expect(schedules[0]).toMatchObject({
operation: 'queueMethod',
cronPattern: '15 * * * *',
priority: 3,
});
});
});
describe('createJobHandler', () => {
it('should create a job handler', async () => {
const handlerFn = mock(async (payload: any) => ({ success: true, payload }));
const jobHandler = createJobHandler(handlerFn);
const result = await jobHandler({ data: 'test' });
expect(handlerFn).toHaveBeenCalledWith({ data: 'test' });
expect(result).toEqual({ success: true, payload: { data: 'test' } });
});
it('should handle errors in job handler', async () => {
const handlerFn = mock(async () => {
throw new Error('Handler error');
});
const jobHandler = createJobHandler(handlerFn);
await expect(jobHandler({})).rejects.toThrow('Handler error');
});
});