844 lines
32 KiB
TypeScript
844 lines
32 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 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: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
<LegalMonetaryTotal>
|
|
<PayableAmount currencyID="EUR">100.00</PayableAmount>
|
|
</LegalMonetaryTotal>
|
|
</Invoice>`,
|
|
expectedErrors: ['BR-01', 'invoice number', 'ID', 'required'],
|
|
errorCount: 1
|
|
},
|
|
{
|
|
name: 'BR-CO-10: Sum of line amounts validation',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>BR-TEST-001</ID>
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
<InvoiceLine>
|
|
<ID>1</ID>
|
|
<InvoicedQuantity unitCode="C62">2</InvoicedQuantity>
|
|
<LineExtensionAmount currencyID="EUR">100.00</LineExtensionAmount>
|
|
<Price>
|
|
<PriceAmount currencyID="EUR">50.00</PriceAmount>
|
|
</Price>
|
|
</InvoiceLine>
|
|
<InvoiceLine>
|
|
<ID>2</ID>
|
|
<InvoicedQuantity unitCode="C62">3</InvoicedQuantity>
|
|
<LineExtensionAmount currencyID="EUR">150.00</LineExtensionAmount>
|
|
<Price>
|
|
<PriceAmount currencyID="EUR">50.00</PriceAmount>
|
|
</Price>
|
|
</InvoiceLine>
|
|
<LegalMonetaryTotal>
|
|
<LineExtensionAmount currencyID="EUR">200.00</LineExtensionAmount>
|
|
<PayableAmount currencyID="EUR">200.00</PayableAmount>
|
|
</LegalMonetaryTotal>
|
|
</Invoice>`,
|
|
expectedErrors: ['BR-CO-10', 'sum', 'line', 'amount', 'calculation'],
|
|
errorCount: 1
|
|
},
|
|
{
|
|
name: 'Multiple validation errors',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>MULTI-ERROR-001</ID>
|
|
<InvoiceTypeCode>999</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>INVALID</DocumentCurrencyCode>
|
|
<TaxTotal>
|
|
<TaxAmount currencyID="EUR">-50.00</TaxAmount>
|
|
</TaxTotal>
|
|
<LegalMonetaryTotal>
|
|
<PayableAmount currencyID="XXX">100.00</PayableAmount>
|
|
</LegalMonetaryTotal>
|
|
</Invoice>`,
|
|
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: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<ID>SCHEMA-001</ID>
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
</Invoice>`,
|
|
expectedErrors: ['order', 'sequence', 'element'],
|
|
description: 'Elements in wrong order'
|
|
},
|
|
{
|
|
name: 'Unknown element',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>SCHEMA-002</ID>
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<UnknownElement>This should not be here</UnknownElement>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
</Invoice>`,
|
|
expectedErrors: ['unknown', 'element', 'unexpected'],
|
|
description: 'Contains unknown element'
|
|
},
|
|
{
|
|
name: 'Invalid attribute',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" invalidAttribute="value">
|
|
<ID>SCHEMA-003</ID>
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
</Invoice>`,
|
|
expectedErrors: ['attribute', 'invalid', 'unexpected'],
|
|
description: 'Invalid attribute on root element'
|
|
},
|
|
{
|
|
name: 'Missing required child element',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>SCHEMA-004</ID>
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<TaxTotal>
|
|
<TaxAmount currencyID="EUR">19.00</TaxAmount>
|
|
<!-- Missing TaxSubtotal -->
|
|
</TaxTotal>
|
|
</Invoice>`,
|
|
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: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>FIELD-001</ID>
|
|
<IssueDate>15-01-2024</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<DueDate>2024/02/15</DueDate>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
</Invoice>`,
|
|
expectedFields: ['IssueDate', 'DueDate'],
|
|
expectedErrors: ['date', 'format', 'ISO', 'YYYY-MM-DD']
|
|
},
|
|
{
|
|
name: 'Invalid currency codes',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>FIELD-002</ID>
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>EURO</DocumentCurrencyCode>
|
|
<LegalMonetaryTotal>
|
|
<PayableAmount currencyID="$$$">100.00</PayableAmount>
|
|
</LegalMonetaryTotal>
|
|
</Invoice>`,
|
|
expectedFields: ['DocumentCurrencyCode', 'currencyID'],
|
|
expectedErrors: ['currency', 'ISO 4217', 'invalid', 'code']
|
|
},
|
|
{
|
|
name: 'Invalid numeric values',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>FIELD-003</ID>
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
<InvoiceLine>
|
|
<ID>1</ID>
|
|
<InvoicedQuantity unitCode="C62">ABC</InvoicedQuantity>
|
|
<LineExtensionAmount currencyID="EUR">not-a-number</LineExtensionAmount>
|
|
</InvoiceLine>
|
|
<TaxTotal>
|
|
<TaxAmount currencyID="EUR">19.999999999</TaxAmount>
|
|
</TaxTotal>
|
|
</Invoice>`,
|
|
expectedFields: ['InvoicedQuantity', 'LineExtensionAmount', 'TaxAmount'],
|
|
expectedErrors: ['numeric', 'number', 'decimal', 'invalid']
|
|
},
|
|
{
|
|
name: 'Invalid code values',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>FIELD-004</ID>
|
|
<IssueDate>2024-01-15</IssueDate>
|
|
<InvoiceTypeCode>999</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
<PaymentMeans>
|
|
<PaymentMeansCode>99</PaymentMeansCode>
|
|
</PaymentMeans>
|
|
<InvoiceLine>
|
|
<ID>1</ID>
|
|
<InvoicedQuantity unitCode="INVALID">1</InvoicedQuantity>
|
|
</InvoiceLine>
|
|
</Invoice>`,
|
|
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<string>();
|
|
|
|
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>COMPLEX-001</ID>
|
|
<IssueDate>invalid-date</IssueDate>
|
|
<InvoiceTypeCode>999</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>XXX</DocumentCurrencyCode>
|
|
<AccountingSupplierParty>
|
|
<Party>
|
|
<!-- Missing required party name -->
|
|
<PostalAddress>
|
|
<StreetName></StreetName>
|
|
<CityName></CityName>
|
|
<Country>
|
|
<IdentificationCode>XX</IdentificationCode>
|
|
</Country>
|
|
</PostalAddress>
|
|
<PartyTaxScheme>
|
|
<CompanyID>INVALID-VAT</CompanyID>
|
|
</PartyTaxScheme>
|
|
</Party>
|
|
</AccountingSupplierParty>
|
|
<InvoiceLine>
|
|
<ID>1</ID>
|
|
<InvoicedQuantity unitCode="INVALID">-5</InvoicedQuantity>
|
|
<LineExtensionAmount currencyID="USD">-100.00</LineExtensionAmount>
|
|
<Item>
|
|
<!-- Missing item name -->
|
|
<ClassifiedTaxCategory>
|
|
<Percent>999</Percent>
|
|
</ClassifiedTaxCategory>
|
|
</Item>
|
|
<Price>
|
|
<PriceAmount currencyID="GBP">-20.00</PriceAmount>
|
|
</Price>
|
|
</InvoiceLine>
|
|
<InvoiceLine>
|
|
<ID>2</ID>
|
|
<InvoicedQuantity>10</InvoicedQuantity>
|
|
<LineExtensionAmount currencyID="JPY">invalid</LineExtensionAmount>
|
|
</InvoiceLine>
|
|
<TaxTotal>
|
|
<TaxAmount currencyID="CHF">invalid-amount</TaxAmount>
|
|
<TaxSubtotal>
|
|
<!-- Missing required elements -->
|
|
</TaxSubtotal>
|
|
</TaxTotal>
|
|
<LegalMonetaryTotal>
|
|
<LineExtensionAmount currencyID="EUR">NaN</LineExtensionAmount>
|
|
<TaxExclusiveAmount currencyID="EUR">-50.00</TaxExclusiveAmount>
|
|
<PayableAmount currencyID="">0.00</PayableAmount>
|
|
</LegalMonetaryTotal>
|
|
</Invoice>`;
|
|
|
|
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.`);
|
|
}); |