375 lines
11 KiB
TypeScript
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 { ConfigError, ConfigValidationError } from '../src/errors';
|
|
import { initializeConfig, initializeServiceConfig, 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);
|
|
});
|
|
});
|