/**
* Unit tests for ConfigurationComponent validation
*
* Tests that the configuration component uses the same validation rules as TUI
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { ConfigurationComponent } from '../../../src/config/ConfigurationComponent';
import { setupDependencyInjection } from '../../../src/di/setup.js';
import { CONFIG_SERVICE_TOKENS } from '../../../src/config/di-setup.js';
import { existsSync, statSync } from 'fs';
import { join } from 'path';
// Mock fs module
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal() as any;
return {
...actual,
existsSync: vi.fn(),
statSync: vi.fn(),
promises: {
writeFile: vi.fn(),
mkdir: vi.fn(),
readFile: vi.fn(),
stat: vi.fn()
}
};
});
describe('ConfigurationComponent', () => {
let configComponent: ConfigurationComponent;
let mockFs: any;
let mockStatSync: any;
let container: any;
beforeEach(async () => {
vi.clearAllMocks();
mockFs = vi.mocked(existsSync);
mockStatSync = vi.mocked(statSync);
// Setup DI container
container = setupDependencyInjection({
logLevel: 'error' // Quiet for tests
});
// Get ConfigurationComponent from DI container
configComponent = container.resolve(CONFIG_SERVICE_TOKENS.CONFIGURATION_COMPONENT);
// Load configuration so the component is ready for use
const configManager = container.resolve(CONFIG_SERVICE_TOKENS.CONFIG_MANAGER);
await configManager.load();
});
afterEach(() => {
// Clear container
if (container) {
container.clear();
}
});
describe('Theme validation', () => {
it('should accept valid themes', async () => {
// Current valid themes from ThemeContext
const validThemes = [
'default', 'light', 'minimal', // Core
'high-contrast', 'colorblind', // Accessibility
'ocean', 'forest', 'sunset', // Nature
'dracula', 'nord', 'monokai', 'solarized', 'gruvbox', // Classic Editor
'bbs', 'cga', 'matrix' // Retro
];
for (const theme of validThemes) {
const result = await configComponent.validate('theme', theme);
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
}
});
it('should reject invalid themes', async () => {
// Invalid themes including old themes that no longer exist
const invalidThemes = ['invalid-theme', 'auto', 'dark', 'light-optimized', '', 'AUTO'];
for (const theme of invalidThemes) {
const result = await configComponent.validate('theme', theme);
expect(result.valid).toBe(false);
expect(result.errors?.[0]?.message).toContain('Theme must be one of:');
}
});
});
describe('Folder path validation', () => {
it('should accept existing folder paths', async () => {
mockFs.mockReturnValue(true);
mockStatSync.mockReturnValue({ isDirectory: () => true });
const result = await configComponent.validate('folders.list[0].path', '/existing/path');
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
});
it('should reject non-existing folder paths', async () => {
mockFs.mockReturnValue(false);
const result = await configComponent.validate('folders.list[0].path', '/nonexistent/path');
expect(result.valid).toBe(false);
expect(result.errors?.[0]?.message).toBe('Selected path does not exist or is not accessible');
});
it('should reject file paths instead of directories', async () => {
mockFs.mockReturnValue(true);
mockStatSync.mockReturnValue({ isDirectory: () => false });
const result = await configComponent.validate('folders.list[0].path', '/path/to/file.txt');
expect(result.valid).toBe(false);
expect(result.errors?.[0]?.message).toBe('Selected path is not a directory');
});
it('should reject empty folder paths', async () => {
const result = await configComponent.validate('folders.list[0].path', '');
expect(result.valid).toBe(false);
expect(result.errors?.[0]?.message).toBe('Please select a folder path');
});
it('should normalize array paths for validation', async () => {
mockFs.mockReturnValue(true);
mockStatSync.mockReturnValue({ isDirectory: () => true });
// Test different array indices use same validation rules
const paths = [
'folders.list[0].path',
'folders.list[1].path',
'folders.list[999].path'
];
for (const path of paths) {
const result = await configComponent.validate(path, '/existing/path');
expect(result.valid).toBe(true);
}
});
});
describe('Embedding model validation', () => {
it('should accept supported embedding models', async () => {
const supportedModels = [
'gpu:bge-m3',
'gpu:paraphrase-multilingual-minilm',
'cpu:xenova-multilingual-e5-small',
'cpu:xenova-multilingual-e5-large'
];
for (const model of supportedModels) {
const result = await configComponent.validate('folders.defaults.embeddings.model', model);
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
}
});
it('should reject unsupported embedding models', async () => {
const unsupportedModels = ['invalid-model', 'gpt-4', '', 'custom-model'];
for (const model of unsupportedModels) {
const result = await configComponent.validate('folders.defaults.embeddings.model', model);
expect(result.valid).toBe(false);
expect(result.errors?.[0]?.message).toBe('Must be a supported embedding model');
}
});
});
describe('Batch size validation', () => {
it('should accept valid batch sizes', async () => {
const validSizes = ['1', '32', '64', '128', '1000'];
for (const size of validSizes) {
const result = await configComponent.validate('folders.defaults.embeddings.batchSize', size);
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
}
});
it('should reject invalid batch sizes', async () => {
const testCases = [
{ value: '0', expectedError: 'Must be at least 1' },
{ value: '-1', expectedError: 'Must be at least 1' },
{ value: '1001', expectedError: 'Must be at most 1000' },
{ value: 'abc', expectedError: 'Must be a number' },
{ value: '', expectedError: 'Must be at least 1' }, // Empty string converts to 0
{ value: 'not-a-number', expectedError: 'Must be a number' }
];
for (const testCase of testCases) {
const result = await configComponent.validate('folders.defaults.embeddings.batchSize', testCase.value);
expect(result.valid).toBe(false);
expect(result.errors?.[0]?.message).toBe(testCase.expectedError);
}
});
it('should accept decimal batch sizes', async () => {
// Note: The TUI validator accepts decimals, though they may not be practical
const result = await configComponent.validate('folders.defaults.embeddings.batchSize', '32.5');
expect(result.valid).toBe(true);
});
});
describe('Server port validation', () => {
it('should accept valid ports', async () => {
const validPorts = ['1000', '3000', '8080', '9876', '65535'];
for (const port of validPorts) {
const result = await configComponent.validate('server.port', port);
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
}
});
it('should reject invalid ports', async () => {
const testCases = [
{ value: '999', expectedError: 'Must be at least 1000' },
{ value: '0', expectedError: 'Must be at least 1000' },
{ value: '65536', expectedError: 'Must be at most 65535' },
{ value: 'abc', expectedError: 'Must be a number' },
{ value: '', expectedError: 'Must be at least 1000' }, // Empty string converts to 0
{ value: 'not-a-port', expectedError: 'Must be a number' }
];
for (const testCase of testCases) {
const result = await configComponent.validate('server.port', testCase.value);
expect(result.valid).toBe(false);
expect(result.errors?.[0]?.message).toBe(testCase.expectedError);
}
});
it('should accept decimal ports', async () => {
// Note: The TUI validator accepts decimals, though they may not be practical
const result = await configComponent.validate('server.port', '8080.5');
expect(result.valid).toBe(true);
});
});
describe('Server host validation', () => {
it('should accept valid hosts', async () => {
const validHosts = ['localhost', '127.0.0.1', '192.168.1.1', '10.0.0.1'];
for (const host of validHosts) {
const result = await configComponent.validate('server.host', host);
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
}
});
it('should reject invalid hosts', async () => {
const testCases = [
{ value: '256.256.256.256', expectedError: 'Invalid IPv4 address' },
{ value: 'invalid-host', expectedError: 'Invalid IPv4 format' },
{ value: '', expectedError: 'IP address is required' },
{ value: '192.168.1', expectedError: 'Invalid IPv4 format' }
];
for (const testCase of testCases) {
const result = await configComponent.validate('server.host', testCase.value);
expect(result.valid).toBe(false);
expect(result.errors?.[0]?.message).toBe(testCase.expectedError);
}
});
});
describe('Configuration operations', () => {
it('should reject invalid values when setting', async () => {
// Invalid set should throw (this tests validation without requiring file operations)
await expect(configComponent.set('theme', 'invalid-theme'))
.rejects.toThrow(/Invalid value for theme: Theme must be one of:/);
});
it('should have get method defined', () => {
// Just verify the method exists
expect(typeof configComponent.get).toBe('function');
});
it('should have set method defined', () => {
// Just verify the method exists
expect(typeof configComponent.set).toBe('function');
});
});
describe('validateAll', () => {
it('should have validateAll method', async () => {
// Just test that the method exists and returns the expected structure
const result = await configComponent.validateAll();
expect(result).toHaveProperty('isValid');
expect(result).toHaveProperty('errors');
expect(Array.isArray(result.errors)).toBe(true);
expect(typeof result.isValid).toBe('boolean');
});
});
describe('No validation rules', () => {
it('should accept values for paths without validation rules', async () => {
const result = await configComponent.validate('some.unknown.path', 'any-value');
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
});
});
});