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
436
libs/core/config/test/file.loader.test.ts
Normal file
436
libs/core/config/test/file.loader.test.ts
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
import { ConfigLoaderError } from '../src/errors';
|
||||
|
||||
// Mock fs module
|
||||
mock.module('fs', () => ({
|
||||
existsSync: mock(() => false),
|
||||
readFileSync: mock(() => '')
|
||||
}));
|
||||
|
||||
describe('FileLoader', () => {
|
||||
let loader: FileLoader;
|
||||
const configPath = '/app/config';
|
||||
const environment = 'development';
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
(existsSync as any).mockReset();
|
||||
(readFileSync as any).mockReset();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should have medium priority', () => {
|
||||
loader = new FileLoader(configPath, environment);
|
||||
expect(loader.priority).toBe(50);
|
||||
});
|
||||
|
||||
it('should store config path and environment', () => {
|
||||
loader = new FileLoader('/custom/path', 'production');
|
||||
expect(loader).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('load', () => {
|
||||
it('should load only default.json when environment file does not exist', () => {
|
||||
const defaultConfig = {
|
||||
name: 'app',
|
||||
port: 3000,
|
||||
features: ['auth', 'cache'],
|
||||
};
|
||||
|
||||
(existsSync as any).mockImplementation((path: string) => {
|
||||
return path.endsWith('default.json');
|
||||
});
|
||||
|
||||
(readFileSync as any).mockImplementation((path: string) => {
|
||||
if (path.endsWith('default.json')) {
|
||||
return JSON.stringify(defaultConfig);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
expect(existsSync).toHaveBeenCalledWith('/app/config/default.json');
|
||||
expect(existsSync).toHaveBeenCalledWith('/app/config/development.json');
|
||||
expect(readFileSync).toHaveBeenCalledWith('/app/config/default.json', 'utf-8');
|
||||
expect(config).toEqual(defaultConfig);
|
||||
});
|
||||
|
||||
it('should load and merge default and environment configs', () => {
|
||||
const defaultConfig = {
|
||||
name: 'app',
|
||||
port: 3000,
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
},
|
||||
};
|
||||
|
||||
const devConfig = {
|
||||
port: 3001,
|
||||
database: {
|
||||
host: 'dev-db',
|
||||
},
|
||||
debug: true,
|
||||
};
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
|
||||
(readFileSync as any).mockImplementation((path: string) => {
|
||||
if (path.endsWith('default.json')) {
|
||||
return JSON.stringify(defaultConfig);
|
||||
}
|
||||
if (path.endsWith('development.json')) {
|
||||
return JSON.stringify(devConfig);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
expect(config).toEqual({
|
||||
name: 'app',
|
||||
port: 3001, // Overridden by dev config
|
||||
database: {
|
||||
host: 'dev-db', // Overridden by dev config
|
||||
port: 5432, // Preserved from default
|
||||
},
|
||||
debug: true, // Added by dev config
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle production environment', () => {
|
||||
const defaultConfig = { name: 'app', debug: true };
|
||||
const prodConfig = { debug: false, secure: true };
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
|
||||
(readFileSync as any).mockImplementation((path: string) => {
|
||||
if (path.endsWith('default.json')) {
|
||||
return JSON.stringify(defaultConfig);
|
||||
}
|
||||
if (path.endsWith('production.json')) {
|
||||
return JSON.stringify(prodConfig);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
loader = new FileLoader(configPath, 'production');
|
||||
const config = loader.load();
|
||||
|
||||
expect(existsSync).toHaveBeenCalledWith('/app/config/production.json');
|
||||
expect(config).toEqual({
|
||||
name: 'app',
|
||||
debug: false,
|
||||
secure: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty object when no config files exist', () => {
|
||||
(existsSync as any).mockReturnValue(false);
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
expect(config).toEqual({});
|
||||
expect(readFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw ConfigLoaderError on JSON parse error', () => {
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockReturnValue('{ invalid json');
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
|
||||
expect(() => loader.load()).toThrow(ConfigLoaderError);
|
||||
expect(() => loader.load()).toThrow('Failed to load configuration files');
|
||||
});
|
||||
|
||||
it('should throw ConfigLoaderError on file read error', () => {
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockImplementation(() => {
|
||||
throw new Error('Permission denied');
|
||||
});
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
|
||||
expect(() => loader.load()).toThrow(ConfigLoaderError);
|
||||
expect(() => loader.load()).toThrow('Failed to load configuration files');
|
||||
});
|
||||
|
||||
it('should handle different config paths', () => {
|
||||
const customPath = '/custom/config/dir';
|
||||
const config = { custom: true };
|
||||
|
||||
(existsSync as any).mockImplementation((path: string) => {
|
||||
return path.startsWith(customPath);
|
||||
});
|
||||
|
||||
(readFileSync as any).mockReturnValue(JSON.stringify(config));
|
||||
|
||||
loader = new FileLoader(customPath, environment);
|
||||
loader.load();
|
||||
|
||||
expect(existsSync).toHaveBeenCalledWith(`${customPath}/default.json`);
|
||||
expect(existsSync).toHaveBeenCalledWith(`${customPath}/development.json`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deepMerge', () => {
|
||||
it('should handle null and undefined values', () => {
|
||||
const defaultConfig = {
|
||||
a: 'value',
|
||||
b: null,
|
||||
c: 'default',
|
||||
};
|
||||
|
||||
const envConfig = {
|
||||
a: null,
|
||||
b: 'updated',
|
||||
// Note: undefined values are not preserved in JSON
|
||||
};
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockImplementation((path: string) => {
|
||||
if (path.endsWith('default.json')) {
|
||||
return JSON.stringify(defaultConfig);
|
||||
}
|
||||
if (path.endsWith('development.json')) {
|
||||
return JSON.stringify(envConfig);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
expect(config).toEqual({
|
||||
a: null,
|
||||
b: 'updated',
|
||||
c: 'default', // Preserved from default since envConfig doesn't have 'c'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle arrays correctly', () => {
|
||||
const defaultConfig = {
|
||||
items: [1, 2, 3],
|
||||
features: ['auth', 'cache'],
|
||||
};
|
||||
|
||||
const envConfig = {
|
||||
items: [4, 5],
|
||||
features: ['auth', 'cache', 'search'],
|
||||
};
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockImplementation((path: string) => {
|
||||
if (path.endsWith('default.json')) {
|
||||
return JSON.stringify(defaultConfig);
|
||||
}
|
||||
if (path.endsWith('development.json')) {
|
||||
return JSON.stringify(envConfig);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
// Arrays should be replaced, not merged
|
||||
expect(config).toEqual({
|
||||
items: [4, 5],
|
||||
features: ['auth', 'cache', 'search'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle deeply nested objects', () => {
|
||||
const defaultConfig = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
},
|
||||
c: 3,
|
||||
},
|
||||
d: 4,
|
||||
},
|
||||
};
|
||||
|
||||
const envConfig = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
b: 22,
|
||||
e: 5,
|
||||
},
|
||||
f: 6,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockImplementation((path: string) => {
|
||||
if (path.endsWith('default.json')) {
|
||||
return JSON.stringify(defaultConfig);
|
||||
}
|
||||
if (path.endsWith('development.json')) {
|
||||
return JSON.stringify(envConfig);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
expect(config).toEqual({
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
a: 1,
|
||||
b: 22,
|
||||
e: 5,
|
||||
},
|
||||
c: 3,
|
||||
f: 6,
|
||||
},
|
||||
d: 4,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Date and RegExp objects', () => {
|
||||
// Dates and RegExps in JSON are serialized as strings
|
||||
const defaultConfig = {
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
pattern: '/test/gi',
|
||||
};
|
||||
|
||||
const envConfig = {
|
||||
updatedAt: '2023-06-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockImplementation((path: string) => {
|
||||
if (path.endsWith('default.json')) {
|
||||
return JSON.stringify(defaultConfig);
|
||||
}
|
||||
if (path.endsWith('development.json')) {
|
||||
return JSON.stringify(envConfig);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
expect(config).toEqual({
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
pattern: '/test/gi',
|
||||
updatedAt: '2023-06-01T00:00:00.000Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty JSON files', () => {
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockReturnValue('{}');
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle whitespace in JSON files', () => {
|
||||
const config = { test: 'value' };
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockReturnValue(` \n\t${JSON.stringify(config)}\n `);
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const result = loader.load();
|
||||
|
||||
expect(result).toEqual(config);
|
||||
});
|
||||
|
||||
it('should handle very large config files', () => {
|
||||
const largeConfig: Record<string, unknown> = {};
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
largeConfig[`key_${i}`] = {
|
||||
value: i,
|
||||
nested: { data: `data_${i}` },
|
||||
};
|
||||
}
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockReturnValue(JSON.stringify(largeConfig));
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const config = loader.load();
|
||||
|
||||
expect(Object.keys(config)).toHaveLength(1000);
|
||||
expect(config.key_500).toEqual({
|
||||
value: 500,
|
||||
nested: { data: 'data_500' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unicode in config values', () => {
|
||||
const config = {
|
||||
emoji: '🚀',
|
||||
chinese: '你好',
|
||||
arabic: 'مرحبا',
|
||||
};
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockReturnValue(JSON.stringify(config));
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const result = loader.load();
|
||||
|
||||
expect(result).toEqual(config);
|
||||
});
|
||||
|
||||
it('should handle config with circular reference patterns', () => {
|
||||
// JSON doesn't support circular references, but we can have
|
||||
// patterns that look circular
|
||||
const config = {
|
||||
parent: {
|
||||
child: {
|
||||
ref: 'parent',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockReturnValue(JSON.stringify(config));
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const result = loader.load();
|
||||
|
||||
expect(result).toEqual(config);
|
||||
});
|
||||
|
||||
it('should handle numeric string keys', () => {
|
||||
const config = {
|
||||
'123': 'numeric key',
|
||||
'456': { nested: 'value' },
|
||||
};
|
||||
|
||||
(existsSync as any).mockReturnValue(true);
|
||||
(readFileSync as any).mockReturnValue(JSON.stringify(config));
|
||||
|
||||
loader = new FileLoader(configPath, environment);
|
||||
const result = loader.load();
|
||||
|
||||
expect(result).toEqual(config);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue