Big-Link-Man/tests/unit/test_template_service.py

316 lines
10 KiB
Python

"""
Unit tests for template service
"""
import pytest
import json
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch, mock_open
from src.templating.service import TemplateService
from src.database.models import GeneratedContent, SiteDeployment
@pytest.fixture
def mock_content_repo():
return Mock()
@pytest.fixture
def mock_site_deployment_repo():
return Mock()
@pytest.fixture
def template_service(mock_content_repo):
return TemplateService(content_repo=mock_content_repo)
@pytest.fixture
def mock_site_deployment():
deployment = Mock(spec=SiteDeployment)
deployment.id = 1
deployment.custom_hostname = "test.example.com"
deployment.site_name = "Test Site"
return deployment
class TestGetAvailableTemplates:
def test_returns_list_of_template_names(self, template_service):
"""Test that available templates are returned"""
templates = template_service.get_available_templates()
assert isinstance(templates, list)
assert len(templates) >= 4
assert "basic" in templates
assert "modern" in templates
assert "classic" in templates
assert "minimal" in templates
def test_templates_are_sorted(self, template_service):
"""Test that templates are returned in sorted order"""
templates = template_service.get_available_templates()
assert templates == sorted(templates)
class TestLoadTemplate:
def test_load_valid_template(self, template_service):
"""Test loading a valid template"""
template_content = template_service.load_template("basic")
assert isinstance(template_content, str)
assert len(template_content) > 0
assert "<!DOCTYPE html>" in template_content
assert "{{ title }}" in template_content
assert "{{ content }}" in template_content
def test_load_all_templates(self, template_service):
"""Test that all templates can be loaded"""
for template_name in ["basic", "modern", "classic", "minimal"]:
template_content = template_service.load_template(template_name)
assert len(template_content) > 0
def test_template_caching(self, template_service):
"""Test that templates are cached after first load"""
template_service.load_template("basic")
assert "basic" in template_service._template_cache
cached_content = template_service._template_cache["basic"]
loaded_content = template_service.load_template("basic")
assert cached_content == loaded_content
def test_load_nonexistent_template(self, template_service):
"""Test that loading nonexistent template raises error"""
with pytest.raises(FileNotFoundError) as exc_info:
template_service.load_template("nonexistent")
assert "nonexistent" in str(exc_info.value)
assert "not found" in str(exc_info.value).lower()
class TestSelectTemplateForContent:
def test_select_random_when_no_site_deployment(self, template_service):
"""Test random selection when site_deployment_id is None"""
template_name = template_service.select_template_for_content(
site_deployment_id=None,
site_deployment_repo=None
)
available_templates = template_service.get_available_templates()
assert template_name in available_templates
@patch('src.templating.service.get_config')
def test_use_existing_mapping(
self,
mock_get_config,
template_service,
mock_site_deployment,
mock_site_deployment_repo
):
"""Test using existing template mapping from config"""
mock_config = Mock()
mock_config.templates.mappings = {
"test.example.com": "modern"
}
mock_get_config.return_value = mock_config
mock_site_deployment_repo.get_by_id.return_value = mock_site_deployment
template_name = template_service.select_template_for_content(
site_deployment_id=1,
site_deployment_repo=mock_site_deployment_repo
)
assert template_name == "modern"
mock_site_deployment_repo.get_by_id.assert_called_once_with(1)
@patch('src.templating.service.get_config')
@patch('builtins.open', new_callable=mock_open)
@patch('pathlib.Path.exists', return_value=True)
def test_create_new_mapping(
self,
mock_exists,
mock_file,
mock_get_config,
template_service,
mock_site_deployment,
mock_site_deployment_repo
):
"""Test creating new mapping when hostname not in config"""
mock_config = Mock()
mock_config.templates.mappings = {}
mock_get_config.return_value = mock_config
mock_file.return_value.read.return_value = json.dumps({
"templates": {"default": "basic", "mappings": {}}
})
mock_site_deployment_repo.get_by_id.return_value = mock_site_deployment
template_name = template_service.select_template_for_content(
site_deployment_id=1,
site_deployment_repo=mock_site_deployment_repo
)
available_templates = template_service.get_available_templates()
assert template_name in available_templates
class TestFormatContent:
def test_format_with_basic_template(self, template_service):
"""Test formatting content with basic template"""
content = "<h2>Test Section</h2><p>Test paragraph.</p>"
title = "Test Article"
meta = "Test meta description"
formatted = template_service.format_content(
content=content,
title=title,
meta_description=meta,
template_name="basic"
)
assert "<!DOCTYPE html>" in formatted
assert "Test Article" in formatted
assert "Test meta description" in formatted
assert "<h2>Test Section</h2>" in formatted
assert "<p>Test paragraph.</p>" in formatted
def test_format_with_all_templates(self, template_service):
"""Test formatting with all available templates"""
content = "<h2>Section</h2><p>Content.</p>"
title = "Title"
meta = "Description"
for template_name in ["basic", "modern", "classic", "minimal"]:
formatted = template_service.format_content(
content=content,
title=title,
meta_description=meta,
template_name=template_name
)
assert len(formatted) > 0
assert "Title" in formatted
assert "Description" in formatted
assert content in formatted
def test_html_escaping_in_title(self, template_service):
"""Test that HTML is escaped in title"""
content = "<p>Content</p>"
title = "Title with <script>alert('xss')</script>"
meta = "Description"
formatted = template_service.format_content(
content=content,
title=title,
meta_description=meta,
template_name="basic"
)
assert "&lt;script&gt;" in formatted
assert "<script>" not in formatted or "<script>" in content
def test_html_escaping_in_meta(self, template_service):
"""Test that HTML is escaped in meta description"""
content = "<p>Content</p>"
title = "Title"
meta = "Description with <tag>"
formatted = template_service.format_content(
content=content,
title=title,
meta_description=meta,
template_name="basic"
)
assert "&lt;tag&gt;" in formatted
@patch('src.templating.service.get_config')
def test_fallback_to_default_template(
self,
mock_get_config,
template_service
):
"""Test fallback to default template when template not found"""
mock_config = Mock()
mock_config.templates.default = "basic"
mock_get_config.return_value = mock_config
content = "<p>Content</p>"
title = "Title"
meta = "Description"
formatted = template_service.format_content(
content=content,
title=title,
meta_description=meta,
template_name="nonexistent"
)
assert len(formatted) > 0
assert "Title" in formatted
class TestEscapeHtml:
def test_escape_special_characters(self, template_service):
"""Test HTML special character escaping"""
text = "Test & <script> \"quotes\" 'single' >"
escaped = template_service._escape_html(text)
assert "&amp;" in escaped
assert "&lt;" in escaped
assert "&gt;" in escaped
assert "&quot;" in escaped
assert "&#39;" in escaped
assert "<script>" not in escaped
def test_escape_empty_string(self, template_service):
"""Test escaping empty string"""
escaped = template_service._escape_html("")
assert escaped == ""
def test_escape_normal_text(self, template_service):
"""Test that normal text is unchanged"""
text = "Normal text without special chars"
escaped = template_service._escape_html(text)
assert escaped == text
class TestPersistTemplateMapping:
@patch('builtins.open', new_callable=mock_open)
@patch('pathlib.Path.exists', return_value=True)
def test_persist_new_mapping(
self,
mock_exists,
mock_file,
template_service
):
"""Test persisting a new template mapping"""
config_data = {
"templates": {
"default": "basic",
"mappings": {}
}
}
mock_file.return_value.read.return_value = json.dumps(config_data)
template_service._persist_template_mapping("new.example.com", "modern")
assert mock_file.call_count >= 2
@patch('builtins.open', side_effect=Exception("File error"))
@patch('pathlib.Path.exists', return_value=True)
def test_persist_handles_errors_gracefully(
self,
mock_exists,
mock_file,
template_service
):
"""Test that persist handles errors without raising"""
template_service._persist_template_mapping("test.com", "basic")