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(' { 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 = ` 3 B `; 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();