""" 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="Test" ) 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="Test" ) 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 = "Test Content" 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 = "About Page" 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 = "Content 1" article1.site_deployment_id = 1 article1.tier = "tier1" article2 = Mock(spec=GeneratedContent) article2.id = 2 article2.title = "Article 2" article2.formatted_html = "Content 2" 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"])