einvoice/test/suite/einvoice_error-handling/test.err-10.configuration-errors.ts

805 lines
24 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
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();