einvoice/test/suite/einvoice_validation/test.val-13.error-reporting.ts
2025-05-25 19:45:37 +00:00

598 lines
21 KiB
TypeScript

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 corpus processing
// VAL-13: Validation Error Reporting
// Tests validation error reporting functionality including error messages,
// error codes, error context, and error aggregation
tap.test('VAL-13: Error Reporting - Error Message Quality', async (tools) => {
const startTime = Date.now();
// Test validation errors with clear, actionable messages
const errorTestCases = [
{
name: 'Missing Required Field',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
</Invoice>`,
expectedErrorType: 'missing-required-field',
expectedFieldName: 'ID'
},
{
name: 'Invalid Date Format',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>31-01-2024</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
</Invoice>`,
expectedErrorType: 'invalid-date-format',
expectedFieldName: 'IssueDate'
},
{
name: 'Invalid Currency Code',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>INVALID</DocumentCurrencyCode>
</Invoice>`,
expectedErrorType: 'invalid-currency-code',
expectedFieldName: 'DocumentCurrencyCode'
},
{
name: 'Invalid Numeric Value',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">NOT_A_NUMBER</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`,
expectedErrorType: 'invalid-numeric-value',
expectedFieldName: 'PayableAmount'
}
];
for (const testCase of errorTestCases) {
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(testCase.xml);
let validationResult;
if (parseResult) {
validationResult = await invoice.validate();
}
// Expect validation to fail
if (validationResult && validationResult.valid) {
tools.log(`⚠ Expected validation to fail for ${testCase.name} but it passed`);
} else {
tools.log(`${testCase.name}: Validation correctly failed`);
// Check error quality if errors are available
if (validationResult?.errors && validationResult.errors.length > 0) {
const errors = validationResult.errors;
// Check for descriptive error messages
for (const error of errors) {
expect(error.message).toBeTruthy();
expect(error.message.length).toBeGreaterThan(10); // Should be descriptive
tools.log(` Error: ${error.message}`);
// Check if error message contains relevant context
if (testCase.expectedFieldName) {
const containsFieldName = error.message.toLowerCase().includes(testCase.expectedFieldName.toLowerCase()) ||
error.path?.includes(testCase.expectedFieldName);
if (containsFieldName) {
tools.log(` ✓ Error message includes field name: ${testCase.expectedFieldName}`);
}
}
}
}
}
} catch (parseError) {
// Parse errors are also valid for testing error reporting
tools.log(`${testCase.name}: Parse error caught: ${parseError.message}`);
expect(parseError.message).toBeTruthy();
expect(parseError.message.length).toBeGreaterThan(5);
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('error-reporting-message-quality', duration);
});
tap.test('VAL-13: Error Reporting - Error Code Classification', async (tools) => {
const startTime = Date.now();
// Test error classification and categorization
const errorClassificationTests = [
{
name: 'Syntax Error',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<UnclosedTag>
</Invoice>`,
expectedCategory: 'syntax',
expectedSeverity: 'error'
},
{
name: 'Business Rule Violation',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<TaxTotal>
<TaxAmount currencyID="EUR">20.00</TaxAmount>
<TaxSubtotal>
<TaxableAmount currencyID="EUR">100.00</TaxableAmount>
<TaxAmount currencyID="EUR">19.00</TaxAmount>
<TaxCategory><Percent>19.00</Percent></TaxCategory>
</TaxSubtotal>
</TaxTotal>
</Invoice>`,
expectedCategory: 'business-rule',
expectedSeverity: 'error'
},
{
name: 'Format Warning',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<Note>This is a very long note that exceeds recommended character limits for invoice notes and should trigger a warning about readability and processing efficiency in some systems</Note>
</Invoice>`,
expectedCategory: 'format',
expectedSeverity: 'warning'
}
];
for (const test of errorClassificationTests) {
try {
const invoice = new EInvoice();
let parseResult;
try {
parseResult = await invoice.fromXmlString(test.xml);
} catch (parseError) {
// Handle syntax errors at parse level
if (test.expectedCategory === 'syntax') {
tools.log(`${test.name}: Syntax error correctly detected at parse time`);
expect(parseError.message).toBeTruthy();
continue;
} else {
throw parseError;
}
}
if (parseResult) {
const validationResult = await invoice.validate();
if (validationResult && !validationResult.valid && validationResult.errors) {
tools.log(`${test.name}: Validation errors detected`);
for (const error of validationResult.errors) {
tools.log(` Error: ${error.message}`);
// Check error classification properties
if (error.code) {
tools.log(` Code: ${error.code}`);
}
if (error.severity) {
tools.log(` Severity: ${error.severity}`);
expect(['error', 'warning', 'info']).toContain(error.severity);
}
if (error.category) {
tools.log(` Category: ${error.category}`);
}
if (error.path) {
tools.log(` Path: ${error.path}`);
}
}
} else if (test.expectedCategory !== 'format') {
tools.log(`⚠ Expected validation errors for ${test.name} but validation passed`);
}
}
} catch (error) {
tools.log(`Error processing ${test.name}: ${error.message}`);
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('error-reporting-classification', duration);
});
tap.test('VAL-13: Error Reporting - Error Context and Location', async (tools) => {
const startTime = Date.now();
// Test error context information (line numbers, XPath, etc.)
const contextTestXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>CONTEXT-TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<AccountingSupplierParty>
<Party>
<PartyName>
<Name></Name>
</PartyName>
<PostalAddress>
<StreetName>Test Street</StreetName>
<CityName></CityName>
<PostalZone>12345</PostalZone>
<Country>
<IdentificationCode>DE</IdentificationCode>
</Country>
</PostalAddress>
</Party>
</AccountingSupplierParty>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">INVALID_AMOUNT</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(contextTestXml);
if (parseResult) {
const validationResult = await invoice.validate();
if (validationResult && !validationResult.valid && validationResult.errors) {
tools.log(`Error context testing - found ${validationResult.errors.length} errors:`);
for (const error of validationResult.errors) {
tools.log(`\nError: ${error.message}`);
// Check for location information
if (error.path) {
tools.log(` XPath/Path: ${error.path}`);
expect(error.path).toBeTruthy();
}
if (error.lineNumber) {
tools.log(` Line: ${error.lineNumber}`);
expect(error.lineNumber).toBeGreaterThan(0);
}
if (error.columnNumber) {
tools.log(` Column: ${error.columnNumber}`);
expect(error.columnNumber).toBeGreaterThan(0);
}
// Check for additional context
if (error.context) {
tools.log(` Context: ${JSON.stringify(error.context)}`);
}
if (error.element) {
tools.log(` Element: ${error.element}`);
}
}
tools.log(`✓ Error context information available`);
} else {
tools.log(`⚠ Expected validation errors but validation passed`);
}
}
} catch (error) {
tools.log(`Context test failed: ${error.message}`);
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('error-reporting-context', duration);
});
tap.test('VAL-13: Error Reporting - Error Aggregation and Summarization', async (tools) => {
const startTime = Date.now();
// Test error aggregation for multiple issues
const multiErrorXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID></ID>
<IssueDate>invalid-date</IssueDate>
<InvoiceTypeCode>999</InvoiceTypeCode>
<DocumentCurrencyCode>INVALID</DocumentCurrencyCode>
<AccountingSupplierParty>
<Party>
<PartyName>
<Name></Name>
</PartyName>
</Party>
</AccountingSupplierParty>
<AccountingCustomerParty>
<Party>
<PartyName>
<Name></Name>
</PartyName>
</Party>
</AccountingCustomerParty>
<InvoiceLine>
<ID></ID>
<InvoicedQuantity unitCode="">0</InvoicedQuantity>
<LineExtensionAmount currencyID="EUR">invalid-amount</LineExtensionAmount>
</InvoiceLine>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">another-invalid-amount</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(multiErrorXml);
if (parseResult) {
const validationResult = await invoice.validate();
if (validationResult && !validationResult.valid && validationResult.errors) {
const errors = validationResult.errors;
tools.log(`Error aggregation test - found ${errors.length} errors:`);
// Group errors by category
const errorsByCategory = {};
const errorsBySeverity = {};
for (const error of errors) {
// Count by category
const category = error.category || 'unknown';
errorsByCategory[category] = (errorsByCategory[category] || 0) + 1;
// Count by severity
const severity = error.severity || 'error';
errorsBySeverity[severity] = (errorsBySeverity[severity] || 0) + 1;
tools.log(` - ${error.message}`);
if (error.path) {
tools.log(` Path: ${error.path}`);
}
}
// Display error summary
tools.log(`\nError Summary:`);
tools.log(` Total errors: ${errors.length}`);
tools.log(` By category:`);
for (const [category, count] of Object.entries(errorsByCategory)) {
tools.log(` ${category}: ${count}`);
}
tools.log(` By severity:`);
for (const [severity, count] of Object.entries(errorsBySeverity)) {
tools.log(` ${severity}: ${count}`);
}
// Expect multiple errors to be found
expect(errors.length).toBeGreaterThan(3);
// Check that errors are properly structured
for (const error of errors) {
expect(error.message).toBeTruthy();
expect(typeof error.message).toBe('string');
}
tools.log(`✓ Error aggregation and categorization working`);
}
}
} catch (error) {
tools.log(`Error aggregation test failed: ${error.message}`);
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('error-reporting-aggregation', duration);
});
tap.test('VAL-13: Error Reporting - Localized Error Messages', async (tools) => {
const startTime = Date.now();
// Test error message localization (if supported)
const localizationTestXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>LOC-TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>INVALID</DocumentCurrencyCode>
</Invoice>`;
const locales = ['en', 'de', 'fr'];
for (const locale of locales) {
try {
const invoice = new EInvoice();
// Set locale if the API supports it
if (typeof invoice.setLocale === 'function') {
invoice.setLocale(locale);
tools.log(`Testing error messages in locale: ${locale}`);
} else {
tools.log(`Locale setting not supported, testing default messages`);
}
const parseResult = await invoice.fromXmlString(localizationTestXml);
if (parseResult) {
const validationResult = await invoice.validate();
if (validationResult && !validationResult.valid && validationResult.errors) {
for (const error of validationResult.errors) {
tools.log(` ${locale}: ${error.message}`);
// Check that error message is not empty and reasonably descriptive
expect(error.message).toBeTruthy();
expect(error.message.length).toBeGreaterThan(5);
// Check for locale-specific characteristics (if implemented)
if (locale === 'de' && error.message.includes('ungültig')) {
tools.log(` ✓ German localization detected`);
} else if (locale === 'fr' && error.message.includes('invalide')) {
tools.log(` ✓ French localization detected`);
}
}
}
}
} catch (error) {
tools.log(`Localization test failed for ${locale}: ${error.message}`);
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('error-reporting-localization', duration);
});
tap.test('VAL-13: Error Reporting - Corpus Error Analysis', { timeout: testTimeout }, async (tools) => {
const startTime = Date.now();
const errorStatistics = {
totalFiles: 0,
filesWithErrors: 0,
totalErrors: 0,
errorsByCategory: {},
errorsBySeverity: {},
mostCommonErrors: {}
};
try {
// Analyze errors across corpus files
const categories = ['UBL_XML_RECHNUNG', 'CII_XML_RECHNUNG'];
for (const category of categories) {
try {
const files = await CorpusLoader.getFiles(category);
for (const filePath of files.slice(0, 8)) { // Process first 8 files per category
errorStatistics.totalFiles++;
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromFile(filePath);
if (parseResult) {
const validationResult = await invoice.validate();
if (validationResult && !validationResult.valid && validationResult.errors) {
errorStatistics.filesWithErrors++;
errorStatistics.totalErrors += validationResult.errors.length;
for (const error of validationResult.errors) {
// Count by category
const category = error.category || 'unknown';
errorStatistics.errorsByCategory[category] = (errorStatistics.errorsByCategory[category] || 0) + 1;
// Count by severity
const severity = error.severity || 'error';
errorStatistics.errorsBySeverity[severity] = (errorStatistics.errorsBySeverity[severity] || 0) + 1;
// Track common error patterns
const errorKey = error.code || error.message.substring(0, 50);
errorStatistics.mostCommonErrors[errorKey] = (errorStatistics.mostCommonErrors[errorKey] || 0) + 1;
}
}
}
} catch (error) {
errorStatistics.filesWithErrors++;
errorStatistics.totalErrors++;
tools.log(`Parse error in ${plugins.path.basename(filePath)}: ${error.message}`);
}
}
} catch (error) {
tools.log(`Failed to process category ${category}: ${error.message}`);
}
}
// Display error analysis results
tools.log(`\n=== Corpus Error Analysis ===`);
tools.log(`Total 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: ${(errorStatistics.totalErrors / errorStatistics.totalFiles).toFixed(1)}`);
if (Object.keys(errorStatistics.errorsByCategory).length > 0) {
tools.log(`\nErrors by category:`);
for (const [category, count] of Object.entries(errorStatistics.errorsByCategory)) {
tools.log(` ${category}: ${count}`);
}
}
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}`);
}
}
// Show most common errors
const commonErrors = Object.entries(errorStatistics.mostCommonErrors)
.sort(([,a], [,b]) => b - a)
.slice(0, 5);
if (commonErrors.length > 0) {
tools.log(`\nMost common errors:`);
for (const [errorKey, count] of commonErrors) {
tools.log(` ${count}x: ${errorKey}`);
}
}
// Error analysis should complete successfully
expect(errorStatistics.totalFiles).toBeGreaterThan(0);
} catch (error) {
tools.log(`Corpus error analysis failed: ${error.message}`);
throw error;
}
const totalDuration = Date.now() - startTime;
PerformanceTracker.recordMetric('error-reporting-corpus', totalDuration);
expect(totalDuration).toBeLessThan(120000); // 2 minutes max
tools.log(`Error analysis completed in ${totalDuration}ms`);
});
tap.test('VAL-13: Performance Summary', async (tools) => {
const operations = [
'error-reporting-message-quality',
'error-reporting-classification',
'error-reporting-context',
'error-reporting-aggregation',
'error-reporting-localization',
'error-reporting-corpus'
];
tools.log(`\n=== Error Reporting 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(`\nError reporting testing completed successfully.`);
});