created lots of tests
This commit is contained in:
parent
42baadae38
commit
54f37f9521
21 changed files with 4577 additions and 215 deletions
275
libs/core/handlers/test/auto-register.test.ts
Normal file
275
libs/core/handlers/test/auto-register.test.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
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 { 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 mockLogger = {
|
||||
info: mock(() => {}),
|
||||
error: mock(() => {}),
|
||||
warn: mock(() => {}),
|
||||
debug: mock(() => {}),
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
it('should use default options when not provided', async () => {
|
||||
await expect(
|
||||
autoRegisterHandlers(mockRegistry)
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAutoHandlerRegistry', () => {
|
||||
it('should create a registry function for a service', () => {
|
||||
const registerFunction = createAutoHandlerRegistry('my-service');
|
||||
|
||||
expect(typeof registerFunction).toBe('function');
|
||||
|
||||
// Test the created function
|
||||
const result = registerFunction(mockRegistry, mockLogger);
|
||||
expect(result).toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
it('should pass through custom options', () => {
|
||||
const customOptions = {
|
||||
pattern: '**/*.custom-handler.ts',
|
||||
directory: './custom-handlers',
|
||||
};
|
||||
|
||||
const registerFunction = createAutoHandlerRegistry('my-service', customOptions);
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// Should extract both, filtering happens during registration
|
||||
expect(handlers).toHaveLength(2);
|
||||
|
||||
const disabledMetadata = Reflect.getMetadata('handler', DisabledHandler);
|
||||
expect(disabledMetadata.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
356
libs/core/handlers/test/base-handler.test.ts
Normal file
356
libs/core/handlers/test/base-handler.test.ts
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
237
libs/core/handlers/test/create-job-handler.test.ts
Normal file
237
libs/core/handlers/test/create-job-handler.test.ts
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
import { describe, expect, it } from 'bun:test';
|
||||
import { createJobHandler } from '../src/utils/create-job-handler';
|
||||
|
||||
describe('createJobHandler', () => {
|
||||
interface TestPayload {
|
||||
userId: string;
|
||||
action: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
success: boolean;
|
||||
processedBy: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
it('should create a type-safe job handler function', () => {
|
||||
const handler = createJobHandler<TestPayload, TestResult>(async (job) => {
|
||||
// Job should have correct payload type
|
||||
const { userId, action, data } = job.data;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
processedBy: userId,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
expect(typeof handler).toBe('function');
|
||||
});
|
||||
|
||||
it('should execute handler with job data', async () => {
|
||||
const testPayload: TestPayload = {
|
||||
userId: 'user-123',
|
||||
action: 'process',
|
||||
data: { value: 42 },
|
||||
};
|
||||
|
||||
const handler = createJobHandler<TestPayload, TestResult>(async (job) => {
|
||||
expect(job.data).toEqual(testPayload);
|
||||
expect(job.id).toBe('job-123');
|
||||
expect(job.name).toBe('test-job');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
processedBy: job.data.userId,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
// Create a mock job
|
||||
const mockJob = {
|
||||
id: 'job-123',
|
||||
name: 'test-job',
|
||||
data: testPayload,
|
||||
opts: {},
|
||||
progress: () => {},
|
||||
log: () => {},
|
||||
updateProgress: async () => {},
|
||||
};
|
||||
|
||||
const result = await handler(mockJob as any);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.processedBy).toBe('user-123');
|
||||
expect(result.timestamp).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should handle errors in handler', async () => {
|
||||
const handler = createJobHandler<TestPayload, TestResult>(async (job) => {
|
||||
if (job.data.action === 'fail') {
|
||||
throw new Error('Handler error');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
processedBy: job.data.userId,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockJob = {
|
||||
id: 'job-456',
|
||||
name: 'test-job',
|
||||
data: {
|
||||
userId: 'user-456',
|
||||
action: 'fail',
|
||||
},
|
||||
opts: {},
|
||||
progress: () => {},
|
||||
log: () => {},
|
||||
updateProgress: async () => {},
|
||||
};
|
||||
|
||||
await expect(handler(mockJob as any)).rejects.toThrow('Handler error');
|
||||
});
|
||||
|
||||
it('should support async operations', async () => {
|
||||
const handler = createJobHandler<TestPayload, TestResult>(async (job) => {
|
||||
// Simulate async operation
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
processedBy: job.data.userId,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockJob = {
|
||||
id: 'job-789',
|
||||
name: 'async-job',
|
||||
data: {
|
||||
userId: 'user-789',
|
||||
action: 'async-process',
|
||||
},
|
||||
opts: {},
|
||||
progress: () => {},
|
||||
log: () => {},
|
||||
updateProgress: async () => {},
|
||||
};
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await handler(mockJob as any);
|
||||
const endTime = Date.now();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(endTime - startTime).toBeGreaterThanOrEqual(10);
|
||||
});
|
||||
|
||||
it('should maintain type safety for complex payloads', () => {
|
||||
interface ComplexPayload {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
};
|
||||
request: {
|
||||
type: 'CREATE' | 'UPDATE' | 'DELETE';
|
||||
resource: string;
|
||||
data: Record<string, any>;
|
||||
};
|
||||
metadata: {
|
||||
timestamp: Date;
|
||||
source: string;
|
||||
version: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ComplexResult {
|
||||
status: 'success' | 'failure';
|
||||
changes: Array<{
|
||||
field: string;
|
||||
oldValue: any;
|
||||
newValue: any;
|
||||
}>;
|
||||
audit: {
|
||||
performedBy: string;
|
||||
performedAt: Date;
|
||||
duration: number;
|
||||
};
|
||||
}
|
||||
|
||||
const handler = createJobHandler<ComplexPayload, ComplexResult>(async (job) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Type-safe access to nested properties
|
||||
const userId = job.data.user.id;
|
||||
const requestType = job.data.request.type;
|
||||
const version = job.data.metadata.version;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
changes: [
|
||||
{
|
||||
field: 'resource',
|
||||
oldValue: null,
|
||||
newValue: job.data.request.resource,
|
||||
},
|
||||
],
|
||||
audit: {
|
||||
performedBy: userId,
|
||||
performedAt: new Date(),
|
||||
duration: Date.now() - startTime,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(typeof handler).toBe('function');
|
||||
});
|
||||
|
||||
it('should work with job progress reporting', async () => {
|
||||
let progressValue = 0;
|
||||
|
||||
const handler = createJobHandler<TestPayload, TestResult>(async (job) => {
|
||||
// Report progress
|
||||
await job.updateProgress(25);
|
||||
progressValue = 25;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
await job.updateProgress(50);
|
||||
progressValue = 50;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
await job.updateProgress(100);
|
||||
progressValue = 100;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
processedBy: job.data.userId,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockJob = {
|
||||
id: 'job-progress',
|
||||
name: 'progress-job',
|
||||
data: {
|
||||
userId: 'user-progress',
|
||||
action: 'long-process',
|
||||
},
|
||||
opts: {},
|
||||
progress: () => progressValue,
|
||||
log: () => {},
|
||||
updateProgress: async (value: number) => {
|
||||
progressValue = value;
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler(mockJob as any);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(progressValue).toBe(100);
|
||||
});
|
||||
});
|
||||
300
libs/core/handlers/test/decorators.test.ts
Normal file
300
libs/core/handlers/test/decorators.test.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import { describe, expect, it, beforeEach } from 'bun:test';
|
||||
import {
|
||||
Handler,
|
||||
Operation,
|
||||
Disabled,
|
||||
QueueSchedule,
|
||||
ScheduledOperation,
|
||||
} from '../src/decorators/decorators';
|
||||
|
||||
describe('Handler Decorators', () => {
|
||||
beforeEach(() => {
|
||||
// Clear metadata between tests
|
||||
(global as any).__handlerMetadata = undefined;
|
||||
});
|
||||
|
||||
describe('@Handler', () => {
|
||||
it('should mark class as handler with name', () => {
|
||||
@Handler('TestHandler')
|
||||
class MyHandler {}
|
||||
|
||||
const instance = new MyHandler();
|
||||
const metadata = Reflect.getMetadata('handler', instance.constructor);
|
||||
|
||||
expect(metadata).toEqual({
|
||||
name: 'TestHandler',
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use class name if no name provided', () => {
|
||||
@Handler()
|
||||
class MyTestHandler {}
|
||||
|
||||
const instance = new MyTestHandler();
|
||||
const metadata = Reflect.getMetadata('handler', instance.constructor);
|
||||
|
||||
expect(metadata).toEqual({
|
||||
name: 'MyTestHandler',
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with inheritance', () => {
|
||||
@Handler('BaseHandler')
|
||||
class BaseTestHandler {}
|
||||
|
||||
@Handler('DerivedHandler')
|
||||
class DerivedTestHandler extends BaseTestHandler {}
|
||||
|
||||
const baseInstance = new BaseTestHandler();
|
||||
const derivedInstance = new DerivedTestHandler();
|
||||
|
||||
const baseMetadata = Reflect.getMetadata('handler', baseInstance.constructor);
|
||||
const derivedMetadata = Reflect.getMetadata('handler', derivedInstance.constructor);
|
||||
|
||||
expect(baseMetadata.name).toBe('BaseHandler');
|
||||
expect(derivedMetadata.name).toBe('DerivedHandler');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Operation', () => {
|
||||
it('should mark method as operation', () => {
|
||||
class TestHandler {
|
||||
@Operation('processData')
|
||||
async process(data: any) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const operations = Reflect.getMetadata('operations', TestHandler.prototype) || {};
|
||||
|
||||
expect(operations.process).toEqual({
|
||||
name: 'processData',
|
||||
batch: false,
|
||||
batchSize: undefined,
|
||||
batchDelay: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use method name if no name provided', () => {
|
||||
class TestHandler {
|
||||
@Operation()
|
||||
async processOrder(data: any) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const operations = Reflect.getMetadata('operations', TestHandler.prototype) || {};
|
||||
|
||||
expect(operations.processOrder).toEqual({
|
||||
name: 'processOrder',
|
||||
batch: false,
|
||||
batchSize: undefined,
|
||||
batchDelay: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support batch configuration', () => {
|
||||
class TestHandler {
|
||||
@Operation('batchProcess', { batch: true, batchSize: 10, batchDelay: 1000 })
|
||||
async processBatch(items: any[]) {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
const operations = Reflect.getMetadata('operations', TestHandler.prototype) || {};
|
||||
|
||||
expect(operations.processBatch).toEqual({
|
||||
name: 'batchProcess',
|
||||
batch: true,
|
||||
batchSize: 10,
|
||||
batchDelay: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with multiple operations', () => {
|
||||
class TestHandler {
|
||||
@Operation('op1')
|
||||
async operation1() {}
|
||||
|
||||
@Operation('op2')
|
||||
async operation2() {}
|
||||
|
||||
@Operation('op3')
|
||||
async operation3() {}
|
||||
}
|
||||
|
||||
const operations = Reflect.getMetadata('operations', TestHandler.prototype) || {};
|
||||
|
||||
expect(Object.keys(operations)).toHaveLength(3);
|
||||
expect(operations.operation1.name).toBe('op1');
|
||||
expect(operations.operation2.name).toBe('op2');
|
||||
expect(operations.operation3.name).toBe('op3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Disabled', () => {
|
||||
it('should mark handler as disabled', () => {
|
||||
@Disabled()
|
||||
@Handler('DisabledHandler')
|
||||
class MyDisabledHandler {}
|
||||
|
||||
const instance = new MyDisabledHandler();
|
||||
const metadata = Reflect.getMetadata('handler', instance.constructor);
|
||||
|
||||
expect(metadata).toEqual({
|
||||
name: 'DisabledHandler',
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work when applied after Handler decorator', () => {
|
||||
@Handler('TestHandler')
|
||||
@Disabled()
|
||||
class MyHandler {}
|
||||
|
||||
const instance = new MyHandler();
|
||||
const metadata = Reflect.getMetadata('handler', instance.constructor);
|
||||
|
||||
expect(metadata).toEqual({
|
||||
name: 'TestHandler',
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('@QueueSchedule', () => {
|
||||
it('should add queue schedule to operation', () => {
|
||||
class TestHandler {
|
||||
@QueueSchedule('0 0 * * *')
|
||||
@Operation('dailyTask')
|
||||
async runDaily() {}
|
||||
}
|
||||
|
||||
const schedules = Reflect.getMetadata('queueSchedules', TestHandler.prototype) || {};
|
||||
|
||||
expect(schedules.runDaily).toEqual({
|
||||
cron: '0 0 * * *',
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with multiple scheduled operations', () => {
|
||||
class TestHandler {
|
||||
@QueueSchedule('0 * * * *')
|
||||
@Operation('hourlyTask')
|
||||
async runHourly() {}
|
||||
|
||||
@QueueSchedule('0 0 * * *')
|
||||
@Operation('dailyTask')
|
||||
async runDaily() {}
|
||||
}
|
||||
|
||||
const schedules = Reflect.getMetadata('queueSchedules', TestHandler.prototype) || {};
|
||||
|
||||
expect(schedules.runHourly.cron).toBe('0 * * * *');
|
||||
expect(schedules.runDaily.cron).toBe('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'),
|
||||
})
|
||||
async syncOperation() {}
|
||||
}
|
||||
|
||||
const scheduled = Reflect.getMetadata('scheduledOperations', TestHandler.prototype) || {};
|
||||
|
||||
expect(scheduled.syncOperation).toEqual({
|
||||
name: 'syncData',
|
||||
schedule: '*/5 * * * *',
|
||||
timezone: 'UTC',
|
||||
startDate: new Date('2024-01-01'),
|
||||
endDate: new Date('2024-12-31'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should use method name if not provided in options', () => {
|
||||
class TestHandler {
|
||||
@ScheduledOperation({
|
||||
schedule: '0 0 * * *',
|
||||
})
|
||||
async dailyCleanup() {}
|
||||
}
|
||||
|
||||
const scheduled = Reflect.getMetadata('scheduledOperations', TestHandler.prototype) || {};
|
||||
|
||||
expect(scheduled.dailyCleanup).toEqual({
|
||||
name: 'dailyCleanup',
|
||||
schedule: '0 0 * * *',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple scheduled operations', () => {
|
||||
class TestHandler {
|
||||
@ScheduledOperation({ schedule: '0 * * * *' })
|
||||
async hourlyCheck() {}
|
||||
|
||||
@ScheduledOperation({ schedule: '0 0 * * *' })
|
||||
async dailyReport() {}
|
||||
|
||||
@ScheduledOperation({ schedule: '0 0 * * 0' })
|
||||
async weeklyAnalysis() {}
|
||||
}
|
||||
|
||||
const scheduled = Reflect.getMetadata('scheduledOperations', TestHandler.prototype) || {};
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decorator composition', () => {
|
||||
it('should work with all decorators combined', () => {
|
||||
@Handler('ComplexHandler')
|
||||
class MyComplexHandler {
|
||||
@Operation('complexOp', { batch: true, batchSize: 5 })
|
||||
@QueueSchedule('0 */6 * * *')
|
||||
async complexOperation(items: any[]) {
|
||||
return items;
|
||||
}
|
||||
|
||||
@ScheduledOperation({
|
||||
name: 'scheduledTask',
|
||||
schedule: '0 0 * * *',
|
||||
timezone: 'America/New_York',
|
||||
})
|
||||
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) || {};
|
||||
|
||||
expect(handlerMetadata.name).toBe('ComplexHandler');
|
||||
expect(operations.complexOperation).toEqual({
|
||||
name: 'complexOp',
|
||||
batch: true,
|
||||
batchSize: 5,
|
||||
batchDelay: undefined,
|
||||
});
|
||||
expect(queueSchedules.complexOperation.cron).toBe('0 */6 * * *');
|
||||
expect(scheduledOps.scheduledTask).toEqual({
|
||||
name: 'scheduledTask',
|
||||
schedule: '0 0 * * *',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue