364 lines
No EOL
12 KiB
TypeScript
364 lines
No EOL
12 KiB
TypeScript
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
|
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
|
|
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
|
|
|
|
// Test handler implementation
|
|
class TestHandler extends BaseHandler {
|
|
testMethod(input: any, context: ExecutionContext) {
|
|
return { result: 'test', input, context };
|
|
}
|
|
|
|
async onInit() {
|
|
// Lifecycle hook
|
|
}
|
|
|
|
protected getScheduledJobPayload(operation: string) {
|
|
return { scheduled: true, operation };
|
|
}
|
|
}
|
|
|
|
// Handler with no operations
|
|
class EmptyHandler extends BaseHandler {}
|
|
|
|
// Handler with missing method
|
|
class BrokenHandler extends BaseHandler {
|
|
constructor(services: IServiceContainer) {
|
|
super(services);
|
|
const ctor = this.constructor as any;
|
|
ctor.__operations = [{ name: 'missing', method: 'nonExistentMethod' }];
|
|
}
|
|
}
|
|
|
|
describe('BaseHandler Edge Cases', () => {
|
|
let mockServices: IServiceContainer;
|
|
|
|
beforeEach(() => {
|
|
mockServices = {
|
|
cache: {
|
|
get: mock(async () => null),
|
|
set: mock(async () => {}),
|
|
del: mock(async () => {}),
|
|
has: mock(async () => false),
|
|
clear: mock(async () => {}),
|
|
keys: mock(async () => []),
|
|
mget: mock(async () => []),
|
|
mset: mock(async () => {}),
|
|
mdel: mock(async () => {}),
|
|
ttl: mock(async () => -1),
|
|
expire: mock(async () => true),
|
|
getClientType: () => 'redis',
|
|
isConnected: () => true,
|
|
},
|
|
globalCache: null,
|
|
queueManager: {
|
|
getQueue: mock(() => ({
|
|
add: mock(async () => ({})),
|
|
addBulk: mock(async () => []),
|
|
pause: mock(async () => {}),
|
|
resume: mock(async () => {}),
|
|
clean: mock(async () => []),
|
|
drain: mock(async () => {}),
|
|
obliterate: mock(async () => {}),
|
|
close: mock(async () => {}),
|
|
isReady: mock(async () => true),
|
|
isClosed: () => false,
|
|
name: 'test-queue',
|
|
})),
|
|
},
|
|
proxy: null,
|
|
browser: null,
|
|
mongodb: null,
|
|
postgres: null,
|
|
questdb: null,
|
|
} as any;
|
|
});
|
|
|
|
describe('Constructor Edge Cases', () => {
|
|
it('should handle handler without decorator metadata', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
expect(handler).toBeInstanceOf(BaseHandler);
|
|
});
|
|
|
|
it('should use provided handler name', () => {
|
|
const handler = new TestHandler(mockServices, 'custom-handler');
|
|
expect(handler).toBeInstanceOf(BaseHandler);
|
|
});
|
|
|
|
it('should handle null queue manager', () => {
|
|
const servicesWithoutQueue = { ...mockServices, queueManager: null };
|
|
const handler = new TestHandler(servicesWithoutQueue);
|
|
expect(handler.queue).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Execute Method Edge Cases', () => {
|
|
it('should throw for unknown operation', async () => {
|
|
const handler = new TestHandler(mockServices);
|
|
const context: ExecutionContext = { type: 'queue', metadata: {} };
|
|
|
|
await expect(handler.execute('unknownOp', {}, context)).rejects.toThrow('Unknown operation: unknownOp');
|
|
});
|
|
|
|
it('should handle operation with no operations metadata', async () => {
|
|
const handler = new EmptyHandler(mockServices);
|
|
const context: ExecutionContext = { type: 'queue', metadata: {} };
|
|
|
|
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow('Unknown operation: anyOp');
|
|
});
|
|
|
|
it('should throw when method is not a function', async () => {
|
|
const handler = new BrokenHandler(mockServices);
|
|
const context: ExecutionContext = { type: 'queue', metadata: {} };
|
|
|
|
await expect(handler.execute('missing', {}, context)).rejects.toThrow(
|
|
"Operation method 'nonExistentMethod' not found on handler"
|
|
);
|
|
});
|
|
|
|
it('should execute operation with proper context', async () => {
|
|
const handler = new TestHandler(mockServices);
|
|
const ctor = handler.constructor as any;
|
|
ctor.__operations = [{ name: 'test', method: 'testMethod' }];
|
|
|
|
const context: ExecutionContext = {
|
|
type: 'queue',
|
|
metadata: { source: 'test' }
|
|
};
|
|
|
|
const result = await handler.execute('test', { data: 'test' }, context);
|
|
expect(result).toEqual({
|
|
result: 'test',
|
|
input: { data: 'test' },
|
|
context,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Service Helper Methods Edge Cases', () => {
|
|
it('should handle missing cache service', async () => {
|
|
const servicesWithoutCache = { ...mockServices, cache: null };
|
|
const handler = new TestHandler(servicesWithoutCache);
|
|
|
|
// Should not throw, just return gracefully
|
|
await handler['cacheSet']('key', 'value');
|
|
const value = await handler['cacheGet']('key');
|
|
expect(value).toBeNull();
|
|
|
|
await handler['cacheDel']('key');
|
|
});
|
|
|
|
it('should handle missing global cache service', async () => {
|
|
const handler = new TestHandler(mockServices); // globalCache is already null
|
|
|
|
await handler['globalCacheSet']('key', 'value');
|
|
const value = await handler['globalCacheGet']('key');
|
|
expect(value).toBeNull();
|
|
|
|
await handler['globalCacheDel']('key');
|
|
});
|
|
|
|
it('should handle missing MongoDB service', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
|
|
expect(() => handler['collection']('test')).toThrow('MongoDB service is not available');
|
|
});
|
|
|
|
it('should schedule operation without queue', async () => {
|
|
const servicesWithoutQueue = { ...mockServices, queueManager: null };
|
|
const handler = new TestHandler(servicesWithoutQueue);
|
|
|
|
await expect(handler.scheduleOperation('test', {})).rejects.toThrow(
|
|
'Queue service is not available for this handler'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Execution Context Creation', () => {
|
|
it('should create execution context with metadata', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
|
|
const context = handler['createExecutionContext']('http', { custom: 'data' });
|
|
|
|
expect(context.type).toBe('http');
|
|
expect(context.metadata.custom).toBe('data');
|
|
expect(context.metadata.timestamp).toBeDefined();
|
|
expect(context.metadata.traceId).toBeDefined();
|
|
expect(context.metadata.traceId).toContain('TestHandler');
|
|
});
|
|
|
|
it('should create execution context without metadata', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
|
|
const context = handler['createExecutionContext']('queue');
|
|
|
|
expect(context.type).toBe('queue');
|
|
expect(context.metadata.timestamp).toBeDefined();
|
|
expect(context.metadata.traceId).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('HTTP Helper Edge Cases', () => {
|
|
it('should provide HTTP methods', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
const http = handler['http'];
|
|
|
|
expect(http.get).toBeDefined();
|
|
expect(http.post).toBeDefined();
|
|
expect(http.put).toBeDefined();
|
|
expect(http.delete).toBeDefined();
|
|
|
|
// All should be functions
|
|
expect(typeof http.get).toBe('function');
|
|
expect(typeof http.post).toBe('function');
|
|
expect(typeof http.put).toBe('function');
|
|
expect(typeof http.delete).toBe('function');
|
|
});
|
|
});
|
|
|
|
describe('Static Methods Edge Cases', () => {
|
|
it('should return null for handler without metadata', () => {
|
|
const metadata = TestHandler.extractMetadata();
|
|
expect(metadata).toBeNull();
|
|
});
|
|
|
|
it('should extract metadata with all fields', () => {
|
|
const HandlerWithMeta = class extends BaseHandler {
|
|
static __handlerName = 'meta-handler';
|
|
static __operations = [
|
|
{ name: 'op1', method: 'method1' },
|
|
{ name: 'op2', method: 'method2' },
|
|
];
|
|
static __schedules = [
|
|
{
|
|
operation: 'method1',
|
|
cronPattern: '* * * * *',
|
|
priority: 10,
|
|
immediately: true,
|
|
description: 'Test schedule',
|
|
payload: { test: true },
|
|
batch: { size: 10 },
|
|
},
|
|
];
|
|
static __description = 'Test handler description';
|
|
};
|
|
|
|
const metadata = HandlerWithMeta.extractMetadata();
|
|
|
|
expect(metadata).toBeDefined();
|
|
expect(metadata?.name).toBe('meta-handler');
|
|
expect(metadata?.operations).toEqual(['op1', 'op2']);
|
|
expect(metadata?.description).toBe('Test handler description');
|
|
expect(metadata?.scheduledJobs).toHaveLength(1);
|
|
|
|
const job = metadata?.scheduledJobs[0];
|
|
expect(job?.type).toBe('meta-handler-method1');
|
|
expect(job?.operation).toBe('op1');
|
|
expect(job?.cronPattern).toBe('* * * * *');
|
|
expect(job?.priority).toBe(10);
|
|
expect(job?.immediately).toBe(true);
|
|
expect(job?.payload).toEqual({ test: true });
|
|
expect(job?.batch).toEqual({ size: 10 });
|
|
});
|
|
});
|
|
|
|
describe('Handler Configuration Creation', () => {
|
|
it('should throw when no metadata found', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
|
|
expect(() => handler.createHandlerConfig()).toThrow('Handler metadata not found');
|
|
});
|
|
|
|
it('should create handler config with operations', () => {
|
|
const HandlerWithMeta = class extends BaseHandler {
|
|
static __handlerName = 'config-handler';
|
|
static __operations = [
|
|
{ name: 'process', method: 'processData' },
|
|
];
|
|
static __schedules = [];
|
|
};
|
|
|
|
const handler = new HandlerWithMeta(mockServices);
|
|
const config = handler.createHandlerConfig();
|
|
|
|
expect(config.name).toBe('config-handler');
|
|
expect(config.operations.process).toBeDefined();
|
|
expect(typeof config.operations.process).toBe('function');
|
|
expect(config.scheduledJobs).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Service Availability Check', () => {
|
|
it('should correctly identify available services', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
|
|
expect(handler['hasService']('cache')).toBe(true);
|
|
expect(handler['hasService']('queueManager')).toBe(true);
|
|
expect(handler['hasService']('globalCache')).toBe(false);
|
|
expect(handler['hasService']('mongodb')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Scheduled Handler Edge Cases', () => {
|
|
it('should be instance of BaseHandler', () => {
|
|
const handler = new ScheduledHandler(mockServices);
|
|
expect(handler).toBeInstanceOf(BaseHandler);
|
|
expect(handler).toBeInstanceOf(ScheduledHandler);
|
|
});
|
|
});
|
|
|
|
describe('Cache Helpers with Namespacing', () => {
|
|
it('should create namespaced cache', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
const nsCache = handler['createNamespacedCache']('api');
|
|
|
|
expect(nsCache).toBeDefined();
|
|
});
|
|
|
|
it('should prefix cache keys with handler name', async () => {
|
|
const TestHandlerWithName = class extends BaseHandler {
|
|
static __handlerName = 'test-handler';
|
|
};
|
|
|
|
const handler = new TestHandlerWithName(mockServices);
|
|
|
|
await handler['cacheSet']('mykey', 'value', 3600);
|
|
|
|
expect(mockServices.cache?.set).toHaveBeenCalledWith('test-handler:mykey', 'value', 3600);
|
|
});
|
|
});
|
|
|
|
describe('Schedule Helper Methods', () => {
|
|
it('should schedule with delay in seconds', async () => {
|
|
const handler = new TestHandler(mockServices);
|
|
|
|
// The queue is already set in the handler constructor
|
|
const mockAdd = handler.queue?.add;
|
|
|
|
await handler['scheduleIn']('test-op', { data: 'test' }, 30, { priority: 10 });
|
|
|
|
expect(mockAdd).toHaveBeenCalledWith(
|
|
'test-op',
|
|
{
|
|
handler: 'testhandler',
|
|
operation: 'test-op',
|
|
payload: { data: 'test' },
|
|
},
|
|
{ delay: 30000, priority: 10 }
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Logging Helper', () => {
|
|
it('should log with handler context', () => {
|
|
const handler = new TestHandler(mockServices);
|
|
|
|
// The log method should exist
|
|
expect(typeof handler['log']).toBe('function');
|
|
|
|
// It should be callable without errors
|
|
expect(() => {
|
|
handler['log']('info', 'Test message', { extra: 'data' });
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
}); |