316 lines
10 KiB
Python
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 "<script>" 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 "<tag>" 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 "&" in escaped
|
|
assert "<" in escaped
|
|
assert ">" in escaped
|
|
assert """ in escaped
|
|
assert "'" 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")
|
|
|