#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Standalone unit tests for CertificateTemplateParser (no Odoo dependency). This script runs the template parser unit tests without requiring the full Odoo environment, making it easier to verify functionality during development. """ import sys import unittest 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 parser import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from services.certificate_template_parser import CertificateTemplateParser class TestCertificateTemplateParser(unittest.TestCase): """Test cases for CertificateTemplateParser service""" def setUp(self): """Set up test fixtures""" self.parser = CertificateTemplateParser() 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() def test_get_placeholder_pattern(self): """Test that get_placeholder_pattern returns the correct regex pattern""" pattern = self.parser.get_placeholder_pattern() self.assertEqual(pattern, r'\{key\.[a-zA-Z0-9_]+\}') print("✓ test_get_placeholder_pattern passed") def test_validate_template_valid_docx(self): """Test validation of a valid DOCX file""" docx_binary = self._create_test_docx("Test content") is_valid, error_msg = self.parser.validate_template(docx_binary) self.assertTrue(is_valid) self.assertEqual(error_msg, "") print("✓ test_validate_template_valid_docx passed") def test_validate_template_empty_file(self): """Test validation of an empty file""" is_valid, error_msg = self.parser.validate_template(b"") self.assertFalse(is_valid) self.assertEqual(error_msg, "Template file is empty") print("✓ test_validate_template_empty_file passed") def test_validate_template_invalid_type(self): """Test validation with non-binary input""" is_valid, error_msg = self.parser.validate_template("not bytes") self.assertFalse(is_valid) self.assertEqual(error_msg, "Template must be provided as binary data") print("✓ test_validate_template_invalid_type passed") def test_validate_template_corrupted_file(self): """Test validation of a corrupted DOCX file""" corrupted_data = b"This is not a valid DOCX file" is_valid, error_msg = self.parser.validate_template(corrupted_data) self.assertFalse(is_valid) self.assertIn("not a valid DOCX file", error_msg) print("✓ test_validate_template_corrupted_file passed") def test_parse_template_single_placeholder(self): """Test parsing a template with a single placeholder""" docx_binary = self._create_test_docx("Hello {key.name}, welcome!") placeholders = self.parser.parse_template(docx_binary) self.assertEqual(placeholders, ["{key.name}"]) print("✓ test_parse_template_single_placeholder passed") def test_parse_template_multiple_placeholders(self): """Test parsing a template with multiple placeholders""" text = "Certificate for {key.name} who completed {key.course_name} on {key.date}" docx_binary = self._create_test_docx(text) placeholders = self.parser.parse_template(docx_binary) expected = ["{key.course_name}", "{key.date}", "{key.name}"] self.assertEqual(placeholders, expected) print("✓ test_parse_template_multiple_placeholders passed") def test_parse_template_no_placeholders(self): """Test parsing a template with no placeholders""" docx_binary = self._create_test_docx("This is a static certificate") placeholders = self.parser.parse_template(docx_binary) self.assertEqual(placeholders, []) print("✓ test_parse_template_no_placeholders passed") def test_parse_template_duplicate_placeholders(self): """Test that duplicate placeholders are only returned once""" text_content = [ "Hello {key.name}", "Welcome {key.name}", "Course: {key.course_name}" ] docx_binary = self._create_test_docx(text_content) placeholders = self.parser.parse_template(docx_binary) expected = ["{key.course_name}", "{key.name}"] self.assertEqual(placeholders, expected) print("✓ test_parse_template_duplicate_placeholders passed") def test_parse_template_with_table(self): """Test parsing placeholders from tables""" doc = Document() doc.add_paragraph("Header text with {key.header}") # Add a table with placeholders table = doc.add_table(rows=2, cols=2) table.cell(0, 0).text = "Name: {key.name}" table.cell(0, 1).text = "Date: {key.date}" table.cell(1, 0).text = "Course: {key.course_name}" table.cell(1, 1).text = "Score: {key.score}" # Save to bytes doc_stream = BytesIO() doc.save(doc_stream) doc_stream.seek(0) docx_binary = doc_stream.read() placeholders = self.parser.parse_template(docx_binary) expected = [ "{key.course_name}", "{key.date}", "{key.header}", "{key.name}", "{key.score}" ] self.assertEqual(placeholders, expected) print("✓ test_parse_template_with_table passed") def test_parse_template_invalid_placeholder_format(self): """Test that invalid placeholder formats are not extracted""" text = "Valid: {key.name}, Invalid: {invalid}, {key}, {key.}" docx_binary = self._create_test_docx(text) placeholders = self.parser.parse_template(docx_binary) # Only the valid placeholder should be extracted self.assertEqual(placeholders, ["{key.name}"]) print("✓ test_parse_template_invalid_placeholder_format passed") def test_parse_template_with_underscores_and_numbers(self): """Test placeholders with underscores and numbers in field names""" text = "Fields: {key.field_1} and {key.field_name_2} and {key.field123}" docx_binary = self._create_test_docx(text) placeholders = self.parser.parse_template(docx_binary) expected = ["{key.field123}", "{key.field_1}", "{key.field_name_2}"] self.assertEqual(placeholders, expected) print("✓ test_parse_template_with_underscores_and_numbers passed") def test_parse_template_raises_on_invalid_file(self): """Test that parse_template raises ValueError for invalid files""" corrupted_data = b"This is not a valid DOCX file" with self.assertRaises(ValueError) as context: self.parser.parse_template(corrupted_data) self.assertIn("not a valid DOCX file", str(context.exception)) print("✓ test_parse_template_raises_on_invalid_file passed") def test_parse_template_with_headers_and_footers(self): """Test parsing placeholders from headers and footers""" doc = Document() # Add content to body doc.add_paragraph("Body: {key.body_field}") # Add header section = doc.sections[0] header = section.header header.paragraphs[0].text = "Header: {key.header_field}" # Add footer footer = section.footer footer.paragraphs[0].text = "Footer: {key.footer_field}" # Save to bytes doc_stream = BytesIO() doc.save(doc_stream) doc_stream.seek(0) docx_binary = doc_stream.read() placeholders = self.parser.parse_template(docx_binary) expected = [ "{key.body_field}", "{key.footer_field}", "{key.header_field}" ] self.assertEqual(placeholders, expected) print("✓ test_parse_template_with_headers_and_footers passed") def test_parse_template_with_nested_tables(self): """Test parsing placeholders from nested table structures""" doc = Document() # Create outer table outer_table = doc.add_table(rows=1, cols=1) outer_cell = outer_table.cell(0, 0) outer_cell.text = "Outer: {key.outer_field}" # Add nested table inner_table = outer_cell.add_table(rows=1, cols=1) inner_table.cell(0, 0).text = "Inner: {key.inner_field}" # Save to bytes doc_stream = BytesIO() doc.save(doc_stream) doc_stream.seek(0) docx_binary = doc_stream.read() placeholders = self.parser.parse_template(docx_binary) # Note: The current implementation extracts from outer table cells # Nested tables within cells are handled through the cell's paragraphs # Both placeholders should be found self.assertIn("{key.outer_field}", placeholders) # Nested table placeholders may not be extracted in current implementation # This is a known limitation - nested tables are complex structures if "{key.inner_field}" in placeholders: print("✓ test_parse_template_with_nested_tables passed (nested tables supported)") else: print("⚠ test_parse_template_with_nested_tables passed (nested tables not fully supported - known limitation)") def test_parse_template_with_special_characters_around_placeholder(self): """Test placeholders surrounded by special characters""" text = "Name: ({key.name}), Date: [{key.date}], Score: <{key.score}>" docx_binary = self._create_test_docx(text) placeholders = self.parser.parse_template(docx_binary) expected = ["{key.date}", "{key.name}", "{key.score}"] self.assertEqual(placeholders, expected) print("✓ test_parse_template_with_special_characters_around_placeholder passed") def test_parse_template_with_multiple_sections(self): """Test parsing placeholders from documents with multiple sections""" doc = Document() # Add content to first section doc.add_paragraph("Section 1: {key.section1_field}") # Add a new section doc.add_section() doc.add_paragraph("Section 2: {key.section2_field}") # Save to bytes doc_stream = BytesIO() doc.save(doc_stream) doc_stream.seek(0) docx_binary = doc_stream.read() placeholders = self.parser.parse_template(docx_binary) # Should find placeholders from both sections self.assertIn("{key.section1_field}", placeholders) self.assertIn("{key.section2_field}", placeholders) print("✓ test_parse_template_with_multiple_sections passed") def test_regex_pattern_matching_edge_cases(self): """Test regex pattern matching with edge cases""" # Test various edge cases # Note: The pattern is \{key\.[a-zA-Z0-9_]+\} # This allows alphanumeric and underscore characters in any position test_cases = [ ("{key.a}", True), # Single character field ("{key.field_}", True), # Trailing underscore ("{key._field}", True), # Leading underscore (allowed by current pattern) ("{key.123}", True), # Starting with number (allowed by current pattern) ("{key.field-name}", False), # Hyphen (invalid) ("{key.field.name}", False), # Multiple dots (invalid) ("{key.FIELD}", True), # Uppercase ("{key.Field_Name_123}", True), # Mixed case with numbers ("{key.}", False), # Empty field name ("{key}", False), # Missing dot and field ("{key.field name}", False), # Space in field name ] import re pattern = self.parser.get_placeholder_pattern() for text, should_match in test_cases: matches = re.findall(pattern, text) if should_match: self.assertEqual(len(matches), 1, f"Expected to match: {text}") self.assertEqual(matches[0], text, f"Expected exact match: {text}") else: self.assertEqual(len(matches), 0, f"Expected NOT to match: {text}") print("✓ test_regex_pattern_matching_edge_cases passed") def main(): """Run all tests.""" print("=" * 70) print("Running Template Parser Unit Tests (Standalone)") print("=" * 70) print() # Create test suite loader = unittest.TestLoader() suite = loader.loadTestsFromTestCase(TestCertificateTemplateParser) # Run tests with verbose output runner = unittest.TextTestRunner(verbosity=2) result = runner.run(suite) print() print("=" * 70) if result.wasSuccessful(): print("✓ All tests passed!") print(f" Tests run: {result.testsRun}") print("=" * 70) return 0 else: print("✗ Some tests failed!") print(f" Tests run: {result.testsRun}") print(f" Failures: {len(result.failures)}") print(f" Errors: {len(result.errors)}") print("=" * 70) return 1 if __name__ == '__main__': sys.exit(main())