517 lines
20 KiB
Python
Executable File
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)
|