fixed format issues
This commit is contained in:
parent
a700818a06
commit
08f713d98b
55 changed files with 5680 additions and 5533 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
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'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
6
libs/core/cache/src/connection-manager.ts
vendored
6
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;
|
||||||
|
|
@ -68,9 +68,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
libs/core/cache/src/redis-cache.ts
vendored
27
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 = {
|
||||||
|
|
@ -72,13 +73,15 @@ 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);
|
||||||
|
|
@ -145,7 +148,7 @@ 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();
|
||||||
|
|
@ -172,7 +175,7 @@ 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) => {
|
||||||
|
|
@ -206,7 +209,9 @@ 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(() => {
|
||||||
|
|
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
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', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
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', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
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';
|
||||||
|
|
||||||
|
|
|
||||||
2
libs/core/cache/test/redis-cache.test.ts
vendored
2
libs/core/cache/test/redis-cache.test.ts
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test';
|
import { beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ConfigManager } from '../src/config-manager';
|
import { ConfigManager } from '../src/config-manager';
|
||||||
import { ConfigError, ConfigValidationError } from '../src/errors';
|
import { ConfigError, ConfigValidationError } from '../src/errors';
|
||||||
|
|
@ -11,7 +11,7 @@ mock.module('@stock-bot/logger', () => ({
|
||||||
error: mock(() => {}),
|
error: mock(() => {}),
|
||||||
warn: mock(() => {}),
|
warn: mock(() => {}),
|
||||||
debug: mock(() => {}),
|
debug: mock(() => {}),
|
||||||
})
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock loader class
|
// Mock loader class
|
||||||
|
|
@ -49,12 +49,12 @@ describe('ConfigManager', () => {
|
||||||
|
|
||||||
it('should handle various environment values', () => {
|
it('should handle various environment values', () => {
|
||||||
const envMap: Record<string, Environment> = {
|
const envMap: Record<string, Environment> = {
|
||||||
'production': 'production',
|
production: 'production',
|
||||||
'prod': 'production',
|
prod: 'production',
|
||||||
'test': 'test',
|
test: 'test',
|
||||||
'development': 'development',
|
development: 'development',
|
||||||
'dev': 'development',
|
dev: 'development',
|
||||||
'unknown': 'development',
|
unknown: 'development',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [input, expected] of Object.entries(envMap)) {
|
for (const [input, expected] of Object.entries(envMap)) {
|
||||||
|
|
@ -348,7 +348,9 @@ describe('ConfigManager', () => {
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
manager = new ConfigManager({ loaders: [new MockLoader({ app: { name: 'test', version: '1.0.0' }, port: 3000 })] });
|
manager = new ConfigManager({
|
||||||
|
loaders: [new MockLoader({ app: { name: 'test', version: '1.0.0' }, port: 3000 })],
|
||||||
|
});
|
||||||
manager.initialize(schema);
|
manager.initialize(schema);
|
||||||
|
|
||||||
// Valid update
|
// Valid update
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { EnvLoader } from '../src/loaders/env.loader';
|
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||||
import { ConfigLoaderError } from '../src/errors';
|
import { ConfigLoaderError } from '../src/errors';
|
||||||
|
import { EnvLoader } from '../src/loaders/env.loader';
|
||||||
|
|
||||||
// Mock fs module
|
// Mock fs module
|
||||||
mock.module('fs', () => ({
|
mock.module('fs', () => ({
|
||||||
readFileSync: mock(() => '')
|
readFileSync: mock(() => ''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('EnvLoader', () => {
|
describe('EnvLoader', () => {
|
||||||
|
|
@ -133,9 +133,9 @@ describe('EnvLoader', () => {
|
||||||
APP: {
|
APP: {
|
||||||
NAME: 'myapp',
|
NAME: 'myapp',
|
||||||
CONFIG: {
|
CONFIG: {
|
||||||
PORT: 3000
|
PORT: 3000,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -152,9 +152,9 @@ describe('EnvLoader', () => {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 5432,
|
port: 5432,
|
||||||
credentials: {
|
credentials: {
|
||||||
user: 'admin'
|
user: 'admin',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -422,7 +422,7 @@ KEY_WITHOUT_VALUE=
|
||||||
const config = { readonly: 'original' };
|
const config = { readonly: 'original' };
|
||||||
Object.defineProperty(config, 'readonly', {
|
Object.defineProperty(config, 'readonly', {
|
||||||
writable: false,
|
writable: false,
|
||||||
configurable: false
|
configurable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
process.env.READONLY = 'new_value';
|
process.env.READONLY = 'new_value';
|
||||||
|
|
@ -463,11 +463,11 @@ KEY_WITHOUT_VALUE=
|
||||||
c: {
|
c: {
|
||||||
d: {
|
d: {
|
||||||
e: {
|
e: {
|
||||||
f: 'deep'
|
f: 'deep',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -513,16 +513,18 @@ KEY_WITHOUT_VALUE=
|
||||||
loader = new EnvLoader();
|
loader = new EnvLoader();
|
||||||
const config = loader.load();
|
const config = loader.load();
|
||||||
|
|
||||||
expect(config.providers).toEqual(expect.objectContaining({
|
expect(config.providers).toEqual(
|
||||||
qm: {
|
expect.objectContaining({
|
||||||
username: 'testuser',
|
qm: {
|
||||||
password: 'testpass',
|
username: 'testuser',
|
||||||
baseUrl: 'https://api.quotemedia.com',
|
password: 'testpass',
|
||||||
webmasterId: '12345',
|
baseUrl: 'https://api.quotemedia.com',
|
||||||
enabled: true,
|
webmasterId: '12345',
|
||||||
priority: 5,
|
enabled: true,
|
||||||
},
|
priority: 5,
|
||||||
}));
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle Yahoo Finance provider mappings', () => {
|
it('should handle Yahoo Finance provider mappings', () => {
|
||||||
|
|
@ -535,15 +537,17 @@ KEY_WITHOUT_VALUE=
|
||||||
loader = new EnvLoader();
|
loader = new EnvLoader();
|
||||||
const config = loader.load();
|
const config = loader.load();
|
||||||
|
|
||||||
expect(config.providers).toEqual(expect.objectContaining({
|
expect(config.providers).toEqual(
|
||||||
yahoo: {
|
expect.objectContaining({
|
||||||
baseUrl: 'https://finance.yahoo.com',
|
yahoo: {
|
||||||
cookieJar: '/path/to/cookies',
|
baseUrl: 'https://finance.yahoo.com',
|
||||||
crumb: 'abc123',
|
cookieJar: '/path/to/cookies',
|
||||||
enabled: false,
|
crumb: 'abc123',
|
||||||
priority: 10,
|
enabled: false,
|
||||||
},
|
priority: 10,
|
||||||
}));
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle additional provider mappings', () => {
|
it('should handle additional provider mappings', () => {
|
||||||
|
|
@ -557,14 +561,18 @@ KEY_WITHOUT_VALUE=
|
||||||
loader = new EnvLoader();
|
loader = new EnvLoader();
|
||||||
const config = loader.load();
|
const config = loader.load();
|
||||||
|
|
||||||
expect(config.webshare).toEqual(expect.objectContaining({
|
expect(config.webshare).toEqual(
|
||||||
apiUrl: 'https://api.webshare.io',
|
expect.objectContaining({
|
||||||
}));
|
apiUrl: 'https://api.webshare.io',
|
||||||
expect(config.providers?.ib).toEqual(expect.objectContaining({
|
})
|
||||||
account: 'DU123456',
|
);
|
||||||
marketDataType: '1',
|
expect(config.providers?.ib).toEqual(
|
||||||
priority: 3,
|
expect.objectContaining({
|
||||||
}));
|
account: 'DU123456',
|
||||||
|
marketDataType: '1',
|
||||||
|
priority: 3,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(config.version).toBe('1.2.3');
|
expect(config.version).toBe('1.2.3');
|
||||||
expect(config.debug).toBe(true);
|
expect(config.debug).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
@ -610,7 +618,7 @@ KEY_WITHOUT_VALUE=
|
||||||
|
|
||||||
// CONFIG should be an object with nested value
|
// CONFIG should be an object with nested value
|
||||||
expect((config as any).config).toEqual({
|
expect((config as any).config).toEqual({
|
||||||
nested: 'nested_value'
|
nested: 'nested_value',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -620,7 +628,7 @@ KEY_WITHOUT_VALUE=
|
||||||
Object.defineProperty(testConfig, 'protected', {
|
Object.defineProperty(testConfig, 'protected', {
|
||||||
value: 'immutable',
|
value: 'immutable',
|
||||||
writable: false,
|
writable: false,
|
||||||
configurable: false
|
configurable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
process.env.PROTECTED_NESTED_VALUE = 'test';
|
process.env.PROTECTED_NESTED_VALUE = 'test';
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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,
|
||||||
|
})
|
||||||
|
).toThrow();
|
||||||
|
|
||||||
expect(() => postgresConfigSchema.parse({
|
expect(() =>
|
||||||
database: 'testdb',
|
postgresConfigSchema.parse({
|
||||||
user: 'testuser',
|
database: 'testdb',
|
||||||
password: 'testpass',
|
user: 'testuser',
|
||||||
poolSize: 101,
|
password: 'testpass',
|
||||||
})).toThrow();
|
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,
|
||||||
|
})
|
||||||
|
).toThrow();
|
||||||
|
|
||||||
expect(() => mongodbConfigSchema.parse({
|
expect(() =>
|
||||||
uri: 'mongodb://localhost',
|
mongodbConfigSchema.parse({
|
||||||
database: 'testdb',
|
uri: 'mongodb://localhost',
|
||||||
poolSize: 101,
|
database: 'testdb',
|
||||||
})).toThrow();
|
poolSize: 101,
|
||||||
|
})
|
||||||
|
).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,5 +1,10 @@
|
||||||
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,
|
||||||
|
ServiceContainerOptions,
|
||||||
|
ServiceCradle,
|
||||||
|
ServiceDefinitions,
|
||||||
|
} from '../src/awilix-container';
|
||||||
|
|
||||||
describe('Awilix Container Types', () => {
|
describe('Awilix Container Types', () => {
|
||||||
it('should export ServiceDefinitions interface', () => {
|
it('should export ServiceDefinitions interface', () => {
|
||||||
|
|
|
||||||
|
|
@ -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,4 +1,4 @@
|
||||||
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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
@ -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());
|
||||||
|
|
@ -243,9 +242,11 @@ describe.skip('ServiceApplication', () => {
|
||||||
|
|
||||||
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');
|
||||||
});
|
});
|
||||||
|
|
@ -260,10 +261,12 @@ describe.skip('ServiceApplication', () => {
|
||||||
|
|
||||||
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -300,7 +303,9 @@ 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;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
@ -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 });
|
||||||
|
|
@ -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;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { describe, it, expect } from 'bun:test';
|
import { describe, expect, it } from 'bun:test';
|
||||||
import type {
|
import type {
|
||||||
GenericClientConfig,
|
|
||||||
ConnectionPoolConfig,
|
|
||||||
MongoDBPoolConfig,
|
|
||||||
PostgreSQLPoolConfig,
|
|
||||||
CachePoolConfig,
|
CachePoolConfig,
|
||||||
QueuePoolConfig,
|
ConnectionFactory,
|
||||||
ConnectionFactoryConfig,
|
ConnectionFactoryConfig,
|
||||||
ConnectionPool,
|
ConnectionPool,
|
||||||
|
ConnectionPoolConfig,
|
||||||
|
GenericClientConfig,
|
||||||
|
MongoDBPoolConfig,
|
||||||
PoolMetrics,
|
PoolMetrics,
|
||||||
ConnectionFactory,
|
PostgreSQLPoolConfig,
|
||||||
|
QueuePoolConfig,
|
||||||
} from '../src/types';
|
} from '../src/types';
|
||||||
|
|
||||||
describe('DI Types', () => {
|
describe('DI Types', () => {
|
||||||
|
|
@ -197,7 +197,7 @@ describe('DI Types', () => {
|
||||||
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: {
|
||||||
|
|
@ -211,7 +211,7 @@ describe('DI Types', () => {
|
||||||
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: {
|
||||||
|
|
@ -225,7 +225,7 @@ describe('DI Types', () => {
|
||||||
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: {
|
||||||
|
|
@ -239,7 +239,7 @@ describe('DI Types', () => {
|
||||||
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: {
|
||||||
|
|
|
||||||
|
|
@ -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,4 +1,4 @@
|
||||||
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';
|
||||||
|
|
||||||
|
|
@ -50,8 +50,8 @@ describe('Handler Registry Package Exports', () => {
|
||||||
totalOperations: 10,
|
totalOperations: 10,
|
||||||
totalSchedules: 3,
|
totalSchedules: 3,
|
||||||
handlersByService: {
|
handlersByService: {
|
||||||
'service1': 2,
|
service1: 2,
|
||||||
'service2': 3,
|
service2: 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||||
|
import type { JobHandler, ScheduledJob } from '@stock-bot/types';
|
||||||
import { HandlerRegistry } from '../src/registry';
|
import { HandlerRegistry } from '../src/registry';
|
||||||
import type {
|
import type {
|
||||||
HandlerConfiguration,
|
HandlerConfiguration,
|
||||||
|
|
@ -6,7 +7,6 @@ import type {
|
||||||
OperationMetadata,
|
OperationMetadata,
|
||||||
ScheduleMetadata,
|
ScheduleMetadata,
|
||||||
} from '../src/types';
|
} from '../src/types';
|
||||||
import type { JobHandler, ScheduledJob } from '@stock-bot/types';
|
|
||||||
|
|
||||||
describe('HandlerRegistry Edge Cases', () => {
|
describe('HandlerRegistry Edge Cases', () => {
|
||||||
let registry: HandlerRegistry;
|
let registry: HandlerRegistry;
|
||||||
|
|
@ -324,9 +324,7 @@ describe('HandlerRegistry 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: [
|
schedules: [
|
||||||
{ operation: 'op1', cronPattern: '* * * * *' },
|
{ operation: 'op1', cronPattern: '* * * * *' },
|
||||||
{ operation: 'op1', cronPattern: '0 * * * *' },
|
{ operation: 'op1', cronPattern: '0 * * * *' },
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 - Simple Tests', () => {
|
describe('Auto Registration - Simple Tests', () => {
|
||||||
describe('autoRegisterHandlers', () => {
|
describe('autoRegisterHandlers', () => {
|
||||||
|
|
@ -33,7 +33,7 @@ describe('Auto Registration - Simple Tests', () => {
|
||||||
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([]);
|
||||||
|
|
@ -43,7 +43,7 @@ describe('Auto Registration - Simple Tests', () => {
|
||||||
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([]);
|
||||||
|
|
@ -66,10 +66,7 @@ describe('Auto Registration - Simple Tests', () => {
|
||||||
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.registered).toEqual([]);
|
||||||
expect(result.failed).toEqual([]);
|
expect(result.failed).toEqual([]);
|
||||||
|
|
|
||||||
|
|
@ -52,27 +52,18 @@ describe('Auto Registration Unit Tests', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
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', () => {
|
it('should handle different patterns', () => {
|
||||||
const files = [
|
const files = ['test.handler.ts', 'test.custom.ts', 'another.custom.ts'];
|
||||||
'test.handler.ts',
|
|
||||||
'test.custom.ts',
|
|
||||||
'another.custom.ts',
|
|
||||||
];
|
|
||||||
|
|
||||||
const customPattern = '.custom.';
|
const customPattern = '.custom.';
|
||||||
const filtered = files.filter(file =>
|
const filtered = files.filter(file => file.includes(customPattern) && file.endsWith('.ts'));
|
||||||
file.includes(customPattern) &&
|
|
||||||
file.endsWith('.ts')
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filtered).toEqual(['test.custom.ts', 'another.custom.ts']);
|
expect(filtered).toEqual(['test.custom.ts', 'another.custom.ts']);
|
||||||
});
|
});
|
||||||
|
|
@ -158,16 +149,10 @@ describe('Auto Registration Unit Tests', () => {
|
||||||
|
|
||||||
describe('Options Handling', () => {
|
describe('Options Handling', () => {
|
||||||
it('should apply exclude patterns', () => {
|
it('should apply exclude patterns', () => {
|
||||||
const files = [
|
const files = ['test.handler.ts', 'excluded.handler.ts', 'another.handler.ts'];
|
||||||
'test.handler.ts',
|
|
||||||
'excluded.handler.ts',
|
|
||||||
'another.handler.ts',
|
|
||||||
];
|
|
||||||
const exclude = ['excluded'];
|
const exclude = ['excluded'];
|
||||||
|
|
||||||
const filtered = files.filter(file =>
|
const filtered = files.filter(file => !exclude.some(ex => file.includes(ex)));
|
||||||
!exclude.some(ex => file.includes(ex))
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
|
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
@ -89,7 +89,7 @@ describe('Auto Registration', () => {
|
||||||
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();
|
||||||
|
|
@ -107,7 +107,7 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||||
|
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
|
||||||
import { BaseHandler } from '../src/base/BaseHandler';
|
import { BaseHandler } from '../src/base/BaseHandler';
|
||||||
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
|
|
||||||
|
|
||||||
// Test handler with metadata
|
// Test handler with metadata
|
||||||
class ConfigTestHandler extends BaseHandler {
|
class ConfigTestHandler extends BaseHandler {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||||
|
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
|
||||||
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
|
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
|
||||||
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
|
|
||||||
|
|
||||||
// Test handler implementation
|
// Test handler implementation
|
||||||
class TestHandler extends BaseHandler {
|
class TestHandler extends BaseHandler {
|
||||||
|
|
@ -96,14 +96,18 @@ describe('BaseHandler Edge Cases', () => {
|
||||||
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 () => {
|
it('should handle operation with no operations metadata', async () => {
|
||||||
const handler = new EmptyHandler(mockServices);
|
const handler = new EmptyHandler(mockServices);
|
||||||
const context: ExecutionContext = { type: 'queue', metadata: {} };
|
const context: ExecutionContext = { type: 'queue', metadata: {} };
|
||||||
|
|
||||||
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow('Unknown operation: anyOp');
|
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow(
|
||||||
|
'Unknown operation: anyOp'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when method is not a function', async () => {
|
it('should throw when method is not a function', async () => {
|
||||||
|
|
@ -122,7 +126,7 @@ describe('BaseHandler Edge Cases', () => {
|
||||||
|
|
||||||
const context: ExecutionContext = {
|
const context: ExecutionContext = {
|
||||||
type: 'queue',
|
type: 'queue',
|
||||||
metadata: { source: 'test' }
|
metadata: { source: 'test' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await handler.execute('test', { data: 'test' }, context);
|
const result = await handler.execute('test', { data: 'test' }, context);
|
||||||
|
|
@ -271,9 +275,7 @@ describe('BaseHandler Edge Cases', () => {
|
||||||
it('should create handler config with operations', () => {
|
it('should create handler config with operations', () => {
|
||||||
const HandlerWithMeta = class extends BaseHandler {
|
const HandlerWithMeta = class extends BaseHandler {
|
||||||
static __handlerName = 'config-handler';
|
static __handlerName = 'config-handler';
|
||||||
static __operations = [
|
static __operations = [{ name: 'process', method: 'processData' }];
|
||||||
{ name: 'process', method: 'processData' },
|
|
||||||
];
|
|
||||||
static __schedules = [];
|
static __schedules = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
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();
|
||||||
|
|
@ -70,7 +70,8 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
|
|
||||||
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(
|
||||||
|
'https://api.example.com/data',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
logger: expect.any(Object),
|
logger: expect.any(Object),
|
||||||
|
|
@ -88,12 +89,13 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
mockFetch.mockResolvedValue(mockResponse);
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
await handler.testGet('https://api.example.com/data', {
|
await handler.testGet('https://api.example.com/data', {
|
||||||
headers: { 'Authorization': 'Bearer token' },
|
headers: { Authorization: 'Bearer token' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data',
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/data',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
headers: { 'Authorization': 'Bearer token' },
|
headers: { Authorization: 'Bearer token' },
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
logger: expect.any(Object),
|
logger: expect.any(Object),
|
||||||
})
|
})
|
||||||
|
|
@ -115,7 +117,8 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
const data = { name: 'test', value: 123 };
|
const data = { name: 'test', value: 123 };
|
||||||
await handler.testPost('https://api.example.com/create', data);
|
await handler.testPost('https://api.example.com/create', data);
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/create',
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/create',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
|
@ -134,11 +137,16 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
};
|
};
|
||||||
mockFetch.mockResolvedValue(mockResponse);
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
await handler.testPost('https://api.example.com/create', { test: 'data' }, {
|
await handler.testPost(
|
||||||
headers: { 'X-Custom': 'value' },
|
'https://api.example.com/create',
|
||||||
});
|
{ test: 'data' },
|
||||||
|
{
|
||||||
|
headers: { 'X-Custom': 'value' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/create',
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/create',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ test: 'data' }),
|
body: JSON.stringify({ test: 'data' }),
|
||||||
|
|
@ -165,7 +173,8 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
const data = { id: 1, name: 'updated' };
|
const data = { id: 1, name: 'updated' };
|
||||||
await handler.testPut('https://api.example.com/update/1', data);
|
await handler.testPut('https://api.example.com/update/1', data);
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/update/1',
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/update/1',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
|
@ -184,12 +193,17 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
};
|
};
|
||||||
mockFetch.mockResolvedValue(mockResponse);
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
await handler.testPut('https://api.example.com/update', { data: 'test' }, {
|
await handler.testPut(
|
||||||
headers: { 'If-Match': 'etag' },
|
'https://api.example.com/update',
|
||||||
timeout: 5000,
|
{ data: 'test' },
|
||||||
});
|
{
|
||||||
|
headers: { 'If-Match': 'etag' },
|
||||||
|
timeout: 5000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/update',
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/update',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ data: 'test' }),
|
body: JSON.stringify({ data: 'test' }),
|
||||||
|
|
@ -216,7 +230,8 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
|
|
||||||
await handler.testDelete('https://api.example.com/delete/1');
|
await handler.testDelete('https://api.example.com/delete/1');
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/delete/1',
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/delete/1',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
logger: expect.any(Object),
|
logger: expect.any(Object),
|
||||||
|
|
@ -234,12 +249,13 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
mockFetch.mockResolvedValue(mockResponse);
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
await handler.testDelete('https://api.example.com/delete/1', {
|
await handler.testDelete('https://api.example.com/delete/1', {
|
||||||
headers: { 'Authorization': 'Bearer token' },
|
headers: { Authorization: 'Bearer token' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/delete/1',
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/delete/1',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
headers: { 'Authorization': 'Bearer token' },
|
headers: { Authorization: 'Bearer token' },
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
logger: expect.any(Object),
|
logger: expect.any(Object),
|
||||||
})
|
})
|
||||||
|
|
@ -251,7 +267,9 @@ describe('BaseHandler HTTP Methods', () => {
|
||||||
it('should propagate fetch errors', async () => {
|
it('should propagate fetch errors', async () => {
|
||||||
mockFetch.mockRejectedValue(new Error('Network error'));
|
mockFetch.mockRejectedValue(new Error('Network error'));
|
||||||
|
|
||||||
await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow('Network error');
|
await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow(
|
||||||
|
'Network error'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle non-ok responses', async () => {
|
it('should handle non-ok responses', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
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,
|
||||||
|
Handler,
|
||||||
|
Operation,
|
||||||
|
QueueSchedule,
|
||||||
|
ScheduledOperation,
|
||||||
|
} from '../src/decorators/decorators';
|
||||||
|
|
||||||
describe('Decorators Edge Cases', () => {
|
describe('Decorators Edge Cases', () => {
|
||||||
describe('Handler Decorator', () => {
|
describe('Handler Decorator', () => {
|
||||||
|
|
@ -22,10 +28,9 @@ describe('Decorators Edge Cases', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with context parameter', () => {
|
it('should work with context parameter', () => {
|
||||||
const HandlerClass = Handler('with-context')(
|
const HandlerClass = Handler('with-context')(class TestClass extends BaseHandler {}, {
|
||||||
class TestClass extends BaseHandler {},
|
kind: 'class',
|
||||||
{ kind: 'class' }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const ctor = HandlerClass as any;
|
const ctor = HandlerClass as any;
|
||||||
expect(ctor.__handlerName).toBe('with-context');
|
expect(ctor.__handlerName).toBe('with-context');
|
||||||
|
|
@ -72,7 +77,7 @@ describe('Decorators Edge Cases', () => {
|
||||||
delayInHours: 24,
|
delayInHours: 24,
|
||||||
priority: 5,
|
priority: 5,
|
||||||
direct: false,
|
direct: false,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
batchMethod() {}
|
batchMethod() {}
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +98,7 @@ describe('Decorators Edge Cases', () => {
|
||||||
batch: {
|
batch: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
size: 50,
|
size: 50,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
partialBatchMethod() {}
|
partialBatchMethod() {}
|
||||||
}
|
}
|
||||||
|
|
@ -288,10 +293,7 @@ describe('Decorators Edge Cases', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with context parameter', () => {
|
it('should work with context parameter', () => {
|
||||||
const DisabledClass = Disabled()(
|
const DisabledClass = Disabled()(class TestClass extends BaseHandler {}, { kind: 'class' });
|
||||||
class TestClass extends BaseHandler {},
|
|
||||||
{ kind: 'class' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const ctor = DisabledClass as any;
|
const ctor = DisabledClass as any;
|
||||||
expect(ctor.__disabled).toBe(true);
|
expect(ctor.__disabled).toBe(true);
|
||||||
|
|
@ -326,11 +328,18 @@ describe('Decorators Edge Cases', () => {
|
||||||
|
|
||||||
// Operations (3 total - simple, batch, and combined)
|
// Operations (3 total - simple, batch, and combined)
|
||||||
expect(ctor.__operations).toHaveLength(3);
|
expect(ctor.__operations).toHaveLength(3);
|
||||||
expect(ctor.__operations.map((op: any) => op.name)).toEqual(['simple-op', 'batch-op', 'combined']);
|
expect(ctor.__operations.map((op: any) => op.name)).toEqual([
|
||||||
|
'simple-op',
|
||||||
|
'batch-op',
|
||||||
|
'combined',
|
||||||
|
]);
|
||||||
|
|
||||||
// Schedules (2 total - scheduledOnly and combined)
|
// Schedules (2 total - scheduledOnly and combined)
|
||||||
expect(ctor.__schedules).toHaveLength(2);
|
expect(ctor.__schedules).toHaveLength(2);
|
||||||
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual(['scheduledOnly', 'combinedMethod']);
|
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual([
|
||||||
|
'scheduledOnly',
|
||||||
|
'combinedMethod',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle disabled handler with operations', () => {
|
it('should handle disabled handler with operations', () => {
|
||||||
|
|
@ -372,7 +381,11 @@ describe('Decorators Edge Cases', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctor = TestHandler as any;
|
const ctor = TestHandler as any;
|
||||||
expect(ctor.__operations.map((op: any) => op.method)).toEqual(['toString', 'valueOf', 'hasOwnProperty']);
|
expect(ctor.__operations.map((op: any) => op.method)).toEqual([
|
||||||
|
'toString',
|
||||||
|
'valueOf',
|
||||||
|
'hasOwnProperty',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,7 +48,9 @@ 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;
|
||||||
|
|
||||||
|
|
@ -71,7 +79,9 @@ 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'];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -128,9 +128,9 @@ describe('Enhanced Fetch', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
|
|
@ -147,9 +147,9 @@ describe('Enhanced Fetch', () => {
|
||||||
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');
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -188,9 +188,9 @@ describe('Enhanced Fetch', () => {
|
||||||
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',
|
||||||
|
|
@ -264,17 +264,15 @@ describe('Enhanced Fetch', () => {
|
||||||
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 () => {
|
it('should handle non-Error objects', async () => {
|
||||||
mockFetch.mockRejectedValue('string error');
|
mockFetch.mockRejectedValue('string error');
|
||||||
|
|
||||||
await expect(
|
await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toBe(
|
||||||
fetch('https://api.example.com/data', { logger: mockLogger })
|
'string error'
|
||||||
).rejects.toBe('string error');
|
);
|
||||||
|
|
||||||
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
|
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
|
||||||
url: 'https://api.example.com/data',
|
url: 'https://api.example.com/data',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue