290 lines
10 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
import { CorpusLoader, PerformanceTracker } from '../../helpers/test-utils.js';
import * as path from 'path';
import * as fs from 'fs/promises';
/**
* Test ID: STD-09
* Test Description: ISO 19005 PDF/A-3 Compliance
* Priority: Medium
*
* This test validates compliance with ISO 19005 PDF/A-3 standard for
* archivable PDF documents with embedded files (used in ZUGFeRD/Factur-X).
*/
tap.test('STD-09: PDF/A-3 Compliance - should validate ISO 19005 PDF/A-3 standard', async (t) => {
// Test 1: PDF/A-3 Identification
t.test('PDF/A-3 identification and metadata', async (st) => {
// Get PDF files from ZUGFeRD corpus
const pdfFiles = await CorpusLoader.getFiles('ZUGFERD_V2_CORRECT');
const testPdfs = pdfFiles.filter(f => f.endsWith('.pdf')).slice(0, 3);
for (const pdfFile of testPdfs) {
const pdfBuffer = await CorpusLoader.loadFile(pdfFile);
// Basic PDF/A markers check
const pdfString = pdfBuffer.toString('latin1');
// Check for PDF/A identification
const hasPDFAMarker = pdfString.includes('pdfaid:part') ||
pdfString.includes('PDF/A') ||
pdfString.includes('19005');
// Check for XMP metadata
const hasXMP = pdfString.includes('<x:xmpmeta') ||
pdfString.includes('<?xpacket');
if (hasPDFAMarker || hasXMP) {
st.pass(`${path.basename(pdfFile)}: Contains PDF/A markers or XMP metadata`);
} else {
st.comment(`${path.basename(pdfFile)}: May not be PDF/A-3 compliant`);
}
}
});
// Test 2: Embedded File Compliance
t.test('PDF/A-3 embedded file requirements', async (st) => {
const invoice = new EInvoice();
invoice.id = 'PDFA3-EMB-001';
invoice.issueDate = new Date();
invoice.from = { name: 'Seller', address: { country: 'DE' } };
invoice.to = { name: 'Buyer', address: { country: 'DE' } };
invoice.items = [{ name: 'Item', quantity: 1, unitPrice: 100 }];
// Generate XML for embedding
const xmlContent = await invoice.toXmlString('cii');
// Test embedding requirements
const embeddingRequirements = {
filename: 'factur-x.xml',
mimeType: 'text/xml',
relationship: 'Alternative',
description: 'Factur-X Invoice',
modDate: new Date().toISOString()
};
// Verify requirements
expect(embeddingRequirements.filename).toMatch(/\.(xml|XML)$/);
expect(embeddingRequirements.mimeType).toEqual('text/xml');
expect(embeddingRequirements.relationship).toEqual('Alternative');
st.pass('✓ PDF/A-3 embedding requirements defined correctly');
});
// Test 3: Color Space Compliance
t.test('PDF/A-3 color space requirements', async (st) => {
// PDF/A-3 requires device-independent color spaces
const allowedColorSpaces = [
'DeviceGray',
'DeviceRGB',
'DeviceCMYK',
'CalGray',
'CalRGB',
'Lab',
'ICCBased'
];
const prohibitedColorSpaces = [
'Separation',
'DeviceN', // Allowed only with alternate space
'Pattern' // Allowed only with specific conditions
];
// In a real implementation, would parse PDF and check color spaces
for (const cs of allowedColorSpaces) {
st.pass(`✓ Allowed color space: ${cs}`);
}
st.comment('Note: Separation and DeviceN require alternate color spaces');
});
// Test 4: Font Embedding Compliance
t.test('PDF/A-3 font embedding requirements', async (st) => {
// PDF/A-3 requires all fonts to be embedded
const fontRequirements = {
embedding: 'All fonts must be embedded',
subset: 'Font subsetting is allowed',
encoding: 'Unicode mapping required for text extraction',
type: 'TrueType and Type 1 fonts supported'
};
// Test files for font compliance markers
const pdfFiles = await CorpusLoader.getFiles('ZUGFERD_V2_CORRECT');
const testPdf = pdfFiles.filter(f => f.endsWith('.pdf'))[0];
if (testPdf) {
const pdfBuffer = await CorpusLoader.loadFile(testPdf);
const pdfString = pdfBuffer.toString('latin1');
// Check for font markers
const hasFontInfo = pdfString.includes('/Font') ||
pdfString.includes('/BaseFont') ||
pdfString.includes('/FontDescriptor');
const hasEmbeddedFont = pdfString.includes('/FontFile') ||
pdfString.includes('/FontFile2') ||
pdfString.includes('/FontFile3');
if (hasFontInfo) {
st.pass(`${path.basename(testPdf)}: Contains font information`);
}
if (hasEmbeddedFont) {
st.pass(`${path.basename(testPdf)}: Contains embedded font data`);
}
}
});
// Test 5: Transparency and Layers Compliance
t.test('PDF/A-3 transparency restrictions', async (st) => {
// PDF/A-3 has specific requirements for transparency
const transparencyRules = {
blendModes: ['Normal', 'Compatible'], // Only these are allowed
transparency: 'Real transparency is allowed in PDF/A-3',
layers: 'Optional Content (layers) allowed with restrictions'
};
// In production, would check PDF for transparency usage
expect(transparencyRules.blendModes).toContain('Normal');
st.pass('✓ PDF/A-3 transparency rules defined');
});
// Test 6: Metadata Requirements
t.test('PDF/A-3 metadata requirements', async (st) => {
const requiredMetadata = {
'dc:title': 'Document title',
'dc:creator': 'Document author',
'xmp:CreateDate': 'Creation date',
'xmp:ModifyDate': 'Modification date',
'pdf:Producer': 'PDF producer',
'pdfaid:part': '3', // PDF/A-3
'pdfaid:conformance': 'B' // Level B (basic)
};
// Test metadata structure
const xmpTemplate = `<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
<pdfaid:part>3</pdfaid:part>
<pdfaid:conformance>B</pdfaid:conformance>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="r"?>`;
expect(xmpTemplate).toInclude('pdfaid:part>3');
expect(xmpTemplate).toInclude('pdfaid:conformance>B');
st.pass('✓ PDF/A-3 metadata structure is compliant');
});
// Test 7: Attachment Relationship Types
t.test('PDF/A-3 attachment relationships', async (st) => {
// PDF/A-3 defines specific relationship types for embedded files
const validRelationships = [
'Source', // The embedded file is the source of the PDF
'Alternative', // Alternative representation (ZUGFeRD/Factur-X use this)
'Supplement', // Supplementary information
'Data', // Data file
'Unspecified' // When relationship is not specified
];
// ZUGFeRD/Factur-X specific
const zugferdRelationship = 'Alternative';
expect(validRelationships).toContain(zugferdRelationship);
st.pass('✓ ZUGFeRD uses correct PDF/A-3 relationship type: Alternative');
});
// Test 8: Security Restrictions
t.test('PDF/A-3 security restrictions', async (st) => {
// PDF/A-3 prohibits encryption and security handlers
const securityRestrictions = {
encryption: 'Not allowed',
passwords: 'Not allowed',
permissions: 'Not allowed',
digitalSignatures: 'Allowed with restrictions'
};
// Check test PDFs for encryption
const pdfFiles = await CorpusLoader.getFiles('ZUGFERD_V2_CORRECT');
const testPdf = pdfFiles.filter(f => f.endsWith('.pdf'))[0];
if (testPdf) {
const pdfBuffer = await CorpusLoader.loadFile(testPdf);
const pdfString = pdfBuffer.toString('latin1', 0, 1024); // Check header
// Check for encryption markers
const hasEncryption = pdfString.includes('/Encrypt');
expect(hasEncryption).toBeFalse();
st.pass(`${path.basename(testPdf)}: No encryption detected (PDF/A-3 compliant)`);
}
});
// Test 9: JavaScript and Actions
t.test('PDF/A-3 JavaScript and actions restrictions', async (st) => {
// PDF/A-3 prohibits JavaScript and certain actions
const prohibitedFeatures = [
'JavaScript',
'Launch actions',
'Sound actions',
'Movie actions',
'ResetForm actions',
'ImportData actions'
];
const allowedActions = [
'GoTo actions', // Navigation within document
'GoToR actions', // With restrictions
'URI actions' // With restrictions
];
// In production, would scan PDF for these features
for (const feature of prohibitedFeatures) {
st.pass(`✓ Check for prohibited feature: ${feature}`);
}
});
// Test 10: File Structure Compliance
t.test('PDF/A-3 file structure requirements', async (st) => {
// Test basic PDF structure requirements
const structureRequirements = {
header: '%PDF-1.4 or higher',
eofMarker: '%%EOF',
xrefTable: 'Required',
linearized: 'Optional but recommended',
objectStreams: 'Allowed in PDF/A-3',
compressedXref: 'Allowed in PDF/A-3'
};
const pdfFiles = await CorpusLoader.getFiles('ZUGFERD_V2_CORRECT');
const testPdf = pdfFiles.filter(f => f.endsWith('.pdf'))[0];
if (testPdf) {
const pdfBuffer = await CorpusLoader.loadFile(testPdf);
// Check PDF header
const header = pdfBuffer.subarray(0, 8).toString();
expect(header).toMatch(/^%PDF-\d\.\d/);
// Check for EOF marker
const tail = pdfBuffer.subarray(-32).toString();
expect(tail).toInclude('%%EOF');
st.pass(`${path.basename(testPdf)}: Basic PDF structure is valid`);
}
});
// Performance summary
const perfSummary = await PerformanceTracker.getSummary('pdfa3-compliance');
if (perfSummary) {
console.log('\nPDF/A-3 Compliance Test Performance:');
console.log(` Average: ${perfSummary.average.toFixed(2)}ms`);
}
});
tap.start();