fix(compliance): improve compliance
This commit is contained in:
@ -1,129 +1,263 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
tap.test('ENC-07: Attribute Encoding - should handle character encoding in XML attributes', async () => {
|
||||
// ENC-07: Verify handling of Attribute Encoding encoded documents
|
||||
console.log('Testing XML attribute character encoding...\n');
|
||||
|
||||
// Test 1: Direct Attribute Encoding encoding (expected to fail)
|
||||
console.log('\nTest 1: Direct Attribute Encoding encoding');
|
||||
const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
|
||||
'attribute-direct',
|
||||
async () => {
|
||||
// XML parsers typically don't support Attribute Encoding directly
|
||||
const xmlContent = `<?xml version="1.0" encoding="Attribute Encoding"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<UBLVersionID>2.1</UBLVersionID>
|
||||
<ID>ATTRIBUTE-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
</Invoice>`;
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlContent);
|
||||
success = newInvoice.id === 'ATTRIBUTE-TEST' ||
|
||||
newInvoice.invoiceId === 'ATTRIBUTE-TEST' ||
|
||||
newInvoice.accountingDocId === 'ATTRIBUTE-TEST';
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.log(` Attribute Encoding not directly supported: ${e.message}`);
|
||||
// Test 1: Special characters in XML attributes
|
||||
const testSpecialCharacters = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ATTR-SPECIAL-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.subject = 'Attribute encoding test with special characters';
|
||||
|
||||
// Create invoice with special characters that need escaping in attributes
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Company & Co. "Special" Ltd',
|
||||
description: 'Testing <special> chars & "quotes"',
|
||||
address: {
|
||||
streetName: 'Street & "Quote" <Test>',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test & "City"',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB & 12345',
|
||||
registrationName: 'Commercial & Register'
|
||||
}
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'John & "Test"',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Customer with <special> & "chars"',
|
||||
address: {
|
||||
streetName: 'Customer & Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer "City"',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Product & "Special" <Item>',
|
||||
articleNumber: 'ATTR&001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export and verify attributes are properly encoded
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Check that special characters are properly escaped in the XML
|
||||
const hasEscapedAmpersand = xmlString.includes('&');
|
||||
const hasEscapedQuotes = xmlString.includes('"');
|
||||
const hasEscapedLt = xmlString.includes('<');
|
||||
const hasEscapedGt = xmlString.includes('>');
|
||||
|
||||
// Verify the XML can be parsed back
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlString);
|
||||
|
||||
const roundTripSuccess = (newInvoice.id === 'ATTR-SPECIAL-TEST' ||
|
||||
newInvoice.invoiceId === 'ATTR-SPECIAL-TEST' ||
|
||||
newInvoice.accountingDocId === 'ATTR-SPECIAL-TEST') &&
|
||||
newInvoice.from?.name?.includes('&') &&
|
||||
newInvoice.from?.name?.includes('"');
|
||||
|
||||
console.log(`Test 1 - Special characters in attributes:`);
|
||||
console.log(` Ampersand escaped: ${hasEscapedAmpersand ? 'Yes' : 'No'}`);
|
||||
console.log(` Quotes escaped: ${hasEscapedQuotes ? 'Yes' : 'No'}`);
|
||||
console.log(` Less-than escaped: ${hasEscapedLt ? 'Yes' : 'No'}`);
|
||||
console.log(` Greater-than escaped: ${hasEscapedGt ? 'Yes' : 'No'}`);
|
||||
console.log(` Round-trip successful: ${roundTripSuccess ? 'Yes' : 'No'}`);
|
||||
|
||||
return { hasEscapedAmpersand, hasEscapedQuotes, hasEscapedLt, hasEscapedGt, roundTripSuccess };
|
||||
};
|
||||
|
||||
console.log(` Attribute Encoding direct test completed in ${directMetric.duration}ms`);
|
||||
// Test 2: Unicode characters in attributes
|
||||
const testUnicodeCharacters = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ATTR-UNICODE-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.subject = 'Unicode attribute test: €äöüßñç';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Företag AB (€äöüß)',
|
||||
description: 'Testing Unicode: ∑∏∆ €£¥₹',
|
||||
address: {
|
||||
streetName: 'Straße Åäöü',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'München',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Handelsregister'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'José',
|
||||
surname: 'Müller-Øst',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Unicode customer: café résumé naïve',
|
||||
address: {
|
||||
streetName: 'Côte d\'Azur',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'São Paulo',
|
||||
country: 'BR'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Café Spécial (™)',
|
||||
articleNumber: 'UNI-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify Unicode characters are preserved
|
||||
const hasUnicodePreserved = xmlString.includes('Företag') &&
|
||||
xmlString.includes('München') &&
|
||||
xmlString.includes('José') &&
|
||||
xmlString.includes('Müller') &&
|
||||
xmlString.includes('Café');
|
||||
|
||||
// Test round-trip
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlString);
|
||||
|
||||
const unicodeRoundTrip = newInvoice.from?.name?.includes('Företag') &&
|
||||
newInvoice.to?.name?.includes('José') &&
|
||||
newInvoice.items?.[0]?.name?.includes('Café');
|
||||
|
||||
console.log(`\nTest 2 - Unicode characters in attributes:`);
|
||||
console.log(` Unicode preserved in XML: ${hasUnicodePreserved ? 'Yes' : 'No'}`);
|
||||
console.log(` Unicode round-trip successful: ${unicodeRoundTrip ? 'Yes' : 'No'}`);
|
||||
|
||||
return { hasUnicodePreserved, unicodeRoundTrip };
|
||||
};
|
||||
|
||||
// Test 2: UTF-8 fallback (should always work)
|
||||
console.log('\nTest 2: UTF-8 fallback');
|
||||
const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
|
||||
'attribute-fallback',
|
||||
async () => {
|
||||
// Test 3: XML predefined entities in attributes
|
||||
const testXmlEntities = async () => {
|
||||
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>ATTR-ENTITY-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Company & Co. "Special" <Ltd></cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ATTRIBUTE-FALLBACK-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'ATTRIBUTE-FALLBACK-TEST';
|
||||
einvoice.accountingDocId = 'ATTRIBUTE-FALLBACK-TEST';
|
||||
einvoice.subject = 'Attribute Encoding fallback test';
|
||||
await einvoice.fromXmlString(testXml);
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing Attribute Encoding encoding',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
};
|
||||
const entitySuccess = einvoice.from?.name?.includes('&') &&
|
||||
einvoice.from?.name?.includes('"') &&
|
||||
einvoice.from?.name?.includes('<') &&
|
||||
einvoice.from?.name?.includes('>');
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
console.log(`\nTest 3 - XML entity parsing:`);
|
||||
console.log(` Entities correctly parsed: ${entitySuccess ? 'Yes' : 'No'}`);
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'ATTRIBUTE-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export as UTF-8 (our default)
|
||||
const utf8Xml = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify UTF-8 works correctly
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(utf8Xml);
|
||||
|
||||
const success = newInvoice.id === 'ATTRIBUTE-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'ATTRIBUTE-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'ATTRIBUTE-FALLBACK-TEST';
|
||||
|
||||
console.log(` UTF-8 fallback works: ${success}`);
|
||||
|
||||
return { success };
|
||||
return { entitySuccess };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 3 - XML entity parsing:`);
|
||||
console.log(` Entity parsing failed: ${error.message}`);
|
||||
return { entitySuccess: false };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
console.log(` Attribute Encoding fallback test completed in ${fallbackMetric.duration}ms`);
|
||||
// Test 4: Attribute value normalization
|
||||
const testAttributeNormalization = async () => {
|
||||
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>ATTR-NORM-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name> Normalized Spaces Test </cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
await einvoice.fromXmlString(testXml);
|
||||
|
||||
// Check if whitespace normalization occurs appropriately
|
||||
const hasNormalization = einvoice.from?.name?.trim() === 'Normalized Spaces Test';
|
||||
|
||||
console.log(`\nTest 4 - Attribute value normalization:`);
|
||||
console.log(` Normalization handling: ${hasNormalization ? 'Correct' : 'Needs review'}`);
|
||||
|
||||
return { hasNormalization };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 4 - Attribute value normalization:`);
|
||||
console.log(` Normalization test failed: ${error.message}`);
|
||||
return { hasNormalization: false };
|
||||
}
|
||||
};
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Attribute Encoding Encoding Test Summary ===');
|
||||
console.log(`Attribute Encoding Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
|
||||
console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
|
||||
// Run all tests
|
||||
const specialCharsResult = await testSpecialCharacters();
|
||||
const unicodeResult = await testUnicodeCharacters();
|
||||
const entitiesResult = await testXmlEntities();
|
||||
const normalizationResult = await testAttributeNormalization();
|
||||
|
||||
// The test passes if UTF-8 fallback works, since Attribute Encoding support is optional
|
||||
expect(fallbackResult.success).toBeTrue();
|
||||
console.log(`\n=== XML Attribute Encoding Test Summary ===`);
|
||||
console.log(`Special character escaping: ${specialCharsResult.hasEscapedAmpersand && specialCharsResult.hasEscapedQuotes ? 'Working' : 'Issues'}`);
|
||||
console.log(`Unicode character support: ${unicodeResult.hasUnicodePreserved ? 'Working' : 'Issues'}`);
|
||||
console.log(`XML entity parsing: ${entitiesResult.entitySuccess ? 'Working' : 'Issues'}`);
|
||||
console.log(`Attribute normalization: ${normalizationResult.hasNormalization ? 'Working' : 'Issues'}`);
|
||||
console.log(`Round-trip consistency: ${specialCharsResult.roundTripSuccess && unicodeResult.unicodeRoundTrip ? 'Working' : 'Issues'}`);
|
||||
|
||||
// Test passes if basic XML character escaping and Unicode support work
|
||||
expect(specialCharsResult.hasEscapedAmpersand || specialCharsResult.roundTripSuccess).toBeTrue();
|
||||
expect(unicodeResult.hasUnicodePreserved || unicodeResult.unicodeRoundTrip).toBeTrue();
|
||||
});
|
||||
|
||||
// Run the test
|
||||
|
Reference in New Issue
Block a user