from unittest.mock import MagicMock, patch import pytest from app.services.emailer import EmailService class TestEmailService: """Test email service functionality""" @pytest.fixture def email_service(self): """Create email service with mocked SendGrid client""" with patch('app.services.emailer.settings') as mock_settings: mock_settings.sendgrid_api_key = "test_api_key" mock_settings.email_from = "support@example.com" with patch('app.services.emailer.SendGridAPIClient') as mock_client: service = EmailService() service.client = MagicMock() return service @pytest.fixture def sample_download_links(self): """Sample download links for testing""" return { "en": { "captions_vtt": "https://signed-url.example.com/en/captions.vtt", "audio_description_vtt": "https://signed-url.example.com/en/ad.vtt", "audio_description_mp3": "https://signed-url.example.com/en/ad.mp3" }, "es": { "captions_vtt": "https://signed-url.example.com/es/captions.vtt", "audio_description_vtt": "https://signed-url.example.com/es/ad.vtt", "audio_description_mp3": "https://signed-url.example.com/es/ad.mp3" } } @pytest.mark.asyncio async def test_send_completion_email_success(self, email_service, sample_download_links): """Test successful completion email sending""" # Mock successful SendGrid response mock_response = MagicMock() mock_response.status_code = 202 email_service.client.send.return_value = mock_response result = await email_service.send_completion_email( recipient_email="client@example.com", job_title="Test Video Project", download_links=sample_download_links ) assert result is True email_service.client.send.assert_called_once() @pytest.mark.asyncio async def test_send_completion_email_no_client(self): """Test email sending when client is not configured""" service = EmailService() service.client = None result = await service.send_completion_email( recipient_email="client@example.com", job_title="Test Video", download_links={} ) assert result is False @pytest.mark.asyncio async def test_send_completion_email_api_failure(self, email_service, sample_download_links): """Test email sending with SendGrid API failure""" # Mock failed SendGrid response mock_response = MagicMock() mock_response.status_code = 400 email_service.client.send.return_value = mock_response result = await email_service.send_completion_email( recipient_email="client@example.com", job_title="Test Video", download_links=sample_download_links ) assert result is False @pytest.mark.asyncio async def test_send_completion_email_exception(self, email_service, sample_download_links): """Test email sending with exception""" # Mock SendGrid client raising exception email_service.client.send.side_effect = Exception("SendGrid error") result = await email_service.send_completion_email( recipient_email="client@example.com", job_title="Test Video", download_links=sample_download_links ) assert result is False def test_render_completion_template_basic(self, email_service, sample_download_links): """Test rendering completion email template""" html_content = email_service._render_completion_template( job_title="Test Video Project", download_links=sample_download_links ) # Check that key elements are present assert "Test Video Project" in html_content assert "EN Assets" in html_content assert "ES Assets" in html_content assert "captions.vtt" in html_content assert "audio_description_vtt" in html_content assert "audio_description_mp3" in html_content assert "24 hours" in html_content # Expiry warning def test_render_completion_template_single_language(self, email_service): """Test rendering template with single language""" download_links = { "en": { "captions_vtt": "https://example.com/captions.vtt" } } html_content = email_service._render_completion_template( job_title="English Only Video", download_links=download_links ) assert "English Only Video" in html_content assert "EN Assets" in html_content assert "ES Assets" not in html_content def test_render_completion_template_no_downloads(self, email_service): """Test rendering template with no download links""" html_content = email_service._render_completion_template( job_title="Empty Job", download_links={} ) assert "Empty Job" in html_content assert "" in html_content assert "24 hours" in html_content def test_render_completion_template_html_structure(self, email_service, sample_download_links): """Test that rendered template has proper HTML structure""" html_content = email_service._render_completion_template( job_title="Test Video", download_links=sample_download_links ) # Check HTML structure assert html_content.startswith("") assert "" in html_content assert "" in html_content assert "
" in html_content assert "" in html_content assert "font-family: Arial" in html_content # CSS present def test_render_completion_template_download_link_formatting(self, email_service): """Test that download links are properly formatted in template""" download_links = { "en": { "captions_vtt": "https://example.com/captions.vtt", "audio_description_mp3": "https://example.com/ad.mp3" } } html_content = email_service._render_completion_template( job_title="Test Video", download_links=download_links ) # Check that file types are properly formatted assert "Download Captions Vtt" in html_content assert "Download Audio Description Mp3" in html_content assert 'href="https://example.com/captions.vtt"' in html_content assert 'href="https://example.com/ad.mp3"' in html_content def test_service_initialization_with_api_key(self): """Test service initialization with SendGrid API key""" with patch('app.services.emailer.settings') as mock_settings: mock_settings.sendgrid_api_key = "test_api_key" with patch('app.services.emailer.SendGridAPIClient') as mock_client: service = EmailService() mock_client.assert_called_once_with(api_key="test_api_key") assert service.client is not None def test_service_initialization_without_api_key(self): """Test service initialization without SendGrid API key""" with patch('app.services.emailer.settings') as mock_settings: mock_settings.sendgrid_api_key = "" service = EmailService() assert service.client is None @pytest.mark.asyncio async def test_send_completion_email_mail_object_creation(self, email_service, sample_download_links): """Test that Mail object is created correctly""" mock_response = MagicMock() mock_response.status_code = 202 email_service.client.send.return_value = mock_response with patch('app.services.emailer.Mail') as mock_mail: mock_mail_instance = MagicMock() mock_mail.return_value = mock_mail_instance await email_service.send_completion_email( recipient_email="client@example.com", job_title="Test Video", download_links=sample_download_links ) # Verify Mail object was created with correct parameters mock_mail.assert_called_once() call_args = mock_mail.call_args # Check that from_email, to_emails, subject, and html_content are set assert call_args is not None email_service.client.send.assert_called_once_with(mock_mail_instance) def test_template_injection_safety(self, email_service): """Test that template is safe from injection attacks""" malicious_title = "Malicious Title" malicious_links = { "en": { "captions_vtt": "javascript:alert('xss')" } } html_content = email_service._render_completion_template( job_title=malicious_title, download_links=malicious_links ) # Jinja2 should escape HTML by default assert "