356 lines
No EOL
10 KiB
TypeScript
356 lines
No EOL
10 KiB
TypeScript
import { describe, expect, it, beforeEach, mock } from 'bun:test';
|
|
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
|
|
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
|
|
|
|
describe('BaseHandler', () => {
|
|
let mockServices: IServiceContainer;
|
|
let mockContext: ExecutionContext;
|
|
|
|
beforeEach(() => {
|
|
const mockQueue = {
|
|
add: mock(async () => ({ id: 'job-456' })),
|
|
getName: mock(() => 'test-queue'),
|
|
};
|
|
|
|
mockServices = {
|
|
cache: {
|
|
get: mock(async () => null),
|
|
set: mock(async () => {}),
|
|
del: mock(async () => {}),
|
|
clear: mock(async () => {}),
|
|
has: mock(async () => false),
|
|
keys: mock(async () => []),
|
|
ttl: mock(async () => -1),
|
|
type: 'memory',
|
|
} as any,
|
|
globalCache: {
|
|
get: mock(async () => null),
|
|
set: mock(async () => {}),
|
|
del: mock(async () => {}),
|
|
clear: mock(async () => {}),
|
|
has: mock(async () => false),
|
|
keys: mock(async () => []),
|
|
ttl: mock(async () => -1),
|
|
type: 'memory',
|
|
} as any,
|
|
queueManager: {
|
|
getQueue: mock(() => mockQueue),
|
|
createQueue: mock(() => mockQueue),
|
|
hasQueue: mock(() => true),
|
|
sendToQueue: mock(async () => 'job-123'),
|
|
} as any,
|
|
proxy: {
|
|
getProxy: mock(() => ({ host: 'proxy.example.com', port: 8080 })),
|
|
} as any,
|
|
browser: {
|
|
scrape: mock(async () => ({ data: 'scraped' })),
|
|
} as any,
|
|
mongodb: {
|
|
db: mock(() => ({
|
|
collection: mock(() => ({
|
|
find: mock(() => ({ toArray: mock(async () => []) })),
|
|
insertOne: mock(async () => ({ insertedId: 'id-123' })),
|
|
})),
|
|
})),
|
|
} as any,
|
|
postgres: {
|
|
query: mock(async () => ({ rows: [] })),
|
|
} as any,
|
|
questdb: null,
|
|
logger: {
|
|
info: mock(() => {}),
|
|
error: mock(() => {}),
|
|
warn: mock(() => {}),
|
|
debug: mock(() => {}),
|
|
} as any,
|
|
queue: mockQueue as any,
|
|
};
|
|
|
|
mockContext = {
|
|
logger: mockServices.logger,
|
|
traceId: 'trace-123',
|
|
handlerName: 'TestHandler',
|
|
operationName: 'testOperation',
|
|
metadata: {},
|
|
startTime: new Date(),
|
|
container: {
|
|
resolve: mock((name: string) => mockServices[name as keyof IServiceContainer]),
|
|
resolveAsync: mock(async (name: string) => mockServices[name as keyof IServiceContainer]),
|
|
},
|
|
} as any;
|
|
|
|
// Reset all mocks
|
|
Object.values(mockServices).forEach(service => {
|
|
if (service && typeof service === 'object') {
|
|
Object.values(service).forEach(method => {
|
|
if (typeof method === 'function' && 'mockClear' in method) {
|
|
(method as any).mockClear();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
class TestHandler extends BaseHandler {
|
|
constructor() {
|
|
super(mockServices, 'TestHandler');
|
|
}
|
|
|
|
async testOperation(data: any) {
|
|
return { processed: data };
|
|
}
|
|
}
|
|
|
|
describe('service access', () => {
|
|
it('should provide access to cache service', async () => {
|
|
const handler = new TestHandler();
|
|
|
|
await handler.cache.set('key', 'value');
|
|
|
|
expect(mockServices.cache.set).toHaveBeenCalledWith('key', 'value');
|
|
});
|
|
|
|
it('should have logger initialized', () => {
|
|
const handler = new TestHandler();
|
|
|
|
expect(handler.logger).toBeDefined();
|
|
// Logger is created by getLogger, not from mockServices
|
|
});
|
|
|
|
it('should provide access to queue service', () => {
|
|
const handler = new TestHandler();
|
|
|
|
expect(handler.queue).toBeDefined();
|
|
expect(handler.queue.getName()).toBe('test-queue');
|
|
});
|
|
|
|
it('should provide access to mongodb', () => {
|
|
const handler = new TestHandler();
|
|
|
|
expect(handler.mongodb).toBe(mockServices.mongodb);
|
|
});
|
|
|
|
it('should provide access to postgres', async () => {
|
|
const handler = new TestHandler();
|
|
|
|
const result = await handler.postgres.query('SELECT 1');
|
|
|
|
expect(result.rows).toEqual([]);
|
|
expect(mockServices.postgres.query).toHaveBeenCalledWith('SELECT 1');
|
|
});
|
|
|
|
it('should provide access to browser', async () => {
|
|
const handler = new TestHandler();
|
|
|
|
const result = await handler.browser.scrape('https://example.com');
|
|
|
|
expect(result).toEqual({ data: 'scraped' });
|
|
expect(mockServices.browser.scrape).toHaveBeenCalledWith('https://example.com');
|
|
});
|
|
|
|
it('should provide access to proxy manager', () => {
|
|
const handler = new TestHandler();
|
|
|
|
const proxy = handler.proxy.getProxy();
|
|
|
|
expect(proxy).toEqual({ host: 'proxy.example.com', port: 8080 });
|
|
});
|
|
});
|
|
|
|
describe('caching utilities', () => {
|
|
it('should generate handler-specific cache keys', async () => {
|
|
const handler = new TestHandler();
|
|
|
|
const key = await handler.cacheKey(mockContext, 'user', '123');
|
|
expect(key).toMatch(/TestHandler:user:123$/);
|
|
});
|
|
|
|
it('should cache handler results', async () => {
|
|
const handler = new TestHandler();
|
|
const operation = mock(async () => ({ result: 'data' }));
|
|
|
|
// First call - executes operation
|
|
const result1 = await handler.cacheHandler(
|
|
mockContext,
|
|
'test-cache',
|
|
operation,
|
|
{ ttl: 300 }
|
|
);
|
|
|
|
expect(result1).toEqual({ result: 'data' });
|
|
expect(operation).toHaveBeenCalledTimes(1);
|
|
expect(mockServices.cache.set).toHaveBeenCalled();
|
|
|
|
// Mock cache hit
|
|
mockServices.cache.get = mock(async () => ({ result: 'data' }));
|
|
|
|
// Second call - returns cached result
|
|
const result2 = await handler.cacheHandler(
|
|
mockContext,
|
|
'test-cache',
|
|
operation,
|
|
{ ttl: 300 }
|
|
);
|
|
|
|
expect(result2).toEqual({ result: 'data' });
|
|
expect(operation).toHaveBeenCalledTimes(1); // Not called again
|
|
});
|
|
});
|
|
|
|
describe('scheduling', () => {
|
|
it('should schedule operations', async () => {
|
|
const handler = new TestHandler();
|
|
|
|
const jobId = await handler.scheduleOperation(
|
|
mockContext,
|
|
'processData',
|
|
{ data: 'test' },
|
|
{ delay: 5000 }
|
|
);
|
|
|
|
expect(jobId).toBe('job-123');
|
|
expect(mockServices.queueManager.sendToQueue).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('HTTP client', () => {
|
|
it('should provide axios instance', () => {
|
|
const handler = new TestHandler();
|
|
|
|
const http = handler.http(mockContext);
|
|
expect(http).toBeDefined();
|
|
expect(http.get).toBeDefined();
|
|
expect(http.post).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('handler metadata', () => {
|
|
it('should extract handler metadata', () => {
|
|
const handler = new TestHandler();
|
|
|
|
const metadata = handler.getHandlerMetadata();
|
|
expect(metadata).toBeDefined();
|
|
expect(metadata.name).toBe('TestHandler');
|
|
});
|
|
});
|
|
|
|
describe('lifecycle hooks', () => {
|
|
class LifecycleHandler extends BaseHandler {
|
|
onInitCalled = false;
|
|
onStartCalled = false;
|
|
onStopCalled = false;
|
|
onDisposeCalled = false;
|
|
|
|
constructor() {
|
|
super(mockServices, 'LifecycleHandler');
|
|
}
|
|
|
|
async onInit() {
|
|
this.onInitCalled = true;
|
|
}
|
|
|
|
async onStart() {
|
|
this.onStartCalled = true;
|
|
}
|
|
|
|
async onStop() {
|
|
this.onStopCalled = true;
|
|
}
|
|
|
|
async onDispose() {
|
|
this.onDisposeCalled = true;
|
|
}
|
|
}
|
|
|
|
it('should call lifecycle hooks', async () => {
|
|
const handler = new LifecycleHandler();
|
|
|
|
await handler.onInit();
|
|
expect(handler.onInitCalled).toBe(true);
|
|
|
|
await handler.onStart();
|
|
expect(handler.onStartCalled).toBe(true);
|
|
|
|
await handler.onStop();
|
|
expect(handler.onStopCalled).toBe(true);
|
|
|
|
await handler.onDispose();
|
|
expect(handler.onDisposeCalled).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('ScheduledHandler', () => {
|
|
const mockServices: IServiceContainer = {
|
|
cache: { type: 'memory' } as any,
|
|
globalCache: { type: 'memory' } as any,
|
|
queueManager: { getQueue: () => null } as any,
|
|
proxy: null,
|
|
browser: null,
|
|
mongodb: null,
|
|
postgres: null,
|
|
questdb: null,
|
|
logger: null as any,
|
|
queue: null as any,
|
|
};
|
|
|
|
class TestScheduledHandler extends ScheduledHandler {
|
|
constructor() {
|
|
super(mockServices, 'TestScheduledHandler');
|
|
}
|
|
|
|
getScheduledJobs() {
|
|
return [
|
|
{
|
|
name: 'dailyJob',
|
|
schedule: '0 0 * * *',
|
|
handler: 'processDailyData',
|
|
},
|
|
{
|
|
name: 'hourlyJob',
|
|
schedule: '0 * * * *',
|
|
handler: 'processHourlyData',
|
|
options: {
|
|
timezone: 'UTC',
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
async processDailyData() {
|
|
return { processed: 'daily' };
|
|
}
|
|
|
|
async processHourlyData() {
|
|
return { processed: 'hourly' };
|
|
}
|
|
}
|
|
|
|
it('should define scheduled jobs', () => {
|
|
const handler = new TestScheduledHandler();
|
|
|
|
const jobs = handler.getScheduledJobs();
|
|
|
|
expect(jobs).toHaveLength(2);
|
|
expect(jobs[0]).toEqual({
|
|
name: 'dailyJob',
|
|
schedule: '0 0 * * *',
|
|
handler: 'processDailyData',
|
|
});
|
|
expect(jobs[1]).toEqual({
|
|
name: 'hourlyJob',
|
|
schedule: '0 * * * *',
|
|
handler: 'processHourlyData',
|
|
options: {
|
|
timezone: 'UTC',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should be a BaseHandler', () => {
|
|
const handler = new TestScheduledHandler();
|
|
|
|
expect(handler).toBeInstanceOf(BaseHandler);
|
|
expect(handler).toBeInstanceOf(ScheduledHandler);
|
|
});
|
|
}); |