fixed format issues

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

View file

@ -1,26 +1,26 @@
{ {
"exclude": [ "exclude": [
"**/node_modules/**", "**/node_modules/**",
"**/dist/**", "**/dist/**",
"**/build/**", "**/build/**",
"**/coverage/**", "**/coverage/**",
"**/*.test.ts", "**/*.test.ts",
"**/*.test.js", "**/*.test.js",
"**/*.spec.ts", "**/*.spec.ts",
"**/*.spec.js", "**/*.spec.js",
"**/test/**", "**/test/**",
"**/tests/**", "**/tests/**",
"**/__tests__/**", "**/__tests__/**",
"**/__mocks__/**", "**/__mocks__/**",
"**/setup.ts", "**/setup.ts",
"**/setup.js" "**/setup.js"
], ],
"reporters": ["terminal", "html"], "reporters": ["terminal", "html"],
"thresholds": { "thresholds": {
"lines": 80, "lines": 80,
"functions": 80, "functions": 80,
"branches": 80, "branches": 80,
"statements": 80 "statements": 80
}, },
"outputDir": "coverage" "outputDir": "coverage"
} }

View file

@ -2,8 +2,8 @@
* Handler registration for data pipeline service * Handler registration for data pipeline service
*/ */
import type { IServiceContainer } from '@stock-bot/types';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import type { IServiceContainer } from '@stock-bot/types';
// Import handlers directly // Import handlers directly
import { ExchangesHandler } from './exchanges/exchanges.handler'; import { ExchangesHandler } from './exchanges/exchanges.handler';
import { SymbolsHandler } from './symbols/symbols.handler'; import { SymbolsHandler } from './symbols/symbols.handler';
@ -18,10 +18,7 @@ export async function initializeAllHandlers(container: IServiceContainer): Promi
try { try {
// Register handlers manually // Register handlers manually
const handlers = [ const handlers = [ExchangesHandler, SymbolsHandler];
ExchangesHandler,
SymbolsHandler,
];
for (const Handler of handlers) { for (const Handler of handlers) {
try { try {
@ -38,4 +35,4 @@ export async function initializeAllHandlers(container: IServiceContainer): Promi
logger.error('Handler registration failed', { error }); logger.error('Handler registration failed', { error });
throw error; throw error;
} }
} }

View file

@ -44,11 +44,13 @@ export function createNamespacedCache(
* Type guard to check if cache is available * Type guard to check if cache is available
*/ */
export function isCacheAvailable(cache: unknown): cache is CacheProvider { export function isCacheAvailable(cache: unknown): cache is CacheProvider {
return cache !== null && return (
cache !== undefined && cache !== null &&
typeof cache === 'object' && cache !== undefined &&
'get' in cache && typeof cache === 'object' &&
typeof (cache as CacheProvider).get === 'function'; 'get' in cache &&
typeof (cache as CacheProvider).get === 'function'
);
} }
/** /**

View file

@ -1,6 +1,6 @@
import Redis from 'ioredis'; import Redis from 'ioredis';
import type { RedisConfig } from './types';
import { REDIS_DEFAULTS } from './constants'; import { REDIS_DEFAULTS } from './constants';
import type { RedisConfig } from './types';
interface ConnectionConfig { interface ConnectionConfig {
name: string; name: string;
@ -29,7 +29,7 @@ export class RedisConnectionManager {
*/ */
getConnection(config: ConnectionConfig): Redis { getConnection(config: ConnectionConfig): Redis {
const { name, singleton = true, redisConfig } = config; const { name, singleton = true, redisConfig } = config;
if (singleton) { if (singleton) {
const existing = RedisConnectionManager.connections.get(name); const existing = RedisConnectionManager.connections.get(name);
if (existing) { if (existing) {
@ -38,11 +38,11 @@ export class RedisConnectionManager {
} }
const connection = this.createConnection(redisConfig); const connection = this.createConnection(redisConfig);
if (singleton) { if (singleton) {
RedisConnectionManager.connections.set(name, connection); RedisConnectionManager.connections.set(name, connection);
} }
return connection; return connection;
} }
@ -68,10 +68,8 @@ export class RedisConnectionManager {
* Close all connections * Close all connections
*/ */
static async closeAll(): Promise<void> { static async closeAll(): Promise<void> {
const promises = Array.from(this.connections.values()).map(conn => const promises = Array.from(this.connections.values()).map(conn => conn.quit().catch(() => {}));
conn.quit().catch(() => {})
);
await Promise.all(promises); await Promise.all(promises);
this.connections.clear(); this.connections.clear();
} }
} }

View file

@ -1,16 +1,16 @@
// Cache constants // Cache constants
export const CACHE_DEFAULTS = { export const CACHE_DEFAULTS = {
TTL: 3600, // 1 hour in seconds TTL: 3600, // 1 hour in seconds
KEY_PREFIX: 'cache:', KEY_PREFIX: 'cache:',
SCAN_COUNT: 100, SCAN_COUNT: 100,
} as const; } as const;
// Redis connection constants // Redis connection constants
export const REDIS_DEFAULTS = { export const REDIS_DEFAULTS = {
DB: 0, DB: 0,
MAX_RETRIES: 3, MAX_RETRIES: 3,
RETRY_DELAY: 100, RETRY_DELAY: 100,
CONNECT_TIMEOUT: 10000, CONNECT_TIMEOUT: 10000,
COMMAND_TIMEOUT: 5000, COMMAND_TIMEOUT: 5000,
KEEP_ALIVE: 0, KEEP_ALIVE: 0,
} as const; } as const;

View file

@ -40,4 +40,4 @@ export function createCache(options: CacheOptions): CacheProvider {
// Export only what's actually used // Export only what's actually used
export type { CacheProvider, CacheStats } from './types'; export type { CacheProvider, CacheStats } from './types';
export { NamespacedCache } from './namespaced-cache'; export { NamespacedCache } from './namespaced-cache';
export { createNamespacedCache } from './cache-factory'; export { createNamespacedCache } from './cache-factory';

View file

@ -8,7 +8,8 @@ import type { CacheOptions, CacheProvider, CacheStats } from './types';
*/ */
export class RedisCache implements CacheProvider { export class RedisCache implements CacheProvider {
private redis: Redis; private redis: Redis;
private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } = console; private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } =
console;
private defaultTTL: number; private defaultTTL: number;
private keyPrefix: string; private keyPrefix: string;
private stats: CacheStats = { private stats: CacheStats = {
@ -28,7 +29,7 @@ export class RedisCache implements CacheProvider {
const manager = RedisConnectionManager.getInstance(); const manager = RedisConnectionManager.getInstance();
const name = options.name || 'CACHE'; const name = options.name || 'CACHE';
this.redis = manager.getConnection({ this.redis = manager.getConnection({
name: `${name}-SERVICE`, name: `${name}-SERVICE`,
singleton: options.shared ?? true, singleton: options.shared ?? true,
@ -72,19 +73,21 @@ export class RedisCache implements CacheProvider {
async set<T>( async set<T>(
key: string, key: string,
value: T, value: T,
options?: number | { options?:
ttl?: number; | number
preserveTTL?: boolean; | {
onlyIfExists?: boolean; ttl?: number;
onlyIfNotExists?: boolean; preserveTTL?: boolean;
getOldValue?: boolean; onlyIfExists?: boolean;
} onlyIfNotExists?: boolean;
getOldValue?: boolean;
}
): Promise<T | null> { ): Promise<T | null> {
try { try {
const fullKey = this.getKey(key); const fullKey = this.getKey(key);
const serialized = JSON.stringify(value); const serialized = JSON.stringify(value);
const opts = typeof options === 'number' ? { ttl: options } : options || {}; const opts = typeof options === 'number' ? { ttl: options } : options || {};
let oldValue: T | null = null; let oldValue: T | null = null;
if (opts.getOldValue) { if (opts.getOldValue) {
const existing = await this.redis.get(fullKey); const existing = await this.redis.get(fullKey);
@ -92,9 +95,9 @@ export class RedisCache implements CacheProvider {
oldValue = JSON.parse(existing); oldValue = JSON.parse(existing);
} }
} }
const ttl = opts.ttl ?? this.defaultTTL; const ttl = opts.ttl ?? this.defaultTTL;
if (opts.onlyIfExists) { if (opts.onlyIfExists) {
const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'XX'); const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'XX');
if (!result) { if (!result) {
@ -115,7 +118,7 @@ export class RedisCache implements CacheProvider {
} else { } else {
await this.redis.setex(fullKey, ttl, serialized); await this.redis.setex(fullKey, ttl, serialized);
} }
return oldValue; return oldValue;
} catch (error) { } catch (error) {
this.updateStats(false, true); this.updateStats(false, true);
@ -145,21 +148,21 @@ export class RedisCache implements CacheProvider {
try { try {
const stream = this.redis.scanStream({ const stream = this.redis.scanStream({
match: `${this.keyPrefix}*`, match: `${this.keyPrefix}*`,
count: CACHE_DEFAULTS.SCAN_COUNT count: CACHE_DEFAULTS.SCAN_COUNT,
}); });
const pipeline = this.redis.pipeline(); const pipeline = this.redis.pipeline();
stream.on('data', (keys: string[]) => { stream.on('data', (keys: string[]) => {
if (keys.length) { if (keys.length) {
keys.forEach(key => pipeline.del(key)); keys.forEach(key => pipeline.del(key));
} }
}); });
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
stream.on('end', resolve); stream.on('end', resolve);
stream.on('error', reject); stream.on('error', reject);
}); });
await pipeline.exec(); await pipeline.exec();
} catch (error) { } catch (error) {
this.updateStats(false, true); this.updateStats(false, true);
@ -172,9 +175,9 @@ export class RedisCache implements CacheProvider {
const keys: string[] = []; const keys: string[] = [];
const stream = this.redis.scanStream({ const stream = this.redis.scanStream({
match: `${this.keyPrefix}${pattern}`, match: `${this.keyPrefix}${pattern}`,
count: CACHE_DEFAULTS.SCAN_COUNT count: CACHE_DEFAULTS.SCAN_COUNT,
}); });
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
stream.on('data', (resultKeys: string[]) => { stream.on('data', (resultKeys: string[]) => {
keys.push(...resultKeys.map(k => k.replace(this.keyPrefix, ''))); keys.push(...resultKeys.map(k => k.replace(this.keyPrefix, '')));
@ -182,7 +185,7 @@ export class RedisCache implements CacheProvider {
stream.on('end', resolve); stream.on('end', resolve);
stream.on('error', reject); stream.on('error', reject);
}); });
return keys; return keys;
} catch { } catch {
this.updateStats(false, true); this.updateStats(false, true);
@ -206,8 +209,10 @@ export class RedisCache implements CacheProvider {
} }
async waitForReady(timeout = 5000): Promise<void> { async waitForReady(timeout = 5000): Promise<void> {
if (this.redis.status === 'ready') {return;} if (this.redis.status === 'ready') {
return;
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
reject(new Error(`Redis connection timeout after ${timeout}ms`)); reject(new Error(`Redis connection timeout after ${timeout}ms`));
@ -223,4 +228,4 @@ export class RedisCache implements CacheProvider {
isReady(): boolean { isReady(): boolean {
return this.redis.status === 'ready'; return this.redis.status === 'ready';
} }
} }

View file

@ -113,7 +113,12 @@ export interface CacheOptions {
name?: string; // Name for connection identification name?: string; // Name for connection identification
shared?: boolean; // Whether to use shared connection shared?: boolean; // Whether to use shared connection
redisConfig: RedisConfig; redisConfig: RedisConfig;
logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void }; logger?: {
info?: (...args: unknown[]) => void;
error?: (...args: unknown[]) => void;
warn?: (...args: unknown[]) => void;
debug?: (...args: unknown[]) => void;
};
} }
export interface CacheStats { export interface CacheStats {

View file

@ -1,94 +1,94 @@
import { describe, it, expect } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import { RedisConnectionManager } from '../src/connection-manager'; import { RedisConnectionManager } from '../src/connection-manager';
describe('RedisConnectionManager', () => { describe('RedisConnectionManager', () => {
it('should be a singleton', () => { it('should be a singleton', () => {
const instance1 = RedisConnectionManager.getInstance(); const instance1 = RedisConnectionManager.getInstance();
const instance2 = RedisConnectionManager.getInstance(); const instance2 = RedisConnectionManager.getInstance();
expect(instance1).toBe(instance2); expect(instance1).toBe(instance2);
}); });
it('should create connections', () => { it('should create connections', () => {
const manager = RedisConnectionManager.getInstance(); const manager = RedisConnectionManager.getInstance();
const connection = manager.getConnection({ const connection = manager.getConnection({
name: 'test', name: 'test',
redisConfig: { redisConfig: {
host: 'localhost', host: 'localhost',
port: 6379, port: 6379,
}, },
}); });
expect(connection).toBeDefined(); expect(connection).toBeDefined();
expect(connection.options.host).toBe('localhost'); expect(connection.options.host).toBe('localhost');
expect(connection.options.port).toBe(6379); expect(connection.options.port).toBe(6379);
}); });
it('should reuse singleton connections', () => { it('should reuse singleton connections', () => {
const manager = RedisConnectionManager.getInstance(); const manager = RedisConnectionManager.getInstance();
const conn1 = manager.getConnection({ const conn1 = manager.getConnection({
name: 'shared', name: 'shared',
singleton: true, singleton: true,
redisConfig: { redisConfig: {
host: 'localhost', host: 'localhost',
port: 6379, port: 6379,
}, },
}); });
const conn2 = manager.getConnection({ const conn2 = manager.getConnection({
name: 'shared', name: 'shared',
singleton: true, singleton: true,
redisConfig: { redisConfig: {
host: 'localhost', host: 'localhost',
port: 6379, port: 6379,
}, },
}); });
expect(conn1).toBe(conn2); expect(conn1).toBe(conn2);
}); });
it('should create separate non-singleton connections', () => { it('should create separate non-singleton connections', () => {
const manager = RedisConnectionManager.getInstance(); const manager = RedisConnectionManager.getInstance();
const conn1 = manager.getConnection({ const conn1 = manager.getConnection({
name: 'separate1', name: 'separate1',
singleton: false, singleton: false,
redisConfig: { redisConfig: {
host: 'localhost', host: 'localhost',
port: 6379, port: 6379,
}, },
}); });
const conn2 = manager.getConnection({ const conn2 = manager.getConnection({
name: 'separate2', name: 'separate2',
singleton: false, singleton: false,
redisConfig: { redisConfig: {
host: 'localhost', host: 'localhost',
port: 6379, port: 6379,
}, },
}); });
expect(conn1).not.toBe(conn2); expect(conn1).not.toBe(conn2);
}); });
it('should close all connections', async () => { it('should close all connections', async () => {
const manager = RedisConnectionManager.getInstance(); const manager = RedisConnectionManager.getInstance();
// Create a few connections // Create a few connections
manager.getConnection({ manager.getConnection({
name: 'close-test-1', name: 'close-test-1',
redisConfig: { host: 'localhost', port: 6379 }, redisConfig: { host: 'localhost', port: 6379 },
}); });
manager.getConnection({ manager.getConnection({
name: 'close-test-2', name: 'close-test-2',
redisConfig: { host: 'localhost', port: 6379 }, redisConfig: { host: 'localhost', port: 6379 },
}); });
// Close all // Close all
await RedisConnectionManager.closeAll(); await RedisConnectionManager.closeAll();
// Should not throw // Should not throw
expect(true).toBe(true); expect(true).toBe(true);
}); });
}); });

View file

@ -1,429 +1,429 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test'; import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { NamespacedCache, CacheAdapter } from '../src/namespaced-cache'; import { CacheAdapter, NamespacedCache } from '../src/namespaced-cache';
import type { CacheProvider, ICache } from '../src/types'; import type { CacheProvider, ICache } from '../src/types';
describe('NamespacedCache', () => { describe('NamespacedCache', () => {
let mockCache: CacheProvider; let mockCache: CacheProvider;
let namespacedCache: NamespacedCache; let namespacedCache: NamespacedCache;
beforeEach(() => { beforeEach(() => {
// Create mock base cache // Create mock base cache
mockCache = { mockCache = {
get: mock(async () => null), get: mock(async () => null),
set: mock(async () => null), set: mock(async () => null),
del: mock(async () => {}), del: mock(async () => {}),
exists: mock(async () => false), exists: mock(async () => false),
clear: mock(async () => {}), clear: mock(async () => {}),
keys: mock(async () => []), keys: mock(async () => []),
getStats: mock(() => ({ getStats: mock(() => ({
hits: 100, hits: 100,
misses: 20, misses: 20,
errors: 5, errors: 5,
hitRate: 0.83, hitRate: 0.83,
total: 120, total: 120,
uptime: 3600, uptime: 3600,
})), })),
health: mock(async () => true), health: mock(async () => true),
waitForReady: mock(async () => {}), waitForReady: mock(async () => {}),
isReady: mock(() => true), isReady: mock(() => true),
}; };
// Create namespaced cache // Create namespaced cache
namespacedCache = new NamespacedCache(mockCache, 'test-namespace'); namespacedCache = new NamespacedCache(mockCache, 'test-namespace');
}); });
describe('constructor', () => { describe('constructor', () => {
it('should set namespace and prefix correctly', () => { it('should set namespace and prefix correctly', () => {
expect(namespacedCache.getNamespace()).toBe('test-namespace'); expect(namespacedCache.getNamespace()).toBe('test-namespace');
expect(namespacedCache.getFullPrefix()).toBe('test-namespace:'); expect(namespacedCache.getFullPrefix()).toBe('test-namespace:');
}); });
it('should handle empty namespace', () => { it('should handle empty namespace', () => {
const emptyNamespace = new NamespacedCache(mockCache, ''); const emptyNamespace = new NamespacedCache(mockCache, '');
expect(emptyNamespace.getNamespace()).toBe(''); expect(emptyNamespace.getNamespace()).toBe('');
expect(emptyNamespace.getFullPrefix()).toBe(':'); expect(emptyNamespace.getFullPrefix()).toBe(':');
}); });
}); });
describe('get', () => { describe('get', () => {
it('should prefix key when getting', async () => { it('should prefix key when getting', async () => {
const testData = { value: 'test' }; const testData = { value: 'test' };
(mockCache.get as any).mockResolvedValue(testData); (mockCache.get as any).mockResolvedValue(testData);
const result = await namespacedCache.get('mykey'); const result = await namespacedCache.get('mykey');
expect(mockCache.get).toHaveBeenCalledWith('test-namespace:mykey'); expect(mockCache.get).toHaveBeenCalledWith('test-namespace:mykey');
expect(result).toEqual(testData); expect(result).toEqual(testData);
}); });
it('should handle null values', async () => { it('should handle null values', async () => {
(mockCache.get as any).mockResolvedValue(null); (mockCache.get as any).mockResolvedValue(null);
const result = await namespacedCache.get('nonexistent'); const result = await namespacedCache.get('nonexistent');
expect(mockCache.get).toHaveBeenCalledWith('test-namespace:nonexistent'); expect(mockCache.get).toHaveBeenCalledWith('test-namespace:nonexistent');
expect(result).toBeNull(); expect(result).toBeNull();
}); });
}); });
describe('set', () => { describe('set', () => {
it('should prefix key when setting with ttl number', async () => { it('should prefix key when setting with ttl number', async () => {
const value = { data: 'test' }; const value = { data: 'test' };
const ttl = 3600; const ttl = 3600;
await namespacedCache.set('mykey', value, ttl); await namespacedCache.set('mykey', value, ttl);
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, ttl); expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, ttl);
}); });
it('should prefix key when setting with options object', async () => { it('should prefix key when setting with options object', async () => {
const value = 'test-value'; const value = 'test-value';
const options = { ttl: 7200 }; const options = { ttl: 7200 };
await namespacedCache.set('mykey', value, options); await namespacedCache.set('mykey', value, options);
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, options); expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, options);
}); });
it('should handle set without TTL', async () => { it('should handle set without TTL', async () => {
const value = [1, 2, 3]; const value = [1, 2, 3];
await namespacedCache.set('mykey', value); await namespacedCache.set('mykey', value);
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, undefined); expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, undefined);
}); });
}); });
describe('del', () => { describe('del', () => {
it('should prefix key when deleting', async () => { it('should prefix key when deleting', async () => {
await namespacedCache.del('mykey'); await namespacedCache.del('mykey');
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:mykey'); expect(mockCache.del).toHaveBeenCalledWith('test-namespace:mykey');
}); });
it('should handle multiple deletes', async () => { it('should handle multiple deletes', async () => {
await namespacedCache.del('key1'); await namespacedCache.del('key1');
await namespacedCache.del('key2'); await namespacedCache.del('key2');
expect(mockCache.del).toHaveBeenCalledTimes(2); expect(mockCache.del).toHaveBeenCalledTimes(2);
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key1'); expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key1');
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key2'); expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key2');
}); });
}); });
describe('exists', () => { describe('exists', () => {
it('should prefix key when checking existence', async () => { it('should prefix key when checking existence', async () => {
(mockCache.exists as any).mockResolvedValue(true); (mockCache.exists as any).mockResolvedValue(true);
const result = await namespacedCache.exists('mykey'); const result = await namespacedCache.exists('mykey');
expect(mockCache.exists).toHaveBeenCalledWith('test-namespace:mykey'); expect(mockCache.exists).toHaveBeenCalledWith('test-namespace:mykey');
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false for non-existent keys', async () => { it('should return false for non-existent keys', async () => {
(mockCache.exists as any).mockResolvedValue(false); (mockCache.exists as any).mockResolvedValue(false);
const result = await namespacedCache.exists('nonexistent'); const result = await namespacedCache.exists('nonexistent');
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
describe('keys', () => { describe('keys', () => {
it('should prefix pattern and strip prefix from results', async () => { it('should prefix pattern and strip prefix from results', async () => {
(mockCache.keys as any).mockResolvedValue([ (mockCache.keys as any).mockResolvedValue([
'test-namespace:key1', 'test-namespace:key1',
'test-namespace:key2', 'test-namespace:key2',
'test-namespace:key3', 'test-namespace:key3',
]); ]);
const keys = await namespacedCache.keys('*'); const keys = await namespacedCache.keys('*');
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*'); expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
expect(keys).toEqual(['key1', 'key2', 'key3']); expect(keys).toEqual(['key1', 'key2', 'key3']);
}); });
it('should handle specific patterns', async () => { it('should handle specific patterns', async () => {
(mockCache.keys as any).mockResolvedValue([ (mockCache.keys as any).mockResolvedValue([
'test-namespace:user:123', 'test-namespace:user:123',
'test-namespace:user:456', 'test-namespace:user:456',
]); ]);
const keys = await namespacedCache.keys('user:*'); const keys = await namespacedCache.keys('user:*');
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:user:*'); expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:user:*');
expect(keys).toEqual(['user:123', 'user:456']); expect(keys).toEqual(['user:123', 'user:456']);
}); });
it('should filter out keys from other namespaces', async () => { it('should filter out keys from other namespaces', async () => {
(mockCache.keys as any).mockResolvedValue([ (mockCache.keys as any).mockResolvedValue([
'test-namespace:key1', 'test-namespace:key1',
'other-namespace:key2', 'other-namespace:key2',
'test-namespace:key3', 'test-namespace:key3',
]); ]);
const keys = await namespacedCache.keys('*'); const keys = await namespacedCache.keys('*');
expect(keys).toEqual(['key1', 'key3']); expect(keys).toEqual(['key1', 'key3']);
}); });
it('should handle empty results', async () => { it('should handle empty results', async () => {
(mockCache.keys as any).mockResolvedValue([]); (mockCache.keys as any).mockResolvedValue([]);
const keys = await namespacedCache.keys('nonexistent*'); const keys = await namespacedCache.keys('nonexistent*');
expect(keys).toEqual([]); expect(keys).toEqual([]);
}); });
}); });
describe('clear', () => { describe('clear', () => {
it('should clear only namespaced keys', async () => { it('should clear only namespaced keys', async () => {
(mockCache.keys as any).mockResolvedValue([ (mockCache.keys as any).mockResolvedValue([
'test-namespace:key1', 'test-namespace:key1',
'test-namespace:key2', 'test-namespace:key2',
'test-namespace:key3', 'test-namespace:key3',
]); ]);
await namespacedCache.clear(); await namespacedCache.clear();
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*'); expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
expect(mockCache.del).toHaveBeenCalledTimes(3); expect(mockCache.del).toHaveBeenCalledTimes(3);
expect(mockCache.del).toHaveBeenCalledWith('key1'); expect(mockCache.del).toHaveBeenCalledWith('key1');
expect(mockCache.del).toHaveBeenCalledWith('key2'); expect(mockCache.del).toHaveBeenCalledWith('key2');
expect(mockCache.del).toHaveBeenCalledWith('key3'); expect(mockCache.del).toHaveBeenCalledWith('key3');
}); });
it('should handle empty namespace', async () => { it('should handle empty namespace', async () => {
(mockCache.keys as any).mockResolvedValue([]); (mockCache.keys as any).mockResolvedValue([]);
await namespacedCache.clear(); await namespacedCache.clear();
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*'); expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
expect(mockCache.del).not.toHaveBeenCalled(); expect(mockCache.del).not.toHaveBeenCalled();
}); });
}); });
describe('delegated methods', () => { describe('delegated methods', () => {
it('should delegate getStats', () => { it('should delegate getStats', () => {
const stats = namespacedCache.getStats(); const stats = namespacedCache.getStats();
expect(mockCache.getStats).toHaveBeenCalled(); expect(mockCache.getStats).toHaveBeenCalled();
expect(stats).toEqual({ expect(stats).toEqual({
hits: 100, hits: 100,
misses: 20, misses: 20,
errors: 5, errors: 5,
hitRate: 0.83, hitRate: 0.83,
total: 120, total: 120,
uptime: 3600, uptime: 3600,
}); });
}); });
it('should delegate health', async () => { it('should delegate health', async () => {
const health = await namespacedCache.health(); const health = await namespacedCache.health();
expect(mockCache.health).toHaveBeenCalled(); expect(mockCache.health).toHaveBeenCalled();
expect(health).toBe(true); expect(health).toBe(true);
}); });
it('should delegate waitForReady', async () => { it('should delegate waitForReady', async () => {
await namespacedCache.waitForReady(5000); await namespacedCache.waitForReady(5000);
expect(mockCache.waitForReady).toHaveBeenCalledWith(5000); expect(mockCache.waitForReady).toHaveBeenCalledWith(5000);
}); });
it('should delegate isReady', () => { it('should delegate isReady', () => {
const ready = namespacedCache.isReady(); const ready = namespacedCache.isReady();
expect(mockCache.isReady).toHaveBeenCalled(); expect(mockCache.isReady).toHaveBeenCalled();
expect(ready).toBe(true); expect(ready).toBe(true);
}); });
}); });
describe('edge cases', () => { describe('edge cases', () => {
it('should handle special characters in namespace', () => { it('should handle special characters in namespace', () => {
const specialNamespace = new NamespacedCache(mockCache, 'test:namespace:with:colons'); const specialNamespace = new NamespacedCache(mockCache, 'test:namespace:with:colons');
expect(specialNamespace.getFullPrefix()).toBe('test:namespace:with:colons:'); expect(specialNamespace.getFullPrefix()).toBe('test:namespace:with:colons:');
}); });
it('should handle very long keys', async () => { it('should handle very long keys', async () => {
const longKey = 'a'.repeat(1000); const longKey = 'a'.repeat(1000);
await namespacedCache.get(longKey); await namespacedCache.get(longKey);
expect(mockCache.get).toHaveBeenCalledWith(`test-namespace:${longKey}`); expect(mockCache.get).toHaveBeenCalledWith(`test-namespace:${longKey}`);
}); });
it('should handle errors from underlying cache', async () => { it('should handle errors from underlying cache', async () => {
const error = new Error('Cache error'); const error = new Error('Cache error');
(mockCache.get as any).mockRejectedValue(error); (mockCache.get as any).mockRejectedValue(error);
await expect(namespacedCache.get('key')).rejects.toThrow('Cache error'); await expect(namespacedCache.get('key')).rejects.toThrow('Cache error');
}); });
}); });
}); });
describe('CacheAdapter', () => { describe('CacheAdapter', () => {
let mockICache: ICache; let mockICache: ICache;
let adapter: CacheAdapter; let adapter: CacheAdapter;
beforeEach(() => { beforeEach(() => {
mockICache = { mockICache = {
get: mock(async () => null), get: mock(async () => null),
set: mock(async () => {}), set: mock(async () => {}),
del: mock(async () => {}), del: mock(async () => {}),
exists: mock(async () => false), exists: mock(async () => false),
clear: mock(async () => {}), clear: mock(async () => {}),
keys: mock(async () => []), keys: mock(async () => []),
ping: mock(async () => true), ping: mock(async () => true),
isConnected: mock(() => true), isConnected: mock(() => true),
has: mock(async () => false), has: mock(async () => false),
ttl: mock(async () => -1), ttl: mock(async () => -1),
type: 'memory' as const, type: 'memory' as const,
}; };
adapter = new CacheAdapter(mockICache); adapter = new CacheAdapter(mockICache);
}); });
describe('get', () => { describe('get', () => {
it('should delegate to ICache.get', async () => { it('should delegate to ICache.get', async () => {
const data = { value: 'test' }; const data = { value: 'test' };
(mockICache.get as any).mockResolvedValue(data); (mockICache.get as any).mockResolvedValue(data);
const result = await adapter.get('key'); const result = await adapter.get('key');
expect(mockICache.get).toHaveBeenCalledWith('key'); expect(mockICache.get).toHaveBeenCalledWith('key');
expect(result).toEqual(data); expect(result).toEqual(data);
}); });
}); });
describe('set', () => { describe('set', () => {
it('should handle TTL as number', async () => { it('should handle TTL as number', async () => {
await adapter.set('key', 'value', 3600); await adapter.set('key', 'value', 3600);
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 3600); expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 3600);
}); });
it('should handle TTL as options object', async () => { it('should handle TTL as options object', async () => {
await adapter.set('key', 'value', { ttl: 7200 }); await adapter.set('key', 'value', { ttl: 7200 });
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 7200); expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 7200);
}); });
it('should handle no TTL', async () => { it('should handle no TTL', async () => {
await adapter.set('key', 'value'); await adapter.set('key', 'value');
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', undefined); expect(mockICache.set).toHaveBeenCalledWith('key', 'value', undefined);
}); });
it('should always return null', async () => { it('should always return null', async () => {
const result = await adapter.set('key', 'value'); const result = await adapter.set('key', 'value');
expect(result).toBeNull(); expect(result).toBeNull();
}); });
}); });
describe('del', () => { describe('del', () => {
it('should delegate to ICache.del', async () => { it('should delegate to ICache.del', async () => {
await adapter.del('key'); await adapter.del('key');
expect(mockICache.del).toHaveBeenCalledWith('key'); expect(mockICache.del).toHaveBeenCalledWith('key');
}); });
}); });
describe('exists', () => { describe('exists', () => {
it('should delegate to ICache.exists', async () => { it('should delegate to ICache.exists', async () => {
(mockICache.exists as any).mockResolvedValue(true); (mockICache.exists as any).mockResolvedValue(true);
const result = await adapter.exists('key'); const result = await adapter.exists('key');
expect(mockICache.exists).toHaveBeenCalledWith('key'); expect(mockICache.exists).toHaveBeenCalledWith('key');
expect(result).toBe(true); expect(result).toBe(true);
}); });
}); });
describe('clear', () => { describe('clear', () => {
it('should delegate to ICache.clear', async () => { it('should delegate to ICache.clear', async () => {
await adapter.clear(); await adapter.clear();
expect(mockICache.clear).toHaveBeenCalled(); expect(mockICache.clear).toHaveBeenCalled();
}); });
}); });
describe('keys', () => { describe('keys', () => {
it('should delegate to ICache.keys', async () => { it('should delegate to ICache.keys', async () => {
const keys = ['key1', 'key2']; const keys = ['key1', 'key2'];
(mockICache.keys as any).mockResolvedValue(keys); (mockICache.keys as any).mockResolvedValue(keys);
const result = await adapter.keys('*'); const result = await adapter.keys('*');
expect(mockICache.keys).toHaveBeenCalledWith('*'); expect(mockICache.keys).toHaveBeenCalledWith('*');
expect(result).toEqual(keys); expect(result).toEqual(keys);
}); });
}); });
describe('getStats', () => { describe('getStats', () => {
it('should return default stats', () => { it('should return default stats', () => {
const stats = adapter.getStats(); const stats = adapter.getStats();
expect(stats).toEqual({ expect(stats).toEqual({
hits: 0, hits: 0,
misses: 0, misses: 0,
errors: 0, errors: 0,
hitRate: 0, hitRate: 0,
total: 0, total: 0,
uptime: expect.any(Number), uptime: expect.any(Number),
}); });
}); });
}); });
describe('health', () => { describe('health', () => {
it('should use ping for health check', async () => { it('should use ping for health check', async () => {
(mockICache.ping as any).mockResolvedValue(true); (mockICache.ping as any).mockResolvedValue(true);
const result = await adapter.health(); const result = await adapter.health();
expect(mockICache.ping).toHaveBeenCalled(); expect(mockICache.ping).toHaveBeenCalled();
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should handle ping failures', async () => { it('should handle ping failures', async () => {
(mockICache.ping as any).mockResolvedValue(false); (mockICache.ping as any).mockResolvedValue(false);
const result = await adapter.health(); const result = await adapter.health();
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
describe('waitForReady', () => { describe('waitForReady', () => {
it('should succeed if connected', async () => { it('should succeed if connected', async () => {
(mockICache.isConnected as any).mockReturnValue(true); (mockICache.isConnected as any).mockReturnValue(true);
await expect(adapter.waitForReady()).resolves.toBeUndefined(); await expect(adapter.waitForReady()).resolves.toBeUndefined();
}); });
it('should throw if not connected', async () => { it('should throw if not connected', async () => {
(mockICache.isConnected as any).mockReturnValue(false); (mockICache.isConnected as any).mockReturnValue(false);
await expect(adapter.waitForReady()).rejects.toThrow('Cache not connected'); await expect(adapter.waitForReady()).rejects.toThrow('Cache not connected');
}); });
}); });
describe('isReady', () => { describe('isReady', () => {
it('should delegate to isConnected', () => { it('should delegate to isConnected', () => {
(mockICache.isConnected as any).mockReturnValue(true); (mockICache.isConnected as any).mockReturnValue(true);
const result = adapter.isReady(); const result = adapter.isReady();
expect(mockICache.isConnected).toHaveBeenCalled(); expect(mockICache.isConnected).toHaveBeenCalled();
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false when not connected', () => { it('should return false when not connected', () => {
(mockICache.isConnected as any).mockReturnValue(false); (mockICache.isConnected as any).mockReturnValue(false);
const result = adapter.isReady(); const result = adapter.isReady();
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
}); });

View file

@ -1,37 +1,37 @@
import { describe, it, expect, beforeEach } from 'bun:test'; import { beforeEach, describe, expect, it } from 'bun:test';
import { RedisCache } from '../src/redis-cache'; import { RedisCache } from '../src/redis-cache';
import type { CacheOptions } from '../src/types'; import type { CacheOptions } from '../src/types';
describe('RedisCache Simple', () => { describe('RedisCache Simple', () => {
let cache: RedisCache; let cache: RedisCache;
beforeEach(() => { beforeEach(() => {
const options: CacheOptions = { const options: CacheOptions = {
keyPrefix: 'test:', keyPrefix: 'test:',
ttl: 3600, ttl: 3600,
redisConfig: { host: 'localhost', port: 6379 }, redisConfig: { host: 'localhost', port: 6379 },
}; };
cache = new RedisCache(options); cache = new RedisCache(options);
}); });
describe('Core functionality', () => { describe('Core functionality', () => {
it('should create cache instance', () => { it('should create cache instance', () => {
expect(cache).toBeDefined(); expect(cache).toBeDefined();
expect(cache.isReady).toBeDefined(); expect(cache.isReady).toBeDefined();
expect(cache.get).toBeDefined(); expect(cache.get).toBeDefined();
expect(cache.set).toBeDefined(); expect(cache.set).toBeDefined();
}); });
it('should have stats tracking', () => { it('should have stats tracking', () => {
const stats = cache.getStats(); const stats = cache.getStats();
expect(stats).toMatchObject({ expect(stats).toMatchObject({
hits: 0, hits: 0,
misses: 0, misses: 0,
errors: 0, errors: 0,
hitRate: 0, hitRate: 0,
total: 0, total: 0,
}); });
expect(stats.uptime).toBeGreaterThanOrEqual(0); expect(stats.uptime).toBeGreaterThanOrEqual(0);
}); });
}); });
}); });

View file

@ -1,210 +1,210 @@
import { describe, it, expect, beforeEach } from 'bun:test'; import { beforeEach, describe, expect, it } from 'bun:test';
import { RedisCache } from '../src/redis-cache'; import { RedisCache } from '../src/redis-cache';
import type { CacheOptions } from '../src/types'; import type { CacheOptions } from '../src/types';
describe('RedisCache', () => { describe('RedisCache', () => {
let cache: RedisCache; let cache: RedisCache;
beforeEach(() => { beforeEach(() => {
const options: CacheOptions = { const options: CacheOptions = {
keyPrefix: 'test:', keyPrefix: 'test:',
ttl: 3600, ttl: 3600,
redisConfig: { host: 'localhost', port: 6379 }, redisConfig: { host: 'localhost', port: 6379 },
}; };
cache = new RedisCache(options); cache = new RedisCache(options);
}); });
describe('Core functionality', () => { describe('Core functionality', () => {
it('should create cache instance', () => { it('should create cache instance', () => {
expect(cache).toBeDefined(); expect(cache).toBeDefined();
expect(cache.isReady).toBeDefined(); expect(cache.isReady).toBeDefined();
expect(cache.get).toBeDefined(); expect(cache.get).toBeDefined();
expect(cache.set).toBeDefined(); expect(cache.set).toBeDefined();
}); });
it('should have stats tracking', () => { it('should have stats tracking', () => {
const stats = cache.getStats(); const stats = cache.getStats();
expect(stats).toMatchObject({ expect(stats).toMatchObject({
hits: 0, hits: 0,
misses: 0, misses: 0,
errors: 0, errors: 0,
hitRate: 0, hitRate: 0,
total: 0, total: 0,
}); });
expect(stats.uptime).toBeGreaterThanOrEqual(0); expect(stats.uptime).toBeGreaterThanOrEqual(0);
}); });
}); });
describe('Basic operations', () => { describe('Basic operations', () => {
it('should handle get/set operations', async () => { it('should handle get/set operations', async () => {
const key = 'test-key'; const key = 'test-key';
const value = { foo: 'bar' }; const value = { foo: 'bar' };
// Should return null for non-existent key // Should return null for non-existent key
const miss = await cache.get(key); const miss = await cache.get(key);
expect(miss).toBeNull(); expect(miss).toBeNull();
// Should set and retrieve value // Should set and retrieve value
await cache.set(key, value); await cache.set(key, value);
const retrieved = await cache.get(key); const retrieved = await cache.get(key);
expect(retrieved).toEqual(value); expect(retrieved).toEqual(value);
// Should delete key // Should delete key
await cache.del(key); await cache.del(key);
const deleted = await cache.get(key); const deleted = await cache.get(key);
expect(deleted).toBeNull(); expect(deleted).toBeNull();
}); });
it('should check key existence', async () => { it('should check key existence', async () => {
const key = 'existence-test'; const key = 'existence-test';
expect(await cache.exists(key)).toBe(false); expect(await cache.exists(key)).toBe(false);
await cache.set(key, 'value'); await cache.set(key, 'value');
expect(await cache.exists(key)).toBe(true); expect(await cache.exists(key)).toBe(true);
await cache.del(key); await cache.del(key);
expect(await cache.exists(key)).toBe(false); expect(await cache.exists(key)).toBe(false);
}); });
it('should handle TTL in set operations', async () => { it('should handle TTL in set operations', async () => {
const key = 'ttl-test'; const key = 'ttl-test';
const value = 'test-value'; const value = 'test-value';
// Set with custom TTL as number // Set with custom TTL as number
await cache.set(key, value, 1); await cache.set(key, value, 1);
expect(await cache.get(key)).toBe(value); expect(await cache.get(key)).toBe(value);
// Set with custom TTL in options // Set with custom TTL in options
await cache.set(key, value, { ttl: 2 }); await cache.set(key, value, { ttl: 2 });
expect(await cache.get(key)).toBe(value); expect(await cache.get(key)).toBe(value);
}); });
}); });
describe('Advanced set options', () => { describe('Advanced set options', () => {
it('should handle onlyIfExists option', async () => { it('should handle onlyIfExists option', async () => {
const key = 'conditional-test'; const key = 'conditional-test';
const value1 = 'value1'; const value1 = 'value1';
const value2 = 'value2'; const value2 = 'value2';
// Should not set if key doesn't exist // Should not set if key doesn't exist
await cache.set(key, value1, { onlyIfExists: true }); await cache.set(key, value1, { onlyIfExists: true });
expect(await cache.get(key)).toBeNull(); expect(await cache.get(key)).toBeNull();
// Create the key // Create the key
await cache.set(key, value1); await cache.set(key, value1);
// Should update if key exists // Should update if key exists
await cache.set(key, value2, { onlyIfExists: true }); await cache.set(key, value2, { onlyIfExists: true });
expect(await cache.get(key)).toBe(value2); expect(await cache.get(key)).toBe(value2);
}); });
it('should handle onlyIfNotExists option', async () => { it('should handle onlyIfNotExists option', async () => {
const key = 'nx-test'; const key = 'nx-test';
const value1 = 'value1'; const value1 = 'value1';
const value2 = 'value2'; const value2 = 'value2';
// Should set if key doesn't exist // Should set if key doesn't exist
await cache.set(key, value1, { onlyIfNotExists: true }); await cache.set(key, value1, { onlyIfNotExists: true });
expect(await cache.get(key)).toBe(value1); expect(await cache.get(key)).toBe(value1);
// Should not update if key exists // Should not update if key exists
await cache.set(key, value2, { onlyIfNotExists: true }); await cache.set(key, value2, { onlyIfNotExists: true });
expect(await cache.get(key)).toBe(value1); expect(await cache.get(key)).toBe(value1);
}); });
it('should handle preserveTTL option', async () => { it('should handle preserveTTL option', async () => {
const key = 'preserve-ttl-test'; const key = 'preserve-ttl-test';
const value1 = 'value1'; const value1 = 'value1';
const value2 = 'value2'; const value2 = 'value2';
// Set with short TTL // Set with short TTL
await cache.set(key, value1, 10); await cache.set(key, value1, 10);
// Update preserving TTL // Update preserving TTL
await cache.set(key, value2, { preserveTTL: true }); await cache.set(key, value2, { preserveTTL: true });
expect(await cache.get(key)).toBe(value2); expect(await cache.get(key)).toBe(value2);
}); });
it('should handle getOldValue option', async () => { it('should handle getOldValue option', async () => {
const key = 'old-value-test'; const key = 'old-value-test';
const value1 = 'value1'; const value1 = 'value1';
const value2 = 'value2'; const value2 = 'value2';
// Should return null when no old value // Should return null when no old value
const oldValue1 = await cache.set(key, value1, { getOldValue: true }); const oldValue1 = await cache.set(key, value1, { getOldValue: true });
expect(oldValue1).toBeNull(); expect(oldValue1).toBeNull();
// Should return old value // Should return old value
const oldValue2 = await cache.set(key, value2, { getOldValue: true }); const oldValue2 = await cache.set(key, value2, { getOldValue: true });
expect(oldValue2).toBe(value1); expect(oldValue2).toBe(value1);
}); });
}); });
describe('Error handling', () => { describe('Error handling', () => {
it('should handle errors gracefully in get', async () => { it('should handle errors gracefully in get', async () => {
// Force an error by using invalid JSON // Force an error by using invalid JSON
const badCache = new RedisCache({ const badCache = new RedisCache({
keyPrefix: 'bad:', keyPrefix: 'bad:',
redisConfig: { host: 'localhost', port: 6379 }, redisConfig: { host: 'localhost', port: 6379 },
}); });
// This would normally throw but should return null // This would normally throw but should return null
const result = await badCache.get('non-existent'); const result = await badCache.get('non-existent');
expect(result).toBeNull(); expect(result).toBeNull();
// Check stats updated // Check stats updated
const stats = badCache.getStats(); const stats = badCache.getStats();
expect(stats.misses).toBe(1); expect(stats.misses).toBe(1);
}); });
}); });
describe('Pattern operations', () => { describe('Pattern operations', () => {
it('should find keys by pattern', async () => { it('should find keys by pattern', async () => {
// Clear first to ensure clean state // Clear first to ensure clean state
await cache.clear(); await cache.clear();
await cache.set('user:1', { id: 1 }); await cache.set('user:1', { id: 1 });
await cache.set('user:2', { id: 2 }); await cache.set('user:2', { id: 2 });
await cache.set('post:1', { id: 1 }); await cache.set('post:1', { id: 1 });
const userKeys = await cache.keys('user:*'); const userKeys = await cache.keys('user:*');
expect(userKeys).toHaveLength(2); expect(userKeys).toHaveLength(2);
expect(userKeys).toContain('user:1'); expect(userKeys).toContain('user:1');
expect(userKeys).toContain('user:2'); expect(userKeys).toContain('user:2');
const allKeys = await cache.keys('*'); const allKeys = await cache.keys('*');
expect(allKeys.length).toBeGreaterThanOrEqual(3); expect(allKeys.length).toBeGreaterThanOrEqual(3);
expect(allKeys).toContain('user:1'); expect(allKeys).toContain('user:1');
expect(allKeys).toContain('user:2'); expect(allKeys).toContain('user:2');
expect(allKeys).toContain('post:1'); expect(allKeys).toContain('post:1');
}); });
it('should clear all keys with prefix', async () => { it('should clear all keys with prefix', async () => {
await cache.set('key1', 'value1'); await cache.set('key1', 'value1');
await cache.set('key2', 'value2'); await cache.set('key2', 'value2');
await cache.clear(); await cache.clear();
const keys = await cache.keys('*'); const keys = await cache.keys('*');
expect(keys).toHaveLength(0); expect(keys).toHaveLength(0);
}); });
}); });
describe('Health checks', () => { describe('Health checks', () => {
it('should check health', async () => { it('should check health', async () => {
const healthy = await cache.health(); const healthy = await cache.health();
expect(healthy).toBe(true); expect(healthy).toBe(true);
}); });
it('should check if ready', () => { it('should check if ready', () => {
// May not be ready immediately // May not be ready immediately
const ready = cache.isReady(); const ready = cache.isReady();
expect(typeof ready).toBe('boolean'); expect(typeof ready).toBe('boolean');
}); });
it('should wait for ready', async () => { it('should wait for ready', async () => {
await expect(cache.waitForReady(1000)).resolves.toBeUndefined(); await expect(cache.waitForReady(1000)).resolves.toBeUndefined();
}); });
}); });
}); });

View file

@ -28,10 +28,7 @@ export class ConfigManager<T = Record<string, unknown>> {
this.loaders = options.loaders; this.loaders = options.loaders;
} else { } else {
const configPath = options.configPath || join(process.cwd(), 'config'); const configPath = options.configPath || join(process.cwd(), 'config');
this.loaders = [ this.loaders = [new FileLoader(configPath, this.environment), new EnvLoader('')];
new FileLoader(configPath, this.environment),
new EnvLoader(''),
];
} }
} }
@ -61,7 +58,11 @@ export class ConfigManager<T = Record<string, unknown>> {
const mergedConfig = this.merge(...configs) as T; const mergedConfig = this.merge(...configs) as T;
// Add environment if not present // Add environment if not present
if (typeof mergedConfig === 'object' && mergedConfig !== null && !('environment' in mergedConfig)) { if (
typeof mergedConfig === 'object' &&
mergedConfig !== null &&
!('environment' in mergedConfig)
) {
(mergedConfig as Record<string, unknown>)['environment'] = this.environment; (mergedConfig as Record<string, unknown>)['environment'] = this.environment;
} }
@ -225,4 +226,4 @@ export class ConfigManager<T = Record<string, unknown>> {
return result; return result;
} }
} }

View file

@ -18,6 +18,9 @@ export {
} from './schemas'; } from './schemas';
// createAppConfig function for apps/stock // createAppConfig function for apps/stock
export function createAppConfig<T>(schema: unknown, options?: ConfigManagerOptions): ConfigManager<T> { export function createAppConfig<T>(
schema: unknown,
options?: ConfigManagerOptions
): ConfigManager<T> {
return new ConfigManager<T>(options); return new ConfigManager<T>(options);
} }

View file

@ -133,10 +133,7 @@ export class EnvLoader implements ConfigLoader {
private shouldPreserveStringForKey(key: string): boolean { private shouldPreserveStringForKey(key: string): boolean {
// Keys that should preserve string values even if they look like numbers // Keys that should preserve string values even if they look like numbers
const preserveStringKeys = [ const preserveStringKeys = ['QM_WEBMASTER_ID', 'IB_MARKET_DATA_TYPE'];
'QM_WEBMASTER_ID',
'IB_MARKET_DATA_TYPE'
];
return preserveStringKeys.includes(key); return preserveStringKeys.includes(key);
} }

View file

@ -24,23 +24,27 @@ export const eodProviderConfigSchema = baseProviderConfigSchema.extend({
// Interactive Brokers provider // Interactive Brokers provider
export const ibProviderConfigSchema = baseProviderConfigSchema.extend({ export const ibProviderConfigSchema = baseProviderConfigSchema.extend({
gateway: z.object({ gateway: z
host: z.string().default('localhost'), .object({
port: z.number().default(5000), host: z.string().default('localhost'),
clientId: z.number().default(1), port: z.number().default(5000),
}).default({ clientId: z.number().default(1),
host: 'localhost', })
port: 5000, .default({
clientId: 1, host: 'localhost',
}), port: 5000,
account: z.string().optional(), clientId: 1,
marketDataType: z.union([
z.enum(['live', 'delayed', 'frozen']),
z.enum(['1', '2', '3']).transform((val) => {
const mapping = { '1': 'live', '2': 'frozen', '3': 'delayed' } as const;
return mapping[val];
}), }),
]).default('delayed'), account: z.string().optional(),
marketDataType: z
.union([
z.enum(['live', 'delayed', 'frozen']),
z.enum(['1', '2', '3']).transform(val => {
const mapping = { '1': 'live', '2': 'frozen', '3': 'delayed' } as const;
return mapping[val];
}),
])
.default('delayed'),
}); });
// QuoteMedia provider // QuoteMedia provider

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,6 @@
import { beforeEach, describe, expect, it } from 'bun:test'; import { beforeEach, describe, expect, it } from 'bun:test';
import { z } from 'zod'; import { z } from 'zod';
import { import { baseAppSchema, ConfigManager, createAppConfig } from '../src';
baseAppSchema,
ConfigManager,
createAppConfig,
} from '../src';
import { ConfigError, ConfigValidationError } from '../src/errors'; import { ConfigError, ConfigValidationError } from '../src/errors';
// Mock loader for testing // Mock loader for testing
@ -160,7 +156,6 @@ describe('ConfigManager', () => {
expect(validated).toEqual({ name: 'test', port: 3000 }); expect(validated).toEqual({ name: 'test', port: 3000 });
}); });
it('should add environment if not present', () => { it('should add environment if not present', () => {
const mockManager = new ConfigManager({ const mockManager = new ConfigManager({
environment: 'test', environment: 'test',
@ -172,7 +167,6 @@ describe('ConfigManager', () => {
}); });
}); });
describe('Config Builders', () => { describe('Config Builders', () => {
it('should create app config with schema', () => { it('should create app config with schema', () => {
const schema = z.object({ const schema = z.object({

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import { FileLoader } from '../src/loaders/file.loader'; import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import { ConfigLoaderError } from '../src/errors'; import { ConfigLoaderError } from '../src/errors';
import { FileLoader } from '../src/loaders/file.loader';
// Mock fs module // Mock fs module
mock.module('fs', () => ({ mock.module('fs', () => ({
existsSync: mock(() => false), existsSync: mock(() => false),
readFileSync: mock(() => '') readFileSync: mock(() => ''),
})); }));
describe('FileLoader', () => { describe('FileLoader', () => {
@ -433,4 +433,4 @@ describe('FileLoader', () => {
expect(result).toEqual(config); expect(result).toEqual(config);
}); });
}); });
}); });

View file

@ -1,27 +1,27 @@
import { describe, it, expect } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import { z } from 'zod'; import { z } from 'zod';
import { import {
baseConfigSchema, baseConfigSchema,
environmentSchema,
serviceConfigSchema,
loggingConfigSchema,
queueConfigSchema,
httpConfigSchema,
webshareConfigSchema,
browserConfigSchema,
proxyConfigSchema,
postgresConfigSchema,
questdbConfigSchema,
mongodbConfigSchema,
dragonflyConfigSchema,
databaseConfigSchema,
baseProviderConfigSchema, baseProviderConfigSchema,
browserConfigSchema,
databaseConfigSchema,
dragonflyConfigSchema,
environmentSchema,
eodProviderConfigSchema, eodProviderConfigSchema,
httpConfigSchema,
ibProviderConfigSchema, ibProviderConfigSchema,
qmProviderConfigSchema, loggingConfigSchema,
yahooProviderConfigSchema, mongodbConfigSchema,
webshareProviderConfigSchema, postgresConfigSchema,
providerConfigSchema, providerConfigSchema,
proxyConfigSchema,
qmProviderConfigSchema,
questdbConfigSchema,
queueConfigSchema,
serviceConfigSchema,
webshareConfigSchema,
webshareProviderConfigSchema,
yahooProviderConfigSchema,
} from '../src/schemas'; } from '../src/schemas';
describe('Config Schemas', () => { describe('Config Schemas', () => {
@ -202,7 +202,7 @@ describe('Config Schemas', () => {
describe('queueConfigSchema', () => { describe('queueConfigSchema', () => {
it('should accept minimal config with defaults', () => { it('should accept minimal config with defaults', () => {
const config = queueConfigSchema.parse({ const config = queueConfigSchema.parse({
redis: {}, // redis is required, but its properties have defaults redis: {}, // redis is required, but its properties have defaults
}); });
expect(config).toEqual({ expect(config).toEqual({
enabled: true, enabled: true,
@ -421,7 +421,7 @@ describe('Config Schemas', () => {
// Empty strings are allowed by z.string() unless .min(1) is specified // Empty strings are allowed by z.string() unless .min(1) is specified
const serviceConfig = serviceConfigSchema.parse({ name: '', port: 3000 }); const serviceConfig = serviceConfigSchema.parse({ name: '', port: 3000 });
expect(serviceConfig.name).toBe(''); expect(serviceConfig.name).toBe('');
const baseConfig = baseConfigSchema.parse({ name: '' }); const baseConfig = baseConfigSchema.parse({ name: '' });
expect(baseConfig.name).toBe(''); expect(baseConfig.name).toBe('');
}); });
@ -493,19 +493,23 @@ describe('Config Schemas', () => {
}); });
it('should validate poolSize range', () => { it('should validate poolSize range', () => {
expect(() => postgresConfigSchema.parse({ expect(() =>
database: 'testdb', postgresConfigSchema.parse({
user: 'testuser', database: 'testdb',
password: 'testpass', user: 'testuser',
poolSize: 0, password: 'testpass',
})).toThrow(); poolSize: 0,
})
expect(() => postgresConfigSchema.parse({ ).toThrow();
database: 'testdb',
user: 'testuser', expect(() =>
password: 'testpass', postgresConfigSchema.parse({
poolSize: 101, database: 'testdb',
})).toThrow(); user: 'testuser',
password: 'testpass',
poolSize: 101,
})
).toThrow();
}); });
}); });
@ -574,24 +578,30 @@ describe('Config Schemas', () => {
}); });
it('should validate URI format', () => { it('should validate URI format', () => {
expect(() => mongodbConfigSchema.parse({ expect(() =>
uri: 'invalid-uri', mongodbConfigSchema.parse({
database: 'testdb', uri: 'invalid-uri',
})).toThrow(); database: 'testdb',
})
).toThrow();
}); });
it('should validate poolSize range', () => { it('should validate poolSize range', () => {
expect(() => mongodbConfigSchema.parse({ expect(() =>
uri: 'mongodb://localhost', mongodbConfigSchema.parse({
database: 'testdb', uri: 'mongodb://localhost',
poolSize: 0, database: 'testdb',
})).toThrow(); poolSize: 0,
})
expect(() => mongodbConfigSchema.parse({ ).toThrow();
uri: 'mongodb://localhost',
database: 'testdb', expect(() =>
poolSize: 101, mongodbConfigSchema.parse({
})).toThrow(); uri: 'mongodb://localhost',
database: 'testdb',
poolSize: 101,
})
).toThrow();
}); });
}); });
@ -645,7 +655,7 @@ describe('Config Schemas', () => {
}, },
dragonfly: {}, dragonfly: {},
}); });
expect(config.postgres.host).toBe('localhost'); expect(config.postgres.host).toBe('localhost');
expect(config.questdb.enabled).toBe(true); expect(config.questdb.enabled).toBe(true);
expect(config.mongodb.poolSize).toBe(10); expect(config.mongodb.poolSize).toBe(10);
@ -703,11 +713,13 @@ describe('Config Schemas', () => {
}); });
it('should validate tier values', () => { it('should validate tier values', () => {
expect(() => eodProviderConfigSchema.parse({ expect(() =>
name: 'eod', eodProviderConfigSchema.parse({
apiKey: 'test-key', name: 'eod',
tier: 'premium', apiKey: 'test-key',
})).toThrow(); tier: 'premium',
})
).toThrow();
const validTiers = ['free', 'fundamentals', 'all-in-one']; const validTiers = ['free', 'fundamentals', 'all-in-one'];
for (const tier of validTiers) { for (const tier of validTiers) {
@ -759,10 +771,12 @@ describe('Config Schemas', () => {
}); });
it('should validate marketDataType', () => { it('should validate marketDataType', () => {
expect(() => ibProviderConfigSchema.parse({ expect(() =>
name: 'ib', ibProviderConfigSchema.parse({
marketDataType: 'realtime', name: 'ib',
})).toThrow(); marketDataType: 'realtime',
})
).toThrow();
const validTypes = ['live', 'delayed', 'frozen']; const validTypes = ['live', 'delayed', 'frozen'];
for (const type of validTypes) { for (const type of validTypes) {
@ -777,9 +791,11 @@ describe('Config Schemas', () => {
describe('qmProviderConfigSchema', () => { describe('qmProviderConfigSchema', () => {
it('should require all credentials', () => { it('should require all credentials', () => {
expect(() => qmProviderConfigSchema.parse({ expect(() =>
name: 'qm', qmProviderConfigSchema.parse({
})).toThrow(); name: 'qm',
})
).toThrow();
const config = qmProviderConfigSchema.parse({ const config = qmProviderConfigSchema.parse({
name: 'qm', name: 'qm',
@ -885,7 +901,7 @@ describe('Config Schemas', () => {
apiKey: 'ws-key', apiKey: 'ws-key',
}, },
}); });
expect(config.eod?.tier).toBe('all-in-one'); expect(config.eod?.tier).toBe('all-in-one');
expect(config.ib?.gateway.port).toBe(7497); expect(config.ib?.gateway.port).toBe(7497);
expect(config.qm?.username).toBe('user'); expect(config.qm?.username).toBe('user');
@ -893,4 +909,4 @@ describe('Config Schemas', () => {
expect(config.webshare?.apiKey).toBe('ws-key'); expect(config.webshare?.apiKey).toBe('ws-key');
}); });
}); });
}); });

View file

@ -1,21 +1,21 @@
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { z } from 'zod'; import { z } from 'zod';
import { import {
SecretValue, checkRequiredEnvVars,
secret, COMMON_SECRET_PATTERNS,
createStrictSchema,
formatValidationResult,
isSecret, isSecret,
redactSecrets,
isSecretEnvVar, isSecretEnvVar,
wrapSecretEnvVars, mergeSchemas,
redactSecrets,
secret,
secretSchema, secretSchema,
secretStringSchema, secretStringSchema,
COMMON_SECRET_PATTERNS, SecretValue,
validateConfig,
checkRequiredEnvVars,
validateCompleteness, validateCompleteness,
formatValidationResult, validateConfig,
createStrictSchema, wrapSecretEnvVars,
mergeSchemas,
type ValidationResult, type ValidationResult,
} from '../src'; } from '../src';
@ -100,7 +100,7 @@ describe('Config Utils', () => {
it('should validate SecretValue instances', () => { it('should validate SecretValue instances', () => {
const schema = secretSchema(z.string()); const schema = secretSchema(z.string());
const secretVal = new SecretValue('test'); const secretVal = new SecretValue('test');
expect(() => schema.parse(secretVal)).not.toThrow(); expect(() => schema.parse(secretVal)).not.toThrow();
expect(() => schema.parse('test')).toThrow(); expect(() => schema.parse('test')).toThrow();
expect(() => schema.parse(null)).toThrow(); expect(() => schema.parse(null)).toThrow();
@ -132,7 +132,7 @@ describe('Config Utils', () => {
}; };
const redacted = redactSecrets(obj, ['password', 'nested.apiKey']); const redacted = redactSecrets(obj, ['password', 'nested.apiKey']);
expect(redacted).toEqual({ expect(redacted).toEqual({
username: 'admin', username: 'admin',
password: '***REDACTED***', password: '***REDACTED***',
@ -153,7 +153,7 @@ describe('Config Utils', () => {
}; };
const redacted = redactSecrets(obj); const redacted = redactSecrets(obj);
expect(redacted).toEqual({ expect(redacted).toEqual({
normal: 'value', normal: 'value',
secret: 'MASKED', secret: 'MASKED',
@ -172,7 +172,7 @@ describe('Config Utils', () => {
}; };
const redacted = redactSecrets(obj); const redacted = redactSecrets(obj);
expect(redacted.items).toEqual([ expect(redacted.items).toEqual([
{ name: 'item1', secret: '***' }, { name: 'item1', secret: '***' },
{ name: 'item2', secret: '***' }, { name: 'item2', secret: '***' },
@ -187,7 +187,7 @@ describe('Config Utils', () => {
}; };
const redacted = redactSecrets(obj); const redacted = redactSecrets(obj);
expect(redacted).toEqual({ expect(redacted).toEqual({
nullValue: null, nullValue: null,
undefinedValue: undefined, undefinedValue: undefined,
@ -240,13 +240,13 @@ describe('Config Utils', () => {
}; };
const wrapped = wrapSecretEnvVars(env); const wrapped = wrapSecretEnvVars(env);
expect(wrapped.USERNAME).toBe('admin'); expect(wrapped.USERNAME).toBe('admin');
expect(wrapped.PORT).toBe('3000'); expect(wrapped.PORT).toBe('3000');
expect(isSecret(wrapped.PASSWORD)).toBe(true); expect(isSecret(wrapped.PASSWORD)).toBe(true);
expect(isSecret(wrapped.API_KEY)).toBe(true); expect(isSecret(wrapped.API_KEY)).toBe(true);
const passwordSecret = wrapped.PASSWORD as SecretValue; const passwordSecret = wrapped.PASSWORD as SecretValue;
expect(passwordSecret.reveal('test')).toBe('secret123'); expect(passwordSecret.reveal('test')).toBe('secret123');
expect(passwordSecret.toString()).toBe('***PASSWORD***'); expect(passwordSecret.toString()).toBe('***PASSWORD***');
@ -259,7 +259,7 @@ describe('Config Utils', () => {
}; };
const wrapped = wrapSecretEnvVars(env); const wrapped = wrapSecretEnvVars(env);
expect(wrapped.PASSWORD).toBeUndefined(); expect(wrapped.PASSWORD).toBeUndefined();
expect(wrapped.USERNAME).toBe('admin'); expect(wrapped.USERNAME).toBe('admin');
}); });
@ -443,9 +443,7 @@ describe('Config Utils', () => {
it('should format warnings', () => { it('should format warnings', () => {
const result: ValidationResult = { const result: ValidationResult = {
valid: true, valid: true,
warnings: [ warnings: [{ path: 'deprecated.feature', message: 'This feature is deprecated' }],
{ path: 'deprecated.feature', message: 'This feature is deprecated' },
],
}; };
const formatted = formatValidationResult(result); const formatted = formatValidationResult(result);
@ -499,7 +497,7 @@ describe('Config Utils', () => {
const schema2 = z.object({ b: z.number(), shared: z.string() }); const schema2 = z.object({ b: z.number(), shared: z.string() });
const merged = mergeSchemas(schema1, schema2); const merged = mergeSchemas(schema1, schema2);
// Both schemas require 'shared' to be a string // Both schemas require 'shared' to be a string
expect(() => merged.parse({ a: 'test', b: 123, shared: 'value' })).not.toThrow(); expect(() => merged.parse({ a: 'test', b: 123, shared: 'value' })).not.toThrow();
expect(() => merged.parse({ a: 'test', b: 123, shared: 123 })).toThrow(); expect(() => merged.parse({ a: 'test', b: 123, shared: 123 })).toThrow();
@ -510,10 +508,10 @@ describe('Config Utils', () => {
it('should be an array of RegExp', () => { it('should be an array of RegExp', () => {
expect(Array.isArray(COMMON_SECRET_PATTERNS)).toBe(true); expect(Array.isArray(COMMON_SECRET_PATTERNS)).toBe(true);
expect(COMMON_SECRET_PATTERNS.length).toBeGreaterThan(0); expect(COMMON_SECRET_PATTERNS.length).toBeGreaterThan(0);
for (const pattern of COMMON_SECRET_PATTERNS) { for (const pattern of COMMON_SECRET_PATTERNS) {
expect(pattern).toBeInstanceOf(RegExp); expect(pattern).toBeInstanceOf(RegExp);
} }
}); });
}); });
}); });

View file

@ -1,3 +1,3 @@
// Export only what's actually used // Export only what's actually used
export { ServiceApplication } from './service-application'; export { ServiceApplication } from './service-application';
export { ServiceContainerBuilder } from './container/builder'; export { ServiceContainerBuilder } from './container/builder';

View file

@ -166,82 +166,102 @@ export class ServiceApplication {
private registerShutdownHandlers(): void { private registerShutdownHandlers(): void {
// Priority 1: Queue system (highest priority) // Priority 1: Queue system (highest priority)
if (this.serviceConfig.enableScheduledJobs) { if (this.serviceConfig.enableScheduledJobs) {
this.shutdown.onShutdown(async () => { this.shutdown.onShutdown(
this.logger.info('Shutting down queue system...'); async () => {
try { this.logger.info('Shutting down queue system...');
const queueManager = this.container?.resolve('queueManager'); try {
if (queueManager) { const queueManager = this.container?.resolve('queueManager');
await queueManager.shutdown(); if (queueManager) {
await queueManager.shutdown();
}
this.logger.info('Queue system shut down');
} catch (error) {
this.logger.error('Error shutting down queue system', { error });
} }
this.logger.info('Queue system shut down'); },
} catch (error) { SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
this.logger.error('Error shutting down queue system', { error }); 'Queue System'
} );
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'Queue System');
} }
// Priority 1: HTTP Server (high priority) // Priority 1: HTTP Server (high priority)
this.shutdown.onShutdown(async () => { this.shutdown.onShutdown(
if (this.server) { async () => {
this.logger.info('Stopping HTTP server...'); if (this.server) {
try { this.logger.info('Stopping HTTP server...');
this.server.stop(); try {
this.logger.info('HTTP server stopped'); this.server.stop();
} catch (error) { this.logger.info('HTTP server stopped');
this.logger.error('Error stopping HTTP server', { error }); } catch (error) {
this.logger.error('Error stopping HTTP server', { error });
}
} }
} },
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'HTTP Server'); SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
'HTTP Server'
);
// Custom shutdown hook // Custom shutdown hook
if (this.hooks.onBeforeShutdown) { if (this.hooks.onBeforeShutdown) {
this.shutdown.onShutdown(async () => { this.shutdown.onShutdown(
try { async () => {
await this.hooks.onBeforeShutdown!(); try {
} catch (error) { await this.hooks.onBeforeShutdown!();
this.logger.error('Error in custom shutdown hook', { error }); } catch (error) {
} this.logger.error('Error in custom shutdown hook', { error });
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'Custom Shutdown'); }
},
SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
'Custom Shutdown'
);
} }
// Priority 2: Services and connections (medium priority) // Priority 2: Services and connections (medium priority)
this.shutdown.onShutdown(async () => { this.shutdown.onShutdown(
this.logger.info('Disposing services and connections...'); async () => {
try { this.logger.info('Disposing services and connections...');
if (this.container) { try {
// Disconnect database clients if (this.container) {
const mongoClient = this.container.resolve('mongoClient'); // Disconnect database clients
if (mongoClient?.disconnect) { const mongoClient = this.container.resolve('mongoClient');
await mongoClient.disconnect(); if (mongoClient?.disconnect) {
} await mongoClient.disconnect();
}
const postgresClient = this.container.resolve('postgresClient'); const postgresClient = this.container.resolve('postgresClient');
if (postgresClient?.disconnect) { if (postgresClient?.disconnect) {
await postgresClient.disconnect(); await postgresClient.disconnect();
} }
const questdbClient = this.container.resolve('questdbClient'); const questdbClient = this.container.resolve('questdbClient');
if (questdbClient?.disconnect) { if (questdbClient?.disconnect) {
await questdbClient.disconnect(); await questdbClient.disconnect();
} }
this.logger.info('All services disposed successfully'); this.logger.info('All services disposed successfully');
}
} catch (error) {
this.logger.error('Error disposing services', { error });
} }
} catch (error) { },
this.logger.error('Error disposing services', { error }); SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY,
} 'Services'
}, SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, 'Services'); );
// Priority 3: Logger shutdown (lowest priority - runs last) // Priority 3: Logger shutdown (lowest priority - runs last)
this.shutdown.onShutdown(async () => { this.shutdown.onShutdown(
try { async () => {
this.logger.info('Shutting down loggers...'); try {
await shutdownLoggers(); this.logger.info('Shutting down loggers...');
// Don't log after shutdown await shutdownLoggers();
} catch { // Don't log after shutdown
// Silently ignore logger shutdown errors } catch {
} // Silently ignore logger shutdown errors
}, SHUTDOWN_DEFAULTS.LOW_PRIORITY, 'Loggers'); }
},
SHUTDOWN_DEFAULTS.LOW_PRIORITY,
'Loggers'
);
} }
/** /**

View file

@ -1,71 +1,76 @@
import { describe, it, expect } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import type { ServiceDefinitions, ServiceContainer, ServiceCradle, ServiceContainerOptions } from '../src/awilix-container'; import type {
ServiceContainer,
describe('Awilix Container Types', () => { ServiceContainerOptions,
it('should export ServiceDefinitions interface', () => { ServiceCradle,
// Type test - if this compiles, the type exists ServiceDefinitions,
const testDefinitions: Partial<ServiceDefinitions> = { } from '../src/awilix-container';
config: {} as any,
logger: {} as any, describe('Awilix Container Types', () => {
cache: null, it('should export ServiceDefinitions interface', () => {
proxyManager: null, // Type test - if this compiles, the type exists
browser: {} as any, const testDefinitions: Partial<ServiceDefinitions> = {
queueManager: null, config: {} as any,
mongoClient: null, logger: {} as any,
postgresClient: null, cache: null,
questdbClient: null, proxyManager: null,
serviceContainer: {} as any, browser: {} as any,
}; queueManager: null,
mongoClient: null,
expect(testDefinitions).toBeDefined(); postgresClient: null,
}); questdbClient: null,
serviceContainer: {} as any,
it('should export ServiceContainer type', () => { };
// Type test - if this compiles, the type exists
const testContainer: ServiceContainer | null = null; expect(testDefinitions).toBeDefined();
expect(testContainer).toBeNull(); });
});
it('should export ServiceContainer type', () => {
it('should export ServiceCradle type', () => { // Type test - if this compiles, the type exists
// Type test - if this compiles, the type exists const testContainer: ServiceContainer | null = null;
const testCradle: Partial<ServiceCradle> = { expect(testContainer).toBeNull();
config: {} as any, });
logger: {} as any,
}; it('should export ServiceCradle type', () => {
// Type test - if this compiles, the type exists
expect(testCradle).toBeDefined(); const testCradle: Partial<ServiceCradle> = {
}); config: {} as any,
logger: {} as any,
it('should export ServiceContainerOptions interface', () => { };
// Type test - if this compiles, the type exists
const testOptions: ServiceContainerOptions = { expect(testCradle).toBeDefined();
enableQuestDB: true, });
enableMongoDB: true,
enablePostgres: true, it('should export ServiceContainerOptions interface', () => {
enableCache: true, // Type test - if this compiles, the type exists
enableQueue: true, const testOptions: ServiceContainerOptions = {
enableBrowser: true, enableQuestDB: true,
enableProxy: true, enableMongoDB: true,
}; enablePostgres: true,
enableCache: true,
expect(testOptions).toBeDefined(); enableQueue: true,
expect(testOptions.enableQuestDB).toBe(true); enableBrowser: true,
expect(testOptions.enableMongoDB).toBe(true); enableProxy: true,
expect(testOptions.enablePostgres).toBe(true); };
expect(testOptions.enableCache).toBe(true);
expect(testOptions.enableQueue).toBe(true); expect(testOptions).toBeDefined();
expect(testOptions.enableBrowser).toBe(true); expect(testOptions.enableQuestDB).toBe(true);
expect(testOptions.enableProxy).toBe(true); expect(testOptions.enableMongoDB).toBe(true);
}); expect(testOptions.enablePostgres).toBe(true);
expect(testOptions.enableCache).toBe(true);
it('should allow partial ServiceContainerOptions', () => { expect(testOptions.enableQueue).toBe(true);
const partialOptions: ServiceContainerOptions = { expect(testOptions.enableBrowser).toBe(true);
enableCache: true, expect(testOptions.enableProxy).toBe(true);
enableQueue: false, });
};
it('should allow partial ServiceContainerOptions', () => {
expect(partialOptions.enableCache).toBe(true); const partialOptions: ServiceContainerOptions = {
expect(partialOptions.enableQueue).toBe(false); enableCache: true,
expect(partialOptions.enableQuestDB).toBeUndefined(); enableQueue: false,
}); };
});
expect(partialOptions.enableCache).toBe(true);
expect(partialOptions.enableQueue).toBe(false);
expect(partialOptions.enableQuestDB).toBeUndefined();
});
});

View file

@ -31,10 +31,18 @@ mock.module('@stock-bot/config', () => ({
} }
// Copy flat configs to nested if they exist // Copy flat configs to nested if they exist
if (result.redis) {result.database.dragonfly = result.redis;} if (result.redis) {
if (result.mongodb) {result.database.mongodb = result.mongodb;} result.database.dragonfly = result.redis;
if (result.postgres) {result.database.postgres = result.postgres;} }
if (result.questdb) {result.database.questdb = result.questdb;} if (result.mongodb) {
result.database.mongodb = result.mongodb;
}
if (result.postgres) {
result.database.postgres = result.postgres;
}
if (result.questdb) {
result.database.questdb = result.questdb;
}
return result; return result;
}, },

View file

@ -1,52 +1,52 @@
import { describe, it, expect } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import * as diExports from '../src/index'; import * as diExports from '../src/index';
describe('DI Package Exports', () => { describe('DI Package Exports', () => {
it('should export OperationContext', () => { it('should export OperationContext', () => {
expect(diExports.OperationContext).toBeDefined(); expect(diExports.OperationContext).toBeDefined();
}); });
it('should export pool size calculator', () => { it('should export pool size calculator', () => {
expect(diExports.calculatePoolSize).toBeDefined(); expect(diExports.calculatePoolSize).toBeDefined();
expect(diExports.getServicePoolSize).toBeDefined(); expect(diExports.getServicePoolSize).toBeDefined();
expect(diExports.getHandlerPoolSize).toBeDefined(); expect(diExports.getHandlerPoolSize).toBeDefined();
}); });
it('should export ServiceContainerBuilder', () => { it('should export ServiceContainerBuilder', () => {
expect(diExports.ServiceContainerBuilder).toBeDefined(); expect(diExports.ServiceContainerBuilder).toBeDefined();
}); });
it('should export ServiceLifecycleManager', () => { it('should export ServiceLifecycleManager', () => {
expect(diExports.ServiceLifecycleManager).toBeDefined(); expect(diExports.ServiceLifecycleManager).toBeDefined();
}); });
it('should export ServiceApplication', () => { it('should export ServiceApplication', () => {
expect(diExports.ServiceApplication).toBeDefined(); expect(diExports.ServiceApplication).toBeDefined();
}); });
it('should export HandlerScanner', () => { it('should export HandlerScanner', () => {
expect(diExports.HandlerScanner).toBeDefined(); expect(diExports.HandlerScanner).toBeDefined();
}); });
it('should export factories', () => { it('should export factories', () => {
expect(diExports.CacheFactory).toBeDefined(); expect(diExports.CacheFactory).toBeDefined();
}); });
it('should export schemas', () => { it('should export schemas', () => {
expect(diExports.appConfigSchema).toBeDefined(); expect(diExports.appConfigSchema).toBeDefined();
expect(diExports.redisConfigSchema).toBeDefined(); expect(diExports.redisConfigSchema).toBeDefined();
expect(diExports.mongodbConfigSchema).toBeDefined(); expect(diExports.mongodbConfigSchema).toBeDefined();
expect(diExports.postgresConfigSchema).toBeDefined(); expect(diExports.postgresConfigSchema).toBeDefined();
expect(diExports.questdbConfigSchema).toBeDefined(); expect(diExports.questdbConfigSchema).toBeDefined();
expect(diExports.proxyConfigSchema).toBeDefined(); expect(diExports.proxyConfigSchema).toBeDefined();
expect(diExports.browserConfigSchema).toBeDefined(); expect(diExports.browserConfigSchema).toBeDefined();
expect(diExports.queueConfigSchema).toBeDefined(); expect(diExports.queueConfigSchema).toBeDefined();
}); });
it('should export type definitions', () => { it('should export type definitions', () => {
// These are type exports - check that the awilix-container module is re-exported // These are type exports - check that the awilix-container module is re-exported
expect(diExports).toBeDefined(); expect(diExports).toBeDefined();
// The types AppConfig, ServiceCradle, etc. are TypeScript types and not runtime values // The types AppConfig, ServiceCradle, etc. are TypeScript types and not runtime values
// We can't test them directly, but we've verified they're exported in the source // We can't test them directly, but we've verified they're exported in the source
}); });
}); });

View file

@ -107,14 +107,14 @@ describe('DI Registrations', () => {
describe('registerDatabaseServices', () => { describe('registerDatabaseServices', () => {
it('should register MongoDB when config exists', () => { it('should register MongoDB when config exists', () => {
const container = createContainer(); const container = createContainer();
// Mock MongoDB client // Mock MongoDB client
const mockMongoClient = { const mockMongoClient = {
connect: mock(() => Promise.resolve()), connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()), disconnect: mock(() => Promise.resolve()),
getDb: mock(() => ({})), getDb: mock(() => ({})),
}; };
// Mock the MongoDB factory // Mock the MongoDB factory
mock.module('@stock-bot/mongodb', () => ({ mock.module('@stock-bot/mongodb', () => ({
MongoDBClient: class { MongoDBClient: class {
@ -123,7 +123,7 @@ describe('DI Registrations', () => {
} }
}, },
})); }));
const config = { const config = {
mongodb: { mongodb: {
enabled: true, enabled: true,
@ -139,14 +139,14 @@ describe('DI Registrations', () => {
it('should register PostgreSQL when config exists', () => { it('should register PostgreSQL when config exists', () => {
const container = createContainer(); const container = createContainer();
// Mock Postgres client // Mock Postgres client
const mockPostgresClient = { const mockPostgresClient = {
connect: mock(() => Promise.resolve()), connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()), disconnect: mock(() => Promise.resolve()),
query: mock(() => Promise.resolve({ rows: [] })), query: mock(() => Promise.resolve({ rows: [] })),
}; };
// Mock the Postgres factory // Mock the Postgres factory
mock.module('@stock-bot/postgres', () => ({ mock.module('@stock-bot/postgres', () => ({
PostgresClient: class { PostgresClient: class {
@ -155,7 +155,7 @@ describe('DI Registrations', () => {
} }
}, },
})); }));
const config = { const config = {
postgres: { postgres: {
enabled: true, enabled: true,
@ -174,14 +174,14 @@ describe('DI Registrations', () => {
it('should register QuestDB when config exists', () => { it('should register QuestDB when config exists', () => {
const container = createContainer(); const container = createContainer();
// Mock QuestDB client // Mock QuestDB client
const mockQuestdbClient = { const mockQuestdbClient = {
connect: mock(() => Promise.resolve()), connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve()), disconnect: mock(() => Promise.resolve()),
query: mock(() => Promise.resolve({ data: [] })), query: mock(() => Promise.resolve({ data: [] })),
}; };
// Mock the QuestDB factory // Mock the QuestDB factory
mock.module('@stock-bot/questdb', () => ({ mock.module('@stock-bot/questdb', () => ({
QuestDBClient: class { QuestDBClient: class {
@ -190,7 +190,7 @@ describe('DI Registrations', () => {
} }
}, },
})); }));
const config = { const config = {
questdb: { questdb: {
enabled: true, enabled: true,
@ -209,7 +209,7 @@ describe('DI Registrations', () => {
it('should not register disabled databases', () => { it('should not register disabled databases', () => {
const container = createContainer(); const container = createContainer();
const config = { const config = {
mongodb: { enabled: false }, mongodb: { enabled: false },
postgres: { enabled: false }, postgres: { enabled: false },
@ -222,7 +222,7 @@ describe('DI Registrations', () => {
expect(container.hasRegistration('mongoClient')).toBe(true); expect(container.hasRegistration('mongoClient')).toBe(true);
expect(container.hasRegistration('postgresClient')).toBe(true); expect(container.hasRegistration('postgresClient')).toBe(true);
expect(container.hasRegistration('questdbClient')).toBe(true); expect(container.hasRegistration('questdbClient')).toBe(true);
// Verify they resolve to null // Verify they resolve to null
expect(container.resolve('mongoClient')).toBeNull(); expect(container.resolve('mongoClient')).toBeNull();
expect(container.resolve('postgresClient')).toBeNull(); expect(container.resolve('postgresClient')).toBeNull();
@ -233,17 +233,17 @@ describe('DI Registrations', () => {
describe('registerApplicationServices', () => { describe('registerApplicationServices', () => {
it('should register browser when config exists', () => { it('should register browser when config exists', () => {
const container = createContainer(); const container = createContainer();
// Mock browser factory // Mock browser factory
const mockBrowser = { const mockBrowser = {
launch: mock(() => Promise.resolve()), launch: mock(() => Promise.resolve()),
close: mock(() => Promise.resolve()), close: mock(() => Promise.resolve()),
}; };
mock.module('@stock-bot/browser', () => ({ mock.module('@stock-bot/browser', () => ({
createBrowser: () => mockBrowser, createBrowser: () => mockBrowser,
})); }));
const config = { const config = {
browser: { browser: {
headless: true, headless: true,
@ -258,16 +258,16 @@ describe('DI Registrations', () => {
it('should register proxy when config exists', () => { it('should register proxy when config exists', () => {
const container = createContainer(); const container = createContainer();
// Mock proxy factory // Mock proxy factory
const mockProxy = { const mockProxy = {
getProxy: mock(() => 'http://proxy:8080'), getProxy: mock(() => 'http://proxy:8080'),
}; };
mock.module('@stock-bot/proxy', () => ({ mock.module('@stock-bot/proxy', () => ({
createProxyManager: () => mockProxy, createProxyManager: () => mockProxy,
})); }));
const config = { const config = {
proxy: { proxy: {
enabled: true, enabled: true,
@ -282,7 +282,7 @@ describe('DI Registrations', () => {
it('should register queue manager when queue config exists', () => { it('should register queue manager when queue config exists', () => {
const container = createContainer(); const container = createContainer();
// Mock dependencies // Mock dependencies
container.register({ container.register({
cache: asValue({ cache: asValue({
@ -300,14 +300,14 @@ describe('DI Registrations', () => {
debug: mock(() => {}), debug: mock(() => {}),
}), }),
}); });
// Mock queue manager // Mock queue manager
const mockQueueManager = { const mockQueueManager = {
getQueue: mock(() => ({})), getQueue: mock(() => ({})),
startAllWorkers: mock(() => {}), startAllWorkers: mock(() => {}),
shutdown: mock(() => Promise.resolve()), shutdown: mock(() => Promise.resolve()),
}; };
mock.module('@stock-bot/queue', () => ({ mock.module('@stock-bot/queue', () => ({
QueueManager: class { QueueManager: class {
constructor() { constructor() {
@ -315,7 +315,7 @@ describe('DI Registrations', () => {
} }
}, },
})); }));
const config = { const config = {
service: { service: {
name: 'test-service', name: 'test-service',
@ -335,7 +335,7 @@ describe('DI Registrations', () => {
it('should not register services when configs are missing', () => { it('should not register services when configs are missing', () => {
const container = createContainer(); const container = createContainer();
const config = {} as any; const config = {} as any;
registerApplicationServices(container, config); registerApplicationServices(container, config);
@ -343,12 +343,12 @@ describe('DI Registrations', () => {
expect(container.hasRegistration('browser')).toBe(true); expect(container.hasRegistration('browser')).toBe(true);
expect(container.hasRegistration('proxyManager')).toBe(true); expect(container.hasRegistration('proxyManager')).toBe(true);
expect(container.hasRegistration('queueManager')).toBe(true); expect(container.hasRegistration('queueManager')).toBe(true);
// They should be registered as null // They should be registered as null
const browser = container.resolve('browser'); const browser = container.resolve('browser');
const proxyManager = container.resolve('proxyManager'); const proxyManager = container.resolve('proxyManager');
const queueManager = container.resolve('queueManager'); const queueManager = container.resolve('queueManager');
expect(browser).toBe(null); expect(browser).toBe(null);
expect(proxyManager).toBe(null); expect(proxyManager).toBe(null);
expect(queueManager).toBe(null); expect(queueManager).toBe(null);
@ -358,7 +358,7 @@ describe('DI Registrations', () => {
describe('dependency resolution', () => { describe('dependency resolution', () => {
it('should properly resolve cache dependencies', () => { it('should properly resolve cache dependencies', () => {
const container = createContainer(); const container = createContainer();
const config = { const config = {
service: { service: {
name: 'test-service', name: 'test-service',
@ -373,7 +373,7 @@ describe('DI Registrations', () => {
} as any; } as any;
registerCacheServices(container, config); registerCacheServices(container, config);
// Should have registered cache // Should have registered cache
expect(container.hasRegistration('cache')).toBe(true); expect(container.hasRegistration('cache')).toBe(true);
expect(container.hasRegistration('globalCache')).toBe(true); expect(container.hasRegistration('globalCache')).toBe(true);
@ -381,13 +381,13 @@ describe('DI Registrations', () => {
it('should handle circular dependencies gracefully', () => { it('should handle circular dependencies gracefully', () => {
const container = createContainer(); const container = createContainer();
// Register services with potential circular deps // Register services with potential circular deps
container.register({ container.register({
serviceA: asFunction(({ serviceB }) => ({ b: serviceB })).singleton(), serviceA: asFunction(({ serviceB }) => ({ b: serviceB })).singleton(),
serviceB: asFunction(({ serviceA }) => ({ a: serviceA })).singleton(), serviceB: asFunction(({ serviceA }) => ({ a: serviceA })).singleton(),
}); });
// This should throw or handle gracefully // This should throw or handle gracefully
expect(() => container.resolve('serviceA')).toThrow(); expect(() => container.resolve('serviceA')).toThrow();
}); });
@ -396,7 +396,7 @@ describe('DI Registrations', () => {
describe('registration options', () => { describe('registration options', () => {
it('should register services as singletons', () => { it('should register services as singletons', () => {
const container = createContainer(); const container = createContainer();
const config = { const config = {
browser: { browser: {
headless: true, headless: true,
@ -405,7 +405,7 @@ describe('DI Registrations', () => {
} as any; } as any;
registerApplicationServices(container, config); registerApplicationServices(container, config);
// Check that browser was registered as singleton // Check that browser was registered as singleton
const registration = container.getRegistration('browser'); const registration = container.getRegistration('browser');
expect(registration).toBeDefined(); expect(registration).toBeDefined();

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import type { BaseAppConfig } from '@stock-bot/config';
import { ServiceApplication } from '../src/service-application'; import { ServiceApplication } from '../src/service-application';
import type { ServiceApplicationConfig, ServiceLifecycleHooks } from '../src/service-application'; import type { ServiceApplicationConfig, ServiceLifecycleHooks } from '../src/service-application';
import type { BaseAppConfig } from '@stock-bot/config';
// Mock logger module // Mock logger module
const mockLogger = { const mockLogger = {
@ -18,7 +18,7 @@ mock.module('@stock-bot/logger', () => ({
shutdownLoggers: mock(() => Promise.resolve()), shutdownLoggers: mock(() => Promise.resolve()),
})); }));
// Mock shutdown module // Mock shutdown module
const mockShutdownInstance = { const mockShutdownInstance = {
onShutdown: mock(() => {}), onShutdown: mock(() => {}),
onShutdownHigh: mock(() => {}), onShutdownHigh: mock(() => {}),
@ -89,7 +89,7 @@ describe.skip('ServiceApplication', () => {
mockShutdownInstance.registerAsync.mockReset(); mockShutdownInstance.registerAsync.mockReset();
mockShutdownInstance.handleTermination.mockReset(); mockShutdownInstance.handleTermination.mockReset();
mockShutdownInstance.executeCallbacks.mockReset(); mockShutdownInstance.executeCallbacks.mockReset();
// Clean up app if it exists // Clean up app if it exists
if (app) { if (app) {
app.stop().catch(() => {}); app.stop().catch(() => {});
@ -193,7 +193,6 @@ describe.skip('ServiceApplication', () => {
app = new ServiceApplication(configWithoutServiceName as any, serviceConfig); app = new ServiceApplication(configWithoutServiceName as any, serviceConfig);
expect(app).toBeDefined(); expect(app).toBeDefined();
}); });
}); });
describe('start method', () => { describe('start method', () => {
@ -228,7 +227,7 @@ describe.skip('ServiceApplication', () => {
const { Hono } = require('hono'); const { Hono } = require('hono');
const routes = new Hono(); const routes = new Hono();
// Add a simple test route // Add a simple test route
routes.get('/test', (c) => c.json({ test: true })); routes.get('/test', c => c.json({ test: true }));
return routes; return routes;
}); });
const mockHandlerInitializer = mock(() => Promise.resolve()); const mockHandlerInitializer = mock(() => Promise.resolve());
@ -240,12 +239,14 @@ describe.skip('ServiceApplication', () => {
}; };
app = new ServiceApplication(mockConfig, serviceConfig); app = new ServiceApplication(mockConfig, serviceConfig);
await app.start(mockContainerFactory, mockRouteFactory); await app.start(mockContainerFactory, mockRouteFactory);
expect(mockContainerFactory).toHaveBeenCalledWith(expect.objectContaining({ expect(mockContainerFactory).toHaveBeenCalledWith(
service: expect.objectContaining({ serviceName: 'test-service' }), expect.objectContaining({
})); service: expect.objectContaining({ serviceName: 'test-service' }),
})
);
expect(mockRouteFactory).toHaveBeenCalledWith({ test: 'container' }); expect(mockRouteFactory).toHaveBeenCalledWith({ test: 'container' });
expect(mockLogger.info).toHaveBeenCalledWith('test-service service started on port 3000'); expect(mockLogger.info).toHaveBeenCalledWith('test-service service started on port 3000');
}); });
@ -257,13 +258,15 @@ describe.skip('ServiceApplication', () => {
}; };
app = new ServiceApplication(mockConfig, serviceConfig); app = new ServiceApplication(mockConfig, serviceConfig);
await app.start(mockContainerFactory, mockRouteFactory, mockHandlerInitializer); await app.start(mockContainerFactory, mockRouteFactory, mockHandlerInitializer);
expect(mockHandlerInitializer).toHaveBeenCalledWith(expect.objectContaining({ expect(mockHandlerInitializer).toHaveBeenCalledWith(
test: 'container', expect.objectContaining({
_diContainer: mockContainer, test: 'container',
})); _diContainer: mockContainer,
})
);
expect(mockLogger.info).toHaveBeenCalledWith('Handlers initialized'); expect(mockLogger.info).toHaveBeenCalledWith('Handlers initialized');
}); });
@ -280,7 +283,7 @@ describe.skip('ServiceApplication', () => {
}; };
app = new ServiceApplication(mockConfig, serviceConfig, hooks); app = new ServiceApplication(mockConfig, serviceConfig, hooks);
await app.start(mockContainerFactory, mockRouteFactory); await app.start(mockContainerFactory, mockRouteFactory);
expect(hooks.onContainerReady).toHaveBeenCalledWith({ test: 'container' }); expect(hooks.onContainerReady).toHaveBeenCalledWith({ test: 'container' });
@ -299,8 +302,10 @@ describe.skip('ServiceApplication', () => {
}; };
app = new ServiceApplication(mockConfig, serviceConfig); app = new ServiceApplication(mockConfig, serviceConfig);
await expect(app.start(errorFactory, mockRouteFactory)).rejects.toThrow('Container creation failed'); await expect(app.start(errorFactory, mockRouteFactory)).rejects.toThrow(
'Container creation failed'
);
expect(mockLogger.error).toHaveBeenCalledWith('DETAILED ERROR:', expect.any(Error)); expect(mockLogger.error).toHaveBeenCalledWith('DETAILED ERROR:', expect.any(Error));
}); });
@ -311,17 +316,23 @@ describe.skip('ServiceApplication', () => {
}; };
const mockHandlerRegistry = { const mockHandlerRegistry = {
getAllHandlersWithSchedule: () => new Map([ getAllHandlersWithSchedule: () =>
['testHandler', { new Map([
scheduledJobs: [{ [
operation: 'processData', 'testHandler',
cronPattern: '0 * * * *', {
priority: 5, scheduledJobs: [
immediately: false, {
payload: { test: true }, operation: 'processData',
}], cronPattern: '0 * * * *',
}], priority: 5,
]), immediately: false,
payload: { test: true },
},
],
},
],
]),
getHandlerService: () => 'test-service', getHandlerService: () => 'test-service',
getHandlerNames: () => ['testHandler'], getHandlerNames: () => ['testHandler'],
getOperation: () => ({ name: 'processData' }), getOperation: () => ({ name: 'processData' }),
@ -339,9 +350,15 @@ describe.skip('ServiceApplication', () => {
const containerWithJobs = { const containerWithJobs = {
resolve: mock((name: string) => { resolve: mock((name: string) => {
if (name === 'serviceContainer') {return { test: 'container' };} if (name === 'serviceContainer') {
if (name === 'handlerRegistry') {return mockHandlerRegistry;} return { test: 'container' };
if (name === 'queueManager') {return mockQueueManager;} }
if (name === 'handlerRegistry') {
return mockHandlerRegistry;
}
if (name === 'queueManager') {
return mockQueueManager;
}
return null; return null;
}), }),
}; };
@ -349,7 +366,7 @@ describe.skip('ServiceApplication', () => {
const jobContainerFactory = mock(async () => containerWithJobs); const jobContainerFactory = mock(async () => containerWithJobs);
app = new ServiceApplication(mockConfig, serviceConfig); app = new ServiceApplication(mockConfig, serviceConfig);
await app.start(jobContainerFactory, mockRouteFactory); await app.start(jobContainerFactory, mockRouteFactory);
expect(mockQueueManager.getQueue).toHaveBeenCalledWith('testHandler', { expect(mockQueueManager.getQueue).toHaveBeenCalledWith('testHandler', {
@ -359,7 +376,7 @@ describe.skip('ServiceApplication', () => {
'processData', 'processData',
{ handler: 'testHandler', operation: 'processData', payload: { test: true } }, { handler: 'testHandler', operation: 'processData', payload: { test: true } },
'0 * * * *', '0 * * * *',
expect.objectContaining({ priority: 5, repeat: { immediately: false } }), expect.objectContaining({ priority: 5, repeat: { immediately: false } })
); );
expect(mockQueueManager.startAllWorkers).toHaveBeenCalled(); expect(mockQueueManager.startAllWorkers).toHaveBeenCalled();
expect(mockLogger.info).toHaveBeenCalledWith('Scheduled jobs created', { totalJobs: 1 }); expect(mockLogger.info).toHaveBeenCalledWith('Scheduled jobs created', { totalJobs: 1 });
@ -386,7 +403,7 @@ describe.skip('ServiceApplication', () => {
}; };
app = new ServiceApplication(mockConfig, serviceConfig); app = new ServiceApplication(mockConfig, serviceConfig);
await app.stop(); await app.stop();
expect(mockShutdownInstance.shutdown).toHaveBeenCalled(); expect(mockShutdownInstance.shutdown).toHaveBeenCalled();
@ -401,7 +418,7 @@ describe.skip('ServiceApplication', () => {
}; };
app = new ServiceApplication(mockConfig, serviceConfig); app = new ServiceApplication(mockConfig, serviceConfig);
// Before start // Before start
expect(app.getServiceContainer()).toBeNull(); expect(app.getServiceContainer()).toBeNull();
expect(app.getApp()).toBeNull(); expect(app.getApp()).toBeNull();
@ -451,18 +468,30 @@ describe.skip('ServiceApplication', () => {
const mockContainer = { const mockContainer = {
resolve: mock((name: string) => { resolve: mock((name: string) => {
if (name === 'serviceContainer') {return { test: 'container' };} if (name === 'serviceContainer') {
if (name === 'handlerRegistry') {return { return { test: 'container' };
getAllHandlersWithSchedule: () => new Map(), }
getHandlerNames: () => [], if (name === 'handlerRegistry') {
};} return {
if (name === 'queueManager') {return { getAllHandlersWithSchedule: () => new Map(),
shutdown: mock(() => Promise.resolve()), getHandlerNames: () => [],
startAllWorkers: mock(() => {}), };
};} }
if (name === 'mongoClient') {return { disconnect: mock(() => Promise.resolve()) };} if (name === 'queueManager') {
if (name === 'postgresClient') {return { disconnect: mock(() => Promise.resolve()) };} return {
if (name === 'questdbClient') {return { disconnect: mock(() => Promise.resolve()) };} shutdown: mock(() => Promise.resolve()),
startAllWorkers: mock(() => {}),
};
}
if (name === 'mongoClient') {
return { disconnect: mock(() => Promise.resolve()) };
}
if (name === 'postgresClient') {
return { disconnect: mock(() => Promise.resolve()) };
}
if (name === 'questdbClient') {
return { disconnect: mock(() => Promise.resolve()) };
}
return null; return null;
}), }),
}; };
@ -486,7 +515,7 @@ describe.skip('ServiceApplication', () => {
await highHandlers[0][0](); await highHandlers[0][0]();
expect(mockContainer.resolve).toHaveBeenCalledWith('queueManager'); expect(mockContainer.resolve).toHaveBeenCalledWith('queueManager');
// Execute services shutdown handler // Execute services shutdown handler
await mediumHandlers[0][0](); await mediumHandlers[0][0]();
expect(mockContainer.resolve).toHaveBeenCalledWith('mongoClient'); expect(mockContainer.resolve).toHaveBeenCalledWith('mongoClient');
expect(mockContainer.resolve).toHaveBeenCalledWith('postgresClient'); expect(mockContainer.resolve).toHaveBeenCalledWith('postgresClient');
@ -566,4 +595,4 @@ describe.skip('ServiceApplication', () => {
expect(response.status).toBe(404); expect(response.status).toBe(404);
}); });
}); });
}); });

View file

@ -1,270 +1,270 @@
import { describe, it, expect } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import type { import type {
GenericClientConfig, CachePoolConfig,
ConnectionPoolConfig, ConnectionFactory,
MongoDBPoolConfig, ConnectionFactoryConfig,
PostgreSQLPoolConfig, ConnectionPool,
CachePoolConfig, ConnectionPoolConfig,
QueuePoolConfig, GenericClientConfig,
ConnectionFactoryConfig, MongoDBPoolConfig,
ConnectionPool, PoolMetrics,
PoolMetrics, PostgreSQLPoolConfig,
ConnectionFactory, QueuePoolConfig,
} from '../src/types'; } from '../src/types';
describe('DI Types', () => { describe('DI Types', () => {
describe('GenericClientConfig', () => { describe('GenericClientConfig', () => {
it('should allow any key-value pairs', () => { it('should allow any key-value pairs', () => {
const config: GenericClientConfig = { const config: GenericClientConfig = {
host: 'localhost', host: 'localhost',
port: 5432, port: 5432,
username: 'test', username: 'test',
password: 'test', password: 'test',
customOption: true, customOption: true,
}; };
expect(config.host).toBe('localhost'); expect(config.host).toBe('localhost');
expect(config.port).toBe(5432); expect(config.port).toBe(5432);
expect(config.customOption).toBe(true); expect(config.customOption).toBe(true);
}); });
}); });
describe('ConnectionPoolConfig', () => { describe('ConnectionPoolConfig', () => {
it('should have required and optional fields', () => { it('should have required and optional fields', () => {
const config: ConnectionPoolConfig = { const config: ConnectionPoolConfig = {
name: 'test-pool', name: 'test-pool',
poolSize: 10, poolSize: 10,
minConnections: 2, minConnections: 2,
maxConnections: 20, maxConnections: 20,
idleTimeoutMillis: 30000, idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000, connectionTimeoutMillis: 5000,
enableMetrics: true, enableMetrics: true,
}; };
expect(config.name).toBe('test-pool'); expect(config.name).toBe('test-pool');
expect(config.poolSize).toBe(10); expect(config.poolSize).toBe(10);
expect(config.enableMetrics).toBe(true); expect(config.enableMetrics).toBe(true);
}); });
it('should allow minimal configuration', () => { it('should allow minimal configuration', () => {
const config: ConnectionPoolConfig = { const config: ConnectionPoolConfig = {
name: 'minimal-pool', name: 'minimal-pool',
}; };
expect(config.name).toBe('minimal-pool'); expect(config.name).toBe('minimal-pool');
expect(config.poolSize).toBeUndefined(); expect(config.poolSize).toBeUndefined();
}); });
}); });
describe('Specific Pool Configs', () => { describe('Specific Pool Configs', () => {
it('should extend ConnectionPoolConfig for MongoDB', () => { it('should extend ConnectionPoolConfig for MongoDB', () => {
const config: MongoDBPoolConfig = { const config: MongoDBPoolConfig = {
name: 'mongo-pool', name: 'mongo-pool',
poolSize: 5, poolSize: 5,
config: { config: {
uri: 'mongodb://localhost:27017', uri: 'mongodb://localhost:27017',
database: 'test', database: 'test',
}, },
}; };
expect(config.name).toBe('mongo-pool'); expect(config.name).toBe('mongo-pool');
expect(config.config.uri).toBe('mongodb://localhost:27017'); expect(config.config.uri).toBe('mongodb://localhost:27017');
}); });
it('should extend ConnectionPoolConfig for PostgreSQL', () => { it('should extend ConnectionPoolConfig for PostgreSQL', () => {
const config: PostgreSQLPoolConfig = { const config: PostgreSQLPoolConfig = {
name: 'postgres-pool', name: 'postgres-pool',
config: { config: {
host: 'localhost', host: 'localhost',
port: 5432, port: 5432,
database: 'test', database: 'test',
}, },
}; };
expect(config.name).toBe('postgres-pool'); expect(config.name).toBe('postgres-pool');
expect(config.config.host).toBe('localhost'); expect(config.config.host).toBe('localhost');
}); });
it('should extend ConnectionPoolConfig for Cache', () => { it('should extend ConnectionPoolConfig for Cache', () => {
const config: CachePoolConfig = { const config: CachePoolConfig = {
name: 'cache-pool', name: 'cache-pool',
config: { config: {
host: 'localhost', host: 'localhost',
port: 6379, port: 6379,
}, },
}; };
expect(config.name).toBe('cache-pool'); expect(config.name).toBe('cache-pool');
expect(config.config.port).toBe(6379); expect(config.config.port).toBe(6379);
}); });
it('should extend ConnectionPoolConfig for Queue', () => { it('should extend ConnectionPoolConfig for Queue', () => {
const config: QueuePoolConfig = { const config: QueuePoolConfig = {
name: 'queue-pool', name: 'queue-pool',
config: { config: {
redis: { redis: {
host: 'localhost', host: 'localhost',
port: 6379, port: 6379,
}, },
}, },
}; };
expect(config.name).toBe('queue-pool'); expect(config.name).toBe('queue-pool');
expect(config.config.redis.host).toBe('localhost'); expect(config.config.redis.host).toBe('localhost');
}); });
}); });
describe('ConnectionFactoryConfig', () => { describe('ConnectionFactoryConfig', () => {
it('should define factory configuration', () => { it('should define factory configuration', () => {
const config: ConnectionFactoryConfig = { const config: ConnectionFactoryConfig = {
service: 'test-service', service: 'test-service',
environment: 'development', environment: 'development',
pools: { pools: {
mongodb: { mongodb: {
poolSize: 10, poolSize: 10,
}, },
postgres: { postgres: {
maxConnections: 20, maxConnections: 20,
}, },
cache: { cache: {
idleTimeoutMillis: 60000, idleTimeoutMillis: 60000,
}, },
queue: { queue: {
enableMetrics: true, enableMetrics: true,
}, },
}, },
}; };
expect(config.service).toBe('test-service'); expect(config.service).toBe('test-service');
expect(config.environment).toBe('development'); expect(config.environment).toBe('development');
expect(config.pools?.mongodb?.poolSize).toBe(10); expect(config.pools?.mongodb?.poolSize).toBe(10);
expect(config.pools?.postgres?.maxConnections).toBe(20); expect(config.pools?.postgres?.maxConnections).toBe(20);
}); });
it('should allow minimal factory config', () => { it('should allow minimal factory config', () => {
const config: ConnectionFactoryConfig = { const config: ConnectionFactoryConfig = {
service: 'minimal-service', service: 'minimal-service',
environment: 'test', environment: 'test',
}; };
expect(config.service).toBe('minimal-service'); expect(config.service).toBe('minimal-service');
expect(config.pools).toBeUndefined(); expect(config.pools).toBeUndefined();
}); });
}); });
describe('ConnectionPool', () => { describe('ConnectionPool', () => {
it('should define connection pool interface', () => { it('should define connection pool interface', () => {
const mockPool: ConnectionPool<any> = { const mockPool: ConnectionPool<any> = {
name: 'test-pool', name: 'test-pool',
client: { connected: true }, client: { connected: true },
metrics: { metrics: {
created: new Date(), created: new Date(),
totalConnections: 10, totalConnections: 10,
activeConnections: 5, activeConnections: 5,
idleConnections: 5, idleConnections: 5,
waitingRequests: 0, waitingRequests: 0,
errors: 0, errors: 0,
}, },
health: async () => true, health: async () => true,
dispose: async () => {}, dispose: async () => {},
}; };
expect(mockPool.name).toBe('test-pool'); expect(mockPool.name).toBe('test-pool');
expect(mockPool.client.connected).toBe(true); expect(mockPool.client.connected).toBe(true);
expect(mockPool.metrics.totalConnections).toBe(10); expect(mockPool.metrics.totalConnections).toBe(10);
}); });
}); });
describe('PoolMetrics', () => { describe('PoolMetrics', () => {
it('should define pool metrics structure', () => { it('should define pool metrics structure', () => {
const metrics: PoolMetrics = { const metrics: PoolMetrics = {
created: new Date('2024-01-01'), created: new Date('2024-01-01'),
totalConnections: 100, totalConnections: 100,
activeConnections: 25, activeConnections: 25,
idleConnections: 75, idleConnections: 75,
waitingRequests: 2, waitingRequests: 2,
errors: 3, errors: 3,
}; };
expect(metrics.totalConnections).toBe(100); expect(metrics.totalConnections).toBe(100);
expect(metrics.activeConnections).toBe(25); expect(metrics.activeConnections).toBe(25);
expect(metrics.idleConnections).toBe(75); expect(metrics.idleConnections).toBe(75);
expect(metrics.waitingRequests).toBe(2); expect(metrics.waitingRequests).toBe(2);
expect(metrics.errors).toBe(3); expect(metrics.errors).toBe(3);
}); });
}); });
describe('ConnectionFactory', () => { describe('ConnectionFactory', () => {
it('should define connection factory interface', () => { it('should define connection factory interface', () => {
const mockFactory: ConnectionFactory = { const mockFactory: ConnectionFactory = {
createMongoDB: async (config) => ({ createMongoDB: async config => ({
name: config.name, name: config.name,
client: {}, client: {},
metrics: { metrics: {
created: new Date(), created: new Date(),
totalConnections: 0, totalConnections: 0,
activeConnections: 0, activeConnections: 0,
idleConnections: 0, idleConnections: 0,
waitingRequests: 0, waitingRequests: 0,
errors: 0, errors: 0,
}, },
health: async () => true, health: async () => true,
dispose: async () => {}, dispose: async () => {},
}), }),
createPostgreSQL: async (config) => ({ createPostgreSQL: async config => ({
name: config.name, name: config.name,
client: {}, client: {},
metrics: { metrics: {
created: new Date(), created: new Date(),
totalConnections: 0, totalConnections: 0,
activeConnections: 0, activeConnections: 0,
idleConnections: 0, idleConnections: 0,
waitingRequests: 0, waitingRequests: 0,
errors: 0, errors: 0,
}, },
health: async () => true, health: async () => true,
dispose: async () => {}, dispose: async () => {},
}), }),
createCache: async (config) => ({ createCache: async config => ({
name: config.name, name: config.name,
client: {}, client: {},
metrics: { metrics: {
created: new Date(), created: new Date(),
totalConnections: 0, totalConnections: 0,
activeConnections: 0, activeConnections: 0,
idleConnections: 0, idleConnections: 0,
waitingRequests: 0, waitingRequests: 0,
errors: 0, errors: 0,
}, },
health: async () => true, health: async () => true,
dispose: async () => {}, dispose: async () => {},
}), }),
createQueue: async (config) => ({ createQueue: async config => ({
name: config.name, name: config.name,
client: {}, client: {},
metrics: { metrics: {
created: new Date(), created: new Date(),
totalConnections: 0, totalConnections: 0,
activeConnections: 0, activeConnections: 0,
idleConnections: 0, idleConnections: 0,
waitingRequests: 0, waitingRequests: 0,
errors: 0, errors: 0,
}, },
health: async () => true, health: async () => true,
dispose: async () => {}, dispose: async () => {},
}), }),
getPool: (type, name) => undefined, getPool: (type, name) => undefined,
listPools: () => [], listPools: () => [],
disposeAll: async () => {}, disposeAll: async () => {},
}; };
expect(mockFactory.createMongoDB).toBeDefined(); expect(mockFactory.createMongoDB).toBeDefined();
expect(mockFactory.createPostgreSQL).toBeDefined(); expect(mockFactory.createPostgreSQL).toBeDefined();
expect(mockFactory.createCache).toBeDefined(); expect(mockFactory.createCache).toBeDefined();
expect(mockFactory.createQueue).toBeDefined(); expect(mockFactory.createQueue).toBeDefined();
expect(mockFactory.getPool).toBeDefined(); expect(mockFactory.getPool).toBeDefined();
expect(mockFactory.listPools).toBeDefined(); expect(mockFactory.listPools).toBeDefined();
expect(mockFactory.disposeAll).toBeDefined(); expect(mockFactory.disposeAll).toBeDefined();
}); });
}); });
}); });

View file

@ -80,7 +80,6 @@ export class HandlerRegistry {
return this.handlers.has(handlerName); return this.handlers.has(handlerName);
} }
/** /**
* Set service ownership for a handler * Set service ownership for a handler
*/ */
@ -107,7 +106,10 @@ export class HandlerRegistry {
getServiceHandlers(serviceName: string): HandlerMetadata[] { getServiceHandlers(serviceName: string): HandlerMetadata[] {
const handlers: HandlerMetadata[] = []; const handlers: HandlerMetadata[] = [];
for (const [handlerName, metadata] of this.handlers) { for (const [handlerName, metadata] of this.handlers) {
if (this.handlerServices.get(handlerName) === serviceName || metadata.service === serviceName) { if (
this.handlerServices.get(handlerName) === serviceName ||
metadata.service === serviceName
) {
handlers.push(metadata); handlers.push(metadata);
} }
} }

View file

@ -1,77 +1,77 @@
import { describe, it, expect } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import * as handlerRegistryExports from '../src'; import * as handlerRegistryExports from '../src';
import { HandlerRegistry } from '../src'; import { HandlerRegistry } from '../src';
describe('Handler Registry Package Exports', () => { describe('Handler Registry Package Exports', () => {
it('should export HandlerRegistry class', () => { it('should export HandlerRegistry class', () => {
expect(handlerRegistryExports.HandlerRegistry).toBeDefined(); expect(handlerRegistryExports.HandlerRegistry).toBeDefined();
expect(handlerRegistryExports.HandlerRegistry).toBe(HandlerRegistry); expect(handlerRegistryExports.HandlerRegistry).toBe(HandlerRegistry);
}); });
it('should export correct types', () => { it('should export correct types', () => {
// Type tests - compile-time checks // Type tests - compile-time checks
type TestHandlerMetadata = handlerRegistryExports.HandlerMetadata; type TestHandlerMetadata = handlerRegistryExports.HandlerMetadata;
type TestOperationMetadata = handlerRegistryExports.OperationMetadata; type TestOperationMetadata = handlerRegistryExports.OperationMetadata;
type TestScheduleMetadata = handlerRegistryExports.ScheduleMetadata; type TestScheduleMetadata = handlerRegistryExports.ScheduleMetadata;
type TestHandlerConfiguration = handlerRegistryExports.HandlerConfiguration; type TestHandlerConfiguration = handlerRegistryExports.HandlerConfiguration;
type TestRegistryStats = handlerRegistryExports.RegistryStats; type TestRegistryStats = handlerRegistryExports.RegistryStats;
type TestHandlerDiscoveryResult = handlerRegistryExports.HandlerDiscoveryResult; type TestHandlerDiscoveryResult = handlerRegistryExports.HandlerDiscoveryResult;
// Runtime type usage tests // Runtime type usage tests
const testHandler: TestHandlerMetadata = { const testHandler: TestHandlerMetadata = {
name: 'TestHandler', name: 'TestHandler',
serviceName: 'test-service', serviceName: 'test-service',
operations: [], operations: [],
}; };
const testOperation: TestOperationMetadata = { const testOperation: TestOperationMetadata = {
operationName: 'testOperation', operationName: 'testOperation',
handlerName: 'TestHandler', handlerName: 'TestHandler',
operationPath: 'test.operation', operationPath: 'test.operation',
serviceName: 'test-service', serviceName: 'test-service',
}; };
const testSchedule: TestScheduleMetadata = { const testSchedule: TestScheduleMetadata = {
handlerName: 'TestHandler', handlerName: 'TestHandler',
scheduleName: 'test-schedule', scheduleName: 'test-schedule',
expression: '*/5 * * * *', expression: '*/5 * * * *',
serviceName: 'test-service', serviceName: 'test-service',
}; };
const testConfig: TestHandlerConfiguration = { const testConfig: TestHandlerConfiguration = {
handlerName: 'TestHandler', handlerName: 'TestHandler',
batchSize: 10, batchSize: 10,
timeout: 5000, timeout: 5000,
retries: 3, retries: 3,
}; };
const testStats: TestRegistryStats = { const testStats: TestRegistryStats = {
totalHandlers: 5, totalHandlers: 5,
totalOperations: 10, totalOperations: 10,
totalSchedules: 3, totalSchedules: 3,
handlersByService: { handlersByService: {
'service1': 2, service1: 2,
'service2': 3, service2: 3,
}, },
}; };
const testDiscoveryResult: TestHandlerDiscoveryResult = { const testDiscoveryResult: TestHandlerDiscoveryResult = {
handlers: [testHandler], handlers: [testHandler],
operations: [testOperation], operations: [testOperation],
schedules: [testSchedule], schedules: [testSchedule],
configurations: [testConfig], configurations: [testConfig],
}; };
expect(testHandler).toBeDefined(); expect(testHandler).toBeDefined();
expect(testOperation).toBeDefined(); expect(testOperation).toBeDefined();
expect(testSchedule).toBeDefined(); expect(testSchedule).toBeDefined();
expect(testConfig).toBeDefined(); expect(testConfig).toBeDefined();
expect(testStats).toBeDefined(); expect(testStats).toBeDefined();
expect(testDiscoveryResult).toBeDefined(); expect(testDiscoveryResult).toBeDefined();
}); });
it('should create HandlerRegistry instance', () => { it('should create HandlerRegistry instance', () => {
const registry = new HandlerRegistry(); const registry = new HandlerRegistry();
expect(registry).toBeInstanceOf(HandlerRegistry); expect(registry).toBeInstanceOf(HandlerRegistry);
}); });
}); });

View file

@ -1,382 +1,380 @@
import { beforeEach, describe, expect, it, mock } from 'bun:test'; import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { HandlerRegistry } from '../src/registry'; import type { JobHandler, ScheduledJob } from '@stock-bot/types';
import type { import { HandlerRegistry } from '../src/registry';
HandlerConfiguration, import type {
HandlerMetadata, HandlerConfiguration,
OperationMetadata, HandlerMetadata,
ScheduleMetadata, OperationMetadata,
} from '../src/types'; ScheduleMetadata,
import type { JobHandler, ScheduledJob } from '@stock-bot/types'; } from '../src/types';
describe('HandlerRegistry Edge Cases', () => { describe('HandlerRegistry Edge Cases', () => {
let registry: HandlerRegistry; let registry: HandlerRegistry;
beforeEach(() => { beforeEach(() => {
registry = new HandlerRegistry(); registry = new HandlerRegistry();
}); });
describe('Metadata Edge Cases', () => { describe('Metadata Edge Cases', () => {
it('should handle metadata without service', () => { it('should handle metadata without service', () => {
const metadata: HandlerMetadata = { const metadata: HandlerMetadata = {
name: 'NoServiceHandler', name: 'NoServiceHandler',
operations: [], operations: [],
}; };
registry.registerMetadata(metadata); registry.registerMetadata(metadata);
expect(registry.getMetadata('NoServiceHandler')).toEqual(metadata); expect(registry.getMetadata('NoServiceHandler')).toEqual(metadata);
expect(registry.getHandlerService('NoServiceHandler')).toBeUndefined(); expect(registry.getHandlerService('NoServiceHandler')).toBeUndefined();
}); });
it('should handle metadata with optional fields', () => { it('should handle metadata with optional fields', () => {
const metadata: HandlerMetadata = { const metadata: HandlerMetadata = {
name: 'FullHandler', name: 'FullHandler',
service: 'test-service', service: 'test-service',
operations: [ operations: [
{ {
name: 'op1', name: 'op1',
method: 'method1', method: 'method1',
description: 'Operation 1', description: 'Operation 1',
}, },
], ],
schedules: [ schedules: [
{ {
operation: 'op1', operation: 'op1',
cronPattern: '*/5 * * * *', cronPattern: '*/5 * * * *',
priority: 10, priority: 10,
immediately: true, immediately: true,
description: 'Every 5 minutes', description: 'Every 5 minutes',
}, },
], ],
version: '1.0.0', version: '1.0.0',
description: 'Full handler with all fields', description: 'Full handler with all fields',
}; };
registry.registerMetadata(metadata); registry.registerMetadata(metadata);
const retrieved = registry.getMetadata('FullHandler'); const retrieved = registry.getMetadata('FullHandler');
expect(retrieved).toEqual(metadata); expect(retrieved).toEqual(metadata);
expect(retrieved?.version).toBe('1.0.0'); expect(retrieved?.version).toBe('1.0.0');
expect(retrieved?.description).toBe('Full handler with all fields'); expect(retrieved?.description).toBe('Full handler with all fields');
expect(retrieved?.schedules?.[0].immediately).toBe(true); expect(retrieved?.schedules?.[0].immediately).toBe(true);
}); });
it('should handle empty operations array', () => { it('should handle empty operations array', () => {
const metadata: HandlerMetadata = { const metadata: HandlerMetadata = {
name: 'EmptyHandler', name: 'EmptyHandler',
operations: [], operations: [],
}; };
registry.registerMetadata(metadata); registry.registerMetadata(metadata);
const stats = registry.getStats(); const stats = registry.getStats();
expect(stats.handlers).toBe(1); expect(stats.handlers).toBe(1);
expect(stats.operations).toBe(0); expect(stats.operations).toBe(0);
}); });
}); });
describe('Configuration Edge Cases', () => { describe('Configuration Edge Cases', () => {
it('should handle configuration without scheduled jobs', () => { it('should handle configuration without scheduled jobs', () => {
const config: HandlerConfiguration = { const config: HandlerConfiguration = {
name: 'SimpleHandler', name: 'SimpleHandler',
operations: { operations: {
process: mock(async () => {}) as JobHandler, process: mock(async () => {}) as JobHandler,
}, },
}; };
registry.registerConfiguration(config); registry.registerConfiguration(config);
const scheduledJobs = registry.getScheduledJobs('SimpleHandler'); const scheduledJobs = registry.getScheduledJobs('SimpleHandler');
expect(scheduledJobs).toEqual([]); expect(scheduledJobs).toEqual([]);
}); });
it('should handle empty operations object', () => { it('should handle empty operations object', () => {
const config: HandlerConfiguration = { const config: HandlerConfiguration = {
name: 'EmptyOpsHandler', name: 'EmptyOpsHandler',
operations: {}, operations: {},
}; };
registry.registerConfiguration(config); registry.registerConfiguration(config);
expect(registry.getOperation('EmptyOpsHandler', 'nonexistent')).toBeUndefined(); expect(registry.getOperation('EmptyOpsHandler', 'nonexistent')).toBeUndefined();
}); });
it('should handle configuration with empty scheduled jobs array', () => { it('should handle configuration with empty scheduled jobs array', () => {
const config: HandlerConfiguration = { const config: HandlerConfiguration = {
name: 'NoScheduleHandler', name: 'NoScheduleHandler',
operations: {}, operations: {},
scheduledJobs: [], scheduledJobs: [],
}; };
registry.registerConfiguration(config); registry.registerConfiguration(config);
const scheduled = registry.getScheduledJobs('NoScheduleHandler'); const scheduled = registry.getScheduledJobs('NoScheduleHandler');
expect(scheduled).toEqual([]); expect(scheduled).toEqual([]);
}); });
}); });
describe('Service Management Edge Cases', () => { describe('Service Management Edge Cases', () => {
it('should update metadata when setting handler service', () => { it('should update metadata when setting handler service', () => {
const metadata: HandlerMetadata = { const metadata: HandlerMetadata = {
name: 'UpdateableHandler', name: 'UpdateableHandler',
operations: [], operations: [],
service: 'old-service', service: 'old-service',
}; };
registry.registerMetadata(metadata); registry.registerMetadata(metadata);
registry.setHandlerService('UpdateableHandler', 'new-service'); registry.setHandlerService('UpdateableHandler', 'new-service');
const updated = registry.getMetadata('UpdateableHandler'); const updated = registry.getMetadata('UpdateableHandler');
expect(updated?.service).toBe('new-service'); expect(updated?.service).toBe('new-service');
expect(registry.getHandlerService('UpdateableHandler')).toBe('new-service'); expect(registry.getHandlerService('UpdateableHandler')).toBe('new-service');
}); });
it('should set service for non-existent handler', () => { it('should set service for non-existent handler', () => {
registry.setHandlerService('NonExistentHandler', 'some-service'); registry.setHandlerService('NonExistentHandler', 'some-service');
expect(registry.getHandlerService('NonExistentHandler')).toBe('some-service'); expect(registry.getHandlerService('NonExistentHandler')).toBe('some-service');
expect(registry.getMetadata('NonExistentHandler')).toBeUndefined(); expect(registry.getMetadata('NonExistentHandler')).toBeUndefined();
}); });
it('should return empty array for service with no handlers', () => { it('should return empty array for service with no handlers', () => {
const handlers = registry.getServiceHandlers('non-existent-service'); const handlers = registry.getServiceHandlers('non-existent-service');
expect(handlers).toEqual([]); expect(handlers).toEqual([]);
}); });
it('should handle multiple handlers for same service', () => { it('should handle multiple handlers for same service', () => {
const metadata1: HandlerMetadata = { const metadata1: HandlerMetadata = {
name: 'Handler1', name: 'Handler1',
service: 'shared-service', service: 'shared-service',
operations: [], operations: [],
}; };
const metadata2: HandlerMetadata = { const metadata2: HandlerMetadata = {
name: 'Handler2', name: 'Handler2',
service: 'shared-service', service: 'shared-service',
operations: [], operations: [],
}; };
const metadata3: HandlerMetadata = { const metadata3: HandlerMetadata = {
name: 'Handler3', name: 'Handler3',
service: 'other-service', service: 'other-service',
operations: [], operations: [],
}; };
registry.registerMetadata(metadata1); registry.registerMetadata(metadata1);
registry.registerMetadata(metadata2); registry.registerMetadata(metadata2);
registry.registerMetadata(metadata3); registry.registerMetadata(metadata3);
const sharedHandlers = registry.getServiceHandlers('shared-service'); const sharedHandlers = registry.getServiceHandlers('shared-service');
expect(sharedHandlers).toHaveLength(2); expect(sharedHandlers).toHaveLength(2);
expect(sharedHandlers.map(h => h.name).sort()).toEqual(['Handler1', 'Handler2']); expect(sharedHandlers.map(h => h.name).sort()).toEqual(['Handler1', 'Handler2']);
}); });
}); });
describe('Operation Access Edge Cases', () => { describe('Operation Access Edge Cases', () => {
it('should return undefined for non-existent handler operation', () => { it('should return undefined for non-existent handler operation', () => {
const op = registry.getOperation('NonExistent', 'operation'); const op = registry.getOperation('NonExistent', 'operation');
expect(op).toBeUndefined(); expect(op).toBeUndefined();
}); });
it('should return undefined for non-existent operation name', () => { it('should return undefined for non-existent operation name', () => {
const config: HandlerConfiguration = { const config: HandlerConfiguration = {
name: 'TestHandler', name: 'TestHandler',
operations: { operations: {
exists: mock(async () => {}) as JobHandler, exists: mock(async () => {}) as JobHandler,
}, },
}; };
registry.registerConfiguration(config); registry.registerConfiguration(config);
const op = registry.getOperation('TestHandler', 'notexists'); const op = registry.getOperation('TestHandler', 'notexists');
expect(op).toBeUndefined(); expect(op).toBeUndefined();
}); });
}); });
describe('getAllHandlersWithSchedule Edge Cases', () => { describe('getAllHandlersWithSchedule Edge Cases', () => {
it('should handle mix of handlers with and without schedules', () => { it('should handle mix of handlers with and without schedules', () => {
const metadata1: HandlerMetadata = { const metadata1: HandlerMetadata = {
name: 'WithSchedule', name: 'WithSchedule',
operations: [], operations: [],
}; };
const config1: HandlerConfiguration = { const config1: HandlerConfiguration = {
name: 'WithSchedule', name: 'WithSchedule',
operations: {}, operations: {},
scheduledJobs: [ scheduledJobs: [
{ {
name: 'job1', name: 'job1',
handler: mock(async () => {}) as JobHandler, handler: mock(async () => {}) as JobHandler,
pattern: '* * * * *', pattern: '* * * * *',
} as ScheduledJob, } as ScheduledJob,
], ],
}; };
const metadata2: HandlerMetadata = { const metadata2: HandlerMetadata = {
name: 'WithoutSchedule', name: 'WithoutSchedule',
operations: [], operations: [],
}; };
const config2: HandlerConfiguration = { const config2: HandlerConfiguration = {
name: 'WithoutSchedule', name: 'WithoutSchedule',
operations: {}, operations: {},
}; };
registry.register(metadata1, config1); registry.register(metadata1, config1);
registry.register(metadata2, config2); registry.register(metadata2, config2);
const allWithSchedule = registry.getAllHandlersWithSchedule(); const allWithSchedule = registry.getAllHandlersWithSchedule();
expect(allWithSchedule.size).toBe(2); expect(allWithSchedule.size).toBe(2);
const withSchedule = allWithSchedule.get('WithSchedule'); const withSchedule = allWithSchedule.get('WithSchedule');
expect(withSchedule?.scheduledJobs).toHaveLength(1); expect(withSchedule?.scheduledJobs).toHaveLength(1);
const withoutSchedule = allWithSchedule.get('WithoutSchedule'); const withoutSchedule = allWithSchedule.get('WithoutSchedule');
expect(withoutSchedule?.scheduledJobs).toEqual([]); expect(withoutSchedule?.scheduledJobs).toEqual([]);
}); });
it('should handle handler with metadata but no configuration', () => { it('should handle handler with metadata but no configuration', () => {
const metadata: HandlerMetadata = { const metadata: HandlerMetadata = {
name: 'MetadataOnly', name: 'MetadataOnly',
operations: [], operations: [],
}; };
registry.registerMetadata(metadata); registry.registerMetadata(metadata);
const allWithSchedule = registry.getAllHandlersWithSchedule(); const allWithSchedule = registry.getAllHandlersWithSchedule();
const handler = allWithSchedule.get('MetadataOnly'); const handler = allWithSchedule.get('MetadataOnly');
expect(handler?.metadata).toEqual(metadata); expect(handler?.metadata).toEqual(metadata);
expect(handler?.scheduledJobs).toEqual([]); expect(handler?.scheduledJobs).toEqual([]);
}); });
}); });
describe('Import/Export Edge Cases', () => { describe('Import/Export Edge Cases', () => {
it('should handle empty export', () => { it('should handle empty export', () => {
const exported = registry.export(); const exported = registry.export();
expect(exported.handlers).toEqual([]); expect(exported.handlers).toEqual([]);
expect(exported.configurations).toEqual([]); expect(exported.configurations).toEqual([]);
expect(exported.services).toEqual([]); expect(exported.services).toEqual([]);
}); });
it('should handle empty import', () => { it('should handle empty import', () => {
// Add some data first // Add some data first
registry.registerMetadata({ registry.registerMetadata({
name: 'ExistingHandler', name: 'ExistingHandler',
operations: [], operations: [],
}); });
// Import empty data // Import empty data
registry.import({ registry.import({
handlers: [], handlers: [],
configurations: [], configurations: [],
services: [], services: [],
}); });
expect(registry.getHandlerNames()).toEqual([]); expect(registry.getHandlerNames()).toEqual([]);
}); });
it('should preserve complex data through export/import cycle', () => { it('should preserve complex data through export/import cycle', () => {
const metadata: HandlerMetadata = { const metadata: HandlerMetadata = {
name: 'ComplexHandler', name: 'ComplexHandler',
service: 'complex-service', service: 'complex-service',
operations: [ operations: [
{ name: 'op1', method: 'method1' }, { name: 'op1', method: 'method1' },
{ name: 'op2', method: 'method2' }, { name: 'op2', method: 'method2' },
], ],
schedules: [ schedules: [
{ {
operation: 'op1', operation: 'op1',
cronPattern: '0 * * * *', cronPattern: '0 * * * *',
}, },
], ],
}; };
const handler = mock(async () => {}) as JobHandler; const handler = mock(async () => {}) as JobHandler;
const config: HandlerConfiguration = { const config: HandlerConfiguration = {
name: 'ComplexHandler', name: 'ComplexHandler',
operations: { operations: {
op1: handler, op1: handler,
op2: handler, op2: handler,
}, },
scheduledJobs: [ scheduledJobs: [
{ {
name: 'scheduled1', name: 'scheduled1',
handler, handler,
pattern: '0 * * * *', pattern: '0 * * * *',
} as ScheduledJob, } as ScheduledJob,
], ],
}; };
registry.register(metadata, config); registry.register(metadata, config);
registry.setHandlerService('ComplexHandler', 'overridden-service'); registry.setHandlerService('ComplexHandler', 'overridden-service');
const exported = registry.export(); const exported = registry.export();
// Create new registry and import // Create new registry and import
const newRegistry = new HandlerRegistry(); const newRegistry = new HandlerRegistry();
newRegistry.import(exported); newRegistry.import(exported);
expect(newRegistry.getMetadata('ComplexHandler')).toEqual(metadata); expect(newRegistry.getMetadata('ComplexHandler')).toEqual(metadata);
expect(newRegistry.getConfiguration('ComplexHandler')).toEqual(config); expect(newRegistry.getConfiguration('ComplexHandler')).toEqual(config);
expect(newRegistry.getHandlerService('ComplexHandler')).toBe('overridden-service'); expect(newRegistry.getHandlerService('ComplexHandler')).toBe('overridden-service');
}); });
}); });
describe('Statistics Edge Cases', () => { describe('Statistics Edge Cases', () => {
it('should count schedules from metadata', () => { it('should count schedules from metadata', () => {
const metadata: HandlerMetadata = { const metadata: HandlerMetadata = {
name: 'ScheduledHandler', name: 'ScheduledHandler',
operations: [ operations: [{ name: 'op1', method: 'method1' }],
{ name: 'op1', method: 'method1' }, schedules: [
], { operation: 'op1', cronPattern: '* * * * *' },
schedules: [ { operation: 'op1', cronPattern: '0 * * * *' },
{ operation: 'op1', cronPattern: '* * * * *' }, ],
{ operation: 'op1', cronPattern: '0 * * * *' }, };
],
}; registry.registerMetadata(metadata);
registry.registerMetadata(metadata); const stats = registry.getStats();
expect(stats.handlers).toBe(1);
const stats = registry.getStats(); expect(stats.operations).toBe(1);
expect(stats.handlers).toBe(1); expect(stats.scheduledJobs).toBe(2);
expect(stats.operations).toBe(1); expect(stats.services).toBe(0); // No service specified
expect(stats.scheduledJobs).toBe(2); });
expect(stats.services).toBe(0); // No service specified
}); it('should not double count services', () => {
registry.registerMetadata({
it('should not double count services', () => { name: 'Handler1',
registry.registerMetadata({ service: 'service1',
name: 'Handler1', operations: [],
service: 'service1', });
operations: [],
}); registry.registerMetadata({
name: 'Handler2',
registry.registerMetadata({ service: 'service1', // Same service
name: 'Handler2', operations: [],
service: 'service1', // Same service });
operations: [],
}); registry.registerMetadata({
name: 'Handler3',
registry.registerMetadata({ service: 'service2',
name: 'Handler3', operations: [],
service: 'service2', });
operations: [],
}); const stats = registry.getStats();
expect(stats.services).toBe(2); // Only 2 unique services
const stats = registry.getStats(); });
expect(stats.services).toBe(2); // Only 2 unique services });
});
}); describe('Error Scenarios', () => {
it('should handle undefined values gracefully', () => {
describe('Error Scenarios', () => { expect(registry.getMetadata(undefined as any)).toBeUndefined();
it('should handle undefined values gracefully', () => { expect(registry.getConfiguration(undefined as any)).toBeUndefined();
expect(registry.getMetadata(undefined as any)).toBeUndefined(); expect(registry.getOperation(undefined as any, 'op')).toBeUndefined();
expect(registry.getConfiguration(undefined as any)).toBeUndefined(); expect(registry.hasHandler(undefined as any)).toBe(false);
expect(registry.getOperation(undefined as any, 'op')).toBeUndefined(); });
expect(registry.hasHandler(undefined as any)).toBe(false);
}); it('should handle null service lookup', () => {
const handlers = registry.getServiceHandlers(null as any);
it('should handle null service lookup', () => { expect(handlers).toEqual([]);
const handlers = registry.getServiceHandlers(null as any); });
expect(handlers).toEqual([]); });
}); });
});
});

View file

@ -6,4 +6,4 @@ export {
QueueSchedule, QueueSchedule,
ScheduledOperation, ScheduledOperation,
Disabled, Disabled,
} from './decorators/decorators'; } from './decorators/decorators';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,6 @@ export {
parseQueueName, parseQueueName,
} from './service-utils'; } from './service-utils';
// Batch processing // Batch processing
export { processBatchJob, processItems } from './batch-processor'; export { processBatchJob, processItems } from './batch-processor';

View file

@ -219,10 +219,14 @@ export class QueueManager {
ttl: 86400, // 24 hours default ttl: 86400, // 24 hours default
enableMetrics: true, enableMetrics: true,
logger: { logger: {
info: (...args: unknown[]) => this.logger.info(String(args[0]), args[1] as Record<string, unknown>), info: (...args: unknown[]) =>
error: (...args: unknown[]) => this.logger.error(String(args[0]), args[1] as Record<string, unknown>), this.logger.info(String(args[0]), args[1] as Record<string, unknown>),
warn: (...args: unknown[]) => this.logger.warn(String(args[0]), args[1] as Record<string, unknown>), error: (...args: unknown[]) =>
debug: (...args: unknown[]) => this.logger.debug(String(args[0]), args[1] as Record<string, unknown>), this.logger.error(String(args[0]), args[1] as Record<string, unknown>),
warn: (...args: unknown[]) =>
this.logger.warn(String(args[0]), args[1] as Record<string, unknown>),
debug: (...args: unknown[]) =>
this.logger.debug(String(args[0]), args[1] as Record<string, unknown>),
}, },
}); });
this.caches.set(queueName, cacheProvider); this.caches.set(queueName, cacheProvider);

View file

@ -1,35 +1,35 @@
/** /**
* Core constants used across the stock-bot application * Core constants used across the stock-bot application
*/ */
// Cache constants // Cache constants
export const CACHE_DEFAULTS = { export const CACHE_DEFAULTS = {
TTL: 3600, // 1 hour in seconds TTL: 3600, // 1 hour in seconds
KEY_PREFIX: 'cache:', KEY_PREFIX: 'cache:',
SCAN_COUNT: 100, SCAN_COUNT: 100,
} as const; } as const;
// Redis connection constants // Redis connection constants
export const REDIS_DEFAULTS = { export const REDIS_DEFAULTS = {
DB: 0, DB: 0,
MAX_RETRIES: 3, MAX_RETRIES: 3,
RETRY_DELAY: 100, RETRY_DELAY: 100,
CONNECT_TIMEOUT: 10000, CONNECT_TIMEOUT: 10000,
COMMAND_TIMEOUT: 5000, COMMAND_TIMEOUT: 5000,
KEEP_ALIVE: 0, KEEP_ALIVE: 0,
} as const; } as const;
// Shutdown constants // Shutdown constants
export const SHUTDOWN_DEFAULTS = { export const SHUTDOWN_DEFAULTS = {
TIMEOUT: 30000, // 30 seconds TIMEOUT: 30000, // 30 seconds
HIGH_PRIORITY: 10, HIGH_PRIORITY: 10,
MEDIUM_PRIORITY: 50, MEDIUM_PRIORITY: 50,
LOW_PRIORITY: 90, LOW_PRIORITY: 90,
} as const; } as const;
// Pool size constants // Pool size constants
export const POOL_SIZE_DEFAULTS = { export const POOL_SIZE_DEFAULTS = {
MIN_POOL_SIZE: 2, MIN_POOL_SIZE: 2,
MAX_POOL_SIZE: 10, MAX_POOL_SIZE: 10,
CPU_MULTIPLIER: 2, CPU_MULTIPLIER: 2,
} as const; } as const;

View file

@ -12,4 +12,4 @@ export function onShutdown(
): void { ): void {
const shutdown = Shutdown.getInstance(); const shutdown = Shutdown.getInstance();
shutdown.onShutdown(callback, priority, name); shutdown.onShutdown(callback, priority, name);
} }

View file

@ -33,8 +33,14 @@ export class Shutdown {
/** /**
* Register a cleanup callback * Register a cleanup callback
*/ */
onShutdown(callback: ShutdownCallback, priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, name?: string): void { onShutdown(
if (this.isShuttingDown) { return }; callback: ShutdownCallback,
priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY,
name?: string
): void {
if (this.isShuttingDown) {
return;
}
this.callbacks.push({ callback, priority, name }); this.callbacks.push({ callback, priority, name });
} }
@ -42,14 +48,16 @@ export class Shutdown {
* Initiate graceful shutdown * Initiate graceful shutdown
*/ */
async shutdown(): Promise<void> { async shutdown(): Promise<void> {
if (this.isShuttingDown) { return }; if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true; this.isShuttingDown = true;
const timeout = new Promise<never>((_, reject) => const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout) setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout)
); );
try { try {
await Promise.race([this.executeCallbacks(), timeout]); await Promise.race([this.executeCallbacks(), timeout]);
} catch (error) { } catch (error) {
@ -60,7 +68,7 @@ export class Shutdown {
private async executeCallbacks(): Promise<void> { private async executeCallbacks(): Promise<void> {
const sorted = [...this.callbacks].sort((a, b) => a.priority - b.priority); const sorted = [...this.callbacks].sort((a, b) => a.priority - b.priority);
for (const { callback, name } of sorted) { for (const { callback, name } of sorted) {
try { try {
await callback(); await callback();
@ -71,10 +79,12 @@ export class Shutdown {
} }
private setupSignalHandlers(): void { private setupSignalHandlers(): void {
if (this.signalHandlersRegistered) { return }; if (this.signalHandlersRegistered) {
return;
}
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT']; const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
signals.forEach(signal => { signals.forEach(signal => {
process.once(signal, async () => { process.once(signal, async () => {
if (!this.isShuttingDown) { if (!this.isShuttingDown) {
@ -87,7 +97,7 @@ export class Shutdown {
} }
}); });
}); });
this.signalHandlersRegistered = true; this.signalHandlersRegistered = true;
} }
} }

View file

@ -1,66 +1,66 @@
import { describe, it, expect } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import * as shutdownExports from '../src'; import * as shutdownExports from '../src';
import { Shutdown } from '../src'; import { Shutdown } from '../src';
describe('Shutdown Package Exports', () => { describe('Shutdown Package Exports', () => {
it('should export all main functions', () => { it('should export all main functions', () => {
expect(shutdownExports.onShutdown).toBeDefined(); expect(shutdownExports.onShutdown).toBeDefined();
expect(shutdownExports.onShutdownHigh).toBeDefined(); expect(shutdownExports.onShutdownHigh).toBeDefined();
expect(shutdownExports.onShutdownMedium).toBeDefined(); expect(shutdownExports.onShutdownMedium).toBeDefined();
expect(shutdownExports.onShutdownLow).toBeDefined(); expect(shutdownExports.onShutdownLow).toBeDefined();
expect(shutdownExports.setShutdownTimeout).toBeDefined(); expect(shutdownExports.setShutdownTimeout).toBeDefined();
expect(shutdownExports.isShuttingDown).toBeDefined(); expect(shutdownExports.isShuttingDown).toBeDefined();
expect(shutdownExports.isShutdownSignalReceived).toBeDefined(); expect(shutdownExports.isShutdownSignalReceived).toBeDefined();
expect(shutdownExports.getShutdownCallbackCount).toBeDefined(); expect(shutdownExports.getShutdownCallbackCount).toBeDefined();
expect(shutdownExports.initiateShutdown).toBeDefined(); expect(shutdownExports.initiateShutdown).toBeDefined();
expect(shutdownExports.shutdownAndExit).toBeDefined(); expect(shutdownExports.shutdownAndExit).toBeDefined();
expect(shutdownExports.resetShutdown).toBeDefined(); expect(shutdownExports.resetShutdown).toBeDefined();
}); });
it('should export Shutdown class', () => { it('should export Shutdown class', () => {
expect(shutdownExports.Shutdown).toBeDefined(); expect(shutdownExports.Shutdown).toBeDefined();
expect(shutdownExports.Shutdown).toBe(Shutdown); expect(shutdownExports.Shutdown).toBe(Shutdown);
}); });
it('should export correct function types', () => { it('should export correct function types', () => {
expect(typeof shutdownExports.onShutdown).toBe('function'); expect(typeof shutdownExports.onShutdown).toBe('function');
expect(typeof shutdownExports.onShutdownHigh).toBe('function'); expect(typeof shutdownExports.onShutdownHigh).toBe('function');
expect(typeof shutdownExports.onShutdownMedium).toBe('function'); expect(typeof shutdownExports.onShutdownMedium).toBe('function');
expect(typeof shutdownExports.onShutdownLow).toBe('function'); expect(typeof shutdownExports.onShutdownLow).toBe('function');
expect(typeof shutdownExports.setShutdownTimeout).toBe('function'); expect(typeof shutdownExports.setShutdownTimeout).toBe('function');
expect(typeof shutdownExports.isShuttingDown).toBe('function'); expect(typeof shutdownExports.isShuttingDown).toBe('function');
expect(typeof shutdownExports.isShutdownSignalReceived).toBe('function'); expect(typeof shutdownExports.isShutdownSignalReceived).toBe('function');
expect(typeof shutdownExports.getShutdownCallbackCount).toBe('function'); expect(typeof shutdownExports.getShutdownCallbackCount).toBe('function');
expect(typeof shutdownExports.initiateShutdown).toBe('function'); expect(typeof shutdownExports.initiateShutdown).toBe('function');
expect(typeof shutdownExports.shutdownAndExit).toBe('function'); expect(typeof shutdownExports.shutdownAndExit).toBe('function');
expect(typeof shutdownExports.resetShutdown).toBe('function'); expect(typeof shutdownExports.resetShutdown).toBe('function');
}); });
it('should export type definitions', () => { it('should export type definitions', () => {
// Type tests - these compile-time checks ensure types are exported // Type tests - these compile-time checks ensure types are exported
type TestShutdownCallback = shutdownExports.ShutdownCallback; type TestShutdownCallback = shutdownExports.ShutdownCallback;
type TestShutdownOptions = shutdownExports.ShutdownOptions; type TestShutdownOptions = shutdownExports.ShutdownOptions;
type TestShutdownResult = shutdownExports.ShutdownResult; type TestShutdownResult = shutdownExports.ShutdownResult;
type TestPrioritizedShutdownCallback = shutdownExports.PrioritizedShutdownCallback; type TestPrioritizedShutdownCallback = shutdownExports.PrioritizedShutdownCallback;
// Runtime check that types can be used // Runtime check that types can be used
const testCallback: TestShutdownCallback = async () => {}; const testCallback: TestShutdownCallback = async () => {};
const testOptions: TestShutdownOptions = { timeout: 5000, autoRegister: false }; const testOptions: TestShutdownOptions = { timeout: 5000, autoRegister: false };
const testResult: TestShutdownResult = { const testResult: TestShutdownResult = {
success: true, success: true,
callbacksExecuted: 1, callbacksExecuted: 1,
callbacksFailed: 0, callbacksFailed: 0,
duration: 100, duration: 100,
}; };
const testPrioritized: TestPrioritizedShutdownCallback = { const testPrioritized: TestPrioritizedShutdownCallback = {
callback: testCallback, callback: testCallback,
priority: 50, priority: 50,
name: 'test', name: 'test',
}; };
expect(testCallback).toBeDefined(); expect(testCallback).toBeDefined();
expect(testOptions).toBeDefined(); expect(testOptions).toBeDefined();
expect(testResult).toBeDefined(); expect(testResult).toBeDefined();
expect(testPrioritized).toBeDefined(); expect(testPrioritized).toBeDefined();
}); });
}); });

View file

@ -10,8 +10,8 @@ import {
onShutdownMedium, onShutdownMedium,
resetShutdown, resetShutdown,
setShutdownTimeout, setShutdownTimeout,
shutdownAndExit,
Shutdown, Shutdown,
shutdownAndExit,
} from '../src'; } from '../src';
import type { ShutdownOptions, ShutdownResult } from '../src/types'; import type { ShutdownOptions, ShutdownResult } from '../src/types';
@ -104,7 +104,9 @@ describe('Shutdown Comprehensive Tests', () => {
it('should handle negative timeout values', () => { it('should handle negative timeout values', () => {
// Should throw for negative values // Should throw for negative values
expect(() => setShutdownTimeout(-1000)).toThrow('Shutdown timeout must be a positive number'); expect(() => setShutdownTimeout(-1000)).toThrow(
'Shutdown timeout must be a positive number'
);
}); });
it('should handle zero timeout', () => { it('should handle zero timeout', () => {
@ -415,7 +417,7 @@ describe('Shutdown Comprehensive Tests', () => {
onShutdown(callback); onShutdown(callback);
await expect(shutdownAndExit('SIGTERM', 1)).rejects.toThrow('Process exit called'); await expect(shutdownAndExit('SIGTERM', 1)).rejects.toThrow('Process exit called');
expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledTimes(1);
expect(exitMock).toHaveBeenCalledWith(1); expect(exitMock).toHaveBeenCalledWith(1);
} finally { } finally {
@ -446,7 +448,7 @@ describe('Shutdown Comprehensive Tests', () => {
onShutdown(callback); onShutdown(callback);
const result = await initiateShutdown('CUSTOM_SIGNAL'); const result = await initiateShutdown('CUSTOM_SIGNAL');
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalled();
}); });
@ -454,7 +456,7 @@ describe('Shutdown Comprehensive Tests', () => {
it('should handle shutdown from getInstance without options', () => { it('should handle shutdown from getInstance without options', () => {
const instance = Shutdown.getInstance(); const instance = Shutdown.getInstance();
expect(instance).toBeInstanceOf(Shutdown); expect(instance).toBeInstanceOf(Shutdown);
// Call again to test singleton // Call again to test singleton
const instance2 = Shutdown.getInstance(); const instance2 = Shutdown.getInstance();
expect(instance2).toBe(instance); expect(instance2).toBe(instance);
@ -464,11 +466,11 @@ describe('Shutdown Comprehensive Tests', () => {
// Start fresh // Start fresh
resetShutdown(); resetShutdown();
expect(getShutdownCallbackCount()).toBe(0); expect(getShutdownCallbackCount()).toBe(0);
// Add callback - this creates global instance // Add callback - this creates global instance
onShutdown(async () => {}); onShutdown(async () => {});
expect(getShutdownCallbackCount()).toBe(1); expect(getShutdownCallbackCount()).toBe(1);
// Reset and verify // Reset and verify
resetShutdown(); resetShutdown();
expect(getShutdownCallbackCount()).toBe(0); expect(getShutdownCallbackCount()).toBe(0);
@ -484,7 +486,7 @@ describe('Shutdown Comprehensive Tests', () => {
onShutdown(undefinedRejectCallback, 'undefined-reject'); onShutdown(undefinedRejectCallback, 'undefined-reject');
const result = await initiateShutdown(); const result = await initiateShutdown();
expect(result.callbacksFailed).toBe(1); expect(result.callbacksFailed).toBe(1);
expect(result.success).toBe(false); expect(result.success).toBe(false);
}); });
@ -497,7 +499,7 @@ describe('Shutdown Comprehensive Tests', () => {
onShutdown(nullRejectCallback, 'null-reject'); onShutdown(nullRejectCallback, 'null-reject');
const result = await initiateShutdown(); const result = await initiateShutdown();
expect(result.callbacksFailed).toBe(1); expect(result.callbacksFailed).toBe(1);
expect(result.success).toBe(false); expect(result.success).toBe(false);
}); });
@ -506,7 +508,7 @@ describe('Shutdown Comprehensive Tests', () => {
const syncCallback = mock(() => { const syncCallback = mock(() => {
// Synchronous - returns void // Synchronous - returns void
}); });
const asyncCallback = mock(async () => { const asyncCallback = mock(async () => {
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
}); });
@ -515,7 +517,7 @@ describe('Shutdown Comprehensive Tests', () => {
onShutdown(asyncCallback); onShutdown(asyncCallback);
const result = await initiateShutdown(); const result = await initiateShutdown();
expect(result.callbacksExecuted).toBe(2); expect(result.callbacksExecuted).toBe(2);
expect(syncCallback).toHaveBeenCalled(); expect(syncCallback).toHaveBeenCalled();
expect(asyncCallback).toHaveBeenCalled(); expect(asyncCallback).toHaveBeenCalled();
@ -525,27 +527,27 @@ describe('Shutdown Comprehensive Tests', () => {
describe('Shutdown Method Variants', () => { describe('Shutdown Method Variants', () => {
it('should handle direct priority parameter in onShutdown', () => { it('should handle direct priority parameter in onShutdown', () => {
const callback = mock(async () => {}); const callback = mock(async () => {});
// Test with name and priority swapped (legacy support) // Test with name and priority swapped (legacy support)
onShutdown(callback, 75, 'custom-name'); onShutdown(callback, 75, 'custom-name');
expect(getShutdownCallbackCount()).toBe(1); expect(getShutdownCallbackCount()).toBe(1);
}); });
it('should handle callback without any parameters', () => { it('should handle callback without any parameters', () => {
const callback = mock(async () => {}); const callback = mock(async () => {});
onShutdown(callback); onShutdown(callback);
expect(getShutdownCallbackCount()).toBe(1); expect(getShutdownCallbackCount()).toBe(1);
}); });
it('should validate setTimeout input', () => { it('should validate setTimeout input', () => {
const shutdown = new Shutdown(); const shutdown = new Shutdown();
// Valid timeout // Valid timeout
expect(() => shutdown.setTimeout(5000)).not.toThrow(); expect(() => shutdown.setTimeout(5000)).not.toThrow();
// Invalid timeouts should throw // Invalid timeouts should throw
expect(() => shutdown.setTimeout(-1)).toThrow(); expect(() => shutdown.setTimeout(-1)).toThrow();
expect(() => shutdown.setTimeout(0)).toThrow(); expect(() => shutdown.setTimeout(0)).toThrow();

View file

@ -1,254 +1,254 @@
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import { Shutdown } from '../src/shutdown'; import { Shutdown } from '../src/shutdown';
describe('Shutdown Signal Handlers', () => { describe('Shutdown Signal Handlers', () => {
let shutdown: Shutdown; let shutdown: Shutdown;
let processOnSpy: any; let processOnSpy: any;
let processExitSpy: any; let processExitSpy: any;
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
const originalOn = process.on; const originalOn = process.on;
const originalExit = process.exit; const originalExit = process.exit;
beforeEach(() => { beforeEach(() => {
// Reset singleton instance // Reset singleton instance
(Shutdown as any).instance = null; (Shutdown as any).instance = null;
// Clean up global flag // Clean up global flag
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__; delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
// Mock process.on // Mock process.on
const listeners: Record<string, Function[]> = {}; const listeners: Record<string, Function[]> = {};
processOnSpy = mock((event: string, handler: Function) => { processOnSpy = mock((event: string, handler: Function) => {
if (!listeners[event]) { if (!listeners[event]) {
listeners[event] = []; listeners[event] = [];
} }
listeners[event].push(handler); listeners[event].push(handler);
}); });
process.on = processOnSpy as any; process.on = processOnSpy as any;
// Mock process.exit // Mock process.exit
processExitSpy = mock((code?: number) => { processExitSpy = mock((code?: number) => {
// Just record the call, don't throw // Just record the call, don't throw
return; return;
}); });
process.exit = processExitSpy as any; process.exit = processExitSpy as any;
// Store listeners for manual triggering // Store listeners for manual triggering
(global as any).__testListeners = listeners; (global as any).__testListeners = listeners;
}); });
afterEach(() => { afterEach(() => {
// Restore original methods // Restore original methods
process.on = originalOn; process.on = originalOn;
process.exit = originalExit; process.exit = originalExit;
if (originalPlatform) { if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform); Object.defineProperty(process, 'platform', originalPlatform);
} }
// Clean up // Clean up
(Shutdown as any).instance = null; (Shutdown as any).instance = null;
delete (global as any).__testListeners; delete (global as any).__testListeners;
}); });
describe('Signal Handler Registration', () => { describe('Signal Handler Registration', () => {
it('should register Unix signal handlers on non-Windows', () => { it('should register Unix signal handlers on non-Windows', () => {
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: 'linux', value: 'linux',
configurable: true, configurable: true,
}); });
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
// Check that Unix signals were registered // Check that Unix signals were registered
expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
expect(processOnSpy).toHaveBeenCalledWith('SIGUSR2', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('SIGUSR2', expect.any(Function));
expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function));
expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function));
}); });
it('should register Windows signal handlers on Windows', () => { it('should register Windows signal handlers on Windows', () => {
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: 'win32', value: 'win32',
configurable: true, configurable: true,
}); });
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
// Check that Windows signals were registered // Check that Windows signals were registered
expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
expect(processOnSpy).not.toHaveBeenCalledWith('SIGUSR2', expect.any(Function)); expect(processOnSpy).not.toHaveBeenCalledWith('SIGUSR2', expect.any(Function));
expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function));
expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function));
}); });
it('should not register handlers when autoRegister is false', () => { it('should not register handlers when autoRegister is false', () => {
shutdown = new Shutdown({ autoRegister: false }); shutdown = new Shutdown({ autoRegister: false });
expect(processOnSpy).not.toHaveBeenCalled(); expect(processOnSpy).not.toHaveBeenCalled();
}); });
it('should not register handlers twice', () => { it('should not register handlers twice', () => {
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
const callCount = processOnSpy.mock.calls.length; const callCount = processOnSpy.mock.calls.length;
// Try to setup handlers again (internally) // Try to setup handlers again (internally)
shutdown['setupSignalHandlers'](); shutdown['setupSignalHandlers']();
// Should not register additional handlers // Should not register additional handlers
expect(processOnSpy.mock.calls.length).toBe(callCount); expect(processOnSpy.mock.calls.length).toBe(callCount);
}); });
}); });
describe('Signal Handler Behavior', () => { describe('Signal Handler Behavior', () => {
it('should handle SIGTERM signal', async () => { it('should handle SIGTERM signal', async () => {
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
const callback = mock(async () => {}); const callback = mock(async () => {});
shutdown.onShutdown(callback); shutdown.onShutdown(callback);
const listeners = (global as any).__testListeners; const listeners = (global as any).__testListeners;
const sigtermHandler = listeners['SIGTERM'][0]; const sigtermHandler = listeners['SIGTERM'][0];
// Trigger SIGTERM (this starts async shutdown) // Trigger SIGTERM (this starts async shutdown)
sigtermHandler(); sigtermHandler();
// Verify flags are set immediately // Verify flags are set immediately
expect(shutdown.isShutdownSignalReceived()).toBe(true); expect(shutdown.isShutdownSignalReceived()).toBe(true);
expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true); expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true);
// Wait a bit for async shutdown to complete // Wait a bit for async shutdown to complete
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
// Now process.exit should have been called // Now process.exit should have been called
expect(processExitSpy).toHaveBeenCalledWith(0); expect(processExitSpy).toHaveBeenCalledWith(0);
}); });
it('should handle SIGINT signal', async () => { it('should handle SIGINT signal', async () => {
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
const callback = mock(async () => {}); const callback = mock(async () => {});
shutdown.onShutdown(callback); shutdown.onShutdown(callback);
const listeners = (global as any).__testListeners; const listeners = (global as any).__testListeners;
const sigintHandler = listeners['SIGINT'][0]; const sigintHandler = listeners['SIGINT'][0];
// Trigger SIGINT (this starts async shutdown) // Trigger SIGINT (this starts async shutdown)
sigintHandler(); sigintHandler();
// Verify flags are set immediately // Verify flags are set immediately
expect(shutdown.isShutdownSignalReceived()).toBe(true); expect(shutdown.isShutdownSignalReceived()).toBe(true);
// Wait a bit for async shutdown to complete // Wait a bit for async shutdown to complete
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
// Now process.exit should have been called // Now process.exit should have been called
expect(processExitSpy).toHaveBeenCalledWith(0); expect(processExitSpy).toHaveBeenCalledWith(0);
}); });
it('should handle uncaughtException', async () => { it('should handle uncaughtException', async () => {
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
const listeners = (global as any).__testListeners; const listeners = (global as any).__testListeners;
const exceptionHandler = listeners['uncaughtException'][0]; const exceptionHandler = listeners['uncaughtException'][0];
// Trigger uncaughtException (this starts async shutdown with exit code 1) // Trigger uncaughtException (this starts async shutdown with exit code 1)
exceptionHandler(new Error('Uncaught error')); exceptionHandler(new Error('Uncaught error'));
// Wait a bit for async shutdown to complete // Wait a bit for async shutdown to complete
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
// Should exit with code 1 for uncaught exceptions // Should exit with code 1 for uncaught exceptions
expect(processExitSpy).toHaveBeenCalledWith(1); expect(processExitSpy).toHaveBeenCalledWith(1);
}); });
it('should handle unhandledRejection', async () => { it('should handle unhandledRejection', async () => {
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
const listeners = (global as any).__testListeners; const listeners = (global as any).__testListeners;
const rejectionHandler = listeners['unhandledRejection'][0]; const rejectionHandler = listeners['unhandledRejection'][0];
// Trigger unhandledRejection (this starts async shutdown with exit code 1) // Trigger unhandledRejection (this starts async shutdown with exit code 1)
rejectionHandler(new Error('Unhandled rejection')); rejectionHandler(new Error('Unhandled rejection'));
// Wait a bit for async shutdown to complete // Wait a bit for async shutdown to complete
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
// Should exit with code 1 for unhandled rejections // Should exit with code 1 for unhandled rejections
expect(processExitSpy).toHaveBeenCalledWith(1); expect(processExitSpy).toHaveBeenCalledWith(1);
}); });
it('should not process signal if already shutting down', async () => { it('should not process signal if already shutting down', async () => {
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
// Start shutdown // Start shutdown
shutdown['isShuttingDown'] = true; shutdown['isShuttingDown'] = true;
const listeners = (global as any).__testListeners; const listeners = (global as any).__testListeners;
const sigtermHandler = listeners['SIGTERM'][0]; const sigtermHandler = listeners['SIGTERM'][0];
// Mock shutdownAndExit to track calls // Mock shutdownAndExit to track calls
const shutdownAndExitSpy = mock(() => Promise.resolve()); const shutdownAndExitSpy = mock(() => Promise.resolve());
shutdown.shutdownAndExit = shutdownAndExitSpy as any; shutdown.shutdownAndExit = shutdownAndExitSpy as any;
// Trigger SIGTERM // Trigger SIGTERM
sigtermHandler(); sigtermHandler();
// Should not call shutdownAndExit since already shutting down // Should not call shutdownAndExit since already shutting down
expect(shutdownAndExitSpy).not.toHaveBeenCalled(); expect(shutdownAndExitSpy).not.toHaveBeenCalled();
}); });
it('should handle shutdown failure in signal handler', async () => { it('should handle shutdown failure in signal handler', async () => {
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
// Mock shutdownAndExit to reject // Mock shutdownAndExit to reject
shutdown.shutdownAndExit = mock(async () => { shutdown.shutdownAndExit = mock(async () => {
throw new Error('Shutdown failed'); throw new Error('Shutdown failed');
}) as any; }) as any;
const listeners = (global as any).__testListeners; const listeners = (global as any).__testListeners;
const sigtermHandler = listeners['SIGTERM'][0]; const sigtermHandler = listeners['SIGTERM'][0];
// Trigger SIGTERM - should fall back to process.exit(1) // Trigger SIGTERM - should fall back to process.exit(1)
sigtermHandler(); sigtermHandler();
// Wait a bit for async shutdown to fail and fallback to occur // Wait a bit for async shutdown to fail and fallback to occur
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
expect(processExitSpy).toHaveBeenCalledWith(1); expect(processExitSpy).toHaveBeenCalledWith(1);
}); });
}); });
describe('Global Flag Behavior', () => { describe('Global Flag Behavior', () => {
it('should set global shutdown flag on signal', async () => { it('should set global shutdown flag on signal', async () => {
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__; delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
shutdown = new Shutdown({ autoRegister: true }); shutdown = new Shutdown({ autoRegister: true });
const listeners = (global as any).__testListeners; const listeners = (global as any).__testListeners;
const sigtermHandler = listeners['SIGTERM'][0]; const sigtermHandler = listeners['SIGTERM'][0];
// Trigger signal (this sets the flag immediately) // Trigger signal (this sets the flag immediately)
sigtermHandler(); sigtermHandler();
expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true); expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true);
// Wait for async shutdown to complete to avoid hanging promises // Wait for async shutdown to complete to avoid hanging promises
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise(resolve => setTimeout(resolve, 10));
}); });
it('should check global flag in isShutdownSignalReceived', () => { it('should check global flag in isShutdownSignalReceived', () => {
shutdown = new Shutdown({ autoRegister: false }); shutdown = new Shutdown({ autoRegister: false });
expect(shutdown.isShutdownSignalReceived()).toBe(false); expect(shutdown.isShutdownSignalReceived()).toBe(false);
// Set global flag // Set global flag
(global as any).__SHUTDOWN_SIGNAL_RECEIVED__ = true; (global as any).__SHUTDOWN_SIGNAL_RECEIVED__ = true;
// Even without instance flag, should return true // Even without instance flag, should return true
expect(shutdown.isShutdownSignalReceived()).toBe(true); expect(shutdown.isShutdownSignalReceived()).toBe(true);
// Clean up // Clean up
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__; delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
}); });
}); });
}); });

View file

@ -16,7 +16,9 @@ export class SimpleMongoDBClient {
} }
async find(collection: string, filter: any = {}): Promise<any[]> { async find(collection: string, filter: any = {}): Promise<any[]> {
if (!this.connected) {await this.connect();} if (!this.connected) {
await this.connect();
}
const docs = this.collections.get(collection) || []; const docs = this.collections.get(collection) || [];
// Simple filter matching // Simple filter matching
@ -26,7 +28,9 @@ export class SimpleMongoDBClient {
return docs.filter(doc => { return docs.filter(doc => {
for (const [key, value] of Object.entries(filter)) { for (const [key, value] of Object.entries(filter)) {
if (doc[key] !== value) {return false;} if (doc[key] !== value) {
return false;
}
} }
return true; return true;
}); });
@ -38,7 +42,9 @@ export class SimpleMongoDBClient {
} }
async insert(collection: string, doc: any): Promise<void> { async insert(collection: string, doc: any): Promise<void> {
if (!this.connected) {await this.connect();} if (!this.connected) {
await this.connect();
}
const docs = this.collections.get(collection) || []; const docs = this.collections.get(collection) || [];
docs.push({ ...doc, _id: Math.random().toString(36) }); docs.push({ ...doc, _id: Math.random().toString(36) });
this.collections.set(collection, docs); this.collections.set(collection, docs);
@ -51,10 +57,14 @@ export class SimpleMongoDBClient {
} }
async update(collection: string, filter: any, update: any): Promise<number> { async update(collection: string, filter: any, update: any): Promise<number> {
if (!this.connected) {await this.connect();} if (!this.connected) {
await this.connect();
}
const docs = await this.find(collection, filter); const docs = await this.find(collection, filter);
if (docs.length === 0) {return 0;} if (docs.length === 0) {
return 0;
}
const doc = docs[0]; const doc = docs[0];
if (update.$set) { if (update.$set) {
@ -65,7 +75,9 @@ export class SimpleMongoDBClient {
} }
async updateMany(collection: string, filter: any, update: any): Promise<number> { async updateMany(collection: string, filter: any, update: any): Promise<number> {
if (!this.connected) {await this.connect();} if (!this.connected) {
await this.connect();
}
const docs = await this.find(collection, filter); const docs = await this.find(collection, filter);
for (const doc of docs) { for (const doc of docs) {
@ -78,11 +90,15 @@ export class SimpleMongoDBClient {
} }
async delete(collection: string, filter: any): Promise<number> { async delete(collection: string, filter: any): Promise<number> {
if (!this.connected) {await this.connect();} if (!this.connected) {
await this.connect();
}
const allDocs = this.collections.get(collection) || []; const allDocs = this.collections.get(collection) || [];
const toDelete = await this.find(collection, filter); const toDelete = await this.find(collection, filter);
if (toDelete.length === 0) {return 0;} if (toDelete.length === 0) {
return 0;
}
const remaining = allDocs.filter(doc => !toDelete.includes(doc)); const remaining = allDocs.filter(doc => !toDelete.includes(doc));
this.collections.set(collection, remaining); this.collections.set(collection, remaining);
@ -91,7 +107,9 @@ export class SimpleMongoDBClient {
} }
async deleteMany(collection: string, filter: any): Promise<number> { async deleteMany(collection: string, filter: any): Promise<number> {
if (!this.connected) {await this.connect();} if (!this.connected) {
await this.connect();
}
const allDocs = this.collections.get(collection) || []; const allDocs = this.collections.get(collection) || [];
const toDelete = await this.find(collection, filter); const toDelete = await this.find(collection, filter);
@ -102,7 +120,9 @@ export class SimpleMongoDBClient {
} }
async batchUpsert(collection: string, documents: any[], uniqueKeys: string[]): Promise<void> { async batchUpsert(collection: string, documents: any[], uniqueKeys: string[]): Promise<void> {
if (!this.connected) {await this.connect();} if (!this.connected) {
await this.connect();
}
for (const doc of documents) { for (const doc of documents) {
const filter: any = {}; const filter: any = {};

View file

@ -22,18 +22,24 @@ export class SimplePostgresClient {
break; break;
} }
} }
if (match) {return row;} if (match) {
return row;
}
} }
return null; return null;
} }
async find(table: string, where: any): Promise<any[]> { async find(table: string, where: any): Promise<any[]> {
const rows = this.tables.get(table) || []; const rows = this.tables.get(table) || [];
if (Object.keys(where).length === 0) {return rows;} if (Object.keys(where).length === 0) {
return rows;
}
return rows.filter(row => { return rows.filter(row => {
for (const [key, value] of Object.entries(where)) { for (const [key, value] of Object.entries(where)) {
if (row[key] !== value) {return false;} if (row[key] !== value) {
return false;
}
} }
return true; return true;
}); });
@ -72,7 +78,9 @@ export class SimplePostgresClient {
const rows = this.tables.get(table) || []; const rows = this.tables.get(table) || [];
const remaining = rows.filter(row => { const remaining = rows.filter(row => {
for (const [key, value] of Object.entries(where)) { for (const [key, value] of Object.entries(where)) {
if (row[key] !== value) {return true;} if (row[key] !== value) {
return true;
}
} }
return false; return false;
}); });

View file

@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, mock } from 'bun:test'; import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { SimpleBrowser } from '../src/simple-browser'; import { SimpleBrowser } from '../src/simple-browser';
describe('Browser', () => { describe('Browser', () => {
let browser: SimpleBrowser; let browser: SimpleBrowser;
const logger = { const logger = {

View file

@ -1,286 +1,284 @@
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import { fetch } from '../src/fetch'; import { fetch } from '../src/fetch';
describe('Enhanced Fetch', () => { describe('Enhanced Fetch', () => {
let originalFetch: typeof globalThis.fetch; let originalFetch: typeof globalThis.fetch;
let mockFetch: any; let mockFetch: any;
let mockLogger: any; let mockLogger: any;
beforeEach(() => { beforeEach(() => {
originalFetch = globalThis.fetch; originalFetch = globalThis.fetch;
mockFetch = mock(() => Promise.resolve(new Response('test'))); mockFetch = mock(() => Promise.resolve(new Response('test')));
globalThis.fetch = mockFetch; globalThis.fetch = mockFetch;
mockLogger = { mockLogger = {
debug: mock(() => {}), debug: mock(() => {}),
info: mock(() => {}), info: mock(() => {}),
error: mock(() => {}), error: mock(() => {}),
}; };
}); });
afterEach(() => { afterEach(() => {
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
}); });
describe('basic fetch', () => { describe('basic fetch', () => {
it('should make simple GET request', async () => { it('should make simple GET request', async () => {
const mockResponse = new Response('test data', { status: 200 }); const mockResponse = new Response('test data', { status: 200 });
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
const response = await fetch('https://api.example.com/data'); const response = await fetch('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', { expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
method: 'GET', method: 'GET',
headers: {}, headers: {},
}); });
expect(response).toBe(mockResponse); expect(response).toBe(mockResponse);
}); });
it('should make POST request with body', async () => { it('should make POST request with body', async () => {
const mockResponse = new Response('created', { status: 201 }); const mockResponse = new Response('created', { status: 201 });
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
const body = JSON.stringify({ name: 'test' }); const body = JSON.stringify({ name: 'test' });
const response = await fetch('https://api.example.com/data', { const response = await fetch('https://api.example.com/data', {
method: 'POST', method: 'POST',
body, body,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', { expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
method: 'POST', method: 'POST',
body, body,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });
expect(response).toBe(mockResponse); expect(response).toBe(mockResponse);
}); });
it('should handle URL objects', async () => { it('should handle URL objects', async () => {
const mockResponse = new Response('test'); const mockResponse = new Response('test');
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
const url = new URL('https://api.example.com/data'); const url = new URL('https://api.example.com/data');
await fetch(url); await fetch(url);
expect(mockFetch).toHaveBeenCalledWith(url, expect.any(Object)); expect(mockFetch).toHaveBeenCalledWith(url, expect.any(Object));
}); });
it('should handle Request objects', async () => { it('should handle Request objects', async () => {
const mockResponse = new Response('test'); const mockResponse = new Response('test');
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
const request = new Request('https://api.example.com/data', { const request = new Request('https://api.example.com/data', {
method: 'PUT', method: 'PUT',
}); });
await fetch(request); await fetch(request);
expect(mockFetch).toHaveBeenCalledWith(request, expect.any(Object)); expect(mockFetch).toHaveBeenCalledWith(request, expect.any(Object));
}); });
}); });
describe('proxy support', () => { describe('proxy support', () => {
it('should add proxy to request options', async () => { it('should add proxy to request options', async () => {
const mockResponse = new Response('proxy test'); const mockResponse = new Response('proxy test');
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
await fetch('https://api.example.com/data', { await fetch('https://api.example.com/data', {
proxy: 'http://proxy.example.com:8080', proxy: 'http://proxy.example.com:8080',
}); });
expect(mockFetch).toHaveBeenCalledWith( expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data', 'https://api.example.com/data',
expect.objectContaining({ expect.objectContaining({
proxy: 'http://proxy.example.com:8080', proxy: 'http://proxy.example.com:8080',
}) })
); );
}); });
it('should handle null proxy', async () => { it('should handle null proxy', async () => {
const mockResponse = new Response('no proxy'); const mockResponse = new Response('no proxy');
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
await fetch('https://api.example.com/data', { await fetch('https://api.example.com/data', {
proxy: null, proxy: null,
}); });
expect(mockFetch).toHaveBeenCalledWith( expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data', 'https://api.example.com/data',
expect.not.objectContaining({ expect.not.objectContaining({
proxy: expect.anything(), proxy: expect.anything(),
}) })
); );
}); });
}); });
describe('timeout support', () => { describe('timeout support', () => {
it('should handle timeout', async () => { it('should handle timeout', async () => {
mockFetch.mockImplementation((url, options) => { mockFetch.mockImplementation((url, options) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => resolve(new Response('delayed')), 100); const timeoutId = setTimeout(() => resolve(new Response('delayed')), 100);
// Listen for abort signal // Listen for abort signal
if (options?.signal) { if (options?.signal) {
options.signal.addEventListener('abort', () => { options.signal.addEventListener('abort', () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
reject(new DOMException('The operation was aborted', 'AbortError')); reject(new DOMException('The operation was aborted', 'AbortError'));
}); });
} }
}); });
}); });
await expect( await expect(fetch('https://api.example.com/data', { timeout: 50 })).rejects.toThrow(
fetch('https://api.example.com/data', { timeout: 50 }) 'The operation was aborted'
).rejects.toThrow('The operation was aborted'); );
}); });
it('should clear timeout on success', async () => { it('should clear timeout on success', async () => {
const mockResponse = new Response('quick response'); const mockResponse = new Response('quick response');
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
const response = await fetch('https://api.example.com/data', { const response = await fetch('https://api.example.com/data', {
timeout: 1000, timeout: 1000,
}); });
expect(response).toBe(mockResponse); expect(response).toBe(mockResponse);
}); });
it('should clear timeout on error', async () => { it('should clear timeout on error', async () => {
mockFetch.mockRejectedValue(new Error('Network error')); mockFetch.mockRejectedValue(new Error('Network error'));
await expect( await expect(fetch('https://api.example.com/data', { timeout: 1000 })).rejects.toThrow(
fetch('https://api.example.com/data', { timeout: 1000 }) 'Network error'
).rejects.toThrow('Network error'); );
}); });
}); });
describe('logging', () => { describe('logging', () => {
it('should log request details', async () => { it('should log request details', async () => {
const mockResponse = new Response('test', { const mockResponse = new Response('test', {
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
headers: new Headers({ 'content-type': 'text/plain' }), headers: new Headers({ 'content-type': 'text/plain' }),
}); });
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
await fetch('https://api.example.com/data', { await fetch('https://api.example.com/data', {
logger: mockLogger, logger: mockLogger,
method: 'POST', method: 'POST',
headers: { Authorization: 'Bearer token' }, headers: { Authorization: 'Bearer token' },
}); });
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP request', { expect(mockLogger.debug).toHaveBeenCalledWith('HTTP request', {
method: 'POST', method: 'POST',
url: 'https://api.example.com/data', url: 'https://api.example.com/data',
headers: { Authorization: 'Bearer token' }, headers: { Authorization: 'Bearer token' },
proxy: null, proxy: null,
}); });
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP response', { expect(mockLogger.debug).toHaveBeenCalledWith('HTTP response', {
url: 'https://api.example.com/data', url: 'https://api.example.com/data',
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
ok: true, ok: true,
headers: { 'content-type': 'text/plain' }, headers: { 'content-type': 'text/plain' },
}); });
}); });
it('should log errors', async () => { it('should log errors', async () => {
const error = new Error('Connection failed'); const error = new Error('Connection failed');
mockFetch.mockRejectedValue(error); mockFetch.mockRejectedValue(error);
await expect( await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toThrow(
fetch('https://api.example.com/data', { logger: mockLogger }) 'Connection failed'
).rejects.toThrow('Connection failed'); );
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', { expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
url: 'https://api.example.com/data', url: 'https://api.example.com/data',
error: 'Connection failed', error: 'Connection failed',
name: 'Error', name: 'Error',
}); });
}); });
it('should use console as default logger', async () => { it('should use console as default logger', async () => {
const consoleSpy = mock(console.debug); const consoleSpy = mock(console.debug);
console.debug = consoleSpy; console.debug = consoleSpy;
const mockResponse = new Response('test'); const mockResponse = new Response('test');
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
await fetch('https://api.example.com/data'); await fetch('https://api.example.com/data');
expect(consoleSpy).toHaveBeenCalledTimes(2); // Request and response expect(consoleSpy).toHaveBeenCalledTimes(2); // Request and response
console.debug = originalFetch as any; console.debug = originalFetch as any;
}); });
}); });
describe('request options', () => { describe('request options', () => {
it('should forward all standard RequestInit options', async () => { it('should forward all standard RequestInit options', async () => {
const mockResponse = new Response('test'); const mockResponse = new Response('test');
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
const controller = new AbortController(); const controller = new AbortController();
const options = { const options = {
method: 'PATCH' as const, method: 'PATCH' as const,
headers: { 'X-Custom': 'value' }, headers: { 'X-Custom': 'value' },
body: 'data', body: 'data',
signal: controller.signal, signal: controller.signal,
credentials: 'include' as const, credentials: 'include' as const,
cache: 'no-store' as const, cache: 'no-store' as const,
redirect: 'manual' as const, redirect: 'manual' as const,
referrer: 'https://referrer.com', referrer: 'https://referrer.com',
referrerPolicy: 'no-referrer' as const, referrerPolicy: 'no-referrer' as const,
integrity: 'sha256-hash', integrity: 'sha256-hash',
keepalive: true, keepalive: true,
mode: 'cors' as const, mode: 'cors' as const,
}; };
await fetch('https://api.example.com/data', options); await fetch('https://api.example.com/data', options);
expect(mockFetch).toHaveBeenCalledWith( expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data', 'https://api.example.com/data',
expect.objectContaining(options) expect.objectContaining(options)
); );
}); });
it('should handle undefined options', async () => { it('should handle undefined options', async () => {
const mockResponse = new Response('test'); const mockResponse = new Response('test');
mockFetch.mockResolvedValue(mockResponse); mockFetch.mockResolvedValue(mockResponse);
await fetch('https://api.example.com/data', undefined); await fetch('https://api.example.com/data', undefined);
expect(mockFetch).toHaveBeenCalledWith( expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data', 'https://api.example.com/data',
expect.objectContaining({ expect.objectContaining({
method: 'GET', method: 'GET',
headers: {}, headers: {},
}) })
); );
}); });
}); });
describe('error handling', () => { describe('error handling', () => {
it('should propagate fetch errors', async () => { it('should propagate fetch errors', async () => {
const error = new TypeError('Failed to fetch'); const error = new TypeError('Failed to fetch');
mockFetch.mockRejectedValue(error); mockFetch.mockRejectedValue(error);
await expect(fetch('https://api.example.com/data')).rejects.toThrow( await expect(fetch('https://api.example.com/data')).rejects.toThrow('Failed to fetch');
'Failed to fetch' });
);
}); it('should handle non-Error objects', async () => {
mockFetch.mockRejectedValue('string error');
it('should handle non-Error objects', async () => {
mockFetch.mockRejectedValue('string error'); await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toBe(
'string error'
await expect( );
fetch('https://api.example.com/data', { logger: mockLogger })
).rejects.toBe('string error'); expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
url: 'https://api.example.com/data',
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', { error: 'string error',
url: 'https://api.example.com/data', name: 'Unknown',
error: 'string error', });
name: 'Unknown', });
}); });
}); });
});
});

View file

@ -1,60 +1,60 @@
import { describe, expect, it } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import { getRandomUserAgent } from '../src/user-agent'; import { getRandomUserAgent } from '../src/user-agent';
describe('User Agent', () => { describe('User Agent', () => {
describe('getRandomUserAgent', () => { describe('getRandomUserAgent', () => {
it('should return a user agent string', () => { it('should return a user agent string', () => {
const userAgent = getRandomUserAgent(); const userAgent = getRandomUserAgent();
expect(typeof userAgent).toBe('string'); expect(typeof userAgent).toBe('string');
expect(userAgent.length).toBeGreaterThan(0); expect(userAgent.length).toBeGreaterThan(0);
}); });
it('should return a valid user agent containing Mozilla', () => { it('should return a valid user agent containing Mozilla', () => {
const userAgent = getRandomUserAgent(); const userAgent = getRandomUserAgent();
expect(userAgent).toContain('Mozilla'); expect(userAgent).toContain('Mozilla');
}); });
it('should return different user agents on multiple calls', () => { it('should return different user agents on multiple calls', () => {
const userAgents = new Set(); const userAgents = new Set();
// Get 20 user agents // Get 20 user agents
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
userAgents.add(getRandomUserAgent()); userAgents.add(getRandomUserAgent());
} }
// Should have at least 2 different user agents // Should have at least 2 different user agents
expect(userAgents.size).toBeGreaterThan(1); expect(userAgents.size).toBeGreaterThan(1);
}); });
it('should return user agents with browser identifiers', () => { it('should return user agents with browser identifiers', () => {
const userAgent = getRandomUserAgent(); const userAgent = getRandomUserAgent();
const hasBrowser = const hasBrowser =
userAgent.includes('Chrome') || userAgent.includes('Chrome') ||
userAgent.includes('Firefox') || userAgent.includes('Firefox') ||
userAgent.includes('Safari') || userAgent.includes('Safari') ||
userAgent.includes('Edg'); userAgent.includes('Edg');
expect(hasBrowser).toBe(true); expect(hasBrowser).toBe(true);
}); });
it('should return user agents with OS identifiers', () => { it('should return user agents with OS identifiers', () => {
const userAgent = getRandomUserAgent(); const userAgent = getRandomUserAgent();
const hasOS = const hasOS =
userAgent.includes('Windows') || userAgent.includes('Windows') ||
userAgent.includes('Macintosh') || userAgent.includes('Macintosh') ||
userAgent.includes('Mac OS X'); userAgent.includes('Mac OS X');
expect(hasOS).toBe(true); expect(hasOS).toBe(true);
}); });
it('should handle multiple concurrent calls', () => { it('should handle multiple concurrent calls', () => {
const promises = Array(10) const promises = Array(10)
.fill(null) .fill(null)
.map(() => Promise.resolve(getRandomUserAgent())); .map(() => Promise.resolve(getRandomUserAgent()));
return Promise.all(promises).then(userAgents => { return Promise.all(promises).then(userAgents => {
expect(userAgents).toHaveLength(10); expect(userAgents).toHaveLength(10);
userAgents.forEach(ua => { userAgents.forEach(ua => {
expect(typeof ua).toBe('string'); expect(typeof ua).toBe('string');
expect(ua.length).toBeGreaterThan(0); expect(ua.length).toBeGreaterThan(0);
}); });
}); });
}); });
}); });
}); });