import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../../../ts/plugins.ts'; import { EInvoice } from '../../../ts/classes.xinvoice.ts'; import { CorpusLoader } from '../../helpers/corpus.loader.ts'; import { PerformanceTracker } from '../../helpers/performance.tracker.ts'; const testTimeout = 300000; // 5 minutes timeout for error handling tests // ERR-02: Validation Error Details // Tests detailed validation error reporting including error messages, // error locations, error codes, and actionable error information tap.test('ERR-02: Validation Error Details - Business Rule Violations', async (tools) => { const startTime = Date.now(); // Test validation errors for various business rule violations const businessRuleViolations = [ { name: 'BR-01: Missing invoice number', xml: ` 2024-01-15 380 EUR 100.00 `, expectedErrors: ['BR-01', 'invoice number', 'ID', 'required'], errorCount: 1 }, { name: 'BR-CO-10: Sum of line amounts validation', xml: ` BR-TEST-001 2024-01-15 380 EUR 1 2 100.00 50.00 2 3 150.00 50.00 200.00 200.00 `, expectedErrors: ['BR-CO-10', 'sum', 'line', 'amount', 'calculation'], errorCount: 1 }, { name: 'Multiple validation errors', xml: ` MULTI-ERROR-001 999 INVALID -50.00 100.00 `, expectedErrors: ['issue date', 'invoice type', 'currency', 'negative', 'tax'], errorCount: 5 } ]; for (const testCase of businessRuleViolations) { tools.log(`Testing ${testCase.name}...`); try { const invoice = new EInvoice(); const parseResult = await invoice.fromXmlString(testCase.xml); if (parseResult) { const validationResult = await invoice.validate(); if (validationResult.valid) { tools.log(` ⚠ Expected validation errors but validation passed`); } else { tools.log(` ✓ Validation failed as expected`); // Analyze validation errors const errors = validationResult.errors || []; tools.log(` Found ${errors.length} validation errors:`); for (const error of errors) { tools.log(`\n Error ${errors.indexOf(error) + 1}:`); // Check error structure expect(error).toHaveProperty('message'); expect(error.message).toBeTruthy(); expect(error.message.length).toBeGreaterThan(10); tools.log(` Message: ${error.message}`); // Check optional error properties if (error.code) { tools.log(` Code: ${error.code}`); expect(error.code).toBeTruthy(); } if (error.path) { tools.log(` Path: ${error.path}`); expect(error.path).toBeTruthy(); } if (error.severity) { tools.log(` Severity: ${error.severity}`); expect(['error', 'warning', 'info']).toContain(error.severity); } if (error.rule) { tools.log(` Rule: ${error.rule}`); } if (error.element) { tools.log(` Element: ${error.element}`); } if (error.value) { tools.log(` Value: ${error.value}`); } if (error.expected) { tools.log(` Expected: ${error.expected}`); } if (error.actual) { tools.log(` Actual: ${error.actual}`); } if (error.suggestion) { tools.log(` Suggestion: ${error.suggestion}`); } // Check if error contains expected keywords const errorLower = error.message.toLowerCase(); let keywordMatches = 0; for (const keyword of testCase.expectedErrors) { if (errorLower.includes(keyword.toLowerCase())) { keywordMatches++; } } if (keywordMatches > 0) { tools.log(` ✓ Error contains expected keywords (${keywordMatches}/${testCase.expectedErrors.length})`); } else { tools.log(` ⚠ Error doesn't contain expected keywords`); } } // Check error count if (testCase.errorCount > 0) { if (errors.length >= testCase.errorCount) { tools.log(`\n ✓ Expected at least ${testCase.errorCount} errors, found ${errors.length}`); } else { tools.log(`\n ⚠ Expected at least ${testCase.errorCount} errors, but found only ${errors.length}`); } } } } else { tools.log(` ✗ Parsing failed unexpectedly`); } } catch (error) { tools.log(` ✗ Unexpected error during validation: ${error.message}`); throw error; } } const duration = Date.now() - startTime; PerformanceTracker.recordMetric('validation-error-details-business-rules', duration); }); tap.test('ERR-02: Validation Error Details - Schema Validation Errors', async (tools) => { const startTime = Date.now(); // Test schema validation error details const schemaViolations = [ { name: 'Invalid element order', xml: ` 380 SCHEMA-001 2024-01-15 EUR `, expectedErrors: ['order', 'sequence', 'element'], description: 'Elements in wrong order' }, { name: 'Unknown element', xml: ` SCHEMA-002 2024-01-15 This should not be here 380 `, expectedErrors: ['unknown', 'element', 'unexpected'], description: 'Contains unknown element' }, { name: 'Invalid attribute', xml: ` SCHEMA-003 2024-01-15 380 `, expectedErrors: ['attribute', 'invalid', 'unexpected'], description: 'Invalid attribute on root element' }, { name: 'Missing required child element', xml: ` SCHEMA-004 2024-01-15 380 19.00 `, expectedErrors: ['required', 'missing', 'TaxSubtotal'], description: 'Missing required child element' } ]; for (const testCase of schemaViolations) { tools.log(`Testing ${testCase.name}: ${testCase.description}`); try { const invoice = new EInvoice(); const parseResult = await invoice.fromXmlString(testCase.xml); if (parseResult) { const validationResult = await invoice.validate(); if (validationResult.valid) { tools.log(` ⚠ Expected schema validation errors but validation passed`); } else { tools.log(` ✓ Schema validation failed as expected`); const errors = validationResult.errors || []; tools.log(` Found ${errors.length} validation errors`); // Analyze schema-specific error details let schemaErrorFound = false; for (const error of errors) { const errorLower = error.message.toLowerCase(); // Check if this is a schema-related error const isSchemaError = errorLower.includes('schema') || errorLower.includes('element') || errorLower.includes('attribute') || errorLower.includes('structure') || errorLower.includes('xml'); if (isSchemaError) { schemaErrorFound = true; tools.log(` Schema error: ${error.message}`); // Check for XPath or location information if (error.path) { tools.log(` Location: ${error.path}`); expect(error.path).toMatch(/^\/|^\w+/); // Should look like a path } // Check for line/column information if (error.line) { tools.log(` Line: ${error.line}`); expect(error.line).toBeGreaterThan(0); } if (error.column) { tools.log(` Column: ${error.column}`); expect(error.column).toBeGreaterThan(0); } // Check if error mentions expected keywords let keywordMatch = false; for (const keyword of testCase.expectedErrors) { if (errorLower.includes(keyword.toLowerCase())) { keywordMatch = true; break; } } if (keywordMatch) { tools.log(` ✓ Error contains expected keywords`); } } } if (!schemaErrorFound) { tools.log(` ⚠ No schema-specific errors found`); } } } else { tools.log(` Schema validation may have failed at parse time`); } } catch (error) { tools.log(` Parse/validation error: ${error.message}`); // Check if the error message is helpful const errorLower = error.message.toLowerCase(); if (errorLower.includes('schema') || errorLower.includes('invalid')) { tools.log(` ✓ Error message indicates schema issue`); } } } const duration = Date.now() - startTime; PerformanceTracker.recordMetric('validation-error-details-schema', duration); }); tap.test('ERR-02: Validation Error Details - Field-Specific Errors', async (tools) => { const startTime = Date.now(); // Test field-specific validation error details const fieldErrors = [ { name: 'Invalid date format', xml: ` FIELD-001 15-01-2024 380 2024/02/15 EUR `, expectedFields: ['IssueDate', 'DueDate'], expectedErrors: ['date', 'format', 'ISO', 'YYYY-MM-DD'] }, { name: 'Invalid currency codes', xml: ` FIELD-002 2024-01-15 380 EURO 100.00 `, expectedFields: ['DocumentCurrencyCode', 'currencyID'], expectedErrors: ['currency', 'ISO 4217', 'invalid', 'code'] }, { name: 'Invalid numeric values', xml: ` FIELD-003 2024-01-15 380 EUR 1 ABC not-a-number 19.999999999 `, expectedFields: ['InvoicedQuantity', 'LineExtensionAmount', 'TaxAmount'], expectedErrors: ['numeric', 'number', 'decimal', 'invalid'] }, { name: 'Invalid code values', xml: ` FIELD-004 2024-01-15 999 EUR 99 1 1 `, expectedFields: ['InvoiceTypeCode', 'PaymentMeansCode', 'unitCode'], expectedErrors: ['code', 'list', 'valid', 'allowed'] } ]; for (const testCase of fieldErrors) { tools.log(`Testing ${testCase.name}...`); try { const invoice = new EInvoice(); const parseResult = await invoice.fromXmlString(testCase.xml); if (parseResult) { const validationResult = await invoice.validate(); if (validationResult.valid) { tools.log(` ⚠ Expected field validation errors but validation passed`); } else { tools.log(` ✓ Field validation failed as expected`); const errors = validationResult.errors || []; tools.log(` Found ${errors.length} validation errors`); // Track which expected fields have errors const fieldsWithErrors = new Set(); for (const error of errors) { tools.log(`\n Field error: ${error.message}`); // Check if error identifies the field if (error.path || error.element || error.field) { const fieldIdentifier = error.path || error.element || error.field; tools.log(` Field: ${fieldIdentifier}`); // Check if this is one of our expected fields for (const expectedField of testCase.expectedFields) { if (fieldIdentifier.includes(expectedField)) { fieldsWithErrors.add(expectedField); } } } // Check if error provides value information if (error.value) { tools.log(` Invalid value: ${error.value}`); } // Check if error provides expected format/values if (error.expected) { tools.log(` Expected: ${error.expected}`); } // Check if error suggests correction if (error.suggestion) { tools.log(` Suggestion: ${error.suggestion}`); expect(error.suggestion).toBeTruthy(); } // Check for specific error keywords const errorLower = error.message.toLowerCase(); let hasExpectedKeyword = false; for (const keyword of testCase.expectedErrors) { if (errorLower.includes(keyword.toLowerCase())) { hasExpectedKeyword = true; break; } } if (hasExpectedKeyword) { tools.log(` ✓ Error contains expected keywords`); } } // Check if all expected fields had errors tools.log(`\n Fields with errors: ${Array.from(fieldsWithErrors).join(', ')}`); if (fieldsWithErrors.size > 0) { tools.log(` ✓ Errors reported for ${fieldsWithErrors.size}/${testCase.expectedFields.length} expected fields`); } else { tools.log(` ⚠ No field-specific errors identified`); } } } else { tools.log(` Parsing failed - field validation may have failed at parse time`); } } catch (error) { tools.log(` Error during validation: ${error.message}`); } } const duration = Date.now() - startTime; PerformanceTracker.recordMetric('validation-error-details-fields', duration); }); tap.test('ERR-02: Validation Error Details - Error Grouping and Summarization', async (tools) => { const startTime = Date.now(); // Test error grouping and summarization for complex validation scenarios const complexValidationXml = ` COMPLEX-001 invalid-date 999 XXX XX INVALID-VAT 1 -5 -100.00 999 -20.00 2 10 invalid invalid-amount NaN -50.00 0.00 `; try { const invoice = new EInvoice(); const parseResult = await invoice.fromXmlString(complexValidationXml); if (parseResult) { const validationResult = await invoice.validate(); if (!validationResult.valid && validationResult.errors) { const errors = validationResult.errors; tools.log(`Total validation errors: ${errors.length}`); // Group errors by category const errorGroups: { [key: string]: any[] } = { 'Date/Time Errors': [], 'Currency Errors': [], 'Code List Errors': [], 'Numeric Value Errors': [], 'Required Field Errors': [], 'Business Rule Errors': [], 'Other Errors': [] }; // Categorize each error for (const error of errors) { const errorLower = error.message.toLowerCase(); if (errorLower.includes('date') || errorLower.includes('time')) { errorGroups['Date/Time Errors'].push(error); } else if (errorLower.includes('currency') || errorLower.includes('currencyid')) { errorGroups['Currency Errors'].push(error); } else if (errorLower.includes('code') || errorLower.includes('type') || errorLower.includes('list')) { errorGroups['Code List Errors'].push(error); } else if (errorLower.includes('numeric') || errorLower.includes('number') || errorLower.includes('negative') || errorLower.includes('amount')) { errorGroups['Numeric Value Errors'].push(error); } else if (errorLower.includes('required') || errorLower.includes('missing') || errorLower.includes('must')) { errorGroups['Required Field Errors'].push(error); } else if (errorLower.includes('br-') || errorLower.includes('rule')) { errorGroups['Business Rule Errors'].push(error); } else { errorGroups['Other Errors'].push(error); } } // Display grouped errors tools.log(`\nError Summary by Category:`); for (const [category, categoryErrors] of Object.entries(errorGroups)) { if (categoryErrors.length > 0) { tools.log(`\n${category}: ${categoryErrors.length} errors`); // Show first few errors in each category const samplesToShow = Math.min(3, categoryErrors.length); for (let i = 0; i < samplesToShow; i++) { const error = categoryErrors[i]; tools.log(` - ${error.message}`); if (error.path) { tools.log(` at: ${error.path}`); } } if (categoryErrors.length > samplesToShow) { tools.log(` ... and ${categoryErrors.length - samplesToShow} more`); } } } // Error statistics tools.log(`\nError Statistics:`); // Count errors by severity if available const severityCounts: { [key: string]: number } = {}; for (const error of errors) { const severity = error.severity || 'error'; severityCounts[severity] = (severityCounts[severity] || 0) + 1; } for (const [severity, count] of Object.entries(severityCounts)) { tools.log(` ${severity}: ${count}`); } // Identify most common error patterns const errorPatterns: { [key: string]: number } = {}; for (const error of errors) { // Extract error pattern (first few words) const pattern = error.message.split(' ').slice(0, 3).join(' ').toLowerCase(); errorPatterns[pattern] = (errorPatterns[pattern] || 0) + 1; } const commonPatterns = Object.entries(errorPatterns) .sort(([,a], [,b]) => b - a) .slice(0, 5); if (commonPatterns.length > 0) { tools.log(`\nMost Common Error Patterns:`); for (const [pattern, count] of commonPatterns) { tools.log(` "${pattern}...": ${count} occurrences`); } } // Check if errors provide actionable information let actionableErrors = 0; for (const error of errors) { if (error.suggestion || error.expected || error.message.includes('should') || error.message.includes('must')) { actionableErrors++; } } const actionablePercentage = (actionableErrors / errors.length) * 100; tools.log(`\nActionable errors: ${actionableErrors}/${errors.length} (${actionablePercentage.toFixed(1)}%)`); if (actionablePercentage >= 50) { tools.log(`✓ Good error actionability`); } else { tools.log(`⚠ Low error actionability - errors may not be helpful enough`); } } else { tools.log(`⚠ Expected validation errors but none found or validation passed`); } } else { tools.log(`Parsing failed - unable to test validation error details`); } } catch (error) { tools.log(`Error during complex validation test: ${error.message}`); } const duration = Date.now() - startTime; PerformanceTracker.recordMetric('validation-error-details-grouping', duration); }); tap.test('ERR-02: Validation Error Details - Corpus Error Analysis', { timeout: testTimeout }, async (tools) => { const startTime = Date.now(); const errorStatistics = { totalFiles: 0, filesWithErrors: 0, totalErrors: 0, errorTypes: {} as { [key: string]: number }, errorsBySeverity: {} as { [key: string]: number }, averageErrorsPerFile: 0, maxErrorsInFile: 0, fileWithMostErrors: '' }; try { // Analyze validation errors across corpus files const files = await CorpusLoader.getFiles('UBL_XML_RECHNUNG'); const filesToProcess = files.slice(0, 10); // Process first 10 files for (const filePath of filesToProcess) { errorStatistics.totalFiles++; const fileName = plugins.path.basename(filePath); try { const invoice = new EInvoice(); const parseResult = await invoice.fromFile(filePath); if (parseResult) { const validationResult = await invoice.validate(); if (!validationResult.valid && validationResult.errors) { errorStatistics.filesWithErrors++; const fileErrorCount = validationResult.errors.length; errorStatistics.totalErrors += fileErrorCount; if (fileErrorCount > errorStatistics.maxErrorsInFile) { errorStatistics.maxErrorsInFile = fileErrorCount; errorStatistics.fileWithMostErrors = fileName; } // Analyze error types for (const error of validationResult.errors) { // Categorize error type const errorType = categorizeError(error); errorStatistics.errorTypes[errorType] = (errorStatistics.errorTypes[errorType] || 0) + 1; // Count by severity const severity = error.severity || 'error'; errorStatistics.errorsBySeverity[severity] = (errorStatistics.errorsBySeverity[severity] || 0) + 1; // Check error quality const hasGoodMessage = error.message && error.message.length > 20; const hasLocation = !!(error.path || error.element || error.line); const hasContext = !!(error.value || error.expected || error.code); if (!hasGoodMessage || !hasLocation || !hasContext) { tools.log(` ⚠ Low quality error in ${fileName}:`); tools.log(` Message quality: ${hasGoodMessage}`); tools.log(` Has location: ${hasLocation}`); tools.log(` Has context: ${hasContext}`); } } } } } catch (error) { tools.log(`Error processing ${fileName}: ${error.message}`); } } // Calculate statistics errorStatistics.averageErrorsPerFile = errorStatistics.filesWithErrors > 0 ? errorStatistics.totalErrors / errorStatistics.filesWithErrors : 0; // Display analysis results tools.log(`\n=== Corpus Validation Error Analysis ===`); tools.log(`Files analyzed: ${errorStatistics.totalFiles}`); tools.log(`Files with errors: ${errorStatistics.filesWithErrors} (${(errorStatistics.filesWithErrors / errorStatistics.totalFiles * 100).toFixed(1)}%)`); tools.log(`Total errors found: ${errorStatistics.totalErrors}`); tools.log(`Average errors per file with errors: ${errorStatistics.averageErrorsPerFile.toFixed(1)}`); tools.log(`Maximum errors in single file: ${errorStatistics.maxErrorsInFile} (${errorStatistics.fileWithMostErrors})`); if (Object.keys(errorStatistics.errorTypes).length > 0) { tools.log(`\nError Types Distribution:`); const sortedTypes = Object.entries(errorStatistics.errorTypes) .sort(([,a], [,b]) => b - a); for (const [type, count] of sortedTypes) { const percentage = (count / errorStatistics.totalErrors * 100).toFixed(1); tools.log(` ${type}: ${count} (${percentage}%)`); } } if (Object.keys(errorStatistics.errorsBySeverity).length > 0) { tools.log(`\nErrors by Severity:`); for (const [severity, count] of Object.entries(errorStatistics.errorsBySeverity)) { tools.log(` ${severity}: ${count}`); } } } catch (error) { tools.log(`Corpus error analysis failed: ${error.message}`); throw error; } const totalDuration = Date.now() - startTime; PerformanceTracker.recordMetric('validation-error-details-corpus', totalDuration); tools.log(`\nCorpus error analysis completed in ${totalDuration}ms`); }); // Helper function to categorize errors function categorizeError(error: any): string { const message = error.message?.toLowerCase() || ''; const code = error.code?.toLowerCase() || ''; if (message.includes('required') || message.includes('missing')) return 'Required Field'; if (message.includes('date') || message.includes('time')) return 'Date/Time'; if (message.includes('currency')) return 'Currency'; if (message.includes('amount') || message.includes('number') || message.includes('numeric')) return 'Numeric'; if (message.includes('code') || message.includes('type')) return 'Code List'; if (message.includes('tax') || message.includes('vat')) return 'Tax Related'; if (message.includes('format') || message.includes('pattern')) return 'Format'; if (code.includes('br-')) return 'Business Rule'; if (message.includes('schema') || message.includes('xml')) return 'Schema'; return 'Other'; } tap.test('ERR-02: Performance Summary', async (tools) => { const operations = [ 'validation-error-details-business-rules', 'validation-error-details-schema', 'validation-error-details-fields', 'validation-error-details-grouping', 'validation-error-details-corpus' ]; tools.log(`\n=== Validation Error Details Performance Summary ===`); for (const operation of operations) { const summary = await PerformanceTracker.getSummary(operation); if (summary) { tools.log(`${operation}:`); tools.log(` avg=${summary.average}ms, min=${summary.min}ms, max=${summary.max}ms, p95=${summary.p95}ms`); } } tools.log(`\nValidation error details testing completed.`); tools.log(`Good error reporting should include: message, location, severity, suggestions, and context.`); });