stock-bot/libs/core/handlers/test/base-handler-edge-cases.test.ts

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();
});
});
});