import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
/**
* Tests for configuration management system
*/
import { loadConfiguration, ServerConfigSchema, mergeConfigs } from '../../config/index.js';
import { writeFileSync, unlinkSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
describe('Configuration System', () => {
const testConfigDir = join(tmpdir(), 'stampchain-mcp-test');
const testConfigPath = join(testConfigDir, 'test-config.json');
// Create test directory
beforeEach(() => {
try {
mkdirSync(testConfigDir, { recursive: true });
} catch {
// Directory might already exist
}
});
// Clean up test files
afterEach(() => {
try {
rmSync(testConfigDir, { recursive: true, force: true });
} catch {
// Directory might not exist
}
});
describe('ServerConfigSchema', () => {
it('should validate default configuration', () => {
const defaultConfig = {
name: 'stampchain-mcp',
version: '0.1.0',
logging: {
level: 'info',
enableColors: true,
},
api: {
baseUrl: 'https://stampchain.io/api',
timeout: 30000,
retries: 3,
retryDelay: 1000,
},
registry: {
maxTools: 1000,
validateOnRegister: true,
allowDuplicateNames: false,
},
development: {
enableStackTraces: false,
},
};
const result = ServerConfigSchema.safeParse(defaultConfig);
expect(result.success).toBe(true);
});
it('should apply default values for missing fields', () => {
const minimalConfig = {
name: 'test-server',
version: '1.0.0',
};
const result = ServerConfigSchema.safeParse(minimalConfig);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.logging.level).toBe('info');
expect(result.data.api.timeout).toBe(30000);
expect(result.data.registry.maxTools).toBe(1000);
}
});
it('should reject invalid log levels', () => {
const invalidConfig = {
name: 'test-server',
version: '1.0.0',
logging: {
level: 'invalid-level',
},
};
const result = ServerConfigSchema.safeParse(invalidConfig);
expect(result.success).toBe(false);
});
it('should validate timeout ranges', () => {
const invalidConfig = {
name: 'test-server',
version: '1.0.0',
api: {
timeout: -1000, // Invalid negative timeout
},
};
const result = ServerConfigSchema.safeParse(invalidConfig);
expect(result.success).toBe(false);
});
it('should validate retry configuration', () => {
const invalidConfig = {
name: 'test-server',
version: '1.0.0',
api: {
retries: -1, // Invalid negative retries
},
};
const result = ServerConfigSchema.safeParse(invalidConfig);
expect(result.success).toBe(false);
});
it('should validate registry limits', () => {
const invalidConfig = {
name: 'test-server',
version: '1.0.0',
registry: {
maxTools: 0, // Invalid zero limit
},
};
const result = ServerConfigSchema.safeParse(invalidConfig);
expect(result.success).toBe(false);
});
});
describe('mergeConfigs', () => {
it('should merge flat configuration objects', () => {
const base = { a: 1, b: 2, c: 3 };
const override = { b: 20, d: 4 };
const result = mergeConfigs(base, override);
expect(result).toEqual({
a: 1,
b: 20, // Overridden
c: 3,
d: 4, // Added
});
});
it('should merge nested configuration objects', () => {
const base = {
logging: { level: 'info', enableColors: true },
api: { timeout: 30000, retries: 3 },
};
const override = {
logging: { level: 'debug' },
api: { timeout: 10000 },
registry: { maxTools: 500 },
};
const result = mergeConfigs(base, override);
expect(result).toEqual({
logging: { level: 'debug', enableColors: true },
api: { timeout: 10000, retries: 3 },
registry: { maxTools: 500 },
});
});
it('should handle null and undefined values', () => {
const base = { a: 1, b: 2 };
const override = { b: null, c: undefined, d: 3 };
const result = mergeConfigs(base, override);
expect(result).toEqual({
a: 1,
b: null,
c: undefined,
d: 3,
});
});
it('should not mutate original objects', () => {
const base = { nested: { value: 1 } };
const override = { nested: { value: 2 } };
const originalBase = JSON.parse(JSON.stringify(base));
const originalOverride = JSON.parse(JSON.stringify(override));
mergeConfigs(base, override);
expect(base).toEqual(originalBase);
expect(override).toEqual(originalOverride);
});
});
describe('loadConfiguration', () => {
beforeEach(() => {
// Clear environment variables
delete process.env.STAMPCHAIN_LOG_LEVEL;
delete process.env.STAMPCHAIN_API_URL;
delete process.env.STAMPCHAIN_API_TIMEOUT;
delete process.env.STAMPCHAIN_MAX_TOOLS;
});
it('should load default configuration', () => {
const config = loadConfiguration();
expect(config.name).toBe('stampchain-mcp');
expect(config.version).toBe('0.1.0');
expect(config.logging.level).toBe('info');
expect(config.api.baseUrl).toBe('https://stampchain.io/api');
});
it('should override with environment variables', () => {
process.env.STAMPCHAIN_LOG_LEVEL = 'debug';
process.env.STAMPCHAIN_API_URL = 'https://custom.api.com';
process.env.STAMPCHAIN_API_TIMEOUT = '15000';
process.env.STAMPCHAIN_MAX_TOOLS = '500';
const config = loadConfiguration();
expect(config.logging.level).toBe('debug');
expect(config.api.baseUrl).toBe('https://custom.api.com');
expect(config.api.timeout).toBe(15000);
expect(config.registry.maxTools).toBe(500);
});
it('should load configuration from file', () => {
const fileConfig = {
name: 'custom-server',
logging: { level: 'warn' },
api: { timeout: 20000 },
};
// Create test config file
writeFileSync(testConfigPath, JSON.stringify(fileConfig, null, 2));
const config = loadConfiguration({ configFile: testConfigPath });
expect(config.name).toBe('custom-server');
expect(config.logging.level).toBe('warn');
expect(config.api.timeout).toBe(20000);
// Default values should still be present
expect(config.api.baseUrl).toBe('https://stampchain.io/api');
});
it('should override with CLI arguments', () => {
const cliArgs = {
logLevel: 'error',
apiUrl: 'https://cli.api.com',
debug: true,
};
const config = loadConfiguration({ cliArgs });
expect(config.logging.level).toBe('error');
expect(config.api.baseUrl).toBe('https://cli.api.com');
expect(config.development.enableStackTraces).toBe(true);
});
it('should apply configuration precedence (CLI > ENV > File > Default)', () => {
// Set up all sources
process.env.STAMPCHAIN_LOG_LEVEL = 'warn';
const fileConfig = {
logging: { level: 'info' },
api: { timeout: 25000 },
};
writeFileSync(testConfigPath, JSON.stringify(fileConfig, null, 2));
const cliArgs = {
logLevel: 'debug', // Should override everything
};
const config = loadConfiguration({
configFile: testConfigPath,
cliArgs,
});
expect(config.logging.level).toBe('debug'); // CLI wins
expect(config.api.timeout).toBe(25000); // From file
});
it('should handle missing configuration file gracefully', () => {
expect(() => {
loadConfiguration({ configFile: '/non/existent/path.json' });
}).not.toThrow();
});
it('should handle invalid JSON in configuration file', () => {
writeFileSync(testConfigPath, 'invalid json content');
expect(() => {
loadConfiguration({ configFile: testConfigPath });
}).toThrow('Failed to parse configuration file');
});
it('should validate final configuration', () => {
const invalidConfig = {
logging: { level: 'invalid-level' },
};
writeFileSync(testConfigPath, JSON.stringify(invalidConfig, null, 2));
expect(() => {
loadConfiguration({ configFile: testConfigPath });
}).toThrow('Configuration validation failed');
});
it('should handle boolean environment variables', () => {
process.env.STAMPCHAIN_ENABLE_COLORS = 'false';
process.env.STAMPCHAIN_VALIDATE_ON_REGISTER = 'true';
const config = loadConfiguration();
expect(config.logging.enableColors).toBe(false);
expect(config.registry.validateOnRegister).toBe(true);
});
it('should handle numeric environment variables', () => {
process.env.STAMPCHAIN_API_TIMEOUT = '45000';
process.env.STAMPCHAIN_API_RETRIES = '5';
process.env.STAMPCHAIN_MAX_TOOLS = '2000';
const config = loadConfiguration();
expect(config.api.timeout).toBe(45000);
expect(config.api.retries).toBe(5);
expect(config.registry.maxTools).toBe(2000);
});
it('should ignore invalid environment variable values', () => {
process.env.STAMPCHAIN_API_TIMEOUT = 'not-a-number';
process.env.STAMPCHAIN_MAX_TOOLS = 'invalid';
const config = loadConfiguration();
// Should fall back to defaults
expect(config.api.timeout).toBe(30000);
expect(config.registry.maxTools).toBe(1000);
});
});
});