Big-Link-Man/tests/integration/test_deployment.py

344 lines
12 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)
site = Mock(spec=SiteDeployment)
site.storage_zone_name = "test-zone"
site.storage_zone_password = "test-password"
site.storage_zone_region = "DE"
result = client.upload_file(
site=site,
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)
site = Mock(spec=SiteDeployment)
site.storage_zone_name = "test-zone"
site.storage_zone_password = "bad-password"
site.storage_zone_region = "DE"
with pytest.raises(Exception) as exc_info:
client.upload_file(
site=site,
file_path="test.html",
content="<html>Test</html>"
)
assert "Authentication failed" in str(exc_info.value)
class TestDeploymentService:
"""Test deployment service integration"""
@patch('src.deployment.deployment_service.create_storage_client')
def test_deploy_article(self, mock_create_client, 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_create_client.return_value = mock_storage
mock_content_repo = Mock()
mock_site_repo = Mock()
mock_page_repo = Mock()
url_logger = URLLogger(logs_dir=str(tmp_path))
service = DeploymentService(
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_create_client.assert_called_once_with(site)
mock_storage.upload_file.assert_called_once_with(
site=site,
file_path="test-article.html",
content="<html>Test Content</html>"
)
@patch('src.deployment.deployment_service.create_storage_client')
def test_deploy_boilerplate_page(self, mock_create_client, 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_create_client.return_value = mock_storage
mock_content_repo = Mock()
mock_site_repo = Mock()
mock_page_repo = Mock()
url_logger = URLLogger(logs_dir=str(tmp_path))
service = DeploymentService(
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_create_client.assert_called_once_with(site)
mock_storage.upload_file.assert_called_once_with(
site=site,
file_path="about.html",
content="<html>About Page</html>"
)
@patch('src.deployment.deployment_service.create_storage_client')
def test_deploy_batch(self, mock_create_client, 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_create_client.return_value = mock_storage
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(
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_create_client.call_count == 2
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"])