fixed format issues

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

View file

@ -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 {

View file

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

View file

@ -1,6 +1,6 @@
import Redis from 'ioredis'; import Redis from 'ioredis';
import type { RedisConfig } from './types';
import { REDIS_DEFAULTS } from './constants'; import { REDIS_DEFAULTS } from './constants';
import type { RedisConfig } from './types';
interface ConnectionConfig { interface ConnectionConfig {
name: string; name: string;
@ -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();
} }

View file

@ -8,7 +8,8 @@ import type { CacheOptions, CacheProvider, CacheStats } from './types';
*/ */
export class RedisCache implements CacheProvider { export class RedisCache implements CacheProvider {
private redis: Redis; private redis: Redis;
private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } = console; private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } =
console;
private defaultTTL: number; private defaultTTL: number;
private keyPrefix: string; private keyPrefix: string;
private stats: CacheStats = { private stats: CacheStats = {
@ -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(() => {

View file

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

View file

@ -1,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', () => {

View file

@ -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', () => {

View file

@ -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';

View file

@ -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';

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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';

View file

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

View file

@ -1,27 +1,27 @@
import { describe, it, expect } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import { z } from 'zod'; import { z } from 'zod';
import { import {
baseConfigSchema, baseConfigSchema,
environmentSchema,
serviceConfigSchema,
loggingConfigSchema,
queueConfigSchema,
httpConfigSchema,
webshareConfigSchema,
browserConfigSchema,
proxyConfigSchema,
postgresConfigSchema,
questdbConfigSchema,
mongodbConfigSchema,
dragonflyConfigSchema,
databaseConfigSchema,
baseProviderConfigSchema, baseProviderConfigSchema,
browserConfigSchema,
databaseConfigSchema,
dragonflyConfigSchema,
environmentSchema,
eodProviderConfigSchema, eodProviderConfigSchema,
httpConfigSchema,
ibProviderConfigSchema, ibProviderConfigSchema,
qmProviderConfigSchema, loggingConfigSchema,
yahooProviderConfigSchema, mongodbConfigSchema,
webshareProviderConfigSchema, postgresConfigSchema,
providerConfigSchema, providerConfigSchema,
proxyConfigSchema,
qmProviderConfigSchema,
questdbConfigSchema,
queueConfigSchema,
serviceConfigSchema,
webshareConfigSchema,
webshareProviderConfigSchema,
yahooProviderConfigSchema,
} from '../src/schemas'; } from '../src/schemas';
describe('Config Schemas', () => { describe('Config Schemas', () => {
@ -202,7 +202,7 @@ describe('Config Schemas', () => {
describe('queueConfigSchema', () => { describe('queueConfigSchema', () => {
it('should accept minimal config with defaults', () => { it('should accept minimal config with defaults', () => {
const config = queueConfigSchema.parse({ const config = queueConfigSchema.parse({
redis: {}, // redis is required, but its properties have defaults redis: {}, // redis is required, but its properties have defaults
}); });
expect(config).toEqual({ expect(config).toEqual({
enabled: true, enabled: true,
@ -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',

View file

@ -1,21 +1,21 @@
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { z } from 'zod'; import { z } from 'zod';
import { import {
SecretValue, checkRequiredEnvVars,
secret, COMMON_SECRET_PATTERNS,
createStrictSchema,
formatValidationResult,
isSecret, isSecret,
redactSecrets,
isSecretEnvVar, isSecretEnvVar,
wrapSecretEnvVars, mergeSchemas,
redactSecrets,
secret,
secretSchema, secretSchema,
secretStringSchema, secretStringSchema,
COMMON_SECRET_PATTERNS, SecretValue,
validateConfig,
checkRequiredEnvVars,
validateCompleteness, validateCompleteness,
formatValidationResult, validateConfig,
createStrictSchema, wrapSecretEnvVars,
mergeSchemas,
type ValidationResult, type ValidationResult,
} from '../src'; } from '../src';
@ -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);

View file

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

View file

@ -1,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', () => {

View file

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

View file

@ -1,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', () => {

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import type { BaseAppConfig } from '@stock-bot/config';
import { ServiceApplication } from '../src/service-application'; import { ServiceApplication } from '../src/service-application';
import type { ServiceApplicationConfig, ServiceLifecycleHooks } from '../src/service-application'; import type { ServiceApplicationConfig, ServiceLifecycleHooks } from '../src/service-application';
import type { BaseAppConfig } from '@stock-bot/config';
// Mock logger module // Mock logger module
const mockLogger = { const mockLogger = {
@ -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;
}), }),
}; };

View file

@ -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: {

View file

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

View file

@ -1,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,
}, },
}; };

View file

@ -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 * * * *' },

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import { 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([]);

View file

@ -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']);
}); });

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test'; import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
import { BaseHandler } from '../src/base/BaseHandler';
import type { IServiceContainer } from '@stock-bot/types'; import type { IServiceContainer } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
describe('Auto Registration', () => { describe('Auto Registration', () => {
describe('autoRegisterHandlers', () => { describe('autoRegisterHandlers', () => {
@ -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();

View file

@ -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 {

View file

@ -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 = [];
}; };

View file

@ -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 () => {

View file

@ -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',
]);
}); });
}); });
}); });

View file

@ -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';

View file

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

View file

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

View file

@ -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'];

View file

@ -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';

View file

@ -10,8 +10,8 @@ import {
onShutdownMedium, onShutdownMedium,
resetShutdown, resetShutdown,
setShutdownTimeout, setShutdownTimeout,
shutdownAndExit,
Shutdown, Shutdown,
shutdownAndExit,
} from '../src'; } from '../src';
import type { ShutdownOptions, ShutdownResult } from '../src/types'; import type { ShutdownOptions, ShutdownResult } from '../src/types';
@ -104,7 +104,9 @@ describe('Shutdown Comprehensive Tests', () => {
it('should handle negative timeout values', () => { it('should handle negative timeout values', () => {
// Should throw for negative values // Should throw for negative values
expect(() => setShutdownTimeout(-1000)).toThrow('Shutdown timeout must be a positive number'); expect(() => setShutdownTimeout(-1000)).toThrow(
'Shutdown timeout must be a positive number'
);
}); });
it('should handle zero timeout', () => { it('should handle zero timeout', () => {

View file

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

View file

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

View file

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

View file

@ -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',