survey_custom_certificate_t.../tests/test_certificate_generator_standalone.py
2025-11-29 08:46:04 +07:00

517 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Standalone unit tests for CertificateGenerator (no Odoo dependency).
This script runs the certificate generator unit tests without requiring the full
Odoo environment, making it easier to verify functionality during development.
Task 5.5: Write unit tests for certificate generator
- Test placeholder replacement logic
- Test data retrieval from models
- Test error handling for missing LibreOffice
- Requirements: 5.2, 5.3, 6.2, 6.4
"""
import sys
import unittest
import os
import tempfile
from io import BytesIO
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)
# Add parent directory to path to import the generator
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from services.certificate_generator import CertificateGenerator
class TestCertificateGeneratorStandalone(unittest.TestCase):
"""Standalone test cases for CertificateGenerator service"""
def setUp(self):
"""Set up test fixtures"""
self.generator = CertificateGenerator()
def _create_test_docx(self, text_content):
"""
Helper method to create a DOCX file with given text content.
Args:
text_content: String or list of strings to add as paragraphs
Returns:
bytes: Binary content of the created DOCX file
"""
doc = Document()
if isinstance(text_content, str):
text_content = [text_content]
for text in text_content:
doc.add_paragraph(text)
# Save to BytesIO
doc_stream = BytesIO()
doc.save(doc_stream)
doc_stream.seek(0)
return doc_stream.read()
# ========================================================================
# PLACEHOLDER REPLACEMENT LOGIC TESTS (Requirement 5.2)
# ========================================================================
def test_build_replacement_dict_with_custom_text(self):
"""Test building replacement dictionary with custom text"""
mappings = {
'placeholders': [
{
'key': '{key.name}',
'value_type': 'custom_text',
'custom_text': 'John Doe'
}
]
}
data = {}
replacements = self.generator._build_replacement_dict(mappings, data)
self.assertEqual(replacements, {'{key.name}': 'John Doe'})
def test_build_replacement_dict_with_dynamic_data(self):
"""Test building replacement dictionary with dynamic data"""
mappings = {
'placeholders': [
{
'key': '{key.name}',
'value_type': 'user_field',
'value_field': 'partner_name'
},
{
'key': '{key.course}',
'value_type': 'survey_field',
'value_field': 'survey_title'
}
]
}
data = {
'partner_name': 'Jane Smith',
'survey_title': 'Python Programming'
}
replacements = self.generator._build_replacement_dict(mappings, data)
expected = {
'{key.name}': 'Jane Smith',
'{key.course}': 'Python Programming'
}
self.assertEqual(replacements, expected)
def test_build_replacement_dict_with_missing_data(self):
"""Test that missing data results in empty string (Requirement 6.4)"""
mappings = {
'placeholders': [
{
'key': '{key.name}',
'value_type': 'user_field',
'value_field': 'partner_name'
}
]
}
data = {} # No data provided
replacements = self.generator._build_replacement_dict(mappings, data)
self.assertEqual(replacements, {'{key.name}': ''})
def test_replace_placeholders_in_paragraph(self):
"""Test replacing placeholders in a simple paragraph"""
# Create a document with placeholders
doc = Document()
doc.add_paragraph("Hello {key.name}, welcome to {key.course}!")
mappings = {
'placeholders': [
{'key': '{key.name}', 'value_type': 'custom_text', 'custom_text': 'Alice'},
{'key': '{key.course}', 'value_type': 'custom_text', 'custom_text': 'Data Science'}
]
}
data = {}
# Replace placeholders
result_doc = self.generator.replace_placeholders(doc, mappings, data)
# Check the result
result_text = result_doc.paragraphs[0].text
self.assertEqual(result_text, "Hello Alice, welcome to Data Science!")
def test_replace_placeholders_in_table(self):
"""Test replacing placeholders in tables"""
# Create a document with a table containing placeholders
doc = Document()
table = doc.add_table(rows=2, cols=2)
table.cell(0, 0).text = "Name: {key.name}"
table.cell(0, 1).text = "Course: {key.course}"
table.cell(1, 0).text = "Date: {key.date}"
table.cell(1, 1).text = "Score: {key.score}"
mappings = {
'placeholders': [
{'key': '{key.name}', 'value_type': 'custom_text', 'custom_text': 'Bob'},
{'key': '{key.course}', 'value_type': 'custom_text', 'custom_text': 'Math'},
{'key': '{key.date}', 'value_type': 'custom_text', 'custom_text': '2024-01-15'},
{'key': '{key.score}', 'value_type': 'custom_text', 'custom_text': '95'}
]
}
data = {}
# Replace placeholders
result_doc = self.generator.replace_placeholders(doc, mappings, data)
# Check the results
result_table = result_doc.tables[0]
self.assertEqual(result_table.cell(0, 0).text, "Name: Bob")
self.assertEqual(result_table.cell(0, 1).text, "Course: Math")
self.assertEqual(result_table.cell(1, 0).text, "Date: 2024-01-15")
self.assertEqual(result_table.cell(1, 1).text, "Score: 95")
def test_replace_placeholders_no_change_when_no_placeholders(self):
"""Test that documents without placeholders remain unchanged"""
# Create a document without placeholders
doc = Document()
original_text = "This is a static certificate"
doc.add_paragraph(original_text)
mappings = {'placeholders': []}
data = {}
# Replace placeholders (should do nothing)
result_doc = self.generator.replace_placeholders(doc, mappings, data)
# Check the result
result_text = result_doc.paragraphs[0].text
self.assertEqual(result_text, original_text)
def test_replace_placeholders_with_headers_and_footers(self):
"""Test replacing placeholders in headers and footers"""
# Create a document with header and footer
doc = Document()
doc.add_paragraph("Body: {key.body}")
# Add header
section = doc.sections[0]
header = section.header
header.paragraphs[0].text = "Header: {key.header}"
# Add footer
footer = section.footer
footer.paragraphs[0].text = "Footer: {key.footer}"
mappings = {
'placeholders': [
{'key': '{key.body}', 'value_type': 'custom_text', 'custom_text': 'Body Text'},
{'key': '{key.header}', 'value_type': 'custom_text', 'custom_text': 'Header Text'},
{'key': '{key.footer}', 'value_type': 'custom_text', 'custom_text': 'Footer Text'}
]
}
data = {}
# Replace placeholders
result_doc = self.generator.replace_placeholders(doc, mappings, data)
# Check the results
self.assertEqual(result_doc.paragraphs[0].text, "Body: Body Text")
self.assertEqual(result_doc.sections[0].header.paragraphs[0].text, "Header: Header Text")
self.assertEqual(result_doc.sections[0].footer.paragraphs[0].text, "Footer: Footer Text")
# ========================================================================
# DATA RETRIEVAL TESTS (Requirement 5.3)
# ========================================================================
def test_data_retrieval_with_nested_fields(self):
"""Test data retrieval from nested field paths"""
mappings = {
'placeholders': [
{
'key': '{key.partner_name}',
'value_type': 'user_field',
'value_field': 'partner.name'
},
{
'key': '{key.partner_email}',
'value_type': 'user_field',
'value_field': 'partner.email'
}
]
}
# Simulate nested data structure
data = {
'partner.name': 'Alice Johnson',
'partner.email': 'alice@example.com'
}
replacements = self.generator._build_replacement_dict(mappings, data)
expected = {
'{key.partner_name}': 'Alice Johnson',
'{key.partner_email}': 'alice@example.com'
}
self.assertEqual(replacements, expected)
def test_error_handling_for_non_string_values(self):
"""Test that non-string values are converted properly"""
mappings = {
'placeholders': [
{'key': '{key.score}', 'value_type': 'user_field', 'value_field': 'score'},
{'key': '{key.passed}', 'value_type': 'user_field', 'value_field': 'passed'},
{'key': '{key.date}', 'value_type': 'user_field', 'value_field': 'completion_date'}
]
}
# Provide non-string values
data = {
'score': 95,
'passed': True,
'completion_date': None
}
replacements = self.generator._build_replacement_dict(mappings, data)
# All values should be converted to strings
self.assertEqual(replacements['{key.score}'], '95')
self.assertEqual(replacements['{key.passed}'], 'True')
self.assertEqual(replacements['{key.date}'], '') # None becomes empty string
def test_error_handling_for_malformed_mappings(self):
"""Test that malformed mappings are handled gracefully (Requirement 6.4)"""
# Test with missing 'key' field
mappings = {
'placeholders': [
{
'value_type': 'custom_text',
'custom_text': 'Some Value'
}
]
}
data = {}
# Should not raise an exception, just skip the malformed mapping
replacements = self.generator._build_replacement_dict(mappings, data)
self.assertEqual(replacements, {})
# Test with missing 'value_type' field
mappings = {
'placeholders': [
{
'key': '{key.test}',
'custom_text': 'Some Value'
}
]
}
# Should handle gracefully and use empty string
replacements = self.generator._build_replacement_dict(mappings, data)
self.assertEqual(replacements, {'{key.test}': ''})
# ========================================================================
# LIBREOFFICE ERROR HANDLING TESTS (Requirement 6.2)
# ========================================================================
def test_libreoffice_availability_check(self):
"""Test LibreOffice availability checking (Requirement 6.2)"""
# Reset the cached check to ensure fresh test
CertificateGenerator.reset_libreoffice_check()
# Check LibreOffice availability
is_available, error_message = CertificateGenerator.check_libreoffice_availability()
# The result should be consistent
self.assertIsInstance(is_available, bool)
self.assertIsInstance(error_message, str)
# If not available, error message should not be empty
if not is_available:
self.assertTrue(len(error_message) > 0)
print(f"\nLibreOffice not available: {error_message}")
else:
# If available, error message should be empty
self.assertEqual(error_message, '')
print("\nLibreOffice is available")
# Second call should return cached result
is_available_2, error_message_2 = CertificateGenerator.check_libreoffice_availability()
self.assertEqual(is_available, is_available_2)
self.assertEqual(error_message, error_message_2)
def test_convert_to_pdf_handles_missing_libreoffice(self):
"""Test that convert_to_pdf handles missing LibreOffice gracefully (Requirement 6.2)"""
# Create a temporary DOCX file
template = self._create_test_docx("Test certificate content")
temp_docx = tempfile.NamedTemporaryFile(suffix='.docx', delete=False)
temp_docx_path = temp_docx.name
temp_docx.write(template)
temp_docx.close()
try:
# Save the original LibreOffice availability state
original_available = CertificateGenerator._libreoffice_available
original_error = CertificateGenerator._libreoffice_check_error
# Simulate LibreOffice not being available
CertificateGenerator._libreoffice_available = False
CertificateGenerator._libreoffice_check_error = "LibreOffice not found (simulated)"
# Attempt to convert to PDF
with self.assertRaises(RuntimeError) as context:
self.generator.convert_to_pdf(temp_docx_path)
# Verify the error message mentions LibreOffice
self.assertIn("LibreOffice", str(context.exception))
self.assertIn("not available", str(context.exception).lower())
print(f"\nCorrectly raised error: {context.exception}")
# Restore the original state
CertificateGenerator._libreoffice_available = original_available
CertificateGenerator._libreoffice_check_error = original_error
finally:
# Clean up temporary file
if os.path.exists(temp_docx_path):
os.unlink(temp_docx_path)
def test_convert_to_pdf_validates_file_exists(self):
"""Test that convert_to_pdf validates file existence"""
non_existent_path = "/tmp/non_existent_file_12345.docx"
with self.assertRaises(ValueError) as context:
self.generator.convert_to_pdf(non_existent_path)
self.assertIn("DOCX file not found", str(context.exception))
# ========================================================================
# INPUT VALIDATION TESTS
# ========================================================================
def test_generate_certificate_validates_inputs(self):
"""Test that generate_certificate validates required inputs"""
# Test with empty template
with self.assertRaises(ValueError) as context:
self.generator.generate_certificate(b"", {}, {})
self.assertIn("Template binary data is required", str(context.exception))
# Test with non-bytes template
with self.assertRaises(ValueError) as context:
self.generator.generate_certificate("not bytes", {}, {})
self.assertIn("Template must be provided as binary data", str(context.exception))
# Test with missing mappings
template = self._create_test_docx("Test")
with self.assertRaises(ValueError) as context:
self.generator.generate_certificate(template, {}, {})
self.assertIn("Mappings must contain 'placeholders' key", str(context.exception))
# Test with missing data
with self.assertRaises(ValueError) as context:
self.generator.generate_certificate(template, {'placeholders': []}, None)
self.assertIn("Data dictionary is required", str(context.exception))
def test_generate_certificate_handles_invalid_template(self):
"""Test that generate_certificate handles invalid DOCX files"""
invalid_template = b"This is not a valid DOCX file"
mappings = {'placeholders': []}
data = {'some_field': 'some_value'} # Provide data to pass validation
with self.assertRaises(ValueError) as context:
self.generator.generate_certificate(invalid_template, mappings, data)
self.assertIn("not a valid DOCX file", str(context.exception))
def test_generate_certificate_end_to_end_without_pdf(self):
"""Test certificate generation without PDF conversion (DOCX only)"""
# Create a template with placeholders
template = self._create_test_docx([
"Certificate of Completion",
"This certifies that {key.name} has completed {key.course}",
"Date: {key.date}"
])
mappings = {
'placeholders': [
{'key': '{key.name}', 'value_type': 'custom_text', 'custom_text': 'John Doe'},
{'key': '{key.course}', 'value_type': 'user_field', 'value_field': 'course_title'},
{'key': '{key.date}', 'value_type': 'custom_text', 'custom_text': '2024-01-15'}
]
}
data = {
'course_title': 'Advanced Python Programming'
}
# Test that the method validates inputs and processes the template
# Note: We're not testing PDF conversion here as it requires LibreOffice
# Instead, we test the DOCX processing part
try:
# This will fail at PDF conversion if LibreOffice is not available
# but should succeed in processing the DOCX part
result = self.generator.generate_certificate(template, mappings, data)
# If we get here, LibreOffice is available and conversion succeeded
self.assertIsInstance(result, bytes)
self.assertTrue(len(result) > 0)
print("\nEnd-to-end test passed with PDF conversion")
except RuntimeError as e:
# If LibreOffice is not available, that's expected
if "LibreOffice" in str(e) or "PDF conversion" in str(e):
# This is expected when LibreOffice is not installed
print(f"\nEnd-to-end test skipped PDF conversion (LibreOffice not available): {e}")
pass
else:
# Re-raise if it's a different error
raise
def run_tests():
"""Run all tests and display results"""
print("=" * 70)
print("CERTIFICATE GENERATOR UNIT TESTS (Task 5.5)")
print("=" * 70)
print("\nTesting:")
print(" - Placeholder replacement logic (Requirement 5.2)")
print(" - Data retrieval from models (Requirement 5.3)")
print(" - Error handling for missing LibreOffice (Requirement 6.2)")
print(" - Error handling for missing data (Requirement 6.4)")
print("=" * 70)
print()
# Create test suite
loader = unittest.TestLoader()
suite = loader.loadTestsFromTestCase(TestCertificateGeneratorStandalone)
# Run tests with verbose output
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Print summary
print("\n" + "=" * 70)
print("TEST SUMMARY")
print("=" * 70)
print(f"Tests run: {result.testsRun}")
print(f"Successes: {result.testsRun - len(result.failures) - len(result.errors)}")
print(f"Failures: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
print("=" * 70)
return result.wasSuccessful()
if __name__ == '__main__':
success = run_tests()
sys.exit(0 if success else 1)