324 lines
11 KiB
Python
324 lines
11 KiB
Python
"""
|
|
Integration tests for Story 4.1: Deploy Content to Cloud Storage
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from datetime import datetime
|
|
from src.deployment.bunny_storage import BunnyStorageClient, UploadResult, BunnyStorageError
|
|
from src.deployment.url_logger import URLLogger
|
|
from src.deployment.deployment_service import DeploymentService
|
|
from src.database.models import GeneratedContent, SiteDeployment, SitePage
|
|
from src.generation.url_generator import (
|
|
generate_public_url,
|
|
generate_file_path,
|
|
generate_page_file_path,
|
|
generate_slug
|
|
)
|
|
|
|
|
|
class TestURLGenerator:
|
|
"""Test URL generation functions"""
|
|
|
|
def test_generate_slug(self):
|
|
"""Test slug generation from titles"""
|
|
assert generate_slug("How to Fix Your Engine") == "how-to-fix-your-engine"
|
|
assert generate_slug("10 Best SEO Tips for 2024!") == "10-best-seo-tips-for-2024"
|
|
assert generate_slug("C++ Programming Guide") == "c-programming-guide"
|
|
assert generate_slug("Multiple Spaces") == "multiple-spaces"
|
|
|
|
def test_generate_public_url(self):
|
|
"""Test public URL generation"""
|
|
site = Mock(spec=SiteDeployment)
|
|
site.custom_hostname = "www.example.com"
|
|
site.pull_zone_bcdn_hostname = "example.b-cdn.net"
|
|
|
|
url = generate_public_url(site, "my-article.html")
|
|
assert url == "https://www.example.com/my-article.html"
|
|
|
|
site.custom_hostname = None
|
|
url = generate_public_url(site, "about.html")
|
|
assert url == "https://example.b-cdn.net/about.html"
|
|
|
|
def test_generate_file_path(self):
|
|
"""Test file path generation for articles"""
|
|
content = Mock(spec=GeneratedContent)
|
|
content.id = 42
|
|
content.title = "How to Fix Your Engine"
|
|
|
|
path = generate_file_path(content)
|
|
assert path == "how-to-fix-your-engine.html"
|
|
|
|
def test_generate_page_file_path(self):
|
|
"""Test file path generation for boilerplate pages"""
|
|
page = Mock(spec=SitePage)
|
|
page.page_type = "about"
|
|
|
|
path = generate_page_file_path(page)
|
|
assert path == "about.html"
|
|
|
|
|
|
class TestURLLogger:
|
|
"""Test URL logging functionality"""
|
|
|
|
def test_tier_number_extraction(self, tmp_path):
|
|
"""Test extracting tier numbers from tier strings"""
|
|
logger = URLLogger(logs_dir=str(tmp_path))
|
|
|
|
assert logger._extract_tier_number("tier1") == 1
|
|
assert logger._extract_tier_number("tier2") == 2
|
|
assert logger._extract_tier_number("tier3") == 3
|
|
|
|
def test_log_article_url(self, tmp_path):
|
|
"""Test logging URLs to tier-segregated files"""
|
|
logger = URLLogger(logs_dir=str(tmp_path))
|
|
|
|
test_date = datetime(2025, 10, 22)
|
|
|
|
logger.log_article_url("https://example.com/article1.html", "tier1", test_date)
|
|
logger.log_article_url("https://example.com/article2.html", "tier2", test_date)
|
|
|
|
tier1_file = tmp_path / "2025-10-22_tier1_urls.txt"
|
|
tier2_file = tmp_path / "2025-10-22_other_tiers_urls.txt"
|
|
|
|
assert tier1_file.exists()
|
|
assert tier2_file.exists()
|
|
|
|
with open(tier1_file) as f:
|
|
urls = f.read().strip().split('\n')
|
|
assert "https://example.com/article1.html" in urls
|
|
|
|
with open(tier2_file) as f:
|
|
urls = f.read().strip().split('\n')
|
|
assert "https://example.com/article2.html" in urls
|
|
|
|
def test_duplicate_prevention(self, tmp_path):
|
|
"""Test that duplicate URLs are not logged twice"""
|
|
logger = URLLogger(logs_dir=str(tmp_path))
|
|
|
|
test_date = datetime(2025, 10, 22)
|
|
url = "https://example.com/article1.html"
|
|
|
|
logger.log_article_url(url, "tier1", test_date)
|
|
logger.log_article_url(url, "tier1", test_date)
|
|
|
|
tier1_file = tmp_path / "2025-10-22_tier1_urls.txt"
|
|
|
|
with open(tier1_file) as f:
|
|
urls = [line.strip() for line in f if line.strip()]
|
|
assert urls.count(url) == 1
|
|
|
|
def test_get_existing_urls(self, tmp_path):
|
|
"""Test retrieving existing URLs from log files"""
|
|
logger = URLLogger(logs_dir=str(tmp_path))
|
|
|
|
test_date = datetime(2025, 10, 22)
|
|
|
|
logger.log_article_url("https://example.com/article1.html", "tier1", test_date)
|
|
logger.log_article_url("https://example.com/article2.html", "tier1", test_date)
|
|
|
|
existing = logger.get_existing_urls("tier1", test_date)
|
|
|
|
assert len(existing) == 2
|
|
assert "https://example.com/article1.html" in existing
|
|
assert "https://example.com/article2.html" in existing
|
|
|
|
|
|
class TestBunnyStorageClient:
|
|
"""Test Bunny Storage client"""
|
|
|
|
@patch('src.deployment.bunny_storage.requests.Session')
|
|
def test_upload_file_success(self, mock_session_class):
|
|
"""Test successful file upload"""
|
|
mock_session = Mock()
|
|
mock_response = Mock()
|
|
mock_response.status_code = 201
|
|
mock_session.put.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = BunnyStorageClient(max_retries=3)
|
|
|
|
result = client.upload_file(
|
|
zone_name="test-zone",
|
|
zone_password="test-password",
|
|
zone_region="DE",
|
|
file_path="test.html",
|
|
content="<html>Test</html>"
|
|
)
|
|
|
|
assert result.success is True
|
|
assert result.file_path == "test.html"
|
|
|
|
mock_session.put.assert_called_once()
|
|
call_args = mock_session.put.call_args
|
|
# DE region uses storage.bunnycdn.com without prefix
|
|
assert call_args[0][0] == "https://storage.bunnycdn.com/test-zone/test.html"
|
|
assert call_args[1]['headers']['AccessKey'] == "test-password"
|
|
|
|
@patch('src.deployment.bunny_storage.requests.Session')
|
|
def test_upload_file_auth_error(self, mock_session_class):
|
|
"""Test authentication error handling"""
|
|
mock_session = Mock()
|
|
mock_response = Mock()
|
|
mock_response.status_code = 401
|
|
mock_session.put.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = BunnyStorageClient(max_retries=3)
|
|
|
|
with pytest.raises(Exception) as exc_info:
|
|
client.upload_file(
|
|
zone_name="test-zone",
|
|
zone_password="bad-password",
|
|
zone_region="DE",
|
|
file_path="test.html",
|
|
content="<html>Test</html>"
|
|
)
|
|
|
|
assert "Authentication failed" in str(exc_info.value)
|
|
|
|
|
|
class TestDeploymentService:
|
|
"""Test deployment service integration"""
|
|
|
|
def test_deploy_article(self, tmp_path):
|
|
"""Test deploying a single article"""
|
|
mock_storage = Mock(spec=BunnyStorageClient)
|
|
mock_storage.upload_file.return_value = UploadResult(
|
|
success=True,
|
|
file_path="test-article.html",
|
|
message="Success"
|
|
)
|
|
|
|
mock_content_repo = Mock()
|
|
mock_site_repo = Mock()
|
|
mock_page_repo = Mock()
|
|
|
|
url_logger = URLLogger(logs_dir=str(tmp_path))
|
|
|
|
service = DeploymentService(
|
|
storage_client=mock_storage,
|
|
content_repo=mock_content_repo,
|
|
site_repo=mock_site_repo,
|
|
page_repo=mock_page_repo,
|
|
url_logger=url_logger
|
|
)
|
|
|
|
article = Mock(spec=GeneratedContent)
|
|
article.id = 1
|
|
article.title = "Test Article"
|
|
article.formatted_html = "<html>Test Content</html>"
|
|
|
|
site = Mock(spec=SiteDeployment)
|
|
site.id = 1
|
|
site.custom_hostname = "www.example.com"
|
|
site.storage_zone_name = "test-zone"
|
|
site.storage_zone_password = "test-password"
|
|
site.storage_zone_region = "DE"
|
|
|
|
url = service.deploy_article(article, site)
|
|
|
|
assert url == "https://www.example.com/test-article.html"
|
|
mock_storage.upload_file.assert_called_once()
|
|
|
|
def test_deploy_boilerplate_page(self, tmp_path):
|
|
"""Test deploying a boilerplate page"""
|
|
mock_storage = Mock(spec=BunnyStorageClient)
|
|
mock_storage.upload_file.return_value = UploadResult(
|
|
success=True,
|
|
file_path="about.html",
|
|
message="Success"
|
|
)
|
|
|
|
mock_content_repo = Mock()
|
|
mock_site_repo = Mock()
|
|
mock_page_repo = Mock()
|
|
|
|
url_logger = URLLogger(logs_dir=str(tmp_path))
|
|
|
|
service = DeploymentService(
|
|
storage_client=mock_storage,
|
|
content_repo=mock_content_repo,
|
|
site_repo=mock_site_repo,
|
|
page_repo=mock_page_repo,
|
|
url_logger=url_logger
|
|
)
|
|
|
|
page = Mock(spec=SitePage)
|
|
page.page_type = "about"
|
|
page.content = "<html>About Page</html>"
|
|
|
|
site = Mock(spec=SiteDeployment)
|
|
site.id = 1
|
|
site.custom_hostname = "www.example.com"
|
|
site.storage_zone_name = "test-zone"
|
|
site.storage_zone_password = "test-password"
|
|
site.storage_zone_region = "DE"
|
|
|
|
url = service.deploy_boilerplate_page(page, site)
|
|
|
|
assert url == "https://www.example.com/about.html"
|
|
mock_storage.upload_file.assert_called_once()
|
|
|
|
def test_deploy_batch(self, tmp_path):
|
|
"""Test deploying an entire batch"""
|
|
mock_storage = Mock(spec=BunnyStorageClient)
|
|
mock_storage.upload_file.return_value = UploadResult(
|
|
success=True,
|
|
file_path="test.html",
|
|
message="Success"
|
|
)
|
|
|
|
mock_content_repo = Mock()
|
|
mock_site_repo = Mock()
|
|
mock_page_repo = Mock()
|
|
|
|
article1 = Mock(spec=GeneratedContent)
|
|
article1.id = 1
|
|
article1.title = "Article 1"
|
|
article1.formatted_html = "<html>Content 1</html>"
|
|
article1.site_deployment_id = 1
|
|
article1.tier = "tier1"
|
|
|
|
article2 = Mock(spec=GeneratedContent)
|
|
article2.id = 2
|
|
article2.title = "Article 2"
|
|
article2.formatted_html = "<html>Content 2</html>"
|
|
article2.site_deployment_id = 1
|
|
article2.tier = "tier2"
|
|
|
|
mock_content_repo.get_by_project_id.return_value = [article1, article2]
|
|
|
|
site = Mock(spec=SiteDeployment)
|
|
site.id = 1
|
|
site.custom_hostname = "www.example.com"
|
|
site.storage_zone_name = "test-zone"
|
|
site.storage_zone_password = "test-password"
|
|
site.storage_zone_region = "DE"
|
|
|
|
mock_site_repo.get_by_id.return_value = site
|
|
mock_page_repo.get_by_site.return_value = []
|
|
|
|
url_logger = URLLogger(logs_dir=str(tmp_path))
|
|
|
|
service = DeploymentService(
|
|
storage_client=mock_storage,
|
|
content_repo=mock_content_repo,
|
|
site_repo=mock_site_repo,
|
|
page_repo=mock_page_repo,
|
|
url_logger=url_logger
|
|
)
|
|
|
|
results = service.deploy_batch(project_id=1, continue_on_error=True)
|
|
|
|
assert results['articles_deployed'] == 2
|
|
assert results['articles_failed'] == 0
|
|
assert results['pages_deployed'] == 0
|
|
assert mock_storage.upload_file.call_count == 2
|
|
assert mock_content_repo.mark_as_deployed.call_count == 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|
|
|