#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Standalone property-based test for PDF conversion. This test can run without the full Odoo environment by importing the generator service directly from the local module structure. Feature: survey-custom-certificate-template, Property 15: PDF conversion Validates: Requirements 5.4 """ import sys import os import tempfile import logging from io import BytesIO # Add parent directory to path to import services sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) try: from hypothesis import given, settings, HealthCheck, assume HYPOTHESIS_AVAILABLE = True except ImportError: print("ERROR: Hypothesis is not installed. Install with: pip install hypothesis") sys.exit(1) try: from docx import Document DOCX_AVAILABLE = True except ImportError: print("ERROR: python-docx is not installed. Install with: pip install python-docx") sys.exit(1) # Import the generator service directly from services.certificate_generator import CertificateGenerator # Import our custom strategies from hypothesis_strategies import ( docx_with_placeholders, docx_with_mappings_and_data, ) _logger = logging.getLogger(__name__) def test_property_15_pdf_conversion(): """ Feature: survey-custom-certificate-template, Property 15: PDF conversion For any generated certificate DOCX file, the system should convert it to PDF format. Validates: Requirements 5.4 This property test verifies that: 1. Any valid DOCX file can be converted to PDF 2. The conversion produces non-empty PDF content 3. The PDF content has the correct PDF file signature 4. The conversion completes without errors 5. Temporary files are cleaned up properly """ print("\nTesting Property 15: PDF conversion") print("=" * 60) generator = CertificateGenerator() # Check if LibreOffice is available is_available, error_msg = CertificateGenerator.check_libreoffice_availability() if not is_available: print(f"⚠ SKIPPED: LibreOffice not available: {error_msg}") print(" PDF conversion tests require LibreOffice to be installed") return True # Skip but don't fail test_count = 0 @given(docx_binary=docx_with_placeholders(min_placeholders=0, max_placeholders=5)) @settings( max_examples=100, deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow] ) def check_pdf_conversion(docx_binary): nonlocal test_count test_count += 1 docx_data, placeholders = docx_binary # Assume we have valid DOCX data assume(len(docx_data) > 0) # Create a temporary DOCX file for conversion temp_docx = tempfile.NamedTemporaryFile( suffix='.docx', delete=False ) temp_docx_path = temp_docx.name try: # Write the DOCX data to the temporary file temp_docx.write(docx_data) temp_docx.close() # Property 1: The conversion should complete without raising exceptions try: pdf_content = generator.convert_to_pdf(temp_docx_path) except Exception as e: raise AssertionError( f"PDF conversion raised an exception: {type(e).__name__}: {str(e)}" ) # Property 2: The PDF content should not be None if pdf_content is None: raise AssertionError("PDF conversion returned None instead of PDF content") # Property 3: The PDF content should not be empty if len(pdf_content) == 0: raise AssertionError("PDF conversion produced empty content") # Property 4: The PDF content should have the correct PDF file signature # PDF files start with "%PDF-" (bytes: 0x25 0x50 0x44 0x46 0x2D) if not pdf_content.startswith(b'%PDF-'): raise AssertionError( f"PDF content does not have valid PDF signature. " f"First 10 bytes: {pdf_content[:10]}" ) # Property 5: The PDF should have a reasonable minimum size # A minimal PDF is typically at least 100 bytes if len(pdf_content) <= 100: raise AssertionError( f"PDF content is suspiciously small ({len(pdf_content)} bytes). " f"This may indicate an incomplete conversion." ) # Property 6: The PDF should contain the PDF EOF marker # PDF files typically end with "%%EOF" if b'%%EOF' not in pdf_content: raise AssertionError("PDF content does not contain the expected EOF marker") finally: # Clean up the temporary DOCX file if os.path.exists(temp_docx_path): try: os.unlink(temp_docx_path) except Exception as e: _logger.warning(f"Failed to delete temporary file {temp_docx_path}: {e}") try: check_pdf_conversion() print(f"✓ Property 15 verified across {test_count} test cases") print(" All DOCX files were successfully converted to valid PDF format") return True except Exception as e: print(f"✗ Property 15 FAILED after {test_count} test cases") print(f" Error: {e}") import traceback traceback.print_exc() return False def test_property_15_pdf_conversion_with_replacements(): """ Feature: survey-custom-certificate-template, Property 15: PDF conversion (with replacements) For any generated certificate DOCX file (with placeholder replacements), the system should convert it to PDF format. Validates: Requirements 5.4 This test verifies that PDF conversion works correctly even after placeholder replacements have been made to the document. """ print("\nTesting Property 15: PDF conversion (with placeholder replacements)") print("=" * 60) generator = CertificateGenerator() # Check if LibreOffice is available is_available, error_msg = CertificateGenerator.check_libreoffice_availability() if not is_available: print(f"⚠ SKIPPED: LibreOffice not available: {error_msg}") return True # Skip but don't fail test_count = 0 @given(test_data=docx_with_mappings_and_data()) @settings( max_examples=50, deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow] ) def check_pdf_conversion_with_replacements(test_data): nonlocal test_count test_count += 1 docx_binary, mappings, data = test_data # Assume we have valid data assume(len(docx_binary) > 0) # Load the document and perform placeholder replacements doc = Document(BytesIO(docx_binary)) modified_doc = generator.replace_placeholders(doc, mappings, data) # Save the modified document to a temporary file temp_docx = tempfile.NamedTemporaryFile( suffix='.docx', delete=False ) temp_docx_path = temp_docx.name temp_docx.close() try: # Save the modified document modified_doc.save(temp_docx_path) # Convert to PDF pdf_content = generator.convert_to_pdf(temp_docx_path) # Verify the PDF is valid if pdf_content is None: raise AssertionError("PDF conversion returned None") if len(pdf_content) == 0: raise AssertionError("PDF conversion produced empty content") if not pdf_content.startswith(b'%PDF-'): raise AssertionError("PDF does not have valid signature") if b'%%EOF' not in pdf_content: raise AssertionError("PDF does not contain EOF marker") finally: # Clean up if os.path.exists(temp_docx_path): try: os.unlink(temp_docx_path) except Exception as e: _logger.warning(f"Failed to delete temporary file {temp_docx_path}: {e}") try: check_pdf_conversion_with_replacements() print(f"✓ Property 15 (with replacements) verified across {test_count} test cases") print(" All modified DOCX files were successfully converted to valid PDF format") return True except Exception as e: print(f"✗ Property 15 (with replacements) FAILED after {test_count} test cases") print(f" Error: {e}") import traceback traceback.print_exc() return False def main(): """Run the property tests.""" print("=" * 60) print("Property-Based Test: PDF Conversion") print("=" * 60) # Run both test variants success1 = test_property_15_pdf_conversion() success2 = test_property_15_pdf_conversion_with_replacements() print("\n" + "=" * 60) if success1 and success2: print("✓ All property tests PASSED") print("=" * 60) return 0 else: print("✗ Some property tests FAILED") print("=" * 60) return 1 if __name__ == '__main__': sys.exit(main())