805 lines
24 KiB
TypeScript
805 lines
24 KiB
TypeScript
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
import * as einvoice from '../../../ts/index.js';
|
||
|
import * as plugins from '../../plugins.js';
|
||
|
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
||
|
|
||
|
tap.test('ERR-10: Configuration Errors - Handle configuration and setup failures', async (t) => {
|
||
|
const performanceTracker = new PerformanceTracker('ERR-10');
|
||
|
|
||
|
await t.test('Invalid configuration values', async () => {
|
||
|
performanceTracker.startOperation('config-validation');
|
||
|
|
||
|
interface IEInvoiceConfig {
|
||
|
validationLevel?: 'strict' | 'normal' | 'lenient';
|
||
|
maxFileSize?: number;
|
||
|
timeout?: number;
|
||
|
supportedFormats?: string[];
|
||
|
locale?: string;
|
||
|
timezone?: string;
|
||
|
apiEndpoint?: string;
|
||
|
retryAttempts?: number;
|
||
|
cacheTTL?: number;
|
||
|
}
|
||
|
|
||
|
class ConfigValidator {
|
||
|
private errors: string[] = [];
|
||
|
|
||
|
validate(config: IEInvoiceConfig): { valid: boolean; errors: string[] } {
|
||
|
this.errors = [];
|
||
|
|
||
|
// Validation level
|
||
|
if (config.validationLevel && !['strict', 'normal', 'lenient'].includes(config.validationLevel)) {
|
||
|
this.errors.push(`Invalid validation level: ${config.validationLevel}`);
|
||
|
}
|
||
|
|
||
|
// Max file size
|
||
|
if (config.maxFileSize !== undefined) {
|
||
|
if (config.maxFileSize <= 0) {
|
||
|
this.errors.push('Max file size must be positive');
|
||
|
}
|
||
|
if (config.maxFileSize > 1024 * 1024 * 1024) { // 1GB
|
||
|
this.errors.push('Max file size exceeds reasonable limit (1GB)');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Timeout
|
||
|
if (config.timeout !== undefined) {
|
||
|
if (config.timeout <= 0) {
|
||
|
this.errors.push('Timeout must be positive');
|
||
|
}
|
||
|
if (config.timeout > 300000) { // 5 minutes
|
||
|
this.errors.push('Timeout exceeds maximum allowed (5 minutes)');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Supported formats
|
||
|
if (config.supportedFormats) {
|
||
|
const validFormats = ['UBL', 'CII', 'ZUGFeRD', 'Factur-X', 'XRechnung', 'FatturaPA', 'PEPPOL'];
|
||
|
const invalidFormats = config.supportedFormats.filter(f => !validFormats.includes(f));
|
||
|
if (invalidFormats.length > 0) {
|
||
|
this.errors.push(`Unknown formats: ${invalidFormats.join(', ')}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Locale
|
||
|
if (config.locale && !/^[a-z]{2}(-[A-Z]{2})?$/.test(config.locale)) {
|
||
|
this.errors.push(`Invalid locale format: ${config.locale}`);
|
||
|
}
|
||
|
|
||
|
// Timezone
|
||
|
if (config.timezone) {
|
||
|
try {
|
||
|
new Intl.DateTimeFormat('en', { timeZone: config.timezone });
|
||
|
} catch (e) {
|
||
|
this.errors.push(`Invalid timezone: ${config.timezone}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// API endpoint
|
||
|
if (config.apiEndpoint) {
|
||
|
try {
|
||
|
new URL(config.apiEndpoint);
|
||
|
} catch (e) {
|
||
|
this.errors.push(`Invalid API endpoint URL: ${config.apiEndpoint}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Retry attempts
|
||
|
if (config.retryAttempts !== undefined) {
|
||
|
if (!Number.isInteger(config.retryAttempts) || config.retryAttempts < 0) {
|
||
|
this.errors.push('Retry attempts must be a non-negative integer');
|
||
|
}
|
||
|
if (config.retryAttempts > 10) {
|
||
|
this.errors.push('Retry attempts exceeds reasonable limit (10)');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Cache TTL
|
||
|
if (config.cacheTTL !== undefined) {
|
||
|
if (config.cacheTTL < 0) {
|
||
|
this.errors.push('Cache TTL must be non-negative');
|
||
|
}
|
||
|
if (config.cacheTTL > 86400000) { // 24 hours
|
||
|
this.errors.push('Cache TTL exceeds maximum (24 hours)');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
valid: this.errors.length === 0,
|
||
|
errors: this.errors
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const validator = new ConfigValidator();
|
||
|
|
||
|
const testConfigs: Array<{ name: string; config: IEInvoiceConfig; shouldBeValid: boolean }> = [
|
||
|
{
|
||
|
name: 'Valid configuration',
|
||
|
config: {
|
||
|
validationLevel: 'strict',
|
||
|
maxFileSize: 10 * 1024 * 1024,
|
||
|
timeout: 30000,
|
||
|
supportedFormats: ['UBL', 'CII'],
|
||
|
locale: 'en-US',
|
||
|
timezone: 'Europe/Berlin',
|
||
|
apiEndpoint: 'https://api.example.com/validate',
|
||
|
retryAttempts: 3,
|
||
|
cacheTTL: 3600000
|
||
|
},
|
||
|
shouldBeValid: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid validation level',
|
||
|
config: { validationLevel: 'extreme' as any },
|
||
|
shouldBeValid: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Negative max file size',
|
||
|
config: { maxFileSize: -1 },
|
||
|
shouldBeValid: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Excessive timeout',
|
||
|
config: { timeout: 600000 },
|
||
|
shouldBeValid: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Unknown format',
|
||
|
config: { supportedFormats: ['UBL', 'UNKNOWN'] },
|
||
|
shouldBeValid: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid locale',
|
||
|
config: { locale: 'english' },
|
||
|
shouldBeValid: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid timezone',
|
||
|
config: { timezone: 'Mars/Olympus_Mons' },
|
||
|
shouldBeValid: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Malformed API endpoint',
|
||
|
config: { apiEndpoint: 'not-a-url' },
|
||
|
shouldBeValid: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Excessive retry attempts',
|
||
|
config: { retryAttempts: 100 },
|
||
|
shouldBeValid: false
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of testConfigs) {
|
||
|
const startTime = performance.now();
|
||
|
const result = validator.validate(test.config);
|
||
|
|
||
|
if (test.shouldBeValid) {
|
||
|
expect(result.valid).toBeTrue();
|
||
|
console.log(`✓ ${test.name}: Configuration is valid`);
|
||
|
} else {
|
||
|
expect(result.valid).toBeFalse();
|
||
|
console.log(`✓ ${test.name}: Invalid - ${result.errors.join('; ')}`);
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('config-validation', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('config-validation');
|
||
|
});
|
||
|
|
||
|
await t.test('Missing required configuration', async () => {
|
||
|
performanceTracker.startOperation('missing-config');
|
||
|
|
||
|
class EInvoiceService {
|
||
|
private config: any;
|
||
|
|
||
|
constructor(config?: any) {
|
||
|
this.config = config || {};
|
||
|
}
|
||
|
|
||
|
async initialize(): Promise<void> {
|
||
|
const required = ['apiKey', 'region', 'validationSchema'];
|
||
|
const missing = required.filter(key => !this.config[key]);
|
||
|
|
||
|
if (missing.length > 0) {
|
||
|
throw new Error(`Missing required configuration: ${missing.join(', ')}`);
|
||
|
}
|
||
|
|
||
|
// Additional initialization checks
|
||
|
if (this.config.region && !['EU', 'US', 'APAC'].includes(this.config.region)) {
|
||
|
throw new Error(`Unsupported region: ${this.config.region}`);
|
||
|
}
|
||
|
|
||
|
if (this.config.validationSchema && !this.config.validationSchema.startsWith('http')) {
|
||
|
throw new Error('Validation schema must be a valid URL');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const testCases = [
|
||
|
{
|
||
|
name: 'Complete configuration',
|
||
|
config: {
|
||
|
apiKey: 'test-key-123',
|
||
|
region: 'EU',
|
||
|
validationSchema: 'https://schema.example.com/v1'
|
||
|
},
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Missing API key',
|
||
|
config: {
|
||
|
region: 'EU',
|
||
|
validationSchema: 'https://schema.example.com/v1'
|
||
|
},
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Missing multiple required fields',
|
||
|
config: {
|
||
|
apiKey: 'test-key-123'
|
||
|
},
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid region',
|
||
|
config: {
|
||
|
apiKey: 'test-key-123',
|
||
|
region: 'MARS',
|
||
|
validationSchema: 'https://schema.example.com/v1'
|
||
|
},
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid schema URL',
|
||
|
config: {
|
||
|
apiKey: 'test-key-123',
|
||
|
region: 'EU',
|
||
|
validationSchema: 'not-a-url'
|
||
|
},
|
||
|
shouldSucceed: false
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of testCases) {
|
||
|
const startTime = performance.now();
|
||
|
const service = new EInvoiceService(test.config);
|
||
|
|
||
|
try {
|
||
|
await service.initialize();
|
||
|
|
||
|
if (test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Initialization successful`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Should have failed`);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
if (!test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: ${error.message}`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Unexpected failure - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('initialization', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('missing-config');
|
||
|
});
|
||
|
|
||
|
await t.test('Environment variable conflicts', async () => {
|
||
|
performanceTracker.startOperation('env-conflicts');
|
||
|
|
||
|
class EnvironmentConfig {
|
||
|
private env: { [key: string]: string | undefined };
|
||
|
|
||
|
constructor(env: { [key: string]: string | undefined } = {}) {
|
||
|
this.env = env;
|
||
|
}
|
||
|
|
||
|
load(): any {
|
||
|
const config: any = {};
|
||
|
const conflicts: string[] = [];
|
||
|
|
||
|
// Check for conflicting environment variables
|
||
|
if (this.env.EINVOICE_MODE && this.env.XINVOICE_MODE) {
|
||
|
conflicts.push('Both EINVOICE_MODE and XINVOICE_MODE are set');
|
||
|
}
|
||
|
|
||
|
if (this.env.EINVOICE_DEBUG === 'true' && this.env.NODE_ENV === 'production') {
|
||
|
conflicts.push('Debug mode enabled in production environment');
|
||
|
}
|
||
|
|
||
|
if (this.env.EINVOICE_PORT && this.env.PORT) {
|
||
|
if (this.env.EINVOICE_PORT !== this.env.PORT) {
|
||
|
conflicts.push(`Port conflict: EINVOICE_PORT=${this.env.EINVOICE_PORT}, PORT=${this.env.PORT}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this.env.EINVOICE_LOG_LEVEL) {
|
||
|
const validLevels = ['error', 'warn', 'info', 'debug', 'trace'];
|
||
|
if (!validLevels.includes(this.env.EINVOICE_LOG_LEVEL)) {
|
||
|
conflicts.push(`Invalid log level: ${this.env.EINVOICE_LOG_LEVEL}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (conflicts.length > 0) {
|
||
|
throw new Error(`Environment configuration conflicts:\n${conflicts.join('\n')}`);
|
||
|
}
|
||
|
|
||
|
// Load configuration
|
||
|
config.mode = this.env.EINVOICE_MODE || 'development';
|
||
|
config.debug = this.env.EINVOICE_DEBUG === 'true';
|
||
|
config.port = parseInt(this.env.EINVOICE_PORT || this.env.PORT || '3000');
|
||
|
config.logLevel = this.env.EINVOICE_LOG_LEVEL || 'info';
|
||
|
|
||
|
return config;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const envTests = [
|
||
|
{
|
||
|
name: 'Clean environment',
|
||
|
env: {
|
||
|
EINVOICE_MODE: 'production',
|
||
|
EINVOICE_PORT: '3000',
|
||
|
NODE_ENV: 'production'
|
||
|
},
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Legacy variable conflict',
|
||
|
env: {
|
||
|
EINVOICE_MODE: 'production',
|
||
|
XINVOICE_MODE: 'development'
|
||
|
},
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Debug in production',
|
||
|
env: {
|
||
|
EINVOICE_DEBUG: 'true',
|
||
|
NODE_ENV: 'production'
|
||
|
},
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Port conflict',
|
||
|
env: {
|
||
|
EINVOICE_PORT: '3000',
|
||
|
PORT: '8080'
|
||
|
},
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid log level',
|
||
|
env: {
|
||
|
EINVOICE_LOG_LEVEL: 'verbose'
|
||
|
},
|
||
|
shouldSucceed: false
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of envTests) {
|
||
|
const startTime = performance.now();
|
||
|
const envConfig = new EnvironmentConfig(test.env);
|
||
|
|
||
|
try {
|
||
|
const config = envConfig.load();
|
||
|
|
||
|
if (test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Configuration loaded successfully`);
|
||
|
console.log(` Config: ${JSON.stringify(config)}`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Should have detected conflicts`);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
if (!test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Conflict detected`);
|
||
|
console.log(` ${error.message.split('\n')[0]}`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Unexpected error - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('env-check', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('env-conflicts');
|
||
|
});
|
||
|
|
||
|
await t.test('Configuration file parsing errors', async () => {
|
||
|
performanceTracker.startOperation('config-parsing');
|
||
|
|
||
|
class ConfigParser {
|
||
|
parse(content: string, format: 'json' | 'yaml' | 'toml'): any {
|
||
|
switch (format) {
|
||
|
case 'json':
|
||
|
return this.parseJSON(content);
|
||
|
case 'yaml':
|
||
|
return this.parseYAML(content);
|
||
|
case 'toml':
|
||
|
return this.parseTOML(content);
|
||
|
default:
|
||
|
throw new Error(`Unsupported configuration format: ${format}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private parseJSON(content: string): any {
|
||
|
try {
|
||
|
return JSON.parse(content);
|
||
|
} catch (error) {
|
||
|
throw new Error(`Invalid JSON: ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private parseYAML(content: string): any {
|
||
|
// Simplified YAML parsing simulation
|
||
|
if (content.includes('\t')) {
|
||
|
throw new Error('YAML parse error: tabs not allowed for indentation');
|
||
|
}
|
||
|
|
||
|
if (content.includes(': -')) {
|
||
|
throw new Error('YAML parse error: invalid sequence syntax');
|
||
|
}
|
||
|
|
||
|
// Simulate successful parse for valid YAML
|
||
|
if (content.trim().startsWith('einvoice:')) {
|
||
|
return { einvoice: { parsed: true } };
|
||
|
}
|
||
|
|
||
|
throw new Error('YAML parse error: invalid structure');
|
||
|
}
|
||
|
|
||
|
private parseTOML(content: string): any {
|
||
|
// Simplified TOML parsing simulation
|
||
|
if (!content.includes('[') && !content.includes('=')) {
|
||
|
throw new Error('TOML parse error: no valid sections or key-value pairs');
|
||
|
}
|
||
|
|
||
|
if (content.includes('[[') && !content.includes(']]')) {
|
||
|
throw new Error('TOML parse error: unclosed array of tables');
|
||
|
}
|
||
|
|
||
|
return { toml: { parsed: true } };
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const parser = new ConfigParser();
|
||
|
|
||
|
const parseTests = [
|
||
|
{
|
||
|
name: 'Valid JSON',
|
||
|
content: '{"einvoice": {"version": "1.0", "formats": ["UBL", "CII"]}}',
|
||
|
format: 'json' as const,
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid JSON',
|
||
|
content: '{"einvoice": {"version": "1.0", "formats": ["UBL", "CII"]}',
|
||
|
format: 'json' as const,
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Valid YAML',
|
||
|
content: 'einvoice:\n version: "1.0"\n formats:\n - UBL\n - CII',
|
||
|
format: 'yaml' as const,
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'YAML with tabs',
|
||
|
content: 'einvoice:\n\tversion: "1.0"',
|
||
|
format: 'yaml' as const,
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Valid TOML',
|
||
|
content: '[einvoice]\nversion = "1.0"\nformats = ["UBL", "CII"]',
|
||
|
format: 'toml' as const,
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid TOML',
|
||
|
content: '[[einvoice.formats\nname = "UBL"',
|
||
|
format: 'toml' as const,
|
||
|
shouldSucceed: false
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of parseTests) {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
try {
|
||
|
const config = parser.parse(test.content, test.format);
|
||
|
|
||
|
if (test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Parsed successfully`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Should have failed to parse`);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
if (!test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: ${error.message}`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Unexpected parse error - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('config-parse', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('config-parsing');
|
||
|
});
|
||
|
|
||
|
await t.test('Configuration migration errors', async () => {
|
||
|
performanceTracker.startOperation('config-migration');
|
||
|
|
||
|
class ConfigMigrator {
|
||
|
private migrations = [
|
||
|
{
|
||
|
version: '1.0',
|
||
|
migrate: (config: any) => {
|
||
|
// Rename old fields
|
||
|
if (config.xmlValidation !== undefined) {
|
||
|
config.validationLevel = config.xmlValidation ? 'strict' : 'lenient';
|
||
|
delete config.xmlValidation;
|
||
|
}
|
||
|
return config;
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
version: '2.0',
|
||
|
migrate: (config: any) => {
|
||
|
// Convert format strings to array
|
||
|
if (typeof config.format === 'string') {
|
||
|
config.supportedFormats = [config.format];
|
||
|
delete config.format;
|
||
|
}
|
||
|
return config;
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
version: '3.0',
|
||
|
migrate: (config: any) => {
|
||
|
// Restructure API settings
|
||
|
if (config.apiKey || config.apiUrl) {
|
||
|
config.api = {
|
||
|
key: config.apiKey,
|
||
|
endpoint: config.apiUrl
|
||
|
};
|
||
|
delete config.apiKey;
|
||
|
delete config.apiUrl;
|
||
|
}
|
||
|
return config;
|
||
|
}
|
||
|
}
|
||
|
];
|
||
|
|
||
|
async migrate(config: any, targetVersion: string): Promise<any> {
|
||
|
let currentConfig = { ...config };
|
||
|
const currentVersion = config.version || '1.0';
|
||
|
|
||
|
if (currentVersion === targetVersion) {
|
||
|
return currentConfig;
|
||
|
}
|
||
|
|
||
|
const startIndex = this.migrations.findIndex(m => m.version === currentVersion);
|
||
|
const endIndex = this.migrations.findIndex(m => m.version === targetVersion);
|
||
|
|
||
|
if (startIndex === -1) {
|
||
|
throw new Error(`Unknown source version: ${currentVersion}`);
|
||
|
}
|
||
|
|
||
|
if (endIndex === -1) {
|
||
|
throw new Error(`Unknown target version: ${targetVersion}`);
|
||
|
}
|
||
|
|
||
|
if (startIndex > endIndex) {
|
||
|
throw new Error('Downgrade migrations not supported');
|
||
|
}
|
||
|
|
||
|
// Apply migrations in sequence
|
||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||
|
try {
|
||
|
currentConfig = this.migrations[i].migrate(currentConfig);
|
||
|
currentConfig.version = this.migrations[i].version;
|
||
|
} catch (error) {
|
||
|
throw new Error(`Migration to v${this.migrations[i].version} failed: ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return currentConfig;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const migrator = new ConfigMigrator();
|
||
|
|
||
|
const migrationTests = [
|
||
|
{
|
||
|
name: 'v1.0 to v3.0 migration',
|
||
|
config: {
|
||
|
version: '1.0',
|
||
|
xmlValidation: true,
|
||
|
format: 'UBL',
|
||
|
apiKey: 'key123',
|
||
|
apiUrl: 'https://api.example.com'
|
||
|
},
|
||
|
targetVersion: '3.0',
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Already at target version',
|
||
|
config: {
|
||
|
version: '3.0',
|
||
|
validationLevel: 'strict'
|
||
|
},
|
||
|
targetVersion: '3.0',
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Unknown source version',
|
||
|
config: {
|
||
|
version: '0.9',
|
||
|
oldField: true
|
||
|
},
|
||
|
targetVersion: '3.0',
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Downgrade attempt',
|
||
|
config: {
|
||
|
version: '3.0',
|
||
|
api: { key: 'test' }
|
||
|
},
|
||
|
targetVersion: '1.0',
|
||
|
shouldSucceed: false
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of migrationTests) {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
try {
|
||
|
const migrated = await migrator.migrate(test.config, test.targetVersion);
|
||
|
|
||
|
if (test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Migration successful`);
|
||
|
console.log(` Result: ${JSON.stringify(migrated)}`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Should have failed`);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
if (!test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: ${error.message}`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Unexpected failure - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('config-migration', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('config-migration');
|
||
|
});
|
||
|
|
||
|
await t.test('Circular configuration dependencies', async () => {
|
||
|
performanceTracker.startOperation('circular-deps');
|
||
|
|
||
|
class ConfigResolver {
|
||
|
private resolved = new Map<string, any>();
|
||
|
private resolving = new Set<string>();
|
||
|
|
||
|
resolve(config: any, key: string): any {
|
||
|
if (this.resolved.has(key)) {
|
||
|
return this.resolved.get(key);
|
||
|
}
|
||
|
|
||
|
if (this.resolving.has(key)) {
|
||
|
throw new Error(`Circular dependency detected: ${Array.from(this.resolving).join(' -> ')} -> ${key}`);
|
||
|
}
|
||
|
|
||
|
this.resolving.add(key);
|
||
|
|
||
|
try {
|
||
|
const value = config[key];
|
||
|
|
||
|
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
|
||
|
// Reference to another config value
|
||
|
const refKey = value.slice(2, -1);
|
||
|
const resolvedValue = this.resolve(config, refKey);
|
||
|
this.resolved.set(key, resolvedValue);
|
||
|
return resolvedValue;
|
||
|
}
|
||
|
|
||
|
this.resolved.set(key, value);
|
||
|
return value;
|
||
|
} finally {
|
||
|
this.resolving.delete(key);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const circularTests = [
|
||
|
{
|
||
|
name: 'No circular dependency',
|
||
|
config: {
|
||
|
baseUrl: 'https://api.example.com',
|
||
|
apiEndpoint: '${baseUrl}/v1',
|
||
|
validationEndpoint: '${apiEndpoint}/validate'
|
||
|
},
|
||
|
resolveKey: 'validationEndpoint',
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Direct circular dependency',
|
||
|
config: {
|
||
|
a: '${b}',
|
||
|
b: '${a}'
|
||
|
},
|
||
|
resolveKey: 'a',
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Indirect circular dependency',
|
||
|
config: {
|
||
|
a: '${b}',
|
||
|
b: '${c}',
|
||
|
c: '${a}'
|
||
|
},
|
||
|
resolveKey: 'a',
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Self-reference',
|
||
|
config: {
|
||
|
recursive: '${recursive}'
|
||
|
},
|
||
|
resolveKey: 'recursive',
|
||
|
shouldSucceed: false
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of circularTests) {
|
||
|
const startTime = performance.now();
|
||
|
const resolver = new ConfigResolver();
|
||
|
|
||
|
try {
|
||
|
const resolved = resolver.resolve(test.config, test.resolveKey);
|
||
|
|
||
|
if (test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Resolved to "${resolved}"`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Should have detected circular dependency`);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
if (!test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: ${error.message}`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Unexpected error - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('circular-check', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('circular-deps');
|
||
|
});
|
||
|
|
||
|
// Performance summary
|
||
|
console.log('\n' + performanceTracker.getSummary());
|
||
|
|
||
|
// Configuration error handling best practices
|
||
|
console.log('\nConfiguration Error Handling Best Practices:');
|
||
|
console.log('1. Validate all configuration values on startup');
|
||
|
console.log('2. Provide clear error messages for invalid configurations');
|
||
|
console.log('3. Support configuration migration between versions');
|
||
|
console.log('4. Detect and prevent circular dependencies');
|
||
|
console.log('5. Use schema validation for configuration files');
|
||
|
console.log('6. Implement sensible defaults for optional settings');
|
||
|
console.log('7. Check for environment variable conflicts');
|
||
|
console.log('8. Log configuration loading process for debugging');
|
||
|
});
|
||
|
|
||
|
tap.start();
|