This commit is contained in:
Boki 2025-06-25 10:47:00 -04:00
parent 54f37f9521
commit 3a7254708e
19 changed files with 1560 additions and 1237 deletions

View file

@ -18,17 +18,22 @@ function findHandlerFiles(dir: string, pattern = '.handler.'): string[] {
const files: string[] = [];
function scan(currentDir: string) {
const entries = readdirSync(currentDir);
try {
const entries = readdirSync(currentDir);
for (const entry of entries) {
const fullPath = join(currentDir, entry);
const stat = statSync(fullPath);
for (const entry of entries) {
const fullPath = join(currentDir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
scan(fullPath);
} else if (stat.isFile() && entry.includes(pattern) && entry.endsWith('.ts')) {
files.push(fullPath);
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
scan(fullPath);
} else if (stat.isFile() && entry.includes(pattern) && entry.endsWith('.ts')) {
files.push(fullPath);
}
}
} catch (error) {
// Directory doesn't exist or can't be read - that's okay
logger.debug(`Cannot read directory ${currentDir}:`, { error });
}
}

View file

@ -2,24 +2,16 @@ import { describe, expect, it, beforeEach, mock } from 'bun:test';
import {
autoRegisterHandlers,
createAutoHandlerRegistry,
findHandlerFiles,
extractHandlerClasses,
} from '../src/registry/auto-register';
import type { HandlerRegistry } from '@stock-bot/handler-registry';
import type { IServiceContainer } from '@stock-bot/types';
import { Handler, Operation } from '../src/decorators/decorators';
describe('Auto Registration', () => {
const mockRegistry: HandlerRegistry = {
registerHandler: mock(() => {}),
getHandler: mock(() => null),
hasHandler: mock(() => false),
getAllHandlers: mock(() => []),
getHandlersByService: mock(() => []),
getHandlerOperations: mock(() => []),
hasOperation: mock(() => false),
executeOperation: mock(async () => ({ result: 'mocked' })),
clear: mock(() => {}),
};
const mockServices: IServiceContainer = {
getService: mock(() => null),
hasService: mock(() => false),
registerService: mock(() => {}),
} as any;
const mockLogger = {
info: mock(() => {}),
@ -30,246 +22,78 @@ describe('Auto Registration', () => {
beforeEach(() => {
// Reset all mocks
Object.values(mockRegistry).forEach(method => {
if (typeof method === 'function' && 'mockClear' in method) {
(method as any).mockClear();
}
});
Object.values(mockLogger).forEach(method => {
if (typeof method === 'function' && 'mockClear' in method) {
(method as any).mockClear();
}
});
});
describe('findHandlerFiles', () => {
it('should find handler files matching default pattern', async () => {
// This test would need actual file system or mocking
// For now, we'll test the function exists and returns an array
const files = await findHandlerFiles();
expect(Array.isArray(files)).toBe(true);
});
it('should find handler files with custom pattern', async () => {
const files = await findHandlerFiles('**/*.handler.ts');
expect(Array.isArray(files)).toBe(true);
});
it('should find handler files in specific directory', async () => {
const files = await findHandlerFiles('*.handler.ts', './src/handlers');
expect(Array.isArray(files)).toBe(true);
});
});
describe('extractHandlerClasses', () => {
it('should extract handler classes from module', () => {
@Handler('TestHandler1')
class Handler1 {
@Operation('op1')
async operation1() {}
}
@Handler('TestHandler2')
class Handler2 {
@Operation('op2')
async operation2() {}
}
class NotAHandler {
async someMethod() {}
}
const testModule = {
Handler1,
Handler2,
NotAHandler,
someFunction: () => {},
someValue: 123,
};
const handlers = extractHandlerClasses(testModule);
expect(handlers).toHaveLength(2);
expect(handlers.map(h => h.name)).toContain('Handler1');
expect(handlers.map(h => h.name)).toContain('Handler2');
});
it('should handle modules with no handlers', () => {
const testModule = {
SomeClass: class {},
someFunction: () => {},
someValue: 'test',
};
const handlers = extractHandlerClasses(testModule);
expect(handlers).toHaveLength(0);
});
it('should handle empty modules', () => {
const handlers = extractHandlerClasses({});
expect(handlers).toHaveLength(0);
});
it('should extract handlers with metadata', () => {
@Handler('MetadataHandler')
class HandlerWithMetadata {
@Operation('process')
async process() {}
}
const testModule = { HandlerWithMetadata };
const handlers = extractHandlerClasses(testModule);
expect(handlers).toHaveLength(1);
const handlerClass = handlers[0];
const metadata = Reflect.getMetadata('handler', handlerClass);
expect(metadata).toEqual({
name: 'MetadataHandler',
disabled: false,
});
});
mockLogger.info = mock(() => {});
mockLogger.error = mock(() => {});
mockLogger.warn = mock(() => {});
mockLogger.debug = mock(() => {});
});
describe('autoRegisterHandlers', () => {
it('should auto-register handlers', async () => {
// Since this function reads from file system, we'll test its behavior
// by mocking the registry calls
const options = {
pattern: '**/*.handler.ts',
directory: './test-handlers',
serviceName: 'test-service',
};
// Since this function reads from file system, we'll create a temporary directory
const result = await autoRegisterHandlers('./non-existent-dir', mockServices, {
pattern: '.handler.',
dryRun: true,
});
// This would normally scan files and register handlers
// For unit testing, we'll verify the function executes without errors
await expect(
autoRegisterHandlers(mockRegistry, options, mockLogger)
).resolves.not.toThrow();
expect(result).toHaveProperty('registered');
expect(result).toHaveProperty('failed');
expect(Array.isArray(result.registered)).toBe(true);
expect(Array.isArray(result.failed)).toBe(true);
});
it('should use default options when not provided', async () => {
await expect(
autoRegisterHandlers(mockRegistry)
).resolves.not.toThrow();
const result = await autoRegisterHandlers('./non-existent-dir', mockServices);
expect(result).toHaveProperty('registered');
expect(result).toHaveProperty('failed');
});
it('should handle registration errors gracefully', async () => {
mockRegistry.registerHandler = mock(() => {
throw new Error('Registration failed');
});
// Should not throw, but log errors
await expect(
autoRegisterHandlers(mockRegistry, {}, mockLogger)
).resolves.not.toThrow();
it('should handle directory not found gracefully', async () => {
// This should not throw but return empty results
const result = await autoRegisterHandlers('./non-existent-directory', mockServices);
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
});
describe('createAutoHandlerRegistry', () => {
it('should create a registry function for a service', () => {
const registerFunction = createAutoHandlerRegistry('my-service');
it('should create a registry with registerDirectory method', () => {
const registry = createAutoHandlerRegistry(mockServices);
expect(typeof registerFunction).toBe('function');
// Test the created function
const result = registerFunction(mockRegistry, mockLogger);
expect(result).toBeInstanceOf(Promise);
expect(registry).toHaveProperty('registerDirectory');
expect(registry).toHaveProperty('registerDirectories');
expect(typeof registry.registerDirectory).toBe('function');
expect(typeof registry.registerDirectories).toBe('function');
});
it('should pass through custom options', () => {
const customOptions = {
pattern: '**/*.custom-handler.ts',
directory: './custom-handlers',
};
const registerFunction = createAutoHandlerRegistry('my-service', customOptions);
it('should register from a directory', async () => {
const registry = createAutoHandlerRegistry(mockServices);
// The function should be created with merged options
expect(typeof registerFunction).toBe('function');
});
it('should use service name in registration', async () => {
@Handler('ServiceHandler')
class TestServiceHandler {
@Operation('serviceOp')
async operation() {}
}
// Mock file discovery to return our test handler
const mockFindFiles = mock(async () => ['test.handler.ts']);
const mockExtract = mock(() => [TestServiceHandler]);
const registerFunction = createAutoHandlerRegistry('test-service');
// Execute registration
await registerFunction(mockRegistry, mockLogger);
// Verify service name would be used in actual implementation
expect(typeof registerFunction).toBe('function');
});
});
describe('integration scenarios', () => {
it('should handle complex handler hierarchies', () => {
@Handler('BaseHandler')
class BaseTestHandler {
@Operation('baseOp')
async baseOperation() {}
}
@Handler('DerivedHandler')
class DerivedTestHandler extends BaseTestHandler {
@Operation('derivedOp')
async derivedOperation() {}
}
const testModule = {
BaseTestHandler,
DerivedTestHandler,
};
const handlers = extractHandlerClasses(testModule);
expect(handlers).toHaveLength(2);
// Both should be valid handler classes
handlers.forEach(handlerClass => {
const metadata = Reflect.getMetadata('handler', handlerClass);
expect(metadata).toBeDefined();
expect(metadata.disabled).toBe(false);
const result = await registry.registerDirectory('./non-existent-dir', {
dryRun: true,
});
expect(result).toHaveProperty('registered');
expect(result).toHaveProperty('failed');
});
it('should skip disabled handlers if needed', () => {
@Handler('EnabledHandler')
class EnabledHandler {
@Operation('op1')
async operation() {}
}
@Handler('DisabledHandler')
class DisabledHandler {
@Operation('op2')
async operation() {}
}
// Mark as disabled
Reflect.defineMetadata('handler', { name: 'DisabledHandler', disabled: true }, DisabledHandler);
const testModule = {
EnabledHandler,
DisabledHandler,
};
const handlers = extractHandlerClasses(testModule);
it('should register from multiple directories', async () => {
const registry = createAutoHandlerRegistry(mockServices);
// Should extract both, filtering happens during registration
expect(handlers).toHaveLength(2);
const result = await registry.registerDirectories([
'./dir1',
'./dir2',
], {
dryRun: true,
});
const disabledMetadata = Reflect.getMetadata('handler', DisabledHandler);
expect(disabledMetadata.disabled).toBe(true);
expect(result).toHaveProperty('registered');
expect(result).toHaveProperty('failed');
expect(Array.isArray(result.registered)).toBe(true);
expect(Array.isArray(result.failed)).toBe(true);
});
});
});

View file

@ -1,356 +1,446 @@
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);
});
import { describe, expect, it, beforeEach, mock, type Mock } from 'bun:test';
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
import { Handler, Operation } from '../src/decorators/decorators';
import type { IServiceContainer, ExecutionContext, ServiceTypes } from '@stock-bot/types';
import type { CacheProvider } from '@stock-bot/cache';
import type { Logger } from '@stock-bot/logger';
import type { QueueManager, Queue } from '@stock-bot/queue';
import type { SimpleBrowser } from '@stock-bot/browser';
import type { SimpleProxyManager } from '@stock-bot/proxy';
import type { MongoClient, Db, Collection } from 'mongodb';
import type { Pool, QueryResult } from 'pg';
type MockQueue = {
add: Mock<(name: string, data: any) => Promise<{ id: string }>>;
getName: Mock<() => string>;
};
type MockQueueManager = {
getQueue: Mock<(name: string) => MockQueue | null>;
createQueue: Mock<(name: string) => MockQueue>;
hasQueue: Mock<(name: string) => boolean>;
sendToQueue: Mock<(service: string, handler: string, data: any) => Promise<string>>;
};
type MockCache = {
get: Mock<(key: string) => Promise<any>>;
set: Mock<(key: string, value: any, ttl?: number) => Promise<void>>;
del: Mock<(key: string) => Promise<void>>;
clear: Mock<() => Promise<void>>;
has: Mock<(key: string) => Promise<boolean>>;
keys: Mock<(pattern?: string) => Promise<string[]>>;
ttl: Mock<(key: string) => Promise<number>>;
type: 'memory';
};
type MockLogger = {
info: Mock<(message: string, meta?: any) => void>;
error: Mock<(message: string, meta?: any) => void>;
warn: Mock<(message: string, meta?: any) => void>;
debug: Mock<(message: string, meta?: any) => void>;
};
type MockBrowser = {
scrape: Mock<(url: string) => Promise<{ data: string }>>;
};
type MockProxy = {
getProxy: Mock<() => { host: string; port: number }>;
};
type MockPostgres = {
query: Mock<(text: string, values?: any[]) => Promise<QueryResult>>;
};
type MockMongoDB = {
db: Mock<(name?: string) => {
collection: Mock<(name: string) => {
find: Mock<(filter: any) => { toArray: Mock<() => Promise<any[]>> }>;
insertOne: Mock<(doc: any) => Promise<{ insertedId: string }>>;
}>;
}>;
};
describe('BaseHandler', () => {
let mockServices: IServiceContainer;
let mockContext: ExecutionContext;
let mockQueue: MockQueue;
let mockQueueManager: MockQueueManager;
let mockCache: MockCache;
let mockLogger: MockLogger;
beforeEach(() => {
mockQueue = {
add: mock(async () => ({ id: 'job-456' })),
getName: mock(() => 'test-queue'),
};
mockQueueManager = {
getQueue: mock(() => mockQueue),
createQueue: mock(() => mockQueue),
hasQueue: mock(() => true),
sendToQueue: mock(async () => 'job-123'),
};
mockCache = {
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',
};
mockLogger = {
info: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
debug: mock(() => {}),
};
const mockBrowser: MockBrowser = {
scrape: mock(async () => ({ data: 'scraped' })),
};
const mockProxy: MockProxy = {
getProxy: mock(() => ({ host: 'proxy.example.com', port: 8080 })),
};
const mockPostgres: MockPostgres = {
query: mock(async () => ({ rows: [], rowCount: 0 } as QueryResult)),
};
const mockMongoDB: MockMongoDB = {
db: mock(() => ({
collection: mock(() => ({
find: mock(() => ({ toArray: mock(async () => []) })),
insertOne: mock(async () => ({ insertedId: 'id-123' })),
})),
})),
};
mockServices = {
cache: mockCache as unknown as ServiceTypes['cache'],
globalCache: { ...mockCache } as unknown as ServiceTypes['globalCache'],
queueManager: mockQueueManager as unknown as ServiceTypes['queueManager'],
proxy: mockProxy as unknown as ServiceTypes['proxy'],
browser: mockBrowser as unknown as ServiceTypes['browser'],
mongodb: mockMongoDB as unknown as ServiceTypes['mongodb'],
postgres: mockPostgres as unknown as ServiceTypes['postgres'],
questdb: null,
logger: mockLogger as unknown as ServiceTypes['logger'],
queue: mockQueue as unknown as ServiceTypes['queue'],
};
mockContext = {
logger: mockLogger as unknown as 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]),
},
};
// 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 unknown as Mock<any>).mockClear();
}
});
}
});
});
class TestHandler extends BaseHandler {
constructor() {
super(mockServices, 'TestHandler');
}
async testOperation(data: unknown): Promise<{ processed: unknown }> {
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(mockCache.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(mockQueue.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 as unknown as MockBrowser).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 set and get cache values with handler namespace', async () => {
const handler = new TestHandler();
mockCache.set.mockClear();
mockCache.get.mockClear();
// Test cacheSet
await handler['cacheSet']('testKey', 'testValue', 3600);
expect(mockCache.set).toHaveBeenCalledWith('TestHandler:testKey', 'testValue', 3600);
// Test cacheGet
mockCache.get.mockImplementation(async () => 'cachedValue');
const result = await handler['cacheGet']('testKey');
expect(mockCache.get).toHaveBeenCalledWith('TestHandler:testKey');
expect(result).toBe('cachedValue');
});
it('should delete cache values with handler namespace', async () => {
const handler = new TestHandler();
mockCache.del.mockClear();
await handler['cacheDel']('testKey');
expect(mockCache.del).toHaveBeenCalledWith('TestHandler:testKey');
});
it('should handle null cache gracefully', async () => {
mockServices.cache = null;
const handler = new TestHandler();
// Should not throw when cache is null
await expect(handler['cacheSet']('key', 'value')).resolves.toBeUndefined();
await expect(handler['cacheGet']('key')).resolves.toBeNull();
await expect(handler['cacheDel']('key')).resolves.toBeUndefined();
});
});
describe('scheduling', () => {
it('should schedule operations', async () => {
const handler = new TestHandler();
mockQueueManager.hasQueue.mockClear();
mockQueue.add.mockClear();
await handler.scheduleOperation(
'processData',
{ data: 'test' },
{ delay: 5000 }
);
expect(mockQueueManager.getQueue).toHaveBeenCalledWith('TestHandler');
expect(mockQueue.add).toHaveBeenCalledWith(
'processData',
{
handler: 'TestHandler',
operation: 'processData',
payload: { data: 'test' },
},
{ delay: 5000 }
);
});
});
describe('HTTP client', () => {
it('should provide http methods', () => {
const handler = new TestHandler();
const http = handler['http'];
expect(http).toBeDefined();
expect(http.get).toBeDefined();
expect(http.post).toBeDefined();
expect(http.put).toBeDefined();
expect(http.delete).toBeDefined();
});
});
describe('handler metadata', () => {
it('should extract handler metadata', () => {
// For metadata extraction, we need a decorated handler
@Handler('MetadataTestHandler')
class MetadataTestHandler extends BaseHandler {
@Operation('testOp')
async handleTestOp() {
return { result: 'success' };
}
}
const metadata = MetadataTestHandler.extractMetadata();
expect(metadata).toBeDefined();
expect(metadata!.name).toBe('MetadataTestHandler');
expect(metadata!.operations).toContain('testOp');
});
});
describe('lifecycle hooks', () => {
class LifecycleHandler extends BaseHandler {
onInitCalled = false;
onStartCalled = false;
onStopCalled = false;
onDisposeCalled = false;
constructor() {
super(mockServices, 'LifecycleHandler');
}
async onInit(): Promise<void> {
this.onInitCalled = true;
}
async onStart(): Promise<void> {
this.onStartCalled = true;
}
async onStop(): Promise<void> {
this.onStopCalled = true;
}
async onDispose(): Promise<void> {
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 mockQueue: MockQueue = {
add: mock(async () => ({ id: 'job-456' })),
getName: mock(() => 'test-queue'),
};
const mockServices: IServiceContainer = {
cache: { type: 'memory' } as unknown as ServiceTypes['cache'],
globalCache: { type: 'memory' } as unknown as ServiceTypes['globalCache'],
queueManager: {
getQueue: () => mockQueue
} as unknown as ServiceTypes['queueManager'],
proxy: null as unknown as ServiceTypes['proxy'],
browser: null as unknown as ServiceTypes['browser'],
mongodb: null as unknown as ServiceTypes['mongodb'],
postgres: null as unknown as ServiceTypes['postgres'],
questdb: null,
logger: null as unknown as ServiceTypes['logger'],
queue: mockQueue as unknown as ServiceTypes['queue'],
};
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(): Promise<{ processed: string }> {
return { processed: 'daily' };
}
async processHourlyData(): Promise<{ processed: string }> {
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);
});
});

View file

@ -18,26 +18,20 @@ describe('Handler Decorators', () => {
@Handler('TestHandler')
class MyHandler {}
const instance = new MyHandler();
const metadata = Reflect.getMetadata('handler', instance.constructor);
const constructor = MyHandler as any;
expect(metadata).toEqual({
name: 'TestHandler',
disabled: false,
});
expect(constructor.__handlerName).toBe('TestHandler');
expect(constructor.__needsAutoRegistration).toBe(true);
});
it('should use class name if no name provided', () => {
@Handler()
// Handler decorator requires a name parameter
@Handler('MyTestHandler')
class MyTestHandler {}
const instance = new MyTestHandler();
const metadata = Reflect.getMetadata('handler', instance.constructor);
const constructor = MyTestHandler as any;
expect(metadata).toEqual({
name: 'MyTestHandler',
disabled: false,
});
expect(constructor.__handlerName).toBe('MyTestHandler');
});
it('should work with inheritance', () => {
@ -47,14 +41,11 @@ describe('Handler Decorators', () => {
@Handler('DerivedHandler')
class DerivedTestHandler extends BaseTestHandler {}
const baseInstance = new BaseTestHandler();
const derivedInstance = new DerivedTestHandler();
const baseConstructor = BaseTestHandler as any;
const derivedConstructor = DerivedTestHandler as any;
const baseMetadata = Reflect.getMetadata('handler', baseInstance.constructor);
const derivedMetadata = Reflect.getMetadata('handler', derivedInstance.constructor);
expect(baseMetadata.name).toBe('BaseHandler');
expect(derivedMetadata.name).toBe('DerivedHandler');
expect(baseConstructor.__handlerName).toBe('BaseHandler');
expect(derivedConstructor.__handlerName).toBe('DerivedHandler');
});
});
@ -62,54 +53,56 @@ describe('Handler Decorators', () => {
it('should mark method as operation', () => {
class TestHandler {
@Operation('processData')
async process(data: any) {
async process(data: unknown) {
return data;
}
}
const operations = Reflect.getMetadata('operations', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(operations.process).toEqual({
expect(constructor.__operations).toBeDefined();
expect(constructor.__operations).toHaveLength(1);
expect(constructor.__operations[0]).toEqual({
name: 'processData',
batch: false,
batchSize: undefined,
batchDelay: undefined,
method: 'process',
batch: undefined,
});
});
it('should use method name if no name provided', () => {
// Operation decorator requires a name parameter
class TestHandler {
@Operation()
async processOrder(data: any) {
@Operation('processOrder')
async processOrder(data: unknown) {
return data;
}
}
const operations = Reflect.getMetadata('operations', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(operations.processOrder).toEqual({
expect(constructor.__operations).toBeDefined();
expect(constructor.__operations[0]).toEqual({
name: 'processOrder',
batch: false,
batchSize: undefined,
batchDelay: undefined,
method: 'processOrder',
batch: undefined,
});
});
it('should support batch configuration', () => {
class TestHandler {
@Operation('batchProcess', { batch: true, batchSize: 10, batchDelay: 1000 })
async processBatch(items: any[]) {
@Operation('batchProcess', { batch: { enabled: true, size: 10, delayInHours: 1 } })
async processBatch(items: unknown[]) {
return items;
}
}
const operations = Reflect.getMetadata('operations', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(operations.processBatch).toEqual({
expect(constructor.__operations).toBeDefined();
expect(constructor.__operations[0]).toEqual({
name: 'batchProcess',
batch: true,
batchSize: 10,
batchDelay: 1000,
method: 'processBatch',
batch: { enabled: true, size: 10, delayInHours: 1 },
});
});
@ -125,12 +118,12 @@ describe('Handler Decorators', () => {
async operation3() {}
}
const operations = Reflect.getMetadata('operations', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(Object.keys(operations)).toHaveLength(3);
expect(operations.operation1.name).toBe('op1');
expect(operations.operation2.name).toBe('op2');
expect(operations.operation3.name).toBe('op3');
expect(constructor.__operations).toHaveLength(3);
expect(constructor.__operations[0]).toMatchObject({ name: 'op1', method: 'operation1' });
expect(constructor.__operations[1]).toMatchObject({ name: 'op2', method: 'operation2' });
expect(constructor.__operations[2]).toMatchObject({ name: 'op3', method: 'operation3' });
});
});
@ -140,13 +133,10 @@ describe('Handler Decorators', () => {
@Handler('DisabledHandler')
class MyDisabledHandler {}
const instance = new MyDisabledHandler();
const metadata = Reflect.getMetadata('handler', instance.constructor);
const constructor = MyDisabledHandler as any;
expect(metadata).toEqual({
name: 'DisabledHandler',
disabled: true,
});
expect(constructor.__handlerName).toBe('DisabledHandler');
expect(constructor.__disabled).toBe(true);
});
it('should work when applied after Handler decorator', () => {
@ -154,13 +144,10 @@ describe('Handler Decorators', () => {
@Disabled()
class MyHandler {}
const instance = new MyHandler();
const metadata = Reflect.getMetadata('handler', instance.constructor);
const constructor = MyHandler as any;
expect(metadata).toEqual({
name: 'TestHandler',
disabled: true,
});
expect(constructor.__handlerName).toBe('TestHandler');
expect(constructor.__disabled).toBe(true);
});
});
@ -172,10 +159,12 @@ describe('Handler Decorators', () => {
async runDaily() {}
}
const schedules = Reflect.getMetadata('queueSchedules', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(schedules.runDaily).toEqual({
cron: '0 0 * * *',
expect(constructor.__schedules).toBeDefined();
expect(constructor.__schedules[0]).toMatchObject({
operation: 'runDaily',
cronPattern: '0 0 * * *',
});
});
@ -190,71 +179,93 @@ describe('Handler Decorators', () => {
async runDaily() {}
}
const schedules = Reflect.getMetadata('queueSchedules', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(schedules.runHourly.cron).toBe('0 * * * *');
expect(schedules.runDaily.cron).toBe('0 0 * * *');
expect(constructor.__schedules).toBeDefined();
expect(constructor.__schedules).toHaveLength(2);
expect(constructor.__schedules[0]).toMatchObject({
operation: 'runHourly',
cronPattern: '0 * * * *',
});
expect(constructor.__schedules[1]).toMatchObject({
operation: 'runDaily',
cronPattern: '0 0 * * *',
});
});
});
describe('@ScheduledOperation', () => {
it('should mark operation as scheduled with options', () => {
class TestHandler {
@ScheduledOperation({
name: 'syncData',
schedule: '*/5 * * * *',
timezone: 'UTC',
startDate: new Date('2024-01-01'),
endDate: new Date('2024-12-31'),
@ScheduledOperation('syncData', '*/5 * * * *', {
priority: 10,
immediately: true,
description: 'Sync data every 5 minutes',
})
async syncOperation() {}
}
const scheduled = Reflect.getMetadata('scheduledOperations', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(scheduled.syncOperation).toEqual({
// ScheduledOperation creates both an operation and a schedule
expect(constructor.__operations).toBeDefined();
expect(constructor.__operations[0]).toMatchObject({
name: 'syncData',
schedule: '*/5 * * * *',
timezone: 'UTC',
startDate: new Date('2024-01-01'),
endDate: new Date('2024-12-31'),
method: 'syncOperation',
});
expect(constructor.__schedules).toBeDefined();
expect(constructor.__schedules[0]).toMatchObject({
operation: 'syncOperation',
cronPattern: '*/5 * * * *',
priority: 10,
immediately: true,
description: 'Sync data every 5 minutes',
});
});
it('should use method name if not provided in options', () => {
it('should use method name if not provided', () => {
class TestHandler {
@ScheduledOperation({
schedule: '0 0 * * *',
})
@ScheduledOperation('dailyCleanup', '0 0 * * *')
async dailyCleanup() {}
}
const scheduled = Reflect.getMetadata('scheduledOperations', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(scheduled.dailyCleanup).toEqual({
expect(constructor.__operations[0]).toMatchObject({
name: 'dailyCleanup',
schedule: '0 0 * * *',
method: 'dailyCleanup',
});
expect(constructor.__schedules[0]).toMatchObject({
operation: 'dailyCleanup',
cronPattern: '0 0 * * *',
});
});
it('should handle multiple scheduled operations', () => {
class TestHandler {
@ScheduledOperation({ schedule: '0 * * * *' })
@ScheduledOperation('hourlyCheck', '0 * * * *')
async hourlyCheck() {}
@ScheduledOperation({ schedule: '0 0 * * *' })
@ScheduledOperation('dailyReport', '0 0 * * *')
async dailyReport() {}
@ScheduledOperation({ schedule: '0 0 * * 0' })
@ScheduledOperation('weeklyAnalysis', '0 0 * * 0')
async weeklyAnalysis() {}
}
const scheduled = Reflect.getMetadata('scheduledOperations', TestHandler.prototype) || {};
const constructor = TestHandler as any;
expect(Object.keys(scheduled)).toHaveLength(3);
expect(scheduled.hourlyCheck.schedule).toBe('0 * * * *');
expect(scheduled.dailyReport.schedule).toBe('0 0 * * *');
expect(scheduled.weeklyAnalysis.schedule).toBe('0 0 * * 0');
expect(constructor.__operations).toHaveLength(3);
expect(constructor.__schedules).toHaveLength(3);
expect(constructor.__operations[0]).toMatchObject({ name: 'hourlyCheck' });
expect(constructor.__operations[1]).toMatchObject({ name: 'dailyReport' });
expect(constructor.__operations[2]).toMatchObject({ name: 'weeklyAnalysis' });
expect(constructor.__schedules[0]).toMatchObject({ cronPattern: '0 * * * *' });
expect(constructor.__schedules[1]).toMatchObject({ cronPattern: '0 0 * * *' });
expect(constructor.__schedules[2]).toMatchObject({ cronPattern: '0 0 * * 0' });
});
});
@ -262,38 +273,46 @@ describe('Handler Decorators', () => {
it('should work with all decorators combined', () => {
@Handler('ComplexHandler')
class MyComplexHandler {
@Operation('complexOp', { batch: true, batchSize: 5 })
@Operation('complexOp', { batch: { enabled: true, size: 5 } })
@QueueSchedule('0 */6 * * *')
async complexOperation(items: any[]) {
async complexOperation(items: unknown[]) {
return items;
}
@ScheduledOperation({
name: 'scheduledTask',
schedule: '0 0 * * *',
timezone: 'America/New_York',
@ScheduledOperation('scheduledTask', '0 0 * * *', {
priority: 5,
description: 'Daily scheduled task',
})
async scheduledTask() {}
}
const instance = new MyComplexHandler();
const handlerMetadata = Reflect.getMetadata('handler', instance.constructor);
const operations = Reflect.getMetadata('operations', MyComplexHandler.prototype) || {};
const queueSchedules = Reflect.getMetadata('queueSchedules', MyComplexHandler.prototype) || {};
const scheduledOps = Reflect.getMetadata('scheduledOperations', MyComplexHandler.prototype) || {};
const constructor = MyComplexHandler as any;
expect(handlerMetadata.name).toBe('ComplexHandler');
expect(operations.complexOperation).toEqual({
expect(constructor.__handlerName).toBe('ComplexHandler');
// Check operations
expect(constructor.__operations).toHaveLength(2);
expect(constructor.__operations[0]).toMatchObject({
name: 'complexOp',
batch: true,
batchSize: 5,
batchDelay: undefined,
method: 'complexOperation',
batch: { enabled: true, size: 5 },
});
expect(queueSchedules.complexOperation.cron).toBe('0 */6 * * *');
expect(scheduledOps.scheduledTask).toEqual({
expect(constructor.__operations[1]).toMatchObject({
name: 'scheduledTask',
schedule: '0 0 * * *',
timezone: 'America/New_York',
method: 'scheduledTask',
});
// Check schedules
expect(constructor.__schedules).toHaveLength(2);
expect(constructor.__schedules[0]).toMatchObject({
operation: 'complexOperation',
cronPattern: '0 */6 * * *',
});
expect(constructor.__schedules[1]).toMatchObject({
operation: 'scheduledTask',
cronPattern: '0 0 * * *',
priority: 5,
description: 'Daily scheduled task',
});
});
});

View file

@ -1,30 +1,63 @@
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test';
import type { ExecutionContext, IServiceContainer, ServiceTypes } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
import { Handler, Operation, QueueSchedule, ScheduledOperation } from '../src/decorators/decorators';
import { createJobHandler } from '../src/utils/create-job-handler';
import type { Logger } from '@stock-bot/logger';
import type { QueueManager, Queue } from '@stock-bot/queue';
import type { CacheProvider } from '@stock-bot/cache';
type MockLogger = {
info: Mock<(message: string, meta?: any) => void>;
error: Mock<(message: string, meta?: any) => void>;
warn: Mock<(message: string, meta?: any) => void>;
debug: Mock<(message: string, meta?: any) => void>;
};
type MockQueue = {
add: Mock<(name: string, data: any, options?: any) => Promise<void>>;
};
type MockQueueManager = {
getQueue: Mock<(name: string) => MockQueue>;
};
type MockCache = {
get: Mock<(key: string) => Promise<any>>;
set: Mock<(key: string, value: any, ttl?: number) => Promise<void>>;
del: Mock<(key: string) => Promise<void>>;
};
// Mock service container
const createMockServices = (): IServiceContainer => ({
logger: {
const createMockServices = (): IServiceContainer => {
const mockLogger: MockLogger = {
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,
});
};
const mockQueue: MockQueue = {
add: mock(() => Promise.resolve()),
};
const mockQueueManager: MockQueueManager = {
getQueue: mock(() => mockQueue),
};
return {
logger: mockLogger as unknown as ServiceTypes['logger'],
cache: null,
globalCache: null,
queueManager: mockQueueManager as unknown as ServiceTypes['queueManager'],
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
queue: mockQueue as unknown as ServiceTypes['queue'],
};
};
describe('BaseHandler', () => {
let mockServices: IServiceContainer;
@ -43,7 +76,7 @@ describe('BaseHandler', () => {
@Handler('test')
class TestHandler extends BaseHandler {
@Operation('testOp')
async handleTestOp(payload: any) {
async handleTestOp(payload: unknown) {
return { result: 'success', payload };
}
}
@ -74,18 +107,19 @@ describe('BaseHandler', () => {
});
it('should schedule operations', async () => {
const mockQueue = {
const mockQueue: MockQueue = {
add: mock(() => Promise.resolve()),
};
mockServices.queueManager = {
const mockQueueManager: MockQueueManager = {
getQueue: mock(() => mockQueue),
} as any;
};
mockServices.queueManager = mockQueueManager as unknown as ServiceTypes['queueManager'];
const handler = new BaseHandler(mockServices, 'test-handler');
await handler.scheduleOperation('test-op', { data: 'test' }, { delay: 1000 });
expect(mockServices.queueManager.getQueue).toHaveBeenCalledWith('test-handler');
expect(mockQueueManager.getQueue).toHaveBeenCalledWith('test-handler');
expect(mockQueue.add).toHaveBeenCalledWith(
'test-op',
{
@ -99,13 +133,13 @@ describe('BaseHandler', () => {
describe('cache helpers', () => {
it('should handle cache operations with namespace', async () => {
const mockCache = {
const mockCache: MockCache = {
set: mock(() => Promise.resolve()),
get: mock(() => Promise.resolve('cached-value')),
del: mock(() => Promise.resolve()),
};
mockServices.cache = mockCache as any;
mockServices.cache = mockCache as unknown as ServiceTypes['cache'];
const handler = new BaseHandler(mockServices, 'my-handler');
await handler['cacheSet']('key', 'value', 3600);
@ -164,7 +198,8 @@ describe('Decorators', () => {
@Handler('test-handler')
class TestClass {}
expect((TestClass as any).__handlerName).toBe('test-handler');
const decoratedClass = TestClass as typeof TestClass & { __handlerName: string };
expect(decoratedClass.__handlerName).toBe('test-handler');
});
it('should apply Operation decorator', () => {
@ -173,7 +208,10 @@ describe('Decorators', () => {
myMethod() {}
}
const operations = (TestClass as any).__operations;
const decoratedClass = TestClass as typeof TestClass & {
__operations: Array<{ name: string; method: string }>;
};
const operations = decoratedClass.__operations;
expect(operations).toBeDefined();
expect(operations).toHaveLength(1);
expect(operations[0]).toMatchObject({
@ -192,7 +230,16 @@ describe('Decorators', () => {
scheduledMethod() {}
}
const schedules = (TestClass as any).__schedules;
const decoratedClass = TestClass as typeof TestClass & {
__schedules: Array<{
operation: string;
cronPattern: string;
priority: number;
payload: any;
batch: { size: number; delayInHours: number };
}>;
};
const schedules = decoratedClass.__schedules;
expect(schedules).toBeDefined();
expect(schedules).toHaveLength(1);
expect(schedules[0]).toMatchObject({
@ -210,7 +257,14 @@ describe('Decorators', () => {
queueMethod() {}
}
const schedules = (TestClass as any).__schedules;
const decoratedClass = TestClass as typeof TestClass & {
__schedules: Array<{
operation: string;
cronPattern: string;
priority: number;
}>;
};
const schedules = decoratedClass.__schedules;
expect(schedules).toBeDefined();
expect(schedules[0]).toMatchObject({
operation: 'queueMethod',
@ -222,7 +276,13 @@ describe('Decorators', () => {
describe('createJobHandler', () => {
it('should create a job handler', async () => {
const handlerFn = mock(async (payload: any) => ({ success: true, payload }));
type TestPayload = { data: string };
type TestResult = { success: boolean; payload: TestPayload };
const handlerFn = mock(async (payload: TestPayload): Promise<TestResult> => ({
success: true,
payload
}));
const jobHandler = createJobHandler(handlerFn);
const result = await jobHandler({ data: 'test' });
@ -239,4 +299,4 @@ describe('createJobHandler', () => {
await expect(jobHandler({})).rejects.toThrow('Handler error');
});
});
});