added cli-covarage tool and fixed more tests
This commit is contained in:
parent
b63e58784c
commit
b845a8eade
57 changed files with 11917 additions and 295 deletions
515
libs/core/config/test/config-manager.test.ts
Normal file
515
libs/core/config/test/config-manager.test.ts
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import { ConfigManager } from '../src/config-manager';
|
||||
import { ConfigError, ConfigValidationError } from '../src/errors';
|
||||
import type { ConfigLoader, Environment } from '../src/types';
|
||||
|
||||
// Mock the logger
|
||||
mock.module('@stock-bot/logger', () => ({
|
||||
getLogger: () => ({
|
||||
info: mock(() => {}),
|
||||
error: mock(() => {}),
|
||||
warn: mock(() => {}),
|
||||
debug: mock(() => {}),
|
||||
})
|
||||
}));
|
||||
|
||||
// Mock loader class
|
||||
class MockLoader implements ConfigLoader {
|
||||
constructor(
|
||||
private data: Record<string, unknown>,
|
||||
public priority: number = 0
|
||||
) {}
|
||||
|
||||
load(): Record<string, unknown> {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ConfigManager', () => {
|
||||
let manager: ConfigManager<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset environment
|
||||
delete process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with default loaders', () => {
|
||||
manager = new ConfigManager();
|
||||
expect(manager).toBeDefined();
|
||||
expect(manager.getEnvironment()).toBe('development');
|
||||
});
|
||||
|
||||
it('should detect environment from NODE_ENV', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
manager = new ConfigManager();
|
||||
expect(manager.getEnvironment()).toBe('production');
|
||||
});
|
||||
|
||||
it('should handle various environment values', () => {
|
||||
const envMap: Record<string, Environment> = {
|
||||
'production': 'production',
|
||||
'prod': 'production',
|
||||
'test': 'test',
|
||||
'development': 'development',
|
||||
'dev': 'development',
|
||||
'unknown': 'development',
|
||||
};
|
||||
|
||||
for (const [input, expected] of Object.entries(envMap)) {
|
||||
process.env.NODE_ENV = input;
|
||||
manager = new ConfigManager();
|
||||
expect(manager.getEnvironment()).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should use custom loaders when provided', () => {
|
||||
const customLoader = new MockLoader({ custom: 'data' });
|
||||
manager = new ConfigManager({
|
||||
loaders: [customLoader],
|
||||
});
|
||||
|
||||
manager.initialize();
|
||||
expect(manager.get()).toEqual({ custom: 'data', environment: 'development' });
|
||||
});
|
||||
|
||||
it('should use custom environment when provided', () => {
|
||||
manager = new ConfigManager({
|
||||
environment: 'test',
|
||||
});
|
||||
expect(manager.getEnvironment()).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should load and merge configurations', () => {
|
||||
const loader1 = new MockLoader({ a: 1, b: { c: 2 } }, 1);
|
||||
const loader2 = new MockLoader({ b: { d: 3 }, e: 4 }, 2);
|
||||
|
||||
manager = new ConfigManager({
|
||||
loaders: [loader1, loader2],
|
||||
});
|
||||
|
||||
const config = manager.initialize();
|
||||
|
||||
expect(config).toEqual({
|
||||
a: 1,
|
||||
b: { c: 2, d: 3 },
|
||||
e: 4,
|
||||
environment: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return cached config on subsequent calls', () => {
|
||||
const loader = new MockLoader({ test: 'data' });
|
||||
const loadSpy = spyOn(loader, 'load');
|
||||
|
||||
manager = new ConfigManager({
|
||||
loaders: [loader],
|
||||
});
|
||||
|
||||
const config1 = manager.initialize();
|
||||
const config2 = manager.initialize();
|
||||
|
||||
expect(config1).toBe(config2);
|
||||
expect(loadSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should validate config with schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
environment: z.string(),
|
||||
});
|
||||
|
||||
const loader = new MockLoader({
|
||||
name: 'test-app',
|
||||
port: 3000,
|
||||
});
|
||||
|
||||
manager = new ConfigManager({
|
||||
loaders: [loader],
|
||||
});
|
||||
|
||||
const config = manager.initialize(schema);
|
||||
|
||||
expect(config).toEqual({
|
||||
name: 'test-app',
|
||||
port: 3000,
|
||||
environment: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw validation error for invalid config', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const loader = new MockLoader({
|
||||
name: 'test-app',
|
||||
port: 'invalid', // Should be number
|
||||
});
|
||||
|
||||
manager = new ConfigManager({
|
||||
loaders: [loader],
|
||||
});
|
||||
|
||||
expect(() => manager.initialize(schema)).toThrow(ConfigValidationError);
|
||||
});
|
||||
|
||||
it('should handle empty loaders', () => {
|
||||
manager = new ConfigManager({
|
||||
loaders: [],
|
||||
});
|
||||
|
||||
const config = manager.initialize();
|
||||
expect(config).toEqual({ environment: 'development' });
|
||||
});
|
||||
|
||||
it('should ignore loaders that return empty config', () => {
|
||||
const loader1 = new MockLoader({});
|
||||
const loader2 = new MockLoader({ data: 'value' });
|
||||
|
||||
manager = new ConfigManager({
|
||||
loaders: [loader1, loader2],
|
||||
});
|
||||
|
||||
const config = manager.initialize();
|
||||
expect(config).toEqual({ data: 'value', environment: 'development' });
|
||||
});
|
||||
|
||||
it('should respect loader priority order', () => {
|
||||
const loader1 = new MockLoader({ value: 'first' }, 1);
|
||||
const loader2 = new MockLoader({ value: 'second' }, 2);
|
||||
const loader3 = new MockLoader({ value: 'third' }, 0);
|
||||
|
||||
manager = new ConfigManager({
|
||||
loaders: [loader1, loader2, loader3],
|
||||
});
|
||||
|
||||
const config = manager.initialize();
|
||||
// Priority order: 0, 1, 2 (lowest to highest)
|
||||
// So 'second' should win
|
||||
expect(config.value).toBe('second');
|
||||
});
|
||||
|
||||
it('should handle validation errors with detailed error info', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number().min(1).max(65535),
|
||||
features: z.object({
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
const loader = new MockLoader({
|
||||
name: 123, // Should be string
|
||||
port: 99999, // Out of range
|
||||
features: {
|
||||
enabled: 'yes', // Should be boolean
|
||||
},
|
||||
});
|
||||
|
||||
manager = new ConfigManager({
|
||||
loaders: [loader],
|
||||
});
|
||||
|
||||
try {
|
||||
manager.initialize(schema);
|
||||
expect(true).toBe(false); // Should not reach here
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ConfigValidationError);
|
||||
const validationError = error as ConfigValidationError;
|
||||
expect(validationError.errors).toBeDefined();
|
||||
expect(validationError.errors.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return config after initialization', () => {
|
||||
const loader = new MockLoader({ test: 'data' });
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
|
||||
manager.initialize();
|
||||
expect(manager.get()).toEqual({ test: 'data', environment: 'development' });
|
||||
});
|
||||
|
||||
it('should throw error if not initialized', () => {
|
||||
manager = new ConfigManager();
|
||||
|
||||
expect(() => manager.get()).toThrow(ConfigError);
|
||||
expect(() => manager.get()).toThrow('Configuration not initialized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue', () => {
|
||||
beforeEach(() => {
|
||||
const loader = new MockLoader({
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
credentials: {
|
||||
username: 'admin',
|
||||
password: 'secret',
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
enabled: true,
|
||||
ttl: 3600,
|
||||
},
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
manager.initialize();
|
||||
});
|
||||
|
||||
it('should get value by path', () => {
|
||||
expect(manager.getValue('database.host')).toBe('localhost');
|
||||
expect(manager.getValue('database.port')).toBe(5432);
|
||||
expect(manager.getValue('cache.enabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should get nested values', () => {
|
||||
expect(manager.getValue('database.credentials.username')).toBe('admin');
|
||||
expect(manager.getValue('database.credentials.password')).toBe('secret');
|
||||
});
|
||||
|
||||
it('should throw error for non-existent path', () => {
|
||||
expect(() => manager.getValue('nonexistent.path')).toThrow(ConfigError);
|
||||
expect(() => manager.getValue('nonexistent.path')).toThrow('Configuration key not found');
|
||||
});
|
||||
|
||||
it('should handle top-level values', () => {
|
||||
expect(manager.getValue('database')).toEqual({
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
credentials: {
|
||||
username: 'admin',
|
||||
password: 'secret',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('has', () => {
|
||||
beforeEach(() => {
|
||||
const loader = new MockLoader({
|
||||
database: { host: 'localhost' },
|
||||
cache: { enabled: true },
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
manager.initialize();
|
||||
});
|
||||
|
||||
it('should return true for existing paths', () => {
|
||||
expect(manager.has('database')).toBe(true);
|
||||
expect(manager.has('database.host')).toBe(true);
|
||||
expect(manager.has('cache.enabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-existent paths', () => {
|
||||
expect(manager.has('nonexistent')).toBe(false);
|
||||
expect(manager.has('database.port')).toBe(false);
|
||||
expect(manager.has('cache.ttl')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
beforeEach(() => {
|
||||
const loader = new MockLoader({
|
||||
app: { name: 'test', version: '1.0.0' },
|
||||
port: 3000,
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
manager.initialize();
|
||||
});
|
||||
|
||||
it('should update configuration values', () => {
|
||||
manager.set({ port: 4000 });
|
||||
expect(manager.get().port).toBe(4000);
|
||||
|
||||
manager.set({ app: { version: '2.0.0' } });
|
||||
expect(manager.get().app.version).toBe('2.0.0');
|
||||
expect(manager.get().app.name).toBe('test'); // Unchanged
|
||||
});
|
||||
|
||||
it('should validate updates when schema is present', () => {
|
||||
const schema = z.object({
|
||||
app: z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
}),
|
||||
port: z.number().min(1000).max(9999),
|
||||
environment: z.string(),
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [new MockLoader({ app: { name: 'test', version: '1.0.0' }, port: 3000 })] });
|
||||
manager.initialize(schema);
|
||||
|
||||
// Valid update
|
||||
manager.set({ port: 4000 });
|
||||
expect(manager.get().port).toBe(4000);
|
||||
|
||||
// Invalid update
|
||||
expect(() => manager.set({ port: 99999 })).toThrow(ConfigValidationError);
|
||||
});
|
||||
|
||||
it('should throw error if not initialized', () => {
|
||||
const newManager = new ConfigManager();
|
||||
expect(() => newManager.set({ test: 'value' })).toThrow(ConfigError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should clear configuration', () => {
|
||||
const loader = new MockLoader({ test: 'data' });
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
|
||||
manager.initialize();
|
||||
expect(manager.get()).toBeDefined();
|
||||
|
||||
manager.reset();
|
||||
expect(() => manager.get()).toThrow(ConfigError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate', () => {
|
||||
it('should validate current config against schema', () => {
|
||||
const loader = new MockLoader({
|
||||
name: 'test-app',
|
||||
port: 3000,
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
manager.initialize();
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
environment: z.string(),
|
||||
});
|
||||
|
||||
const validated = manager.validate(schema);
|
||||
expect(validated).toEqual({
|
||||
name: 'test-app',
|
||||
port: 3000,
|
||||
environment: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if validation fails', () => {
|
||||
const loader = new MockLoader({
|
||||
name: 'test-app',
|
||||
port: 'invalid',
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
manager.initialize();
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
expect(() => manager.validate(schema)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTypedGetter', () => {
|
||||
it('should create a typed getter function', () => {
|
||||
const loader = new MockLoader({
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
},
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
manager.initialize();
|
||||
|
||||
const schema = z.object({
|
||||
database: z.object({
|
||||
host: z.string(),
|
||||
port: z.number(),
|
||||
}),
|
||||
environment: z.string(),
|
||||
});
|
||||
|
||||
const getConfig = manager.createTypedGetter(schema);
|
||||
const config = getConfig();
|
||||
|
||||
expect(config.database.host).toBe('localhost');
|
||||
expect(config.database.port).toBe(5432);
|
||||
expect(config.environment).toBe('development');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deepMerge', () => {
|
||||
it('should handle circular references', () => {
|
||||
const obj1: any = { a: 1 };
|
||||
const obj2: any = { b: 2 };
|
||||
obj1.circular = obj1; // Create circular reference
|
||||
obj2.ref = obj1;
|
||||
|
||||
const loader1 = new MockLoader(obj1);
|
||||
const loader2 = new MockLoader(obj2);
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader1, loader2] });
|
||||
|
||||
// Should not throw on circular reference
|
||||
const config = manager.initialize();
|
||||
expect(config.a).toBe(1);
|
||||
expect(config.b).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle null and undefined values', () => {
|
||||
const loader1 = new MockLoader({ a: null, b: 'value' });
|
||||
const loader2 = new MockLoader({ a: 'overridden', c: undefined });
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader1, loader2] });
|
||||
const config = manager.initialize();
|
||||
|
||||
expect(config.a).toBe('overridden');
|
||||
expect(config.b).toBe('value');
|
||||
expect(config.c).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle Date and RegExp objects', () => {
|
||||
const date = new Date('2024-01-01');
|
||||
const regex = /test/gi;
|
||||
|
||||
const loader = new MockLoader({
|
||||
date: date,
|
||||
pattern: regex,
|
||||
nested: {
|
||||
date: date,
|
||||
pattern: regex,
|
||||
},
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader] });
|
||||
const config = manager.initialize();
|
||||
|
||||
expect(config.date).toBe(date);
|
||||
expect(config.pattern).toBe(regex);
|
||||
expect(config.nested.date).toBe(date);
|
||||
expect(config.nested.pattern).toBe(regex);
|
||||
});
|
||||
|
||||
it('should handle arrays without merging', () => {
|
||||
const loader1 = new MockLoader({ items: [1, 2, 3] });
|
||||
const loader2 = new MockLoader({ items: [4, 5, 6] });
|
||||
|
||||
manager = new ConfigManager({ loaders: [loader1, loader2] });
|
||||
const config = manager.initialize();
|
||||
|
||||
// Arrays should be replaced, not merged
|
||||
expect(config.items).toEqual([4, 5, 6]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue