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();