fixed format issues
This commit is contained in:
parent
a700818a06
commit
08f713d98b
55 changed files with 5680 additions and 5533 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
libs/core/cache/src/cache-factory.ts
vendored
12
libs/core/cache/src/cache-factory.ts
vendored
|
|
@ -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'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
14
libs/core/cache/src/connection-manager.ts
vendored
14
libs/core/cache/src/connection-manager.ts
vendored
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
libs/core/cache/src/constants.ts
vendored
32
libs/core/cache/src/constants.ts
vendored
|
|
@ -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;
|
||||||
|
|
|
||||||
2
libs/core/cache/src/index.ts
vendored
2
libs/core/cache/src/index.ts
vendored
|
|
@ -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';
|
||||||
|
|
|
||||||
51
libs/core/cache/src/redis-cache.ts
vendored
51
libs/core/cache/src/redis-cache.ts
vendored
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
libs/core/cache/src/types.ts
vendored
7
libs/core/cache/src/types.ts
vendored
|
|
@ -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 {
|
||||||
|
|
|
||||||
188
libs/core/cache/test/connection-manager.test.ts
vendored
188
libs/core/cache/test/connection-manager.test.ts
vendored
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
858
libs/core/cache/test/namespaced-cache.test.ts
vendored
858
libs/core/cache/test/namespaced-cache.test.ts
vendored
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
74
libs/core/cache/test/redis-cache-simple.test.ts
vendored
74
libs/core/cache/test/redis-cache-simple.test.ts
vendored
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
420
libs/core/cache/test/redis-cache.test.ts
vendored
420
libs/core/cache/test/redis-cache.test.ts
vendored
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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([]);
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@ export {
|
||||||
QueueSchedule,
|
QueueSchedule,
|
||||||
ScheduledOperation,
|
ScheduledOperation,
|
||||||
Disabled,
|
Disabled,
|
||||||
} from './decorators/decorators';
|
} from './decorators/decorators';
|
||||||
|
|
|
||||||
|
|
@ -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([]);
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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__;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 = {};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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',
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue