fixed format issues
This commit is contained in:
parent
a700818a06
commit
08f713d98b
55 changed files with 5680 additions and 5533 deletions
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue