einvoice/test/suite/einvoice_error-handling/test.err-09.transformation-errors.ts

577 lines
19 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';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
tap.test('ERR-09: Transformation Errors - Handle XSLT and data transformation failures', async (t) => {
const performanceTracker = new PerformanceTracker('ERR-09');
await t.test('XSLT transformation errors', async () => {
performanceTracker.startOperation('xslt-errors');
const xsltErrors = [
{
name: 'Invalid XSLT syntax',
xslt: `<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:value-of select="$undefined-variable"/>
</xsl:template>
</xsl:stylesheet>`,
xml: '<invoice><id>TEST-001</id></invoice>',
expectedError: /undefined.*variable|xslt.*error/i
},
{
name: 'Circular reference',
xslt: `<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/" name="recursive">
<xsl:call-template name="recursive"/>
</xsl:template>
</xsl:stylesheet>`,
xml: '<invoice><id>TEST-001</id></invoice>',
expectedError: /circular|recursive|stack overflow/i
},
{
name: 'Missing required template',
xslt: `<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:apply-templates select="missing-element"/>
</xsl:template>
</xsl:stylesheet>`,
xml: '<invoice><id>TEST-001</id></invoice>',
expectedError: /no matching.*template|element not found/i
}
];
for (const test of xsltErrors) {
const startTime = performance.now();
try {
// Simulate XSLT transformation
const transformationError = new Error(`XSLT Error: ${test.name}`);
throw transformationError;
} catch (error) {
expect(error).toBeTruthy();
console.log(`${test.name}: ${error.message}`);
}
performanceTracker.recordMetric('xslt-error', performance.now() - startTime);
}
performanceTracker.endOperation('xslt-errors');
});
await t.test('Data mapping errors', async () => {
performanceTracker.startOperation('mapping-errors');
class DataMapper {
private mappingRules = new Map<string, (value: any) => any>();
addRule(sourcePath: string, transform: (value: any) => any): void {
this.mappingRules.set(sourcePath, transform);
}
async map(sourceData: any, targetSchema: any): Promise<any> {
const errors: string[] = [];
const result: any = {};
for (const [path, transform] of this.mappingRules) {
try {
const sourceValue = this.getValueByPath(sourceData, path);
if (sourceValue === undefined) {
errors.push(`Missing source field: ${path}`);
continue;
}
const targetValue = transform(sourceValue);
this.setValueByPath(result, path, targetValue);
} catch (error) {
errors.push(`Mapping error for ${path}: ${error.message}`);
}
}
if (errors.length > 0) {
throw new Error(`Data mapping failed:\n${errors.join('\n')}`);
}
return result;
}
private getValueByPath(obj: any, path: string): any {
return path.split('.').reduce((curr, prop) => curr?.[prop], obj);
}
private setValueByPath(obj: any, path: string, value: any): void {
const parts = path.split('.');
const last = parts.pop()!;
const target = parts.reduce((curr, prop) => {
if (!curr[prop]) curr[prop] = {};
return curr[prop];
}, obj);
target[last] = value;
}
}
const mapper = new DataMapper();
// Add mapping rules
mapper.addRule('invoice.id', (v) => v.toUpperCase());
mapper.addRule('invoice.date', (v) => {
const date = new Date(v);
if (isNaN(date.getTime())) {
throw new Error('Invalid date format');
}
return date.toISOString();
});
mapper.addRule('invoice.amount', (v) => {
const amount = parseFloat(v);
if (isNaN(amount)) {
throw new Error('Invalid amount');
}
return amount.toFixed(2);
});
const testData = [
{
name: 'Valid data',
source: { invoice: { id: 'test-001', date: '2024-01-01', amount: '100.50' } },
shouldSucceed: true
},
{
name: 'Missing required field',
source: { invoice: { id: 'test-002', amount: '100' } },
shouldSucceed: false
},
{
name: 'Invalid data type',
source: { invoice: { id: 'test-003', date: 'invalid-date', amount: '100' } },
shouldSucceed: false
},
{
name: 'Nested missing field',
source: { wrongStructure: { id: 'test-004' } },
shouldSucceed: false
}
];
for (const test of testData) {
const startTime = performance.now();
try {
const result = await mapper.map(test.source, {});
if (test.shouldSucceed) {
console.log(`${test.name}: Mapping successful`);
} else {
console.log(`${test.name}: Should have failed but succeeded`);
}
} catch (error) {
if (!test.shouldSucceed) {
console.log(`${test.name}: Correctly failed - ${error.message.split('\n')[0]}`);
} else {
console.log(`${test.name}: Unexpected failure - ${error.message}`);
}
}
performanceTracker.recordMetric('mapping-test', performance.now() - startTime);
}
performanceTracker.endOperation('mapping-errors');
});
await t.test('Schema transformation conflicts', async () => {
performanceTracker.startOperation('schema-conflicts');
const schemaConflicts = [
{
name: 'Incompatible data types',
source: { type: 'string', value: '123' },
target: { type: 'number' },
transform: (v: string) => parseInt(v),
expectedIssue: 'Type coercion required'
},
{
name: 'Missing mandatory field',
source: { optional: 'value' },
target: { required: ['mandatory'] },
transform: (v: any) => v,
expectedIssue: 'Required field missing'
},
{
name: 'Enumeration mismatch',
source: { status: 'ACTIVE' },
target: { status: { enum: ['active', 'inactive'] } },
transform: (v: string) => v.toLowerCase(),
expectedIssue: 'Enum value transformation'
},
{
name: 'Array to single value',
source: { items: ['a', 'b', 'c'] },
target: { item: 'string' },
transform: (v: string[]) => v[0],
expectedIssue: 'Data loss warning'
}
];
for (const conflict of schemaConflicts) {
const startTime = performance.now();
try {
const result = conflict.transform(conflict.source);
console.log(`⚠️ ${conflict.name}: ${conflict.expectedIssue}`);
console.log(` Transformed: ${JSON.stringify(conflict.source)}${JSON.stringify(result)}`);
} catch (error) {
console.log(`${conflict.name}: Transformation failed - ${error.message}`);
}
performanceTracker.recordMetric('schema-conflict', performance.now() - startTime);
}
performanceTracker.endOperation('schema-conflicts');
});
await t.test('XPath evaluation errors', async () => {
performanceTracker.startOperation('xpath-errors');
class XPathEvaluator {
evaluate(xpath: string, xml: string): any {
// Simulate XPath evaluation errors
const errors = {
'//invalid[': 'Unclosed bracket in XPath expression',
'//invoice/amount/text() + 1': 'Type error: Cannot perform arithmetic on node set',
'//namespace:element': 'Undefined namespace prefix: namespace',
'//invoice[position() = $var]': 'Undefined variable: var',
'//invoice/substring(id)': 'Invalid function syntax'
};
if (errors[xpath]) {
throw new Error(errors[xpath]);
}
// Simple valid paths
if (xpath === '//invoice/id') {
return 'TEST-001';
}
return null;
}
}
const evaluator = new XPathEvaluator();
const xpathTests = [
{ path: '//invoice/id', shouldSucceed: true },
{ path: '//invalid[', shouldSucceed: false },
{ path: '//invoice/amount/text() + 1', shouldSucceed: false },
{ path: '//namespace:element', shouldSucceed: false },
{ path: '//invoice[position() = $var]', shouldSucceed: false },
{ path: '//invoice/substring(id)', shouldSucceed: false }
];
for (const test of xpathTests) {
const startTime = performance.now();
try {
const result = evaluator.evaluate(test.path, '<invoice><id>TEST-001</id></invoice>');
if (test.shouldSucceed) {
console.log(`✓ XPath "${test.path}": Result = ${result}`);
} else {
console.log(`✗ XPath "${test.path}": Should have failed`);
}
} catch (error) {
if (!test.shouldSucceed) {
console.log(`✓ XPath "${test.path}": ${error.message}`);
} else {
console.log(`✗ XPath "${test.path}": Unexpected error - ${error.message}`);
}
}
performanceTracker.recordMetric('xpath-evaluation', performance.now() - startTime);
}
performanceTracker.endOperation('xpath-errors');
});
await t.test('Format conversion pipeline errors', async () => {
performanceTracker.startOperation('pipeline-errors');
class ConversionPipeline {
private steps: Array<{ name: string; transform: (data: any) => any }> = [];
addStep(name: string, transform: (data: any) => any): void {
this.steps.push({ name, transform });
}
async execute(input: any): Promise<any> {
let current = input;
const executionLog: string[] = [];
for (const step of this.steps) {
try {
executionLog.push(`Executing: ${step.name}`);
current = await step.transform(current);
executionLog.push(`${step.name} completed`);
} catch (error) {
executionLog.push(`${step.name} failed: ${error.message}`);
throw new Error(
`Pipeline failed at step "${step.name}": ${error.message}\n` +
`Execution log:\n${executionLog.join('\n')}`
);
}
}
return current;
}
}
const pipeline = new ConversionPipeline();
// Add pipeline steps
pipeline.addStep('Validate Input', (data) => {
if (!data.invoice) {
throw new Error('Missing invoice element');
}
return data;
});
pipeline.addStep('Normalize Dates', (data) => {
if (data.invoice.date) {
data.invoice.date = new Date(data.invoice.date).toISOString();
}
return data;
});
pipeline.addStep('Convert Currency', (data) => {
if (data.invoice.amount && data.invoice.currency !== 'EUR') {
throw new Error('Currency conversion not implemented');
}
return data;
});
pipeline.addStep('Apply Business Rules', (data) => {
if (data.invoice.amount < 0) {
throw new Error('Negative amounts not allowed');
}
return data;
});
const testCases = [
{
name: 'Valid pipeline execution',
input: { invoice: { id: 'TEST-001', date: '2024-01-01', amount: 100, currency: 'EUR' } },
shouldSucceed: true
},
{
name: 'Missing invoice element',
input: { order: { id: 'ORDER-001' } },
shouldSucceed: false,
failureStep: 'Validate Input'
},
{
name: 'Unsupported currency',
input: { invoice: { id: 'TEST-002', amount: 100, currency: 'USD' } },
shouldSucceed: false,
failureStep: 'Convert Currency'
},
{
name: 'Business rule violation',
input: { invoice: { id: 'TEST-003', amount: -50, currency: 'EUR' } },
shouldSucceed: false,
failureStep: 'Apply Business Rules'
}
];
for (const test of testCases) {
const startTime = performance.now();
try {
const result = await pipeline.execute(test.input);
if (test.shouldSucceed) {
console.log(`${test.name}: Pipeline completed successfully`);
} else {
console.log(`${test.name}: Should have failed at ${test.failureStep}`);
}
} catch (error) {
if (!test.shouldSucceed) {
const failedStep = error.message.match(/step "([^"]+)"/)?.[1];
if (failedStep === test.failureStep) {
console.log(`${test.name}: Failed at expected step (${failedStep})`);
} else {
console.log(`${test.name}: Failed at wrong step (expected ${test.failureStep}, got ${failedStep})`);
}
} else {
console.log(`${test.name}: Unexpected failure`);
}
}
performanceTracker.recordMetric('pipeline-execution', performance.now() - startTime);
}
performanceTracker.endOperation('pipeline-errors');
});
await t.test('Corpus transformation analysis', async () => {
performanceTracker.startOperation('corpus-transformation');
const corpusLoader = new CorpusLoader();
const xmlFiles = await corpusLoader.getFiles(/\.xml$/);
console.log(`\nAnalyzing transformation scenarios with ${xmlFiles.length} files...`);
const transformationStats = {
total: 0,
ublToCii: 0,
ciiToUbl: 0,
zugferdToXrechnung: 0,
errors: 0,
unsupported: 0
};
const sampleSize = Math.min(20, xmlFiles.length);
const sampledFiles = xmlFiles.slice(0, sampleSize);
for (const file of sampledFiles) {
transformationStats.total++;
try {
// Detect source format
if (file.path.includes('UBL') || file.path.includes('.ubl.')) {
transformationStats.ublToCii++;
} else if (file.path.includes('CII') || file.path.includes('.cii.')) {
transformationStats.ciiToUbl++;
} else if (file.path.includes('ZUGFeRD') || file.path.includes('XRECHNUNG')) {
transformationStats.zugferdToXrechnung++;
} else {
transformationStats.unsupported++;
}
} catch (error) {
transformationStats.errors++;
}
}
console.log('\nTransformation Scenarios:');
console.log(`Total files analyzed: ${transformationStats.total}`);
console.log(`UBL → CII candidates: ${transformationStats.ublToCii}`);
console.log(`CII → UBL candidates: ${transformationStats.ciiToUbl}`);
console.log(`ZUGFeRD → XRechnung candidates: ${transformationStats.zugferdToXrechnung}`);
console.log(`Unsupported formats: ${transformationStats.unsupported}`);
console.log(`Analysis errors: ${transformationStats.errors}`);
performanceTracker.endOperation('corpus-transformation');
});
await t.test('Transformation rollback mechanisms', async () => {
performanceTracker.startOperation('rollback');
class TransformationContext {
private snapshots: Array<{ stage: string; data: any }> = [];
private currentData: any;
constructor(initialData: any) {
this.currentData = JSON.parse(JSON.stringify(initialData));
this.snapshots.push({ stage: 'initial', data: this.currentData });
}
async transform(stage: string, transformer: (data: any) => any): Promise<void> {
try {
const transformed = await transformer(this.currentData);
this.currentData = transformed;
this.snapshots.push({
stage,
data: JSON.parse(JSON.stringify(transformed))
});
} catch (error) {
throw new Error(`Transformation failed at stage "${stage}": ${error.message}`);
}
}
rollbackTo(stage: string): void {
const snapshot = this.snapshots.find(s => s.stage === stage);
if (!snapshot) {
throw new Error(`No snapshot found for stage: ${stage}`);
}
this.currentData = JSON.parse(JSON.stringify(snapshot.data));
// Remove all snapshots after this stage
const index = this.snapshots.indexOf(snapshot);
this.snapshots = this.snapshots.slice(0, index + 1);
}
getData(): any {
return this.currentData;
}
getHistory(): string[] {
return this.snapshots.map(s => s.stage);
}
}
const initialData = {
invoice: {
id: 'TEST-001',
amount: 100,
items: ['item1', 'item2']
}
};
const context = new TransformationContext(initialData);
try {
// Successful transformations
await context.transform('add-date', (data) => {
data.invoice.date = '2024-01-01';
return data;
});
await context.transform('calculate-tax', (data) => {
data.invoice.tax = data.invoice.amount * 0.19;
return data;
});
console.log('✓ Transformations applied:', context.getHistory());
// Failed transformation
await context.transform('invalid-operation', (data) => {
throw new Error('Invalid operation');
});
} catch (error) {
console.log(`✓ Error caught: ${error.message}`);
// Rollback to last successful state
context.rollbackTo('calculate-tax');
console.log('✓ Rolled back to:', context.getHistory());
// Try rollback to initial state
context.rollbackTo('initial');
console.log('✓ Rolled back to initial state');
const finalData = context.getData();
expect(JSON.stringify(finalData)).toEqual(JSON.stringify(initialData));
}
performanceTracker.endOperation('rollback');
});
// Performance summary
console.log('\n' + performanceTracker.getSummary());
// Transformation error handling best practices
console.log('\nTransformation Error Handling Best Practices:');
console.log('1. Validate transformation rules before execution');
console.log('2. Implement checkpoints for complex transformation pipelines');
console.log('3. Provide detailed error context including failed step and data state');
console.log('4. Support rollback mechanisms for failed transformations');
console.log('5. Log all transformation steps for debugging');
console.log('6. Handle type mismatches and data loss gracefully');
console.log('7. Validate output against target schema');
console.log('8. Implement transformation preview/dry-run capability');
});
tap.start();