tests
This commit is contained in:
parent
3a7254708e
commit
b63e58784c
41 changed files with 5762 additions and 4477 deletions
|
|
@ -1,99 +1,92 @@
|
|||
import { describe, expect, it, beforeEach, mock } from 'bun:test';
|
||||
import {
|
||||
autoRegisterHandlers,
|
||||
createAutoHandlerRegistry,
|
||||
} from '../src/registry/auto-register';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import { Handler, Operation } from '../src/decorators/decorators';
|
||||
|
||||
describe('Auto Registration', () => {
|
||||
const mockServices: IServiceContainer = {
|
||||
getService: mock(() => null),
|
||||
hasService: mock(() => false),
|
||||
registerService: mock(() => {}),
|
||||
} as any;
|
||||
|
||||
const mockLogger = {
|
||||
info: mock(() => {}),
|
||||
error: mock(() => {}),
|
||||
warn: mock(() => {}),
|
||||
debug: mock(() => {}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
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 create a temporary directory
|
||||
const result = await autoRegisterHandlers('./non-existent-dir', mockServices, {
|
||||
pattern: '.handler.',
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const result = await autoRegisterHandlers('./non-existent-dir', mockServices);
|
||||
|
||||
expect(result).toHaveProperty('registered');
|
||||
expect(result).toHaveProperty('failed');
|
||||
});
|
||||
|
||||
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 with registerDirectory method', () => {
|
||||
const registry = createAutoHandlerRegistry(mockServices);
|
||||
|
||||
expect(registry).toHaveProperty('registerDirectory');
|
||||
expect(registry).toHaveProperty('registerDirectories');
|
||||
expect(typeof registry.registerDirectory).toBe('function');
|
||||
expect(typeof registry.registerDirectories).toBe('function');
|
||||
});
|
||||
|
||||
it('should register from a directory', async () => {
|
||||
const registry = createAutoHandlerRegistry(mockServices);
|
||||
|
||||
const result = await registry.registerDirectory('./non-existent-dir', {
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('registered');
|
||||
expect(result).toHaveProperty('failed');
|
||||
});
|
||||
|
||||
it('should register from multiple directories', async () => {
|
||||
const registry = createAutoHandlerRegistry(mockServices);
|
||||
|
||||
const result = await registry.registerDirectories([
|
||||
'./dir1',
|
||||
'./dir2',
|
||||
], {
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('registered');
|
||||
expect(result).toHaveProperty('failed');
|
||||
expect(Array.isArray(result.registered)).toBe(true);
|
||||
expect(Array.isArray(result.failed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import { Handler, Operation } from '../src/decorators/decorators';
|
||||
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
|
||||
|
||||
describe('Auto Registration', () => {
|
||||
const mockServices: IServiceContainer = {
|
||||
getService: mock(() => null),
|
||||
hasService: mock(() => false),
|
||||
registerService: mock(() => {}),
|
||||
} as any;
|
||||
|
||||
const mockLogger = {
|
||||
info: mock(() => {}),
|
||||
error: mock(() => {}),
|
||||
warn: mock(() => {}),
|
||||
debug: mock(() => {}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
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 create a temporary directory
|
||||
const result = await autoRegisterHandlers('./non-existent-dir', mockServices, {
|
||||
pattern: '.handler.',
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const result = await autoRegisterHandlers('./non-existent-dir', mockServices);
|
||||
|
||||
expect(result).toHaveProperty('registered');
|
||||
expect(result).toHaveProperty('failed');
|
||||
});
|
||||
|
||||
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 with registerDirectory method', () => {
|
||||
const registry = createAutoHandlerRegistry(mockServices);
|
||||
|
||||
expect(registry).toHaveProperty('registerDirectory');
|
||||
expect(registry).toHaveProperty('registerDirectories');
|
||||
expect(typeof registry.registerDirectory).toBe('function');
|
||||
expect(typeof registry.registerDirectories).toBe('function');
|
||||
});
|
||||
|
||||
it('should register from a directory', async () => {
|
||||
const registry = createAutoHandlerRegistry(mockServices);
|
||||
|
||||
const result = await registry.registerDirectory('./non-existent-dir', {
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('registered');
|
||||
expect(result).toHaveProperty('failed');
|
||||
});
|
||||
|
||||
it('should register from multiple directories', async () => {
|
||||
const registry = createAutoHandlerRegistry(mockServices);
|
||||
|
||||
const result = await registry.registerDirectories(['./dir1', './dir2'], {
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('registered');
|
||||
expect(result).toHaveProperty('failed');
|
||||
expect(Array.isArray(result.registered)).toBe(true);
|
||||
expect(Array.isArray(result.failed)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
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 { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test';
|
||||
import type { Collection, Db, MongoClient } from 'mongodb';
|
||||
import type { Pool, QueryResult } from 'pg';
|
||||
import type { SimpleBrowser } from '@stock-bot/browser';
|
||||
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';
|
||||
import type { Queue, QueueManager } from '@stock-bot/queue';
|
||||
import type { ExecutionContext, IServiceContainer, ServiceTypes } from '@stock-bot/types';
|
||||
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
|
||||
import { Handler, Operation } from '../src/decorators/decorators';
|
||||
|
||||
type MockQueue = {
|
||||
add: Mock<(name: string, data: any) => Promise<{ id: string }>>;
|
||||
|
|
@ -53,12 +53,16 @@ type MockPostgres = {
|
|||
};
|
||||
|
||||
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 }>>;
|
||||
}>;
|
||||
}>;
|
||||
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', () => {
|
||||
|
|
@ -109,7 +113,7 @@ describe('BaseHandler', () => {
|
|||
};
|
||||
|
||||
const mockPostgres: MockPostgres = {
|
||||
query: mock(async () => ({ rows: [], rowCount: 0 } as QueryResult)),
|
||||
query: mock(async () => ({ rows: [], rowCount: 0 }) as QueryResult),
|
||||
};
|
||||
|
||||
const mockMongoDB: MockMongoDB = {
|
||||
|
|
@ -163,7 +167,7 @@ describe('BaseHandler', () => {
|
|||
constructor() {
|
||||
super(mockServices, 'TestHandler');
|
||||
}
|
||||
|
||||
|
||||
async testOperation(data: unknown): Promise<{ processed: unknown }> {
|
||||
return { processed: data };
|
||||
}
|
||||
|
|
@ -172,55 +176,57 @@ describe('BaseHandler', () => {
|
|||
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');
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -230,11 +236,11 @@ describe('BaseHandler', () => {
|
|||
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');
|
||||
|
|
@ -245,7 +251,7 @@ describe('BaseHandler', () => {
|
|||
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');
|
||||
});
|
||||
|
|
@ -253,7 +259,7 @@ describe('BaseHandler', () => {
|
|||
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();
|
||||
|
|
@ -266,13 +272,9 @@ describe('BaseHandler', () => {
|
|||
const handler = new TestHandler();
|
||||
mockQueueManager.hasQueue.mockClear();
|
||||
mockQueue.add.mockClear();
|
||||
|
||||
await handler.scheduleOperation(
|
||||
'processData',
|
||||
{ data: 'test' },
|
||||
{ delay: 5000 }
|
||||
);
|
||||
|
||||
|
||||
await handler.scheduleOperation('processData', { data: 'test' }, { delay: 5000 });
|
||||
|
||||
expect(mockQueueManager.getQueue).toHaveBeenCalledWith('TestHandler');
|
||||
expect(mockQueue.add).toHaveBeenCalledWith(
|
||||
'processData',
|
||||
|
|
@ -289,7 +291,7 @@ describe('BaseHandler', () => {
|
|||
describe('HTTP client', () => {
|
||||
it('should provide http methods', () => {
|
||||
const handler = new TestHandler();
|
||||
|
||||
|
||||
const http = handler['http'];
|
||||
expect(http).toBeDefined();
|
||||
expect(http.get).toBeDefined();
|
||||
|
|
@ -309,7 +311,7 @@ describe('BaseHandler', () => {
|
|||
return { result: 'success' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const metadata = MetadataTestHandler.extractMetadata();
|
||||
expect(metadata).toBeDefined();
|
||||
expect(metadata!.name).toBe('MetadataTestHandler');
|
||||
|
|
@ -323,40 +325,40 @@ describe('BaseHandler', () => {
|
|||
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);
|
||||
});
|
||||
|
|
@ -372,8 +374,8 @@ describe('ScheduledHandler', () => {
|
|||
const mockServices: IServiceContainer = {
|
||||
cache: { type: 'memory' } as unknown as ServiceTypes['cache'],
|
||||
globalCache: { type: 'memory' } as unknown as ServiceTypes['globalCache'],
|
||||
queueManager: {
|
||||
getQueue: () => mockQueue
|
||||
queueManager: {
|
||||
getQueue: () => mockQueue,
|
||||
} as unknown as ServiceTypes['queueManager'],
|
||||
proxy: null as unknown as ServiceTypes['proxy'],
|
||||
browser: null as unknown as ServiceTypes['browser'],
|
||||
|
|
@ -388,7 +390,7 @@ describe('ScheduledHandler', () => {
|
|||
constructor() {
|
||||
super(mockServices, 'TestScheduledHandler');
|
||||
}
|
||||
|
||||
|
||||
getScheduledJobs() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -397,7 +399,7 @@ describe('ScheduledHandler', () => {
|
|||
handler: 'processDailyData',
|
||||
},
|
||||
{
|
||||
name: 'hourlyJob',
|
||||
name: 'hourlyJob',
|
||||
schedule: '0 * * * *',
|
||||
handler: 'processHourlyData',
|
||||
options: {
|
||||
|
|
@ -406,21 +408,21 @@ describe('ScheduledHandler', () => {
|
|||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
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',
|
||||
|
|
@ -436,11 +438,11 @@ describe('ScheduledHandler', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should be a BaseHandler', () => {
|
||||
const handler = new TestScheduledHandler();
|
||||
|
||||
|
||||
expect(handler).toBeInstanceOf(BaseHandler);
|
||||
expect(handler).toBeInstanceOf(ScheduledHandler);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,237 +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);
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,319 +1,319 @@
|
|||
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 constructor = MyHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('TestHandler');
|
||||
expect(constructor.__needsAutoRegistration).toBe(true);
|
||||
});
|
||||
|
||||
it('should use class name if no name provided', () => {
|
||||
// Handler decorator requires a name parameter
|
||||
@Handler('MyTestHandler')
|
||||
class MyTestHandler {}
|
||||
|
||||
const constructor = MyTestHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('MyTestHandler');
|
||||
});
|
||||
|
||||
it('should work with inheritance', () => {
|
||||
@Handler('BaseHandler')
|
||||
class BaseTestHandler {}
|
||||
|
||||
@Handler('DerivedHandler')
|
||||
class DerivedTestHandler extends BaseTestHandler {}
|
||||
|
||||
const baseConstructor = BaseTestHandler as any;
|
||||
const derivedConstructor = DerivedTestHandler as any;
|
||||
|
||||
expect(baseConstructor.__handlerName).toBe('BaseHandler');
|
||||
expect(derivedConstructor.__handlerName).toBe('DerivedHandler');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Operation', () => {
|
||||
it('should mark method as operation', () => {
|
||||
class TestHandler {
|
||||
@Operation('processData')
|
||||
async process(data: unknown) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__operations).toBeDefined();
|
||||
expect(constructor.__operations).toHaveLength(1);
|
||||
expect(constructor.__operations[0]).toEqual({
|
||||
name: 'processData',
|
||||
method: 'process',
|
||||
batch: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use method name if no name provided', () => {
|
||||
// Operation decorator requires a name parameter
|
||||
class TestHandler {
|
||||
@Operation('processOrder')
|
||||
async processOrder(data: unknown) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__operations).toBeDefined();
|
||||
expect(constructor.__operations[0]).toEqual({
|
||||
name: 'processOrder',
|
||||
method: 'processOrder',
|
||||
batch: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support batch configuration', () => {
|
||||
class TestHandler {
|
||||
@Operation('batchProcess', { batch: { enabled: true, size: 10, delayInHours: 1 } })
|
||||
async processBatch(items: unknown[]) {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__operations).toBeDefined();
|
||||
expect(constructor.__operations[0]).toEqual({
|
||||
name: 'batchProcess',
|
||||
method: 'processBatch',
|
||||
batch: { enabled: true, size: 10, delayInHours: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with multiple operations', () => {
|
||||
class TestHandler {
|
||||
@Operation('op1')
|
||||
async operation1() {}
|
||||
|
||||
@Operation('op2')
|
||||
async operation2() {}
|
||||
|
||||
@Operation('op3')
|
||||
async operation3() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Disabled', () => {
|
||||
it('should mark handler as disabled', () => {
|
||||
@Disabled()
|
||||
@Handler('DisabledHandler')
|
||||
class MyDisabledHandler {}
|
||||
|
||||
const constructor = MyDisabledHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('DisabledHandler');
|
||||
expect(constructor.__disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should work when applied after Handler decorator', () => {
|
||||
@Handler('TestHandler')
|
||||
@Disabled()
|
||||
class MyHandler {}
|
||||
|
||||
const constructor = MyHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('TestHandler');
|
||||
expect(constructor.__disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('@QueueSchedule', () => {
|
||||
it('should add queue schedule to operation', () => {
|
||||
class TestHandler {
|
||||
@QueueSchedule('0 0 * * *')
|
||||
@Operation('dailyTask')
|
||||
async runDaily() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__schedules).toBeDefined();
|
||||
expect(constructor.__schedules[0]).toMatchObject({
|
||||
operation: 'runDaily',
|
||||
cronPattern: '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 constructor = TestHandler as any;
|
||||
|
||||
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('syncData', '*/5 * * * *', {
|
||||
priority: 10,
|
||||
immediately: true,
|
||||
description: 'Sync data every 5 minutes',
|
||||
})
|
||||
async syncOperation() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
// ScheduledOperation creates both an operation and a schedule
|
||||
expect(constructor.__operations).toBeDefined();
|
||||
expect(constructor.__operations[0]).toMatchObject({
|
||||
name: 'syncData',
|
||||
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', () => {
|
||||
class TestHandler {
|
||||
@ScheduledOperation('dailyCleanup', '0 0 * * *')
|
||||
async dailyCleanup() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__operations[0]).toMatchObject({
|
||||
name: 'dailyCleanup',
|
||||
method: 'dailyCleanup',
|
||||
});
|
||||
expect(constructor.__schedules[0]).toMatchObject({
|
||||
operation: 'dailyCleanup',
|
||||
cronPattern: '0 0 * * *',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple scheduled operations', () => {
|
||||
class TestHandler {
|
||||
@ScheduledOperation('hourlyCheck', '0 * * * *')
|
||||
async hourlyCheck() {}
|
||||
|
||||
@ScheduledOperation('dailyReport', '0 0 * * *')
|
||||
async dailyReport() {}
|
||||
|
||||
@ScheduledOperation('weeklyAnalysis', '0 0 * * 0')
|
||||
async weeklyAnalysis() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('decorator composition', () => {
|
||||
it('should work with all decorators combined', () => {
|
||||
@Handler('ComplexHandler')
|
||||
class MyComplexHandler {
|
||||
@Operation('complexOp', { batch: { enabled: true, size: 5 } })
|
||||
@QueueSchedule('0 */6 * * *')
|
||||
async complexOperation(items: unknown[]) {
|
||||
return items;
|
||||
}
|
||||
|
||||
@ScheduledOperation('scheduledTask', '0 0 * * *', {
|
||||
priority: 5,
|
||||
description: 'Daily scheduled task',
|
||||
})
|
||||
async scheduledTask() {}
|
||||
}
|
||||
|
||||
const constructor = MyComplexHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('ComplexHandler');
|
||||
|
||||
// Check operations
|
||||
expect(constructor.__operations).toHaveLength(2);
|
||||
expect(constructor.__operations[0]).toMatchObject({
|
||||
name: 'complexOp',
|
||||
method: 'complexOperation',
|
||||
batch: { enabled: true, size: 5 },
|
||||
});
|
||||
expect(constructor.__operations[1]).toMatchObject({
|
||||
name: 'scheduledTask',
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
Disabled,
|
||||
Handler,
|
||||
Operation,
|
||||
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 constructor = MyHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('TestHandler');
|
||||
expect(constructor.__needsAutoRegistration).toBe(true);
|
||||
});
|
||||
|
||||
it('should use class name if no name provided', () => {
|
||||
// Handler decorator requires a name parameter
|
||||
@Handler('MyTestHandler')
|
||||
class MyTestHandler {}
|
||||
|
||||
const constructor = MyTestHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('MyTestHandler');
|
||||
});
|
||||
|
||||
it('should work with inheritance', () => {
|
||||
@Handler('BaseHandler')
|
||||
class BaseTestHandler {}
|
||||
|
||||
@Handler('DerivedHandler')
|
||||
class DerivedTestHandler extends BaseTestHandler {}
|
||||
|
||||
const baseConstructor = BaseTestHandler as any;
|
||||
const derivedConstructor = DerivedTestHandler as any;
|
||||
|
||||
expect(baseConstructor.__handlerName).toBe('BaseHandler');
|
||||
expect(derivedConstructor.__handlerName).toBe('DerivedHandler');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Operation', () => {
|
||||
it('should mark method as operation', () => {
|
||||
class TestHandler {
|
||||
@Operation('processData')
|
||||
async process(data: unknown) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__operations).toBeDefined();
|
||||
expect(constructor.__operations).toHaveLength(1);
|
||||
expect(constructor.__operations[0]).toEqual({
|
||||
name: 'processData',
|
||||
method: 'process',
|
||||
batch: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use method name if no name provided', () => {
|
||||
// Operation decorator requires a name parameter
|
||||
class TestHandler {
|
||||
@Operation('processOrder')
|
||||
async processOrder(data: unknown) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__operations).toBeDefined();
|
||||
expect(constructor.__operations[0]).toEqual({
|
||||
name: 'processOrder',
|
||||
method: 'processOrder',
|
||||
batch: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support batch configuration', () => {
|
||||
class TestHandler {
|
||||
@Operation('batchProcess', { batch: { enabled: true, size: 10, delayInHours: 1 } })
|
||||
async processBatch(items: unknown[]) {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__operations).toBeDefined();
|
||||
expect(constructor.__operations[0]).toEqual({
|
||||
name: 'batchProcess',
|
||||
method: 'processBatch',
|
||||
batch: { enabled: true, size: 10, delayInHours: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with multiple operations', () => {
|
||||
class TestHandler {
|
||||
@Operation('op1')
|
||||
async operation1() {}
|
||||
|
||||
@Operation('op2')
|
||||
async operation2() {}
|
||||
|
||||
@Operation('op3')
|
||||
async operation3() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Disabled', () => {
|
||||
it('should mark handler as disabled', () => {
|
||||
@Disabled()
|
||||
@Handler('DisabledHandler')
|
||||
class MyDisabledHandler {}
|
||||
|
||||
const constructor = MyDisabledHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('DisabledHandler');
|
||||
expect(constructor.__disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should work when applied after Handler decorator', () => {
|
||||
@Handler('TestHandler')
|
||||
@Disabled()
|
||||
class MyHandler {}
|
||||
|
||||
const constructor = MyHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('TestHandler');
|
||||
expect(constructor.__disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('@QueueSchedule', () => {
|
||||
it('should add queue schedule to operation', () => {
|
||||
class TestHandler {
|
||||
@QueueSchedule('0 0 * * *')
|
||||
@Operation('dailyTask')
|
||||
async runDaily() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__schedules).toBeDefined();
|
||||
expect(constructor.__schedules[0]).toMatchObject({
|
||||
operation: 'runDaily',
|
||||
cronPattern: '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 constructor = TestHandler as any;
|
||||
|
||||
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('syncData', '*/5 * * * *', {
|
||||
priority: 10,
|
||||
immediately: true,
|
||||
description: 'Sync data every 5 minutes',
|
||||
})
|
||||
async syncOperation() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
// ScheduledOperation creates both an operation and a schedule
|
||||
expect(constructor.__operations).toBeDefined();
|
||||
expect(constructor.__operations[0]).toMatchObject({
|
||||
name: 'syncData',
|
||||
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', () => {
|
||||
class TestHandler {
|
||||
@ScheduledOperation('dailyCleanup', '0 0 * * *')
|
||||
async dailyCleanup() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
expect(constructor.__operations[0]).toMatchObject({
|
||||
name: 'dailyCleanup',
|
||||
method: 'dailyCleanup',
|
||||
});
|
||||
expect(constructor.__schedules[0]).toMatchObject({
|
||||
operation: 'dailyCleanup',
|
||||
cronPattern: '0 0 * * *',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple scheduled operations', () => {
|
||||
class TestHandler {
|
||||
@ScheduledOperation('hourlyCheck', '0 * * * *')
|
||||
async hourlyCheck() {}
|
||||
|
||||
@ScheduledOperation('dailyReport', '0 0 * * *')
|
||||
async dailyReport() {}
|
||||
|
||||
@ScheduledOperation('weeklyAnalysis', '0 0 * * 0')
|
||||
async weeklyAnalysis() {}
|
||||
}
|
||||
|
||||
const constructor = TestHandler as any;
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('decorator composition', () => {
|
||||
it('should work with all decorators combined', () => {
|
||||
@Handler('ComplexHandler')
|
||||
class MyComplexHandler {
|
||||
@Operation('complexOp', { batch: { enabled: true, size: 5 } })
|
||||
@QueueSchedule('0 */6 * * *')
|
||||
async complexOperation(items: unknown[]) {
|
||||
return items;
|
||||
}
|
||||
|
||||
@ScheduledOperation('scheduledTask', '0 0 * * *', {
|
||||
priority: 5,
|
||||
description: 'Daily scheduled task',
|
||||
})
|
||||
async scheduledTask() {}
|
||||
}
|
||||
|
||||
const constructor = MyComplexHandler as any;
|
||||
|
||||
expect(constructor.__handlerName).toBe('ComplexHandler');
|
||||
|
||||
// Check operations
|
||||
expect(constructor.__operations).toHaveLength(2);
|
||||
expect(constructor.__operations[0]).toMatchObject({
|
||||
name: 'complexOp',
|
||||
method: 'complexOperation',
|
||||
batch: { enabled: true, size: 5 },
|
||||
});
|
||||
expect(constructor.__operations[1]).toMatchObject({
|
||||
name: 'scheduledTask',
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test';
|
||||
import type { CacheProvider } from '@stock-bot/cache';
|
||||
import type { Logger } from '@stock-bot/logger';
|
||||
import type { Queue, QueueManager } from '@stock-bot/queue';
|
||||
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 {
|
||||
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>;
|
||||
|
|
@ -278,11 +283,13 @@ describe('createJobHandler', () => {
|
|||
it('should create a job handler', async () => {
|
||||
type TestPayload = { data: string };
|
||||
type TestResult = { success: boolean; payload: TestPayload };
|
||||
|
||||
const handlerFn = mock(async (payload: TestPayload): Promise<TestResult> => ({
|
||||
success: true,
|
||||
payload
|
||||
}));
|
||||
|
||||
const handlerFn = mock(
|
||||
async (payload: TestPayload): Promise<TestResult> => ({
|
||||
success: true,
|
||||
payload,
|
||||
})
|
||||
);
|
||||
const jobHandler = createJobHandler(handlerFn);
|
||||
|
||||
const result = await jobHandler({ data: 'test' });
|
||||
|
|
@ -299,4 +306,4 @@ describe('createJobHandler', () => {
|
|||
|
||||
await expect(jobHandler({})).rejects.toThrow('Handler error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue