fixed format issues

This commit is contained in:
Boki 2025-06-26 16:12:27 -04:00
parent a700818a06
commit 08f713d98b
55 changed files with 5680 additions and 5533 deletions

View file

@ -1,78 +1,75 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
import { BaseHandler } from '../src/base/BaseHandler';
import type { IServiceContainer } from '@stock-bot/types';
describe('Auto Registration - Simple Tests', () => {
describe('autoRegisterHandlers', () => {
it('should return empty results for non-existent directory', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./non-existent-directory', mockServices);
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
it('should handle directory with no handler files', async () => {
const mockServices = {} as IServiceContainer;
// Use the test directory itself which has no handler files
const result = await autoRegisterHandlers('./test', mockServices);
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
it('should support dry run mode', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./non-existent', mockServices, { dryRun: true });
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
it('should handle excluded patterns', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./test', mockServices, {
exclude: ['test']
});
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
it('should accept custom pattern', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./test', mockServices, {
pattern: '.custom.'
});
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
});
describe('createAutoHandlerRegistry', () => {
it('should create registry with registerDirectory method', () => {
const mockServices = {} as IServiceContainer;
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 multiple directories', async () => {
const mockServices = {} as IServiceContainer;
const registry = createAutoHandlerRegistry(mockServices);
const result = await registry.registerDirectories([
'./non-existent-1',
'./non-existent-2'
]);
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
});
});
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import type { IServiceContainer } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
describe('Auto Registration - Simple Tests', () => {
describe('autoRegisterHandlers', () => {
it('should return empty results for non-existent directory', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./non-existent-directory', mockServices);
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
it('should handle directory with no handler files', async () => {
const mockServices = {} as IServiceContainer;
// Use the test directory itself which has no handler files
const result = await autoRegisterHandlers('./test', mockServices);
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
it('should support dry run mode', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./non-existent', mockServices, { dryRun: true });
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
it('should handle excluded patterns', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./test', mockServices, {
exclude: ['test'],
});
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
it('should accept custom pattern', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./test', mockServices, {
pattern: '.custom.',
});
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
});
describe('createAutoHandlerRegistry', () => {
it('should create registry with registerDirectory method', () => {
const mockServices = {} as IServiceContainer;
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 multiple directories', async () => {
const mockServices = {} as IServiceContainer;
const registry = createAutoHandlerRegistry(mockServices);
const result = await registry.registerDirectories(['./non-existent-1', './non-existent-2']);
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);
});
});
});

View file

@ -1,219 +1,204 @@
import { describe, expect, it, mock } from 'bun:test';
import { BaseHandler } from '../src/base/BaseHandler';
// Test the internal functions by mocking module imports
describe('Auto Registration Unit Tests', () => {
describe('extractHandlerClasses', () => {
it('should extract handler classes from module', () => {
// Test handler class
class TestHandler extends BaseHandler {}
class AnotherHandler extends BaseHandler {}
class NotAHandler {}
const module = {
TestHandler,
AnotherHandler,
NotAHandler,
someFunction: () => {},
someVariable: 42,
};
// Access the private function through module internals
const autoRegister = require('../src/registry/auto-register');
// Mock the extractHandlerClasses function behavior
const handlers = [];
for (const key of Object.keys(module)) {
const exported = module[key];
if (
typeof exported === 'function' &&
exported.prototype &&
exported.prototype instanceof BaseHandler
) {
handlers.push(exported);
}
}
expect(handlers).toHaveLength(2);
expect(handlers).toContain(TestHandler);
expect(handlers).toContain(AnotherHandler);
expect(handlers).not.toContain(NotAHandler);
});
});
describe('findHandlerFiles', () => {
it('should filter files by pattern', () => {
const files = [
'test.handler.ts',
'test.service.ts',
'another.handler.ts',
'test.handler.js',
'.hidden.handler.ts',
];
const pattern = '.handler.';
const filtered = files.filter(file =>
file.includes(pattern) &&
file.endsWith('.ts') &&
!file.startsWith('.')
);
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
});
it('should handle different patterns', () => {
const files = [
'test.handler.ts',
'test.custom.ts',
'another.custom.ts',
];
const customPattern = '.custom.';
const filtered = files.filter(file =>
file.includes(customPattern) &&
file.endsWith('.ts')
);
expect(filtered).toEqual(['test.custom.ts', 'another.custom.ts']);
});
});
describe('Handler Registration Logic', () => {
it('should skip disabled handlers', () => {
class DisabledHandler extends BaseHandler {
static __disabled = true;
}
class EnabledHandler extends BaseHandler {}
const handlers = [DisabledHandler, EnabledHandler];
const registered = handlers.filter(h => !(h as any).__disabled);
expect(registered).toHaveLength(1);
expect(registered).toContain(EnabledHandler);
expect(registered).not.toContain(DisabledHandler);
});
it('should handle handler with auto-registration flag', () => {
class AutoRegisterHandler extends BaseHandler {
static __handlerName = 'auto-handler';
static __needsAutoRegistration = true;
}
expect((AutoRegisterHandler as any).__needsAutoRegistration).toBe(true);
expect((AutoRegisterHandler as any).__handlerName).toBe('auto-handler');
});
it('should create handler instance with services', () => {
const mockServices = {
cache: null,
globalCache: null,
queueManager: null,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
} as any;
class TestHandler extends BaseHandler {}
const instance = new TestHandler(mockServices);
expect(instance).toBeInstanceOf(BaseHandler);
});
});
describe('Error Handling', () => {
it('should handle module import errors gracefully', () => {
const errors = [];
const modules = ['valid', 'error', 'another'];
for (const mod of modules) {
try {
if (mod === 'error') {
throw new Error('Module not found');
}
// Process module
} catch (error) {
errors.push(mod);
}
}
expect(errors).toEqual(['error']);
});
it('should handle filesystem errors', () => {
let result;
try {
// Simulate filesystem error
throw new Error('EACCES: permission denied');
} catch (error) {
// Should handle gracefully
result = { registered: [], failed: [] };
}
expect(result).toEqual({ registered: [], failed: [] });
});
});
describe('Options Handling', () => {
it('should apply exclude patterns', () => {
const files = [
'test.handler.ts',
'excluded.handler.ts',
'another.handler.ts',
];
const exclude = ['excluded'];
const filtered = files.filter(file =>
!exclude.some(ex => file.includes(ex))
);
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
});
it('should handle service name option', () => {
const options = {
pattern: '.handler.',
exclude: [],
dryRun: false,
serviceName: 'test-service',
};
expect(options.serviceName).toBe('test-service');
});
it('should handle dry run mode', () => {
const options = { dryRun: true };
const actions = [];
if (options.dryRun) {
actions.push('[DRY RUN] Would register handler');
} else {
actions.push('Registering handler');
}
expect(actions).toEqual(['[DRY RUN] Would register handler']);
});
});
describe('Registry Methods', () => {
it('should handle multiple directories', () => {
const directories = ['./dir1', './dir2', './dir3'];
const results = {
registered: [] as string[],
failed: [] as string[],
};
for (const dir of directories) {
// Simulate processing each directory
results.registered.push(`${dir}-handler`);
}
expect(results.registered).toHaveLength(3);
expect(results.registered).toContain('./dir1-handler');
expect(results.registered).toContain('./dir2-handler');
expect(results.registered).toContain('./dir3-handler');
});
});
});
import { describe, expect, it, mock } from 'bun:test';
import { BaseHandler } from '../src/base/BaseHandler';
// Test the internal functions by mocking module imports
describe('Auto Registration Unit Tests', () => {
describe('extractHandlerClasses', () => {
it('should extract handler classes from module', () => {
// Test handler class
class TestHandler extends BaseHandler {}
class AnotherHandler extends BaseHandler {}
class NotAHandler {}
const module = {
TestHandler,
AnotherHandler,
NotAHandler,
someFunction: () => {},
someVariable: 42,
};
// Access the private function through module internals
const autoRegister = require('../src/registry/auto-register');
// Mock the extractHandlerClasses function behavior
const handlers = [];
for (const key of Object.keys(module)) {
const exported = module[key];
if (
typeof exported === 'function' &&
exported.prototype &&
exported.prototype instanceof BaseHandler
) {
handlers.push(exported);
}
}
expect(handlers).toHaveLength(2);
expect(handlers).toContain(TestHandler);
expect(handlers).toContain(AnotherHandler);
expect(handlers).not.toContain(NotAHandler);
});
});
describe('findHandlerFiles', () => {
it('should filter files by pattern', () => {
const files = [
'test.handler.ts',
'test.service.ts',
'another.handler.ts',
'test.handler.js',
'.hidden.handler.ts',
];
const pattern = '.handler.';
const filtered = files.filter(
file => file.includes(pattern) && file.endsWith('.ts') && !file.startsWith('.')
);
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
});
it('should handle different patterns', () => {
const files = ['test.handler.ts', 'test.custom.ts', 'another.custom.ts'];
const customPattern = '.custom.';
const filtered = files.filter(file => file.includes(customPattern) && file.endsWith('.ts'));
expect(filtered).toEqual(['test.custom.ts', 'another.custom.ts']);
});
});
describe('Handler Registration Logic', () => {
it('should skip disabled handlers', () => {
class DisabledHandler extends BaseHandler {
static __disabled = true;
}
class EnabledHandler extends BaseHandler {}
const handlers = [DisabledHandler, EnabledHandler];
const registered = handlers.filter(h => !(h as any).__disabled);
expect(registered).toHaveLength(1);
expect(registered).toContain(EnabledHandler);
expect(registered).not.toContain(DisabledHandler);
});
it('should handle handler with auto-registration flag', () => {
class AutoRegisterHandler extends BaseHandler {
static __handlerName = 'auto-handler';
static __needsAutoRegistration = true;
}
expect((AutoRegisterHandler as any).__needsAutoRegistration).toBe(true);
expect((AutoRegisterHandler as any).__handlerName).toBe('auto-handler');
});
it('should create handler instance with services', () => {
const mockServices = {
cache: null,
globalCache: null,
queueManager: null,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
} as any;
class TestHandler extends BaseHandler {}
const instance = new TestHandler(mockServices);
expect(instance).toBeInstanceOf(BaseHandler);
});
});
describe('Error Handling', () => {
it('should handle module import errors gracefully', () => {
const errors = [];
const modules = ['valid', 'error', 'another'];
for (const mod of modules) {
try {
if (mod === 'error') {
throw new Error('Module not found');
}
// Process module
} catch (error) {
errors.push(mod);
}
}
expect(errors).toEqual(['error']);
});
it('should handle filesystem errors', () => {
let result;
try {
// Simulate filesystem error
throw new Error('EACCES: permission denied');
} catch (error) {
// Should handle gracefully
result = { registered: [], failed: [] };
}
expect(result).toEqual({ registered: [], failed: [] });
});
});
describe('Options Handling', () => {
it('should apply exclude patterns', () => {
const files = ['test.handler.ts', 'excluded.handler.ts', 'another.handler.ts'];
const exclude = ['excluded'];
const filtered = files.filter(file => !exclude.some(ex => file.includes(ex)));
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
});
it('should handle service name option', () => {
const options = {
pattern: '.handler.',
exclude: [],
dryRun: false,
serviceName: 'test-service',
};
expect(options.serviceName).toBe('test-service');
});
it('should handle dry run mode', () => {
const options = { dryRun: true };
const actions = [];
if (options.dryRun) {
actions.push('[DRY RUN] Would register handler');
} else {
actions.push('Registering handler');
}
expect(actions).toEqual(['[DRY RUN] Would register handler']);
});
});
describe('Registry Methods', () => {
it('should handle multiple directories', () => {
const directories = ['./dir1', './dir2', './dir3'];
const results = {
registered: [] as string[],
failed: [] as string[],
};
for (const dir of directories) {
// Simulate processing each directory
results.registered.push(`${dir}-handler`);
}
expect(results.registered).toHaveLength(3);
expect(results.registered).toContain('./dir1-handler');
expect(results.registered).toContain('./dir2-handler');
expect(results.registered).toContain('./dir3-handler');
});
});
});

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
import { BaseHandler } from '../src/base/BaseHandler';
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { IServiceContainer } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
describe('Auto Registration', () => {
describe('autoRegisterHandlers', () => {
@ -9,7 +9,7 @@ describe('Auto Registration', () => {
const mockServices = {} as IServiceContainer;
// Using a directory that doesn't exist - the function handles this gracefully
const result = await autoRegisterHandlers('./non-existent', mockServices);
expect(result).toHaveProperty('registered');
expect(result).toHaveProperty('failed');
expect(result.registered).toEqual([]);
@ -19,7 +19,7 @@ describe('Auto Registration', () => {
it('should use default options when not provided', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./test', mockServices);
expect(result).toBeDefined();
expect(result.registered).toBeInstanceOf(Array);
expect(result.failed).toBeInstanceOf(Array);
@ -27,7 +27,7 @@ describe('Auto Registration', () => {
it('should handle directory not found gracefully', async () => {
const mockServices = {} as IServiceContainer;
// Should not throw for non-existent directory
const result = await autoRegisterHandlers('./non-existent-directory', mockServices);
expect(result.registered).toEqual([]);
@ -39,7 +39,7 @@ describe('Auto Registration', () => {
it('should create a registry with registerDirectory method', () => {
const mockServices = {} as IServiceContainer;
const registry = createAutoHandlerRegistry(mockServices);
expect(registry).toHaveProperty('registerDirectory');
expect(typeof registry.registerDirectory).toBe('function');
});
@ -47,7 +47,7 @@ describe('Auto Registration', () => {
it('should register from a directory', async () => {
const mockServices = {} as IServiceContainer;
const registry = createAutoHandlerRegistry(mockServices);
const result = await registry.registerDirectory('./non-existent-dir');
expect(result).toHaveProperty('registered');
expect(result).toHaveProperty('failed');
@ -56,7 +56,7 @@ describe('Auto Registration', () => {
it('should register from multiple directories', async () => {
const mockServices = {} as IServiceContainer;
const registry = createAutoHandlerRegistry(mockServices);
const result = await registry.registerDirectories(['./dir1', './dir2']);
expect(result).toHaveProperty('registered');
expect(result).toHaveProperty('failed');
@ -68,7 +68,7 @@ describe('Auto Registration', () => {
describe('Edge Cases', () => {
it('should handle non-existent directories gracefully', async () => {
const mockServices = {} as any;
// Should not throw, just return empty results
const result = await autoRegisterHandlers('./definitely-does-not-exist-12345', mockServices);
expect(result.registered).toEqual([]);
@ -77,7 +77,7 @@ describe('Auto Registration', () => {
it('should handle empty options', async () => {
const mockServices = {} as any;
// Should use default options
const result = await autoRegisterHandlers('./test', mockServices, {});
expect(result).toBeDefined();
@ -87,18 +87,18 @@ describe('Auto Registration', () => {
it('should support service name in options', async () => {
const mockServices = {} as any;
const result = await autoRegisterHandlers('./test', mockServices, {
serviceName: 'test-service'
serviceName: 'test-service',
});
expect(result).toBeDefined();
});
it('should handle dry run mode', async () => {
const mockServices = {} as any;
const result = await autoRegisterHandlers('./test', mockServices, { dryRun: true });
expect(result).toBeDefined();
expect(result.registered).toBeInstanceOf(Array);
expect(result.failed).toBeInstanceOf(Array);
@ -106,10 +106,10 @@ describe('Auto Registration', () => {
it('should handle excluded files', async () => {
const mockServices = {} as any;
const result = await autoRegisterHandlers('./test', mockServices, {
exclude: ['test']
const result = await autoRegisterHandlers('./test', mockServices, {
exclude: ['test'],
});
expect(result).toBeDefined();
expect(result.registered).toBeInstanceOf(Array);
expect(result.failed).toBeInstanceOf(Array);
@ -118,7 +118,7 @@ describe('Auto Registration', () => {
it('should handle custom pattern', async () => {
const mockServices = {} as any;
const result = await autoRegisterHandlers('./test', mockServices, { pattern: '.custom.' });
expect(result).toBeDefined();
expect(result.registered).toBeInstanceOf(Array);
expect(result.failed).toBeInstanceOf(Array);
@ -126,13 +126,13 @@ describe('Auto Registration', () => {
it('should handle errors gracefully', async () => {
const mockServices = {} as any;
// Even with a protected directory, it should handle gracefully
const result = await autoRegisterHandlers('./protected-dir', mockServices);
expect(result).toBeDefined();
expect(result.registered).toBeInstanceOf(Array);
expect(result.failed).toBeInstanceOf(Array);
});
});
});
});

View file

@ -1,215 +1,215 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { BaseHandler } from '../src/base/BaseHandler';
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
// Test handler with metadata
class ConfigTestHandler extends BaseHandler {
static __handlerName = 'config-test';
static __operations = [
{ name: 'process', method: 'processData' },
{ name: 'validate', method: 'validateData' },
];
static __schedules = [
{
operation: 'processData',
cronPattern: '0 * * * *',
priority: 5,
immediately: false,
description: 'Hourly processing',
payload: { type: 'scheduled' },
batch: { size: 100 },
},
];
static __description = 'Test handler for configuration';
async processData(input: any, context: ExecutionContext) {
return { processed: true, input };
}
async validateData(input: any, context: ExecutionContext) {
return { valid: true, input };
}
}
// Handler without metadata
class NoMetadataHandler extends BaseHandler {}
describe('BaseHandler Configuration', () => {
let mockServices: IServiceContainer;
beforeEach(() => {
mockServices = {
cache: null,
globalCache: null,
queueManager: null,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
} as any;
});
describe('createHandlerConfig', () => {
it('should create handler config from metadata', () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
expect(config.name).toBe('config-test');
expect(Object.keys(config.operations)).toEqual(['process', 'validate']);
expect(config.scheduledJobs).toHaveLength(1);
});
it('should create job handlers for operations', () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
expect(typeof config.operations.process).toBe('function');
expect(typeof config.operations.validate).toBe('function');
});
it('should include scheduled job details', () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
const scheduledJob = config.scheduledJobs[0];
expect(scheduledJob.type).toBe('config-test-processData');
expect(scheduledJob.operation).toBe('process');
expect(scheduledJob.cronPattern).toBe('0 * * * *');
expect(scheduledJob.priority).toBe(5);
expect(scheduledJob.immediately).toBe(false);
expect(scheduledJob.description).toBe('Hourly processing');
expect(scheduledJob.payload).toEqual({ type: 'scheduled' });
expect(scheduledJob.batch).toEqual({ size: 100 });
});
it('should execute operations through job handlers', async () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
// Mock the job execution
const processJob = config.operations.process;
const result = await processJob({ data: 'test' }, {} as any);
expect(result).toEqual({ processed: true, input: { data: 'test' } });
});
it('should throw error when no metadata found', () => {
const handler = new NoMetadataHandler(mockServices);
expect(() => handler.createHandlerConfig()).toThrow('Handler metadata not found');
});
it('should handle schedule without matching operation', () => {
class ScheduleOnlyHandler extends BaseHandler {
static __handlerName = 'schedule-only';
static __operations = [];
static __schedules = [
{
operation: 'nonExistentMethod',
cronPattern: '* * * * *',
},
];
}
const handler = new ScheduleOnlyHandler(mockServices);
const config = handler.createHandlerConfig();
expect(config.operations).toEqual({});
expect(config.scheduledJobs).toHaveLength(1);
expect(config.scheduledJobs[0].operation).toBe('nonExistentMethod');
});
it('should handle empty schedules array', () => {
class NoScheduleHandler extends BaseHandler {
static __handlerName = 'no-schedule';
static __operations = [{ name: 'test', method: 'testMethod' }];
static __schedules = [];
testMethod() {}
}
const handler = new NoScheduleHandler(mockServices);
const config = handler.createHandlerConfig();
expect(config.scheduledJobs).toEqual([]);
expect(config.operations).toHaveProperty('test');
});
it('should create execution context with proper metadata', async () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
// Spy on execute method
const executeSpy = mock();
handler.execute = executeSpy;
executeSpy.mockResolvedValue({ result: 'test' });
// Execute through job handler
await config.operations.process({ input: 'data' }, {} as any);
expect(executeSpy).toHaveBeenCalledWith(
'process',
{ input: 'data' },
expect.objectContaining({
type: 'queue',
metadata: expect.objectContaining({
source: 'queue',
timestamp: expect.any(Number),
}),
})
);
});
});
describe('extractMetadata', () => {
it('should extract complete metadata', () => {
const metadata = ConfigTestHandler.extractMetadata();
expect(metadata).not.toBeNull();
expect(metadata?.name).toBe('config-test');
expect(metadata?.operations).toEqual(['process', 'validate']);
expect(metadata?.description).toBe('Test handler for configuration');
expect(metadata?.scheduledJobs).toHaveLength(1);
});
it('should return null for handler without metadata', () => {
const metadata = NoMetadataHandler.extractMetadata();
expect(metadata).toBeNull();
});
it('should handle missing optional fields', () => {
class MinimalHandler extends BaseHandler {
static __handlerName = 'minimal';
static __operations = [];
}
const metadata = MinimalHandler.extractMetadata();
expect(metadata).not.toBeNull();
expect(metadata?.name).toBe('minimal');
expect(metadata?.operations).toEqual([]);
expect(metadata?.scheduledJobs).toEqual([]);
expect(metadata?.description).toBeUndefined();
});
it('should map schedule operations correctly', () => {
class MappedScheduleHandler extends BaseHandler {
static __handlerName = 'mapped';
static __operations = [
{ name: 'op1', method: 'method1' },
{ name: 'op2', method: 'method2' },
];
static __schedules = [
{ operation: 'method1', cronPattern: '* * * * *' },
{ operation: 'method2', cronPattern: '0 * * * *' },
];
}
const metadata = MappedScheduleHandler.extractMetadata();
expect(metadata?.scheduledJobs[0].operation).toBe('op1');
expect(metadata?.scheduledJobs[1].operation).toBe('op2');
});
});
});
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
// Test handler with metadata
class ConfigTestHandler extends BaseHandler {
static __handlerName = 'config-test';
static __operations = [
{ name: 'process', method: 'processData' },
{ name: 'validate', method: 'validateData' },
];
static __schedules = [
{
operation: 'processData',
cronPattern: '0 * * * *',
priority: 5,
immediately: false,
description: 'Hourly processing',
payload: { type: 'scheduled' },
batch: { size: 100 },
},
];
static __description = 'Test handler for configuration';
async processData(input: any, context: ExecutionContext) {
return { processed: true, input };
}
async validateData(input: any, context: ExecutionContext) {
return { valid: true, input };
}
}
// Handler without metadata
class NoMetadataHandler extends BaseHandler {}
describe('BaseHandler Configuration', () => {
let mockServices: IServiceContainer;
beforeEach(() => {
mockServices = {
cache: null,
globalCache: null,
queueManager: null,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
} as any;
});
describe('createHandlerConfig', () => {
it('should create handler config from metadata', () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
expect(config.name).toBe('config-test');
expect(Object.keys(config.operations)).toEqual(['process', 'validate']);
expect(config.scheduledJobs).toHaveLength(1);
});
it('should create job handlers for operations', () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
expect(typeof config.operations.process).toBe('function');
expect(typeof config.operations.validate).toBe('function');
});
it('should include scheduled job details', () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
const scheduledJob = config.scheduledJobs[0];
expect(scheduledJob.type).toBe('config-test-processData');
expect(scheduledJob.operation).toBe('process');
expect(scheduledJob.cronPattern).toBe('0 * * * *');
expect(scheduledJob.priority).toBe(5);
expect(scheduledJob.immediately).toBe(false);
expect(scheduledJob.description).toBe('Hourly processing');
expect(scheduledJob.payload).toEqual({ type: 'scheduled' });
expect(scheduledJob.batch).toEqual({ size: 100 });
});
it('should execute operations through job handlers', async () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
// Mock the job execution
const processJob = config.operations.process;
const result = await processJob({ data: 'test' }, {} as any);
expect(result).toEqual({ processed: true, input: { data: 'test' } });
});
it('should throw error when no metadata found', () => {
const handler = new NoMetadataHandler(mockServices);
expect(() => handler.createHandlerConfig()).toThrow('Handler metadata not found');
});
it('should handle schedule without matching operation', () => {
class ScheduleOnlyHandler extends BaseHandler {
static __handlerName = 'schedule-only';
static __operations = [];
static __schedules = [
{
operation: 'nonExistentMethod',
cronPattern: '* * * * *',
},
];
}
const handler = new ScheduleOnlyHandler(mockServices);
const config = handler.createHandlerConfig();
expect(config.operations).toEqual({});
expect(config.scheduledJobs).toHaveLength(1);
expect(config.scheduledJobs[0].operation).toBe('nonExistentMethod');
});
it('should handle empty schedules array', () => {
class NoScheduleHandler extends BaseHandler {
static __handlerName = 'no-schedule';
static __operations = [{ name: 'test', method: 'testMethod' }];
static __schedules = [];
testMethod() {}
}
const handler = new NoScheduleHandler(mockServices);
const config = handler.createHandlerConfig();
expect(config.scheduledJobs).toEqual([]);
expect(config.operations).toHaveProperty('test');
});
it('should create execution context with proper metadata', async () => {
const handler = new ConfigTestHandler(mockServices);
const config = handler.createHandlerConfig();
// Spy on execute method
const executeSpy = mock();
handler.execute = executeSpy;
executeSpy.mockResolvedValue({ result: 'test' });
// Execute through job handler
await config.operations.process({ input: 'data' }, {} as any);
expect(executeSpy).toHaveBeenCalledWith(
'process',
{ input: 'data' },
expect.objectContaining({
type: 'queue',
metadata: expect.objectContaining({
source: 'queue',
timestamp: expect.any(Number),
}),
})
);
});
});
describe('extractMetadata', () => {
it('should extract complete metadata', () => {
const metadata = ConfigTestHandler.extractMetadata();
expect(metadata).not.toBeNull();
expect(metadata?.name).toBe('config-test');
expect(metadata?.operations).toEqual(['process', 'validate']);
expect(metadata?.description).toBe('Test handler for configuration');
expect(metadata?.scheduledJobs).toHaveLength(1);
});
it('should return null for handler without metadata', () => {
const metadata = NoMetadataHandler.extractMetadata();
expect(metadata).toBeNull();
});
it('should handle missing optional fields', () => {
class MinimalHandler extends BaseHandler {
static __handlerName = 'minimal';
static __operations = [];
}
const metadata = MinimalHandler.extractMetadata();
expect(metadata).not.toBeNull();
expect(metadata?.name).toBe('minimal');
expect(metadata?.operations).toEqual([]);
expect(metadata?.scheduledJobs).toEqual([]);
expect(metadata?.description).toBeUndefined();
});
it('should map schedule operations correctly', () => {
class MappedScheduleHandler extends BaseHandler {
static __handlerName = 'mapped';
static __operations = [
{ name: 'op1', method: 'method1' },
{ name: 'op2', method: 'method2' },
];
static __schedules = [
{ operation: 'method1', cronPattern: '* * * * *' },
{ operation: 'method2', cronPattern: '0 * * * *' },
];
}
const metadata = MappedScheduleHandler.extractMetadata();
expect(metadata?.scheduledJobs[0].operation).toBe('op1');
expect(metadata?.scheduledJobs[1].operation).toBe('op2');
});
});
});

View file

@ -1,364 +1,366 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
// Test handler implementation
class TestHandler extends BaseHandler {
testMethod(input: any, context: ExecutionContext) {
return { result: 'test', input, context };
}
async onInit() {
// Lifecycle hook
}
protected getScheduledJobPayload(operation: string) {
return { scheduled: true, operation };
}
}
// Handler with no operations
class EmptyHandler extends BaseHandler {}
// Handler with missing method
class BrokenHandler extends BaseHandler {
constructor(services: IServiceContainer) {
super(services);
const ctor = this.constructor as any;
ctor.__operations = [{ name: 'missing', method: 'nonExistentMethod' }];
}
}
describe('BaseHandler Edge Cases', () => {
let mockServices: IServiceContainer;
beforeEach(() => {
mockServices = {
cache: {
get: mock(async () => null),
set: mock(async () => {}),
del: mock(async () => {}),
has: mock(async () => false),
clear: mock(async () => {}),
keys: mock(async () => []),
mget: mock(async () => []),
mset: mock(async () => {}),
mdel: mock(async () => {}),
ttl: mock(async () => -1),
expire: mock(async () => true),
getClientType: () => 'redis',
isConnected: () => true,
},
globalCache: null,
queueManager: {
getQueue: mock(() => ({
add: mock(async () => ({})),
addBulk: mock(async () => []),
pause: mock(async () => {}),
resume: mock(async () => {}),
clean: mock(async () => []),
drain: mock(async () => {}),
obliterate: mock(async () => {}),
close: mock(async () => {}),
isReady: mock(async () => true),
isClosed: () => false,
name: 'test-queue',
})),
},
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
} as any;
});
describe('Constructor Edge Cases', () => {
it('should handle handler without decorator metadata', () => {
const handler = new TestHandler(mockServices);
expect(handler).toBeInstanceOf(BaseHandler);
});
it('should use provided handler name', () => {
const handler = new TestHandler(mockServices, 'custom-handler');
expect(handler).toBeInstanceOf(BaseHandler);
});
it('should handle null queue manager', () => {
const servicesWithoutQueue = { ...mockServices, queueManager: null };
const handler = new TestHandler(servicesWithoutQueue);
expect(handler.queue).toBeUndefined();
});
});
describe('Execute Method Edge Cases', () => {
it('should throw for unknown operation', async () => {
const handler = new TestHandler(mockServices);
const context: ExecutionContext = { type: 'queue', metadata: {} };
await expect(handler.execute('unknownOp', {}, context)).rejects.toThrow('Unknown operation: unknownOp');
});
it('should handle operation with no operations metadata', async () => {
const handler = new EmptyHandler(mockServices);
const context: ExecutionContext = { type: 'queue', metadata: {} };
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow('Unknown operation: anyOp');
});
it('should throw when method is not a function', async () => {
const handler = new BrokenHandler(mockServices);
const context: ExecutionContext = { type: 'queue', metadata: {} };
await expect(handler.execute('missing', {}, context)).rejects.toThrow(
"Operation method 'nonExistentMethod' not found on handler"
);
});
it('should execute operation with proper context', async () => {
const handler = new TestHandler(mockServices);
const ctor = handler.constructor as any;
ctor.__operations = [{ name: 'test', method: 'testMethod' }];
const context: ExecutionContext = {
type: 'queue',
metadata: { source: 'test' }
};
const result = await handler.execute('test', { data: 'test' }, context);
expect(result).toEqual({
result: 'test',
input: { data: 'test' },
context,
});
});
});
describe('Service Helper Methods Edge Cases', () => {
it('should handle missing cache service', async () => {
const servicesWithoutCache = { ...mockServices, cache: null };
const handler = new TestHandler(servicesWithoutCache);
// Should not throw, just return gracefully
await handler['cacheSet']('key', 'value');
const value = await handler['cacheGet']('key');
expect(value).toBeNull();
await handler['cacheDel']('key');
});
it('should handle missing global cache service', async () => {
const handler = new TestHandler(mockServices); // globalCache is already null
await handler['globalCacheSet']('key', 'value');
const value = await handler['globalCacheGet']('key');
expect(value).toBeNull();
await handler['globalCacheDel']('key');
});
it('should handle missing MongoDB service', () => {
const handler = new TestHandler(mockServices);
expect(() => handler['collection']('test')).toThrow('MongoDB service is not available');
});
it('should schedule operation without queue', async () => {
const servicesWithoutQueue = { ...mockServices, queueManager: null };
const handler = new TestHandler(servicesWithoutQueue);
await expect(handler.scheduleOperation('test', {})).rejects.toThrow(
'Queue service is not available for this handler'
);
});
});
describe('Execution Context Creation', () => {
it('should create execution context with metadata', () => {
const handler = new TestHandler(mockServices);
const context = handler['createExecutionContext']('http', { custom: 'data' });
expect(context.type).toBe('http');
expect(context.metadata.custom).toBe('data');
expect(context.metadata.timestamp).toBeDefined();
expect(context.metadata.traceId).toBeDefined();
expect(context.metadata.traceId).toContain('TestHandler');
});
it('should create execution context without metadata', () => {
const handler = new TestHandler(mockServices);
const context = handler['createExecutionContext']('queue');
expect(context.type).toBe('queue');
expect(context.metadata.timestamp).toBeDefined();
expect(context.metadata.traceId).toBeDefined();
});
});
describe('HTTP Helper Edge Cases', () => {
it('should provide HTTP methods', () => {
const handler = new TestHandler(mockServices);
const http = handler['http'];
expect(http.get).toBeDefined();
expect(http.post).toBeDefined();
expect(http.put).toBeDefined();
expect(http.delete).toBeDefined();
// All should be functions
expect(typeof http.get).toBe('function');
expect(typeof http.post).toBe('function');
expect(typeof http.put).toBe('function');
expect(typeof http.delete).toBe('function');
});
});
describe('Static Methods Edge Cases', () => {
it('should return null for handler without metadata', () => {
const metadata = TestHandler.extractMetadata();
expect(metadata).toBeNull();
});
it('should extract metadata with all fields', () => {
const HandlerWithMeta = class extends BaseHandler {
static __handlerName = 'meta-handler';
static __operations = [
{ name: 'op1', method: 'method1' },
{ name: 'op2', method: 'method2' },
];
static __schedules = [
{
operation: 'method1',
cronPattern: '* * * * *',
priority: 10,
immediately: true,
description: 'Test schedule',
payload: { test: true },
batch: { size: 10 },
},
];
static __description = 'Test handler description';
};
const metadata = HandlerWithMeta.extractMetadata();
expect(metadata).toBeDefined();
expect(metadata?.name).toBe('meta-handler');
expect(metadata?.operations).toEqual(['op1', 'op2']);
expect(metadata?.description).toBe('Test handler description');
expect(metadata?.scheduledJobs).toHaveLength(1);
const job = metadata?.scheduledJobs[0];
expect(job?.type).toBe('meta-handler-method1');
expect(job?.operation).toBe('op1');
expect(job?.cronPattern).toBe('* * * * *');
expect(job?.priority).toBe(10);
expect(job?.immediately).toBe(true);
expect(job?.payload).toEqual({ test: true });
expect(job?.batch).toEqual({ size: 10 });
});
});
describe('Handler Configuration Creation', () => {
it('should throw when no metadata found', () => {
const handler = new TestHandler(mockServices);
expect(() => handler.createHandlerConfig()).toThrow('Handler metadata not found');
});
it('should create handler config with operations', () => {
const HandlerWithMeta = class extends BaseHandler {
static __handlerName = 'config-handler';
static __operations = [
{ name: 'process', method: 'processData' },
];
static __schedules = [];
};
const handler = new HandlerWithMeta(mockServices);
const config = handler.createHandlerConfig();
expect(config.name).toBe('config-handler');
expect(config.operations.process).toBeDefined();
expect(typeof config.operations.process).toBe('function');
expect(config.scheduledJobs).toEqual([]);
});
});
describe('Service Availability Check', () => {
it('should correctly identify available services', () => {
const handler = new TestHandler(mockServices);
expect(handler['hasService']('cache')).toBe(true);
expect(handler['hasService']('queueManager')).toBe(true);
expect(handler['hasService']('globalCache')).toBe(false);
expect(handler['hasService']('mongodb')).toBe(false);
});
});
describe('Scheduled Handler Edge Cases', () => {
it('should be instance of BaseHandler', () => {
const handler = new ScheduledHandler(mockServices);
expect(handler).toBeInstanceOf(BaseHandler);
expect(handler).toBeInstanceOf(ScheduledHandler);
});
});
describe('Cache Helpers with Namespacing', () => {
it('should create namespaced cache', () => {
const handler = new TestHandler(mockServices);
const nsCache = handler['createNamespacedCache']('api');
expect(nsCache).toBeDefined();
});
it('should prefix cache keys with handler name', async () => {
const TestHandlerWithName = class extends BaseHandler {
static __handlerName = 'test-handler';
};
const handler = new TestHandlerWithName(mockServices);
await handler['cacheSet']('mykey', 'value', 3600);
expect(mockServices.cache?.set).toHaveBeenCalledWith('test-handler:mykey', 'value', 3600);
});
});
describe('Schedule Helper Methods', () => {
it('should schedule with delay in seconds', async () => {
const handler = new TestHandler(mockServices);
// The queue is already set in the handler constructor
const mockAdd = handler.queue?.add;
await handler['scheduleIn']('test-op', { data: 'test' }, 30, { priority: 10 });
expect(mockAdd).toHaveBeenCalledWith(
'test-op',
{
handler: 'testhandler',
operation: 'test-op',
payload: { data: 'test' },
},
{ delay: 30000, priority: 10 }
);
});
});
describe('Logging Helper', () => {
it('should log with handler context', () => {
const handler = new TestHandler(mockServices);
// The log method should exist
expect(typeof handler['log']).toBe('function');
// It should be callable without errors
expect(() => {
handler['log']('info', 'Test message', { extra: 'data' });
}).not.toThrow();
});
});
});
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
// Test handler implementation
class TestHandler extends BaseHandler {
testMethod(input: any, context: ExecutionContext) {
return { result: 'test', input, context };
}
async onInit() {
// Lifecycle hook
}
protected getScheduledJobPayload(operation: string) {
return { scheduled: true, operation };
}
}
// Handler with no operations
class EmptyHandler extends BaseHandler {}
// Handler with missing method
class BrokenHandler extends BaseHandler {
constructor(services: IServiceContainer) {
super(services);
const ctor = this.constructor as any;
ctor.__operations = [{ name: 'missing', method: 'nonExistentMethod' }];
}
}
describe('BaseHandler Edge Cases', () => {
let mockServices: IServiceContainer;
beforeEach(() => {
mockServices = {
cache: {
get: mock(async () => null),
set: mock(async () => {}),
del: mock(async () => {}),
has: mock(async () => false),
clear: mock(async () => {}),
keys: mock(async () => []),
mget: mock(async () => []),
mset: mock(async () => {}),
mdel: mock(async () => {}),
ttl: mock(async () => -1),
expire: mock(async () => true),
getClientType: () => 'redis',
isConnected: () => true,
},
globalCache: null,
queueManager: {
getQueue: mock(() => ({
add: mock(async () => ({})),
addBulk: mock(async () => []),
pause: mock(async () => {}),
resume: mock(async () => {}),
clean: mock(async () => []),
drain: mock(async () => {}),
obliterate: mock(async () => {}),
close: mock(async () => {}),
isReady: mock(async () => true),
isClosed: () => false,
name: 'test-queue',
})),
},
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
} as any;
});
describe('Constructor Edge Cases', () => {
it('should handle handler without decorator metadata', () => {
const handler = new TestHandler(mockServices);
expect(handler).toBeInstanceOf(BaseHandler);
});
it('should use provided handler name', () => {
const handler = new TestHandler(mockServices, 'custom-handler');
expect(handler).toBeInstanceOf(BaseHandler);
});
it('should handle null queue manager', () => {
const servicesWithoutQueue = { ...mockServices, queueManager: null };
const handler = new TestHandler(servicesWithoutQueue);
expect(handler.queue).toBeUndefined();
});
});
describe('Execute Method Edge Cases', () => {
it('should throw for unknown operation', async () => {
const handler = new TestHandler(mockServices);
const context: ExecutionContext = { type: 'queue', metadata: {} };
await expect(handler.execute('unknownOp', {}, context)).rejects.toThrow(
'Unknown operation: unknownOp'
);
});
it('should handle operation with no operations metadata', async () => {
const handler = new EmptyHandler(mockServices);
const context: ExecutionContext = { type: 'queue', metadata: {} };
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow(
'Unknown operation: anyOp'
);
});
it('should throw when method is not a function', async () => {
const handler = new BrokenHandler(mockServices);
const context: ExecutionContext = { type: 'queue', metadata: {} };
await expect(handler.execute('missing', {}, context)).rejects.toThrow(
"Operation method 'nonExistentMethod' not found on handler"
);
});
it('should execute operation with proper context', async () => {
const handler = new TestHandler(mockServices);
const ctor = handler.constructor as any;
ctor.__operations = [{ name: 'test', method: 'testMethod' }];
const context: ExecutionContext = {
type: 'queue',
metadata: { source: 'test' },
};
const result = await handler.execute('test', { data: 'test' }, context);
expect(result).toEqual({
result: 'test',
input: { data: 'test' },
context,
});
});
});
describe('Service Helper Methods Edge Cases', () => {
it('should handle missing cache service', async () => {
const servicesWithoutCache = { ...mockServices, cache: null };
const handler = new TestHandler(servicesWithoutCache);
// Should not throw, just return gracefully
await handler['cacheSet']('key', 'value');
const value = await handler['cacheGet']('key');
expect(value).toBeNull();
await handler['cacheDel']('key');
});
it('should handle missing global cache service', async () => {
const handler = new TestHandler(mockServices); // globalCache is already null
await handler['globalCacheSet']('key', 'value');
const value = await handler['globalCacheGet']('key');
expect(value).toBeNull();
await handler['globalCacheDel']('key');
});
it('should handle missing MongoDB service', () => {
const handler = new TestHandler(mockServices);
expect(() => handler['collection']('test')).toThrow('MongoDB service is not available');
});
it('should schedule operation without queue', async () => {
const servicesWithoutQueue = { ...mockServices, queueManager: null };
const handler = new TestHandler(servicesWithoutQueue);
await expect(handler.scheduleOperation('test', {})).rejects.toThrow(
'Queue service is not available for this handler'
);
});
});
describe('Execution Context Creation', () => {
it('should create execution context with metadata', () => {
const handler = new TestHandler(mockServices);
const context = handler['createExecutionContext']('http', { custom: 'data' });
expect(context.type).toBe('http');
expect(context.metadata.custom).toBe('data');
expect(context.metadata.timestamp).toBeDefined();
expect(context.metadata.traceId).toBeDefined();
expect(context.metadata.traceId).toContain('TestHandler');
});
it('should create execution context without metadata', () => {
const handler = new TestHandler(mockServices);
const context = handler['createExecutionContext']('queue');
expect(context.type).toBe('queue');
expect(context.metadata.timestamp).toBeDefined();
expect(context.metadata.traceId).toBeDefined();
});
});
describe('HTTP Helper Edge Cases', () => {
it('should provide HTTP methods', () => {
const handler = new TestHandler(mockServices);
const http = handler['http'];
expect(http.get).toBeDefined();
expect(http.post).toBeDefined();
expect(http.put).toBeDefined();
expect(http.delete).toBeDefined();
// All should be functions
expect(typeof http.get).toBe('function');
expect(typeof http.post).toBe('function');
expect(typeof http.put).toBe('function');
expect(typeof http.delete).toBe('function');
});
});
describe('Static Methods Edge Cases', () => {
it('should return null for handler without metadata', () => {
const metadata = TestHandler.extractMetadata();
expect(metadata).toBeNull();
});
it('should extract metadata with all fields', () => {
const HandlerWithMeta = class extends BaseHandler {
static __handlerName = 'meta-handler';
static __operations = [
{ name: 'op1', method: 'method1' },
{ name: 'op2', method: 'method2' },
];
static __schedules = [
{
operation: 'method1',
cronPattern: '* * * * *',
priority: 10,
immediately: true,
description: 'Test schedule',
payload: { test: true },
batch: { size: 10 },
},
];
static __description = 'Test handler description';
};
const metadata = HandlerWithMeta.extractMetadata();
expect(metadata).toBeDefined();
expect(metadata?.name).toBe('meta-handler');
expect(metadata?.operations).toEqual(['op1', 'op2']);
expect(metadata?.description).toBe('Test handler description');
expect(metadata?.scheduledJobs).toHaveLength(1);
const job = metadata?.scheduledJobs[0];
expect(job?.type).toBe('meta-handler-method1');
expect(job?.operation).toBe('op1');
expect(job?.cronPattern).toBe('* * * * *');
expect(job?.priority).toBe(10);
expect(job?.immediately).toBe(true);
expect(job?.payload).toEqual({ test: true });
expect(job?.batch).toEqual({ size: 10 });
});
});
describe('Handler Configuration Creation', () => {
it('should throw when no metadata found', () => {
const handler = new TestHandler(mockServices);
expect(() => handler.createHandlerConfig()).toThrow('Handler metadata not found');
});
it('should create handler config with operations', () => {
const HandlerWithMeta = class extends BaseHandler {
static __handlerName = 'config-handler';
static __operations = [{ name: 'process', method: 'processData' }];
static __schedules = [];
};
const handler = new HandlerWithMeta(mockServices);
const config = handler.createHandlerConfig();
expect(config.name).toBe('config-handler');
expect(config.operations.process).toBeDefined();
expect(typeof config.operations.process).toBe('function');
expect(config.scheduledJobs).toEqual([]);
});
});
describe('Service Availability Check', () => {
it('should correctly identify available services', () => {
const handler = new TestHandler(mockServices);
expect(handler['hasService']('cache')).toBe(true);
expect(handler['hasService']('queueManager')).toBe(true);
expect(handler['hasService']('globalCache')).toBe(false);
expect(handler['hasService']('mongodb')).toBe(false);
});
});
describe('Scheduled Handler Edge Cases', () => {
it('should be instance of BaseHandler', () => {
const handler = new ScheduledHandler(mockServices);
expect(handler).toBeInstanceOf(BaseHandler);
expect(handler).toBeInstanceOf(ScheduledHandler);
});
});
describe('Cache Helpers with Namespacing', () => {
it('should create namespaced cache', () => {
const handler = new TestHandler(mockServices);
const nsCache = handler['createNamespacedCache']('api');
expect(nsCache).toBeDefined();
});
it('should prefix cache keys with handler name', async () => {
const TestHandlerWithName = class extends BaseHandler {
static __handlerName = 'test-handler';
};
const handler = new TestHandlerWithName(mockServices);
await handler['cacheSet']('mykey', 'value', 3600);
expect(mockServices.cache?.set).toHaveBeenCalledWith('test-handler:mykey', 'value', 3600);
});
});
describe('Schedule Helper Methods', () => {
it('should schedule with delay in seconds', async () => {
const handler = new TestHandler(mockServices);
// The queue is already set in the handler constructor
const mockAdd = handler.queue?.add;
await handler['scheduleIn']('test-op', { data: 'test' }, 30, { priority: 10 });
expect(mockAdd).toHaveBeenCalledWith(
'test-op',
{
handler: 'testhandler',
operation: 'test-op',
payload: { data: 'test' },
},
{ delay: 30000, priority: 10 }
);
});
});
describe('Logging Helper', () => {
it('should log with handler context', () => {
const handler = new TestHandler(mockServices);
// The log method should exist
expect(typeof handler['log']).toBe('function');
// It should be callable without errors
expect(() => {
handler['log']('info', 'Test message', { extra: 'data' });
}).not.toThrow();
});
});
});

View file

@ -1,272 +1,290 @@
import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from 'bun:test';
import { BaseHandler } from '../src/base/BaseHandler';
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
import * as utils from '@stock-bot/utils';
// Mock fetch
const mockFetch = mock();
class TestHandler extends BaseHandler {
async testGet(url: string, options?: any) {
return this.http.get(url, options);
}
async testPost(url: string, data?: any, options?: any) {
return this.http.post(url, data, options);
}
async testPut(url: string, data?: any, options?: any) {
return this.http.put(url, data, options);
}
async testDelete(url: string, options?: any) {
return this.http.delete(url, options);
}
}
describe('BaseHandler HTTP Methods', () => {
let handler: TestHandler;
let mockServices: IServiceContainer;
beforeEach(() => {
mockServices = {
cache: null,
globalCache: null,
queueManager: null,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
logger: {
info: mock(),
debug: mock(),
error: mock(),
warn: mock(),
} as any,
} as IServiceContainer;
handler = new TestHandler(mockServices, 'TestHandler');
// Mock utils.fetch
spyOn(utils, 'fetch').mockImplementation(mockFetch);
mockFetch.mockReset();
});
afterEach(() => {
// spyOn automatically restores
});
describe('GET requests', () => {
it('should make GET requests with fetch', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
json: async () => ({ data: 'test' }),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testGet('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data',
expect.objectContaining({
method: 'GET',
logger: expect.any(Object),
})
);
});
it('should pass custom options to GET requests', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testGet('https://api.example.com/data', {
headers: { 'Authorization': 'Bearer token' },
});
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data',
expect.objectContaining({
headers: { 'Authorization': 'Bearer token' },
method: 'GET',
logger: expect.any(Object),
})
);
});
});
describe('POST requests', () => {
it('should make POST requests with JSON data', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
json: async () => ({ success: true }),
};
mockFetch.mockResolvedValue(mockResponse);
const data = { name: 'test', value: 123 };
await handler.testPost('https://api.example.com/create', data);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/create',
expect.objectContaining({
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
logger: expect.any(Object),
})
);
});
it('should merge custom headers in POST requests', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testPost('https://api.example.com/create', { test: 'data' }, {
headers: { 'X-Custom': 'value' },
});
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/create',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ test: 'data' }),
headers: {
'Content-Type': 'application/json',
'X-Custom': 'value',
},
logger: expect.any(Object),
})
);
});
});
describe('PUT requests', () => {
it('should make PUT requests with JSON data', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
const data = { id: 1, name: 'updated' };
await handler.testPut('https://api.example.com/update/1', data);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/update/1',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
logger: expect.any(Object),
})
);
});
it('should handle PUT requests with custom options', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testPut('https://api.example.com/update', { data: 'test' }, {
headers: { 'If-Match': 'etag' },
timeout: 5000,
});
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/update',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ data: 'test' }),
headers: {
'Content-Type': 'application/json',
'If-Match': 'etag',
},
timeout: 5000,
logger: expect.any(Object),
})
);
});
});
describe('DELETE requests', () => {
it('should make DELETE requests', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testDelete('https://api.example.com/delete/1');
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/delete/1',
expect.objectContaining({
method: 'DELETE',
logger: expect.any(Object),
})
);
});
it('should pass options to DELETE requests', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testDelete('https://api.example.com/delete/1', {
headers: { 'Authorization': 'Bearer token' },
});
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/delete/1',
expect.objectContaining({
headers: { 'Authorization': 'Bearer token' },
method: 'DELETE',
logger: expect.any(Object),
})
);
});
});
describe('Error handling', () => {
it('should propagate fetch errors', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow('Network error');
});
it('should handle non-ok responses', async () => {
const mockResponse = {
ok: false,
status: 404,
statusText: 'Not Found',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
const response = await handler.testGet('https://api.example.com/missing');
expect(response.ok).toBe(false);
expect(response.status).toBe(404);
});
});
});
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
import * as utils from '@stock-bot/utils';
import { BaseHandler } from '../src/base/BaseHandler';
// Mock fetch
const mockFetch = mock();
class TestHandler extends BaseHandler {
async testGet(url: string, options?: any) {
return this.http.get(url, options);
}
async testPost(url: string, data?: any, options?: any) {
return this.http.post(url, data, options);
}
async testPut(url: string, data?: any, options?: any) {
return this.http.put(url, data, options);
}
async testDelete(url: string, options?: any) {
return this.http.delete(url, options);
}
}
describe('BaseHandler HTTP Methods', () => {
let handler: TestHandler;
let mockServices: IServiceContainer;
beforeEach(() => {
mockServices = {
cache: null,
globalCache: null,
queueManager: null,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
logger: {
info: mock(),
debug: mock(),
error: mock(),
warn: mock(),
} as any,
} as IServiceContainer;
handler = new TestHandler(mockServices, 'TestHandler');
// Mock utils.fetch
spyOn(utils, 'fetch').mockImplementation(mockFetch);
mockFetch.mockReset();
});
afterEach(() => {
// spyOn automatically restores
});
describe('GET requests', () => {
it('should make GET requests with fetch', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
json: async () => ({ data: 'test' }),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testGet('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
method: 'GET',
logger: expect.any(Object),
})
);
});
it('should pass custom options to GET requests', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testGet('https://api.example.com/data', {
headers: { Authorization: 'Bearer token' },
});
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
headers: { Authorization: 'Bearer token' },
method: 'GET',
logger: expect.any(Object),
})
);
});
});
describe('POST requests', () => {
it('should make POST requests with JSON data', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
json: async () => ({ success: true }),
};
mockFetch.mockResolvedValue(mockResponse);
const data = { name: 'test', value: 123 };
await handler.testPost('https://api.example.com/create', data);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/create',
expect.objectContaining({
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
logger: expect.any(Object),
})
);
});
it('should merge custom headers in POST requests', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testPost(
'https://api.example.com/create',
{ test: 'data' },
{
headers: { 'X-Custom': 'value' },
}
);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/create',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ test: 'data' }),
headers: {
'Content-Type': 'application/json',
'X-Custom': 'value',
},
logger: expect.any(Object),
})
);
});
});
describe('PUT requests', () => {
it('should make PUT requests with JSON data', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
const data = { id: 1, name: 'updated' };
await handler.testPut('https://api.example.com/update/1', data);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/update/1',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
logger: expect.any(Object),
})
);
});
it('should handle PUT requests with custom options', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testPut(
'https://api.example.com/update',
{ data: 'test' },
{
headers: { 'If-Match': 'etag' },
timeout: 5000,
}
);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/update',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ data: 'test' }),
headers: {
'Content-Type': 'application/json',
'If-Match': 'etag',
},
timeout: 5000,
logger: expect.any(Object),
})
);
});
});
describe('DELETE requests', () => {
it('should make DELETE requests', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testDelete('https://api.example.com/delete/1');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/delete/1',
expect.objectContaining({
method: 'DELETE',
logger: expect.any(Object),
})
);
});
it('should pass options to DELETE requests', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testDelete('https://api.example.com/delete/1', {
headers: { Authorization: 'Bearer token' },
});
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/delete/1',
expect.objectContaining({
headers: { Authorization: 'Bearer token' },
method: 'DELETE',
logger: expect.any(Object),
})
);
});
});
describe('Error handling', () => {
it('should propagate fetch errors', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow(
'Network error'
);
});
it('should handle non-ok responses', async () => {
const mockResponse = {
ok: false,
status: 404,
statusText: 'Not Found',
headers: new Headers(),
};
mockFetch.mockResolvedValue(mockResponse);
const response = await handler.testGet('https://api.example.com/missing');
expect(response.ok).toBe(false);
expect(response.status).toBe(404);
});
});
});

View file

@ -1,378 +1,391 @@
import { describe, it, expect } from 'bun:test';
import { Handler, Operation, QueueSchedule, ScheduledOperation, Disabled } from '../src/decorators/decorators';
import { BaseHandler } from '../src/base/BaseHandler';
describe('Decorators Edge Cases', () => {
describe('Handler Decorator', () => {
it('should add metadata to class constructor', () => {
@Handler('test-handler')
class TestHandler extends BaseHandler {}
const ctor = TestHandler as any;
expect(ctor.__handlerName).toBe('test-handler');
expect(ctor.__needsAutoRegistration).toBe(true);
});
it('should handle empty handler name', () => {
@Handler('')
class EmptyNameHandler extends BaseHandler {}
const ctor = EmptyNameHandler as any;
expect(ctor.__handlerName).toBe('');
});
it('should work with context parameter', () => {
const HandlerClass = Handler('with-context')(
class TestClass extends BaseHandler {},
{ kind: 'class' }
);
const ctor = HandlerClass as any;
expect(ctor.__handlerName).toBe('with-context');
});
});
describe('Operation Decorator', () => {
it('should add operation metadata', () => {
class TestHandler extends BaseHandler {
@Operation('test-op')
testMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations).toBeDefined();
expect(ctor.__operations).toHaveLength(1);
expect(ctor.__operations[0]).toEqual({
name: 'test-op',
method: 'testMethod',
batch: undefined,
});
});
it('should handle multiple operations', () => {
class TestHandler extends BaseHandler {
@Operation('op1')
method1() {}
@Operation('op2')
method2() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations).toHaveLength(2);
expect(ctor.__operations.map((op: any) => op.name)).toEqual(['op1', 'op2']);
});
it('should handle batch configuration', () => {
class TestHandler extends BaseHandler {
@Operation('batch-op', {
batch: {
enabled: true,
size: 100,
delayInHours: 24,
priority: 5,
direct: false,
}
})
batchMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations[0].batch).toEqual({
enabled: true,
size: 100,
delayInHours: 24,
priority: 5,
direct: false,
});
});
it('should handle partial batch configuration', () => {
class TestHandler extends BaseHandler {
@Operation('partial-batch', {
batch: {
enabled: true,
size: 50,
}
})
partialBatchMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations[0].batch).toEqual({
enabled: true,
size: 50,
});
});
it('should handle empty operation name', () => {
class TestHandler extends BaseHandler {
@Operation('')
emptyOp() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations[0].name).toBe('');
});
});
describe('QueueSchedule Decorator', () => {
it('should add schedule metadata', () => {
class TestHandler extends BaseHandler {
@QueueSchedule('* * * * *')
scheduledMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__schedules).toBeDefined();
expect(ctor.__schedules).toHaveLength(1);
expect(ctor.__schedules[0]).toEqual({
operation: 'scheduledMethod',
cronPattern: '* * * * *',
});
});
it('should handle full options', () => {
class TestHandler extends BaseHandler {
@QueueSchedule('0 * * * *', {
priority: 10,
immediately: true,
description: 'Hourly job',
payload: { type: 'scheduled' },
batch: {
enabled: true,
size: 200,
delayInHours: 1,
priority: 8,
direct: true,
},
})
hourlyJob() {}
}
const ctor = TestHandler as any;
const schedule = ctor.__schedules[0];
expect(schedule.priority).toBe(10);
expect(schedule.immediately).toBe(true);
expect(schedule.description).toBe('Hourly job');
expect(schedule.payload).toEqual({ type: 'scheduled' });
expect(schedule.batch).toEqual({
enabled: true,
size: 200,
delayInHours: 1,
priority: 8,
direct: true,
});
});
it('should handle invalid cron pattern', () => {
// Decorator doesn't validate - it just stores the pattern
class TestHandler extends BaseHandler {
@QueueSchedule('invalid cron')
invalidSchedule() {}
}
const ctor = TestHandler as any;
expect(ctor.__schedules[0].cronPattern).toBe('invalid cron');
});
it('should handle multiple schedules', () => {
class TestHandler extends BaseHandler {
@QueueSchedule('*/5 * * * *')
every5Minutes() {}
@QueueSchedule('0 0 * * *')
daily() {}
}
const ctor = TestHandler as any;
expect(ctor.__schedules).toHaveLength(2);
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual(['every5Minutes', 'daily']);
});
});
describe('ScheduledOperation Decorator', () => {
it('should apply both Operation and QueueSchedule', () => {
class TestHandler extends BaseHandler {
@ScheduledOperation('combined-op', '*/10 * * * *')
combinedMethod() {}
}
const ctor = TestHandler as any;
// Check operation was added
expect(ctor.__operations).toBeDefined();
expect(ctor.__operations).toHaveLength(1);
expect(ctor.__operations[0].name).toBe('combined-op');
// Check schedule was added
expect(ctor.__schedules).toBeDefined();
expect(ctor.__schedules).toHaveLength(1);
expect(ctor.__schedules[0].cronPattern).toBe('*/10 * * * *');
});
it('should pass batch config to both decorators', () => {
class TestHandler extends BaseHandler {
@ScheduledOperation('batch-scheduled', '0 */6 * * *', {
priority: 7,
immediately: false,
description: 'Every 6 hours',
payload: { scheduled: true },
batch: {
enabled: true,
size: 500,
delayInHours: 6,
},
})
batchScheduledMethod() {}
}
const ctor = TestHandler as any;
// Check operation has batch config
expect(ctor.__operations[0].batch).toEqual({
enabled: true,
size: 500,
delayInHours: 6,
});
// Check schedule has all options
const schedule = ctor.__schedules[0];
expect(schedule.priority).toBe(7);
expect(schedule.immediately).toBe(false);
expect(schedule.description).toBe('Every 6 hours');
expect(schedule.payload).toEqual({ scheduled: true });
expect(schedule.batch).toEqual({
enabled: true,
size: 500,
delayInHours: 6,
});
});
it('should handle minimal configuration', () => {
class TestHandler extends BaseHandler {
@ScheduledOperation('minimal', '* * * * *')
minimalMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations[0]).toEqual({
name: 'minimal',
method: 'minimalMethod',
batch: undefined,
});
expect(ctor.__schedules[0]).toEqual({
operation: 'minimalMethod',
cronPattern: '* * * * *',
});
});
});
describe('Disabled Decorator', () => {
it('should mark handler as disabled', () => {
@Disabled()
@Handler('disabled-handler')
class DisabledHandler extends BaseHandler {}
const ctor = DisabledHandler as any;
expect(ctor.__disabled).toBe(true);
expect(ctor.__handlerName).toBe('disabled-handler');
});
it('should work without Handler decorator', () => {
@Disabled()
class JustDisabled extends BaseHandler {}
const ctor = JustDisabled as any;
expect(ctor.__disabled).toBe(true);
});
it('should work with context parameter', () => {
const DisabledClass = Disabled()(
class TestClass extends BaseHandler {},
{ kind: 'class' }
);
const ctor = DisabledClass as any;
expect(ctor.__disabled).toBe(true);
});
});
describe('Decorator Combinations', () => {
it('should handle all decorators on one class', () => {
@Handler('full-handler')
class FullHandler extends BaseHandler {
@Operation('simple-op')
simpleMethod() {}
@Operation('batch-op', { batch: { enabled: true, size: 50 } })
batchMethod() {}
@QueueSchedule('*/15 * * * *', { priority: 5 })
scheduledOnly() {}
@ScheduledOperation('combined', '0 0 * * *', {
immediately: true,
batch: { enabled: true },
})
combinedMethod() {}
}
const ctor = FullHandler as any;
// Handler metadata
expect(ctor.__handlerName).toBe('full-handler');
expect(ctor.__needsAutoRegistration).toBe(true);
// Operations (3 total - simple, batch, and combined)
expect(ctor.__operations).toHaveLength(3);
expect(ctor.__operations.map((op: any) => op.name)).toEqual(['simple-op', 'batch-op', 'combined']);
// Schedules (2 total - scheduledOnly and combined)
expect(ctor.__schedules).toHaveLength(2);
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual(['scheduledOnly', 'combinedMethod']);
});
it('should handle disabled handler with operations', () => {
@Disabled()
@Handler('disabled-with-ops')
class DisabledWithOps extends BaseHandler {
@Operation('op1')
method1() {}
@QueueSchedule('* * * * *')
scheduled() {}
}
const ctor = DisabledWithOps as any;
expect(ctor.__disabled).toBe(true);
expect(ctor.__handlerName).toBe('disabled-with-ops');
expect(ctor.__operations).toHaveLength(1);
expect(ctor.__schedules).toHaveLength(1);
});
});
describe('Edge Cases with Method Names', () => {
it('should handle special method names', () => {
class TestHandler extends BaseHandler {
@Operation('toString-op')
toString() {
return 'test';
}
@Operation('valueOf-op')
valueOf() {
return 42;
}
@Operation('hasOwnProperty-op')
hasOwnProperty(v: string | symbol): boolean {
return super.hasOwnProperty(v);
}
}
const ctor = TestHandler as any;
expect(ctor.__operations.map((op: any) => op.method)).toEqual(['toString', 'valueOf', 'hasOwnProperty']);
});
});
});
import { describe, expect, it } from 'bun:test';
import { BaseHandler } from '../src/base/BaseHandler';
import {
Disabled,
Handler,
Operation,
QueueSchedule,
ScheduledOperation,
} from '../src/decorators/decorators';
describe('Decorators Edge Cases', () => {
describe('Handler Decorator', () => {
it('should add metadata to class constructor', () => {
@Handler('test-handler')
class TestHandler extends BaseHandler {}
const ctor = TestHandler as any;
expect(ctor.__handlerName).toBe('test-handler');
expect(ctor.__needsAutoRegistration).toBe(true);
});
it('should handle empty handler name', () => {
@Handler('')
class EmptyNameHandler extends BaseHandler {}
const ctor = EmptyNameHandler as any;
expect(ctor.__handlerName).toBe('');
});
it('should work with context parameter', () => {
const HandlerClass = Handler('with-context')(class TestClass extends BaseHandler {}, {
kind: 'class',
});
const ctor = HandlerClass as any;
expect(ctor.__handlerName).toBe('with-context');
});
});
describe('Operation Decorator', () => {
it('should add operation metadata', () => {
class TestHandler extends BaseHandler {
@Operation('test-op')
testMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations).toBeDefined();
expect(ctor.__operations).toHaveLength(1);
expect(ctor.__operations[0]).toEqual({
name: 'test-op',
method: 'testMethod',
batch: undefined,
});
});
it('should handle multiple operations', () => {
class TestHandler extends BaseHandler {
@Operation('op1')
method1() {}
@Operation('op2')
method2() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations).toHaveLength(2);
expect(ctor.__operations.map((op: any) => op.name)).toEqual(['op1', 'op2']);
});
it('should handle batch configuration', () => {
class TestHandler extends BaseHandler {
@Operation('batch-op', {
batch: {
enabled: true,
size: 100,
delayInHours: 24,
priority: 5,
direct: false,
},
})
batchMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations[0].batch).toEqual({
enabled: true,
size: 100,
delayInHours: 24,
priority: 5,
direct: false,
});
});
it('should handle partial batch configuration', () => {
class TestHandler extends BaseHandler {
@Operation('partial-batch', {
batch: {
enabled: true,
size: 50,
},
})
partialBatchMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations[0].batch).toEqual({
enabled: true,
size: 50,
});
});
it('should handle empty operation name', () => {
class TestHandler extends BaseHandler {
@Operation('')
emptyOp() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations[0].name).toBe('');
});
});
describe('QueueSchedule Decorator', () => {
it('should add schedule metadata', () => {
class TestHandler extends BaseHandler {
@QueueSchedule('* * * * *')
scheduledMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__schedules).toBeDefined();
expect(ctor.__schedules).toHaveLength(1);
expect(ctor.__schedules[0]).toEqual({
operation: 'scheduledMethod',
cronPattern: '* * * * *',
});
});
it('should handle full options', () => {
class TestHandler extends BaseHandler {
@QueueSchedule('0 * * * *', {
priority: 10,
immediately: true,
description: 'Hourly job',
payload: { type: 'scheduled' },
batch: {
enabled: true,
size: 200,
delayInHours: 1,
priority: 8,
direct: true,
},
})
hourlyJob() {}
}
const ctor = TestHandler as any;
const schedule = ctor.__schedules[0];
expect(schedule.priority).toBe(10);
expect(schedule.immediately).toBe(true);
expect(schedule.description).toBe('Hourly job');
expect(schedule.payload).toEqual({ type: 'scheduled' });
expect(schedule.batch).toEqual({
enabled: true,
size: 200,
delayInHours: 1,
priority: 8,
direct: true,
});
});
it('should handle invalid cron pattern', () => {
// Decorator doesn't validate - it just stores the pattern
class TestHandler extends BaseHandler {
@QueueSchedule('invalid cron')
invalidSchedule() {}
}
const ctor = TestHandler as any;
expect(ctor.__schedules[0].cronPattern).toBe('invalid cron');
});
it('should handle multiple schedules', () => {
class TestHandler extends BaseHandler {
@QueueSchedule('*/5 * * * *')
every5Minutes() {}
@QueueSchedule('0 0 * * *')
daily() {}
}
const ctor = TestHandler as any;
expect(ctor.__schedules).toHaveLength(2);
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual(['every5Minutes', 'daily']);
});
});
describe('ScheduledOperation Decorator', () => {
it('should apply both Operation and QueueSchedule', () => {
class TestHandler extends BaseHandler {
@ScheduledOperation('combined-op', '*/10 * * * *')
combinedMethod() {}
}
const ctor = TestHandler as any;
// Check operation was added
expect(ctor.__operations).toBeDefined();
expect(ctor.__operations).toHaveLength(1);
expect(ctor.__operations[0].name).toBe('combined-op');
// Check schedule was added
expect(ctor.__schedules).toBeDefined();
expect(ctor.__schedules).toHaveLength(1);
expect(ctor.__schedules[0].cronPattern).toBe('*/10 * * * *');
});
it('should pass batch config to both decorators', () => {
class TestHandler extends BaseHandler {
@ScheduledOperation('batch-scheduled', '0 */6 * * *', {
priority: 7,
immediately: false,
description: 'Every 6 hours',
payload: { scheduled: true },
batch: {
enabled: true,
size: 500,
delayInHours: 6,
},
})
batchScheduledMethod() {}
}
const ctor = TestHandler as any;
// Check operation has batch config
expect(ctor.__operations[0].batch).toEqual({
enabled: true,
size: 500,
delayInHours: 6,
});
// Check schedule has all options
const schedule = ctor.__schedules[0];
expect(schedule.priority).toBe(7);
expect(schedule.immediately).toBe(false);
expect(schedule.description).toBe('Every 6 hours');
expect(schedule.payload).toEqual({ scheduled: true });
expect(schedule.batch).toEqual({
enabled: true,
size: 500,
delayInHours: 6,
});
});
it('should handle minimal configuration', () => {
class TestHandler extends BaseHandler {
@ScheduledOperation('minimal', '* * * * *')
minimalMethod() {}
}
const ctor = TestHandler as any;
expect(ctor.__operations[0]).toEqual({
name: 'minimal',
method: 'minimalMethod',
batch: undefined,
});
expect(ctor.__schedules[0]).toEqual({
operation: 'minimalMethod',
cronPattern: '* * * * *',
});
});
});
describe('Disabled Decorator', () => {
it('should mark handler as disabled', () => {
@Disabled()
@Handler('disabled-handler')
class DisabledHandler extends BaseHandler {}
const ctor = DisabledHandler as any;
expect(ctor.__disabled).toBe(true);
expect(ctor.__handlerName).toBe('disabled-handler');
});
it('should work without Handler decorator', () => {
@Disabled()
class JustDisabled extends BaseHandler {}
const ctor = JustDisabled as any;
expect(ctor.__disabled).toBe(true);
});
it('should work with context parameter', () => {
const DisabledClass = Disabled()(class TestClass extends BaseHandler {}, { kind: 'class' });
const ctor = DisabledClass as any;
expect(ctor.__disabled).toBe(true);
});
});
describe('Decorator Combinations', () => {
it('should handle all decorators on one class', () => {
@Handler('full-handler')
class FullHandler extends BaseHandler {
@Operation('simple-op')
simpleMethod() {}
@Operation('batch-op', { batch: { enabled: true, size: 50 } })
batchMethod() {}
@QueueSchedule('*/15 * * * *', { priority: 5 })
scheduledOnly() {}
@ScheduledOperation('combined', '0 0 * * *', {
immediately: true,
batch: { enabled: true },
})
combinedMethod() {}
}
const ctor = FullHandler as any;
// Handler metadata
expect(ctor.__handlerName).toBe('full-handler');
expect(ctor.__needsAutoRegistration).toBe(true);
// Operations (3 total - simple, batch, and combined)
expect(ctor.__operations).toHaveLength(3);
expect(ctor.__operations.map((op: any) => op.name)).toEqual([
'simple-op',
'batch-op',
'combined',
]);
// Schedules (2 total - scheduledOnly and combined)
expect(ctor.__schedules).toHaveLength(2);
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual([
'scheduledOnly',
'combinedMethod',
]);
});
it('should handle disabled handler with operations', () => {
@Disabled()
@Handler('disabled-with-ops')
class DisabledWithOps extends BaseHandler {
@Operation('op1')
method1() {}
@QueueSchedule('* * * * *')
scheduled() {}
}
const ctor = DisabledWithOps as any;
expect(ctor.__disabled).toBe(true);
expect(ctor.__handlerName).toBe('disabled-with-ops');
expect(ctor.__operations).toHaveLength(1);
expect(ctor.__schedules).toHaveLength(1);
});
});
describe('Edge Cases with Method Names', () => {
it('should handle special method names', () => {
class TestHandler extends BaseHandler {
@Operation('toString-op')
toString() {
return 'test';
}
@Operation('valueOf-op')
valueOf() {
return 42;
}
@Operation('hasOwnProperty-op')
hasOwnProperty(v: string | symbol): boolean {
return super.hasOwnProperty(v);
}
}
const ctor = TestHandler as any;
expect(ctor.__operations.map((op: any) => op.method)).toEqual([
'toString',
'valueOf',
'hasOwnProperty',
]);
});
});
});

View file

@ -1,103 +1,103 @@
import { describe, it, expect } from 'bun:test';
import * as handlersExports from '../src';
import { BaseHandler, ScheduledHandler } from '../src';
describe('Handlers Package Exports', () => {
it('should export base handler classes', () => {
expect(handlersExports.BaseHandler).toBeDefined();
expect(handlersExports.ScheduledHandler).toBeDefined();
expect(handlersExports.BaseHandler).toBe(BaseHandler);
expect(handlersExports.ScheduledHandler).toBe(ScheduledHandler);
});
it('should export utility functions', () => {
expect(handlersExports.createJobHandler).toBeDefined();
expect(typeof handlersExports.createJobHandler).toBe('function');
});
it('should export decorators', () => {
expect(handlersExports.Handler).toBeDefined();
expect(handlersExports.Operation).toBeDefined();
expect(handlersExports.QueueSchedule).toBeDefined();
expect(handlersExports.ScheduledOperation).toBeDefined();
expect(handlersExports.Disabled).toBeDefined();
// All decorators should be functions
expect(typeof handlersExports.Handler).toBe('function');
expect(typeof handlersExports.Operation).toBe('function');
expect(typeof handlersExports.QueueSchedule).toBe('function');
expect(typeof handlersExports.ScheduledOperation).toBe('function');
expect(typeof handlersExports.Disabled).toBe('function');
});
it('should export auto-registration utilities', () => {
expect(handlersExports.autoRegisterHandlers).toBeDefined();
expect(handlersExports.createAutoHandlerRegistry).toBeDefined();
expect(typeof handlersExports.autoRegisterHandlers).toBe('function');
expect(typeof handlersExports.createAutoHandlerRegistry).toBe('function');
});
it('should export types', () => {
// Type tests - compile-time checks
type TestJobScheduleOptions = handlersExports.JobScheduleOptions;
type TestExecutionContext = handlersExports.ExecutionContext;
type TestIHandler = handlersExports.IHandler;
type TestJobHandler = handlersExports.JobHandler;
type TestScheduledJob = handlersExports.ScheduledJob;
type TestHandlerConfig = handlersExports.HandlerConfig;
type TestHandlerConfigWithSchedule = handlersExports.HandlerConfigWithSchedule;
type TestTypedJobHandler = handlersExports.TypedJobHandler;
type TestHandlerMetadata = handlersExports.HandlerMetadata;
type TestOperationMetadata = handlersExports.OperationMetadata;
type TestIServiceContainer = handlersExports.IServiceContainer;
// Runtime type usage tests
const scheduleOptions: TestJobScheduleOptions = {
pattern: '*/5 * * * *',
priority: 10,
};
const executionContext: TestExecutionContext = {
jobId: 'test-job',
attemptNumber: 1,
maxAttempts: 3,
};
const handlerMetadata: TestHandlerMetadata = {
handlerName: 'TestHandler',
operationName: 'testOperation',
queueName: 'test-queue',
options: {},
};
const operationMetadata: TestOperationMetadata = {
operationName: 'testOp',
handlerName: 'TestHandler',
operationPath: 'test.op',
serviceName: 'test-service',
};
expect(scheduleOptions).toBeDefined();
expect(executionContext).toBeDefined();
expect(handlerMetadata).toBeDefined();
expect(operationMetadata).toBeDefined();
});
it('should have correct class inheritance', () => {
// ScheduledHandler should extend BaseHandler
const mockServices = {
cache: null,
globalCache: null,
queueManager: null,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
} as any;
const handler = new ScheduledHandler(mockServices);
expect(handler).toBeInstanceOf(BaseHandler);
expect(handler).toBeInstanceOf(ScheduledHandler);
});
});
import { describe, expect, it } from 'bun:test';
import * as handlersExports from '../src';
import { BaseHandler, ScheduledHandler } from '../src';
describe('Handlers Package Exports', () => {
it('should export base handler classes', () => {
expect(handlersExports.BaseHandler).toBeDefined();
expect(handlersExports.ScheduledHandler).toBeDefined();
expect(handlersExports.BaseHandler).toBe(BaseHandler);
expect(handlersExports.ScheduledHandler).toBe(ScheduledHandler);
});
it('should export utility functions', () => {
expect(handlersExports.createJobHandler).toBeDefined();
expect(typeof handlersExports.createJobHandler).toBe('function');
});
it('should export decorators', () => {
expect(handlersExports.Handler).toBeDefined();
expect(handlersExports.Operation).toBeDefined();
expect(handlersExports.QueueSchedule).toBeDefined();
expect(handlersExports.ScheduledOperation).toBeDefined();
expect(handlersExports.Disabled).toBeDefined();
// All decorators should be functions
expect(typeof handlersExports.Handler).toBe('function');
expect(typeof handlersExports.Operation).toBe('function');
expect(typeof handlersExports.QueueSchedule).toBe('function');
expect(typeof handlersExports.ScheduledOperation).toBe('function');
expect(typeof handlersExports.Disabled).toBe('function');
});
it('should export auto-registration utilities', () => {
expect(handlersExports.autoRegisterHandlers).toBeDefined();
expect(handlersExports.createAutoHandlerRegistry).toBeDefined();
expect(typeof handlersExports.autoRegisterHandlers).toBe('function');
expect(typeof handlersExports.createAutoHandlerRegistry).toBe('function');
});
it('should export types', () => {
// Type tests - compile-time checks
type TestJobScheduleOptions = handlersExports.JobScheduleOptions;
type TestExecutionContext = handlersExports.ExecutionContext;
type TestIHandler = handlersExports.IHandler;
type TestJobHandler = handlersExports.JobHandler;
type TestScheduledJob = handlersExports.ScheduledJob;
type TestHandlerConfig = handlersExports.HandlerConfig;
type TestHandlerConfigWithSchedule = handlersExports.HandlerConfigWithSchedule;
type TestTypedJobHandler = handlersExports.TypedJobHandler;
type TestHandlerMetadata = handlersExports.HandlerMetadata;
type TestOperationMetadata = handlersExports.OperationMetadata;
type TestIServiceContainer = handlersExports.IServiceContainer;
// Runtime type usage tests
const scheduleOptions: TestJobScheduleOptions = {
pattern: '*/5 * * * *',
priority: 10,
};
const executionContext: TestExecutionContext = {
jobId: 'test-job',
attemptNumber: 1,
maxAttempts: 3,
};
const handlerMetadata: TestHandlerMetadata = {
handlerName: 'TestHandler',
operationName: 'testOperation',
queueName: 'test-queue',
options: {},
};
const operationMetadata: TestOperationMetadata = {
operationName: 'testOp',
handlerName: 'TestHandler',
operationPath: 'test.op',
serviceName: 'test-service',
};
expect(scheduleOptions).toBeDefined();
expect(executionContext).toBeDefined();
expect(handlerMetadata).toBeDefined();
expect(operationMetadata).toBeDefined();
});
it('should have correct class inheritance', () => {
// ScheduledHandler should extend BaseHandler
const mockServices = {
cache: null,
globalCache: null,
queueManager: null,
proxy: null,
browser: null,
mongodb: null,
postgres: null,
questdb: null,
} as any;
const handler = new ScheduledHandler(mockServices);
expect(handler).toBeInstanceOf(BaseHandler);
expect(handler).toBeInstanceOf(ScheduledHandler);
});
});