import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { CorpusLoader } from '../corpus.loader.js'; import { PerformanceTracker } from '../performance.tracker.js'; tap.test('ENC-07: Attribute Encoding - should handle XML attribute encoding correctly', async (t) => { // ENC-07: Verify proper encoding of XML attributes including special chars and quotes // This test ensures attributes are properly encoded across different scenarios const performanceTracker = new PerformanceTracker('ENC-07: Attribute Encoding'); const corpusLoader = new CorpusLoader(); t.test('Basic attribute encoding', async () => { const startTime = performance.now(); const xmlContent = ` 2.1 ATTR-BASIC-001 2025-01-25 EUR 19.00 S 19 VAT 1 10 100.00 `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify basic attributes are preserved expect(xmlString).toMatch(/schemeID\s*=\s*["']INVOICE["']/); expect(xmlString).toMatch(/schemeAgencyID\s*=\s*["']6["']/); expect(xmlString).toMatch(/listID\s*=\s*["']ISO4217["']/); expect(xmlString).toMatch(/listVersionID\s*=\s*["']2001["']/); expect(xmlString).toMatch(/currencyID\s*=\s*["']EUR["']/); expect(xmlString).toMatch(/unitCode\s*=\s*["']C62["']/); expect(xmlString).toMatch(/unitCodeListID\s*=\s*["']UNECERec20["']/); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('basic-attributes', elapsed); }); t.test('Attributes with special characters', async () => { const startTime = performance.now(); const xmlContent = ` 2.1 ATTR-SPECIAL-001 Rechnung für Bücher & Zeitschriften 30 PAY-123 DE89 3704 0044 0532 0130 00 Sparkasse false Volume discount 5.00 `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify special characters in attributes are properly escaped expect(xmlString).toMatch(/name\s*=\s*["']Überweisung \(Bank & SEPA\)["']/); expect(xmlString).toMatch(/reference\s*=\s*["']Order <2025-001>["']/); expect(xmlString).toMatch(/type\s*=\s*["']IBAN & BIC["']/); expect(xmlString).toMatch(/branch\s*=\s*["']München ("|")Zentrum("|")["']/); expect(xmlString).toMatch(/description\s*=\s*["']Discount for > 100€ orders["']/); expect(xmlString).toMatch(/percentage\s*=\s*["']5%["']/); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('special-char-attributes', elapsed); }); t.test('Quote handling in attributes', async () => { const startTime = performance.now(); const xmlContent = ` 2.1 ATTR-QUOTES-001 Test note DOC-001 Manual for "advanced" users http://example.com/doc?id=123&type="pdf" Item with quotes Complex quoting test Quote test Quoted value `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify quote handling - implementation may use different strategies // Either escape quotes or switch quote style expect(xmlString).toBeTruthy(); // Should contain the attribute values somehow expect(xmlString).toMatch(/Single quotes with .*double quotes.* inside/); expect(xmlString).toMatch(/Product .*Premium.* edition/); expect(xmlString).toMatch(/User.*s guide/); expect(xmlString).toMatch(/Special.*product/); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('quote-attributes', elapsed); }); t.test('International characters in attributes', async () => { const startTime = performance.now(); const xmlContent = ` 2.1 ATTR-INTL-001 International attributes SG Group Champs-Élysées Paris FR République française Multi-currency payment International Books Multilingual content `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify international characters in attributes expect(xmlString).toContain('Europa/歐洲/यूरोप'); expect(xmlString).toContain('Société Générale'); expect(xmlString).toContain('ソシエテ・ジェネラル'); expect(xmlString).toContain('Avenue/大道/एवेन्यू'); expect(xmlString).toContain('Île-de-France'); expect(xmlString).toContain('α2'); // Greek alpha expect(xmlString).toContain('République française'); expect(xmlString).toContain('30 días/天/दिन'); expect(xmlString).toContain('€/¥/₹'); expect(xmlString).toContain('Bücher/书籍/पुस्तकें'); expect(xmlString).toContain('佛朗索瓦·穆勒'); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('intl-attributes', elapsed); }); t.test('Empty and whitespace attributes', async () => { const startTime = performance.now(); const xmlContent = ` 2.1 ATTR-WHITESPACE-001 Empty attributes REF-001 Trimmed content PAY-001 Note with spaces 100.00 Item description `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify empty attributes are preserved expect(xmlString).toMatch(/title\s*=\s*["'](\s*)["']/); expect(xmlString).toMatch(/language\s*=\s*["'](\s*)["']/); // Whitespace handling may vary expect(xmlString).toContain('schemeID='); expect(xmlString).toContain('reference='); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('whitespace-attributes', elapsed); }); t.test('Numeric and boolean attribute values', async () => { const startTime = performance.now(); const xmlContent = ` 2.1 ATTR-NUMERIC-001 true 1 19.99 100.00 19.00 100.00 S 19.0 Not exempt 1 10 10.00 1 `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify numeric and boolean attributes expect(xmlString).toMatch(/decimals\s*=\s*["']2["']/); expect(xmlString).toMatch(/precision\s*=\s*["']0\.01["']/); expect(xmlString).toMatch(/percentage\s*=\s*["']19\.5["']/); expect(xmlString).toMatch(/factor\s*=\s*["']0\.195["']/); expect(xmlString).toMatch(/rate\s*=\s*["']19["']/); expect(xmlString).toMatch(/rounded\s*=\s*["']false["']/); expect(xmlString).toMatch(/active\s*=\s*["']true["']/); expect(xmlString).toMatch(/sequence\s*=\s*["']001["']/); expect(xmlString).toMatch(/index\s*=\s*["']0["']/); expect(xmlString).toMatch(/isInteger\s*=\s*["']true["']/); expect(xmlString).toMatch(/negative\s*=\s*["']false["']/); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('numeric-boolean-attributes', elapsed); }); t.test('Namespace-prefixed attributes', async () => { const startTime = performance.now(); const xmlContent = ` 2.1 ATTR-NS-PREFIX-001 urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 DOC-001 http://example.com/doc.pdf JVBERi0xLjQKJeLjz9MKNCAwIG9iago= SIG-001 RSA-SHA256 `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify namespace-prefixed attributes expect(xmlString).toContain('xsi:schemaLocation='); expect(xmlString).toContain('xsi:type='); expect(xmlString).toContain('xlink:type='); expect(xmlString).toContain('xlink:href='); expect(xmlString).toContain('xlink:title='); expect(xmlString).toContain('ds:algorithm='); expect(xmlString).toContain('ds:Algorithm='); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('ns-prefixed-attributes', elapsed); }); t.test('Corpus attribute analysis', async () => { const startTime = performance.now(); let processedCount = 0; const attributeStats = { totalAttributes: 0, escapedAttributes: 0, unicodeAttributes: 0, numericAttributes: 0, emptyAttributes: 0, commonAttributes: new Map() }; const files = await corpusLoader.getAllFiles(); const xmlFiles = files.filter(f => f.endsWith('.xml')); // Analyze attribute usage in corpus const sampleSize = Math.min(80, xmlFiles.length); const sample = xmlFiles.slice(0, sampleSize); for (const file of sample) { try { const content = await corpusLoader.readFile(file); let xmlString: string; if (Buffer.isBuffer(content)) { xmlString = content.toString('utf8'); } else { xmlString = content; } // Count attributes const attrMatches = xmlString.match(/\s(\w+(?::\w+)?)\s*=\s*["'][^"']*["']/g); if (attrMatches) { attributeStats.totalAttributes += attrMatches.length; attrMatches.forEach(attr => { // Check for escaped content if (attr.includes('&') || attr.includes('<') || attr.includes('>') || attr.includes('"') || attr.includes(''')) { attributeStats.escapedAttributes++; } // Check for Unicode if (/[^\x00-\x7F]/.test(attr)) { attributeStats.unicodeAttributes++; } // Check for numeric values if (/=\s*["']\d+(?:\.\d+)?["']/.test(attr)) { attributeStats.numericAttributes++; } // Check for empty values if (/=\s*["']\s*["']/.test(attr)) { attributeStats.emptyAttributes++; } // Extract attribute name const nameMatch = attr.match(/(\w+(?::\w+)?)\s*=/); if (nameMatch) { const attrName = nameMatch[1]; attributeStats.commonAttributes.set( attrName, (attributeStats.commonAttributes.get(attrName) || 0) + 1 ); } }); } processedCount++; } catch (error) { console.log(`Attribute parsing issue in ${file}:`, error.message); } } console.log(`Attribute corpus analysis (${processedCount} files):`); console.log(`- Total attributes: ${attributeStats.totalAttributes}`); console.log(`- Escaped attributes: ${attributeStats.escapedAttributes}`); console.log(`- Unicode attributes: ${attributeStats.unicodeAttributes}`); console.log(`- Numeric attributes: ${attributeStats.numericAttributes}`); console.log(`- Empty attributes: ${attributeStats.emptyAttributes}`); const topAttributes = Array.from(attributeStats.commonAttributes.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10); console.log('Top 10 attribute names:', topAttributes); expect(processedCount).toBeGreaterThan(0); expect(attributeStats.totalAttributes).toBeGreaterThan(0); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('corpus-attributes', elapsed); }); // Print performance summary performanceTracker.printSummary(); // Performance assertions const avgTime = performanceTracker.getAverageTime(); expect(avgTime).toBeLessThan(120); // Attribute operations should be reasonably fast }); tap.start();