stock-bot/libs/core/config/test/edge-cases.test.ts
2025-06-23 18:14:43 -04:00

375 lines
11 KiB
TypeScript

import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { ConfigManager } from '../src/config-manager';
import { ConfigValidationError } from '../src/errors';
import { initializeConfig, resetConfig } from '../src/index';
import { EnvLoader } from '../src/loaders/env.loader';
import { FileLoader } from '../src/loaders/file.loader';
import { appConfigSchema } from '../src/schemas';
const TEST_DIR = join(__dirname, 'edge-case-tests');
describe('Edge Cases and Error Handling', () => {
let originalEnv: NodeJS.ProcessEnv;
let originalCwd: string;
beforeEach(() => {
originalEnv = { ...process.env };
originalCwd = process.cwd();
resetConfig();
if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true, force: true });
}
mkdirSync(TEST_DIR, { recursive: true });
});
afterEach(() => {
process.env = originalEnv;
process.chdir(originalCwd);
resetConfig();
if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true, force: true });
}
});
test('should handle missing .env files gracefully', async () => {
// No .env file exists
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
// Should not throw even without .env file
const config = await manager.initialize(appConfigSchema);
expect(config).toBeDefined();
});
test('should handle corrupted JSON config files', async () => {
const configDir = join(TEST_DIR, 'config');
mkdirSync(configDir, { recursive: true });
// Create corrupted JSON file
writeFileSync(join(configDir, 'development.json'), '{ "app": { "name": "test", invalid json }');
const manager = new ConfigManager({
loaders: [new FileLoader(configDir, 'development')],
});
// Should throw error for invalid JSON
await expect(manager.initialize(appConfigSchema)).rejects.toThrow();
});
test('should handle missing config directories', async () => {
const nonExistentDir = join(TEST_DIR, 'nonexistent');
const manager = new ConfigManager({
loaders: [new FileLoader(nonExistentDir, 'development')],
});
// Should not throw, should return empty config
const config = await manager.initialize();
expect(config).toBeDefined();
});
test('should handle permission denied on config files', async () => {
const configDir = join(TEST_DIR, 'config');
mkdirSync(configDir, { recursive: true });
const configFile = join(configDir, 'development.json');
writeFileSync(configFile, JSON.stringify({ app: { name: 'test' } }));
// Make file unreadable (this might not work on all systems)
try {
chmodSync(configFile, 0o000);
const manager = new ConfigManager({
loaders: [new FileLoader(configDir, 'development')],
});
// Should handle permission error gracefully
const config = await manager.initialize();
expect(config).toBeDefined();
} finally {
// Restore permissions for cleanup
try {
chmodSync(configFile, 0o644);
} catch {
// Ignore errors during cleanup
}
}
});
test('should handle circular references in config merging', async () => {
// This tests deep merge with potential circular references
const configDir = join(TEST_DIR, 'config');
mkdirSync(configDir, { recursive: true });
writeFileSync(
join(configDir, 'development.json'),
JSON.stringify({
app: {
name: 'test',
settings: {
ref: 'settings',
},
},
})
);
process.env.APP_SETTINGS_NESTED_VALUE = 'deep-value';
const manager = new ConfigManager({
loaders: [new FileLoader(configDir, 'development'), new EnvLoader('')],
});
const config = await manager.initialize(appConfigSchema);
expect(config.app.name).toBe('test');
});
test('should handle extremely deep nesting in environment variables', async () => {
// Test very deep nesting
process.env.LEVEL1_LEVEL2_LEVEL3_LEVEL4_LEVEL5_VALUE = 'deep-value';
const manager = new ConfigManager({
loaders: [new EnvLoader('', { nestedDelimiter: '_' })],
});
const config = await manager.initialize();
// Should create nested structure
expect((config as any).level1?.level2?.level3?.level4?.level5?.value).toBe('deep-value');
});
test('should handle conflicting data types in config merging', async () => {
const configDir = join(TEST_DIR, 'config');
mkdirSync(configDir, { recursive: true });
// File config has object
writeFileSync(
join(configDir, 'development.json'),
JSON.stringify({
database: {
host: 'localhost',
port: 5432,
},
})
);
// Environment variable tries to override with string
process.env.DATABASE = 'simple-string';
const manager = new ConfigManager({
loaders: [new FileLoader(configDir, 'development'), new EnvLoader('')],
});
const config = await manager.initialize(appConfigSchema);
// Environment variable should win
expect(config.database).toBe('simple-string');
});
test('should handle different working directories', async () => {
// Create multiple config setups in different directories
const dir1 = join(TEST_DIR, 'dir1');
const dir2 = join(TEST_DIR, 'dir2');
mkdirSync(join(dir1, 'config'), { recursive: true });
mkdirSync(join(dir2, 'config'), { recursive: true });
writeFileSync(
join(dir1, 'config', 'development.json'),
JSON.stringify({ app: { name: 'dir1-app' } })
);
writeFileSync(
join(dir2, 'config', 'development.json'),
JSON.stringify({ app: { name: 'dir2-app' } })
);
// Test from dir1
process.chdir(dir1);
resetConfig();
let config = await initializeConfig();
expect(config.app.name).toBe('dir1-app');
// Test from dir2
process.chdir(dir2);
resetConfig();
config = await initializeConfig();
expect(config.app.name).toBe('dir2-app');
});
test('should handle malformed .env files', async () => {
// Create malformed .env file
writeFileSync(
join(TEST_DIR, '.env'),
`# Good line
VALID_KEY=valid_value
# Malformed lines
MISSING_VALUE=
=MISSING_KEY
SPACES IN KEY=value
KEY_WITH_QUOTES="quoted value"
KEY_WITH_SINGLE_QUOTES='single quoted'
# Complex value
JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
`
);
process.chdir(TEST_DIR);
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
const _config = await manager.initialize();
// Should handle valid entries
expect(process.env.VALID_KEY).toBe('valid_value');
expect(process.env.KEY_WITH_QUOTES).toBe('quoted value');
expect(process.env.KEY_WITH_SINGLE_QUOTES).toBe('single quoted');
});
test('should handle empty config files', async () => {
const configDir = join(TEST_DIR, 'config');
mkdirSync(configDir, { recursive: true });
// Create empty JSON file
writeFileSync(join(configDir, 'development.json'), '{}');
const manager = new ConfigManager({
loaders: [new FileLoader(configDir, 'development')],
});
const config = await manager.initialize(appConfigSchema);
expect(config).toBeDefined();
expect(config.environment).toBe('development'); // Should have default
});
test('should handle config initialization without schema', async () => {
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
// Initialize without schema
const config = await manager.initialize();
expect(config).toBeDefined();
expect(typeof config).toBe('object');
});
test('should handle accessing config before initialization', () => {
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
// Should throw error when accessing uninitialized config
expect(() => manager.get()).toThrow('Configuration not initialized');
expect(() => manager.getValue('some.path')).toThrow('Configuration not initialized');
expect(() => manager.has('some.path')).toThrow('Configuration not initialized');
});
test('should handle invalid config paths in getValue', async () => {
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
const _config = await manager.initialize(appConfigSchema);
// Should throw for invalid paths
expect(() => manager.getValue('nonexistent.path')).toThrow('Configuration key not found');
expect(() => manager.getValue('app.nonexistent')).toThrow('Configuration key not found');
// Should work for valid paths
expect(() => manager.getValue('environment')).not.toThrow();
});
test('should handle null and undefined values in config', async () => {
process.env.NULL_VALUE = 'null';
process.env.UNDEFINED_VALUE = 'undefined';
process.env.EMPTY_VALUE = '';
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
const config = await manager.initialize();
expect((config as any).null_value).toBe(null);
expect((config as any).undefined_value).toBe(undefined);
expect((config as any).empty_value).toBe('');
});
test('should handle schema validation failures', async () => {
// Set up config that will fail schema validation
process.env.APP_NAME = 'valid-name';
process.env.APP_VERSION = 'valid-version';
process.env.SERVICE_PORT = 'not-a-number'; // This should cause validation to fail
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
await expect(manager.initialize(appConfigSchema)).rejects.toThrow(ConfigValidationError);
});
test('should handle config updates with invalid schema', async () => {
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
await manager.initialize(appConfigSchema);
// Try to update with invalid data
expect(() => {
manager.set({
service: {
port: 'invalid-port' as any,
},
});
}).toThrow(ConfigValidationError);
});
test('should handle loader priority conflicts', async () => {
const configDir = join(TEST_DIR, 'config');
mkdirSync(configDir, { recursive: true });
writeFileSync(
join(configDir, 'development.json'),
JSON.stringify({ app: { name: 'file-config' } })
);
process.env.APP_NAME = 'env-config';
// Create loaders with different priorities
const manager = new ConfigManager({
loaders: [
new FileLoader(configDir, 'development'), // priority 50
new EnvLoader(''), // priority 100
],
});
const config = await manager.initialize(appConfigSchema);
// Environment should win due to higher priority
expect(config.app.name).toBe('env-config');
});
test('should handle readonly environment variables', async () => {
// Some system environment variables might be readonly
const originalPath = process.env.PATH;
// This should not cause the loader to fail
const manager = new ConfigManager({
loaders: [new EnvLoader('')],
});
const config = await manager.initialize();
expect(config).toBeDefined();
// PATH should not be modified
expect(process.env.PATH).toBe(originalPath);
});
});