@apistudio/apim-cli
Version:
CLI for API Management Products
316 lines (262 loc) • 12.9 kB
text/typescript
import { ConfigLoader } from '../core/impl/config-loader.impl';
import fs from 'fs/promises';
import path from 'path';
// Mock fs/promises
jest.mock('fs/promises', () => ({
access: jest.fn().mockResolvedValue(undefined),
readFile: jest.fn()
}));
// Mock path
jest.mock('path', () => ({
join: jest.fn((base, ...parts) => `${base}/${parts.join('/')}`),
isAbsolute: jest.fn(p => p.startsWith('/')),
}));
describe('ConfigLoader', () => {
let configLoader: ConfigLoader;
const baseDir = '/test/base/dir';
beforeEach(() => {
jest.clearAllMocks();
// Reset mock implementations to defaults
(path.join as jest.Mock).mockImplementation((base, ...parts) => `${base}/${parts.join('/')}`);
(path.isAbsolute as jest.Mock).mockImplementation(p => p.startsWith('/'));
(fs.access as jest.Mock).mockResolvedValue(undefined);
// Create a fresh instance for each test
configLoader = new ConfigLoader(baseDir);
});
describe('loadConfig', () => {
it('should load JSON config file', async () => {
// Setup
const configPath = 'test-config.json';
const resolvedPath = `${baseDir}/${configPath}`;
const configContent = JSON.stringify({
skipTransform: false,
transformations: {
mappings: [{ source: '$.spec.value', target: '$.Test.value' }],
replacements: []
}
});
(fs.readFile as jest.Mock).mockResolvedValue(configContent);
(path.join as jest.Mock).mockReturnValue(resolvedPath);
// Execute
const result = await configLoader.loadConfig(configPath);
// Verify
expect(fs.access).toHaveBeenCalledWith(resolvedPath);
expect(fs.readFile).toHaveBeenCalledWith(resolvedPath, 'utf-8');
expect(result).toEqual({
skipTransform: false,
transformations: {
mappings: [{ source: '$.spec.value', target: '$.Test.value' }],
replacements: []
}
});
});
it('should handle config with inheritance', async () => {
// Setup
const configPath = 'child-config.json';
const parentPath = 'parent-config.json';
const resolvedConfigPath = `${baseDir}/${configPath}`;
const resolvedParentPath = `${baseDir}/${parentPath}`;
const parentConfig = {
skipTransform: false,
transformations: {
mappings: [{ source: '$.spec.parentValue', target: '$.Test.parentValue' }],
replacements: []
}
};
const childConfig = {
extends: parentPath,
transformations: {
mappings: [{ source: '$.spec.childValue', target: '$.Test.childValue' }],
replacements: [{ target: '$.Test.replacement', value: 'test', precedence: 10, operation: 'replace' }]
}
};
(fs.readFile as jest.Mock).mockImplementation((path) => {
if (path === resolvedConfigPath) {
return Promise.resolve(JSON.stringify(childConfig));
} else if (path === resolvedParentPath) {
return Promise.resolve(JSON.stringify(parentConfig));
}
return Promise.reject(new Error(`Unexpected path: ${path}`));
});
(path.join as jest.Mock).mockImplementation((base, file) => {
if (file === configPath) return resolvedConfigPath;
if (file === parentPath) return resolvedParentPath;
return `${base}/${file}`;
});
// Execute
const result = await configLoader.loadConfig(configPath);
// Verify
expect(fs.access).toHaveBeenCalledWith(resolvedConfigPath);
expect(fs.access).toHaveBeenCalledWith(resolvedParentPath);
expect(fs.readFile).toHaveBeenCalledWith(resolvedConfigPath, 'utf-8');
expect(fs.readFile).toHaveBeenCalledWith(resolvedParentPath, 'utf-8');
// The actual implementation might not set skipTransform if it's not in the config
expect(result).toHaveProperty('extends', parentPath);
expect(result.transformations).toBeDefined();
if (result.transformations) {
expect(result.transformations.mappings).toContainEqual({ source: '$.spec.parentValue', target: '$.Test.parentValue' });
expect(result.transformations.mappings).toContainEqual({ source: '$.spec.childValue', target: '$.Test.childValue' });
expect(result.transformations.replacements).toContainEqual({ target: '$.Test.replacement', value: 'test', precedence: 10, operation: 'replace' });
}
});
it('should handle config with custom transformer', async () => {
// Setup
const configPath = 'custom-config.json';
const resolvedPath = `${baseDir}/${configPath}`;
const configContent = JSON.stringify({
skipTransform: false,
custom: 'custom-transformer',
transformations: {
mappings: [{ source: '$.spec.value', target: '$.Test.value' }],
replacements: []
}
});
(fs.readFile as jest.Mock).mockResolvedValue(configContent);
(path.join as jest.Mock).mockReturnValue(resolvedPath);
// Execute
const result = await configLoader.loadConfig(configPath);
// Verify
expect(result).toEqual({
skipTransform: false,
custom: 'custom-transformer'
// transformations should be removed when custom is specified
});
});
it('should handle config with inheritance and custom transformer', async () => {
// Setup
const configPath = 'child-config.json';
const parentPath = 'parent-config.json';
const resolvedConfigPath = `${baseDir}/${configPath}`;
const resolvedParentPath = `${baseDir}/${parentPath}`;
const parentConfig = {
skipTransform: false,
transformations: {
mappings: [{ source: '$.spec.parentValue', target: '$.Test.parentValue' }],
replacements: []
}
};
const childConfig = {
extends: parentPath,
custom: 'custom-transformer',
transformations: {
mappings: [{ source: '$.spec.childValue', target: '$.Test.childValue' }],
replacements: []
}
};
(fs.readFile as jest.Mock).mockImplementation((path) => {
if (path === resolvedConfigPath) {
return Promise.resolve(JSON.stringify(childConfig));
} else if (path === resolvedParentPath) {
return Promise.resolve(JSON.stringify(parentConfig));
}
return Promise.reject(new Error(`Unexpected path: ${path}`));
});
(path.join as jest.Mock).mockImplementation((base, file) => {
if (file === configPath) return resolvedConfigPath;
if (file === parentPath) return resolvedParentPath;
return `${base}/${file}`;
});
// Execute
const result = await configLoader.loadConfig(configPath);
// Verify
// The actual implementation might not set skipTransform if it's not in the config
expect(result).toHaveProperty('extends', parentPath);
expect(result).toHaveProperty('custom', 'custom-transformer');
// transformations should be removed when custom is specified
expect(result.transformations).toBeUndefined();
});
it('should cache loaded configs', async () => {
// Setup
const configPath = 'test-config.json';
const resolvedPath = `${baseDir}/${configPath}`;
const configContent = JSON.stringify({
skipTransform: false,
transformations: {
mappings: [{ source: '$.spec.value', target: '$.Test.value' }],
replacements: []
}
});
(fs.readFile as jest.Mock).mockResolvedValue(configContent);
(path.join as jest.Mock).mockReturnValue(resolvedPath);
// Execute
await configLoader.loadConfig(configPath);
await configLoader.loadConfig(configPath);
// Verify
expect(fs.readFile).toHaveBeenCalledTimes(1);
});
it('should handle errors when loading config', async () => {
// Setup
const configPath = 'nonexistent-config.json';
const resolvedPath = `${baseDir}/${configPath}`;
(fs.access as jest.Mock).mockRejectedValue(new Error('File not found'));
(path.join as jest.Mock).mockReturnValue(resolvedPath);
// Execute & Verify
await expect(configLoader.loadConfig(configPath)).rejects.toThrow();
});
});
describe('resolveConfigPath', () => {
it('should resolve paths starting with src/', async () => {
// Setup
const configPath = 'src/configs/test-config.json';
const expectedPath = `${baseDir}/configs/test-config.json`;
(path.join as jest.Mock).mockReturnValueOnce(expectedPath);
// Execute
const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);
// Verify
expect(resolvedPath).toBe(expectedPath);
expect(path.join).toHaveBeenCalledWith(baseDir, 'configs/test-config.json');
});
it('should resolve paths starting with /src/', async () => {
// Setup
const configPath = '/src/configs/test-config.json';
const expectedPath = `${baseDir}/configs/test-config.json`;
(path.join as jest.Mock).mockReturnValue(expectedPath);
// Execute
const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);
// Verify
expect(resolvedPath).toBe(expectedPath);
expect(path.join).toHaveBeenCalledWith(baseDir, 'configs/test-config.json');
});
it('should resolve relative paths', async () => {
// Setup
const configPath = 'configs/test-config.json';
const expectedPath = `${baseDir}/configs/test-config.json`;
(path.join as jest.Mock).mockReturnValue(expectedPath);
(path.isAbsolute as jest.Mock).mockReturnValue(false);
// Execute
const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);
// Verify
expect(resolvedPath).toBe(expectedPath);
expect(path.join).toHaveBeenCalledWith(baseDir, configPath);
});
it('should resolve absolute paths containing /src/', async () => {
// Setup
const configPath = '/absolute/path/src/configs/test-config.json';
const expectedPath = `${baseDir}/configs/test-config.json`;
(path.isAbsolute as jest.Mock).mockReturnValue(true);
(path.join as jest.Mock).mockReturnValue(expectedPath);
// Execute
const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);
// Verify
expect(resolvedPath).toBe(expectedPath);
});
it('should handle other absolute paths', async () => {
// Setup
const configPath = '/absolute/path/configs/test-config.json';
(path.isAbsolute as jest.Mock).mockReturnValue(true);
// Execute
const resolvedPath = await (configLoader as any).resolveConfigPath(configPath);
// Verify
expect(resolvedPath).toBe(configPath);
});
it('should throw error if file does not exist', async () => {
// Setup
const configPath = 'nonexistent-config.json';
const expectedPath = `${baseDir}/nonexistent-config.json`;
(path.join as jest.Mock).mockReturnValue(expectedPath);
(fs.access as jest.Mock).mockRejectedValue(new Error('File not found'));
// Execute & Verify
await expect((configLoader as any).resolveConfigPath(configPath)).rejects.toThrow();
});
});
});