# tests/unit/services/test_email_service.py """Unit tests for EmailService - email sending and template rendering.""" import json from unittest.mock import MagicMock, patch import pytest from app.modules.messaging.models import ( EmailCategory, EmailLog, EmailStatus, EmailTemplate, ) from app.modules.messaging.services.email_service import ( DebugProvider, EmailService, SMTPProvider, get_provider, ) @pytest.mark.unit @pytest.mark.email class TestEmailProviders: """Test suite for email providers.""" def test_debug_provider_send(self): """Test DebugProvider logs instead of sending.""" provider = DebugProvider() success, message_id, error = provider.send( to_email="test@example.com", to_name="Test User", subject="Test Subject", body_html="

Hello

", body_text="Hello", from_email="noreply@orion.lu", from_name="Orion", ) assert success is True assert message_id == "debug-test@example.com" assert error is None def test_debug_provider_with_reply_to(self): """Test DebugProvider with reply-to header.""" provider = DebugProvider() success, message_id, error = provider.send( to_email="test@example.com", to_name="Test User", subject="Test Subject", body_html="

Hello

", body_text=None, from_email="noreply@orion.lu", from_name="Orion", reply_to="support@orion.lu", ) assert success is True @patch("app.modules.messaging.services.email_service.settings") def test_get_provider_debug_mode(self, mock_settings): """Test get_provider returns DebugProvider in debug mode.""" mock_settings.email_debug = True provider = get_provider() assert isinstance(provider, DebugProvider) @patch("app.modules.messaging.services.email_service.settings") def test_get_provider_smtp(self, mock_settings): """Test get_provider returns SMTPProvider for smtp config.""" mock_settings.email_debug = False mock_settings.email_provider = "smtp" provider = get_provider() assert isinstance(provider, SMTPProvider) @patch("app.modules.messaging.services.email_service.settings") def test_get_provider_unknown_defaults_to_smtp(self, mock_settings): """Test get_provider defaults to SMTP for unknown providers.""" mock_settings.email_debug = False mock_settings.email_provider = "unknown_provider" provider = get_provider() assert isinstance(provider, SMTPProvider) @pytest.mark.unit @pytest.mark.email class TestEmailService: """Test suite for EmailService.""" def test_render_template_simple(self, db): """Test simple template rendering.""" service = EmailService(db) result = service.render_template( "Hello {{ name }}!", {"name": "World"} ) assert result == "Hello World!" def test_render_template_multiple_vars(self, db): """Test template rendering with multiple variables.""" service = EmailService(db) result = service.render_template( "Hi {{ first_name }}, your code is {{ store_code }}.", {"first_name": "John", "store_code": "ACME"} ) assert result == "Hi John, your code is ACME." def test_render_template_missing_var(self, db): """Test template rendering with missing variable returns empty.""" service = EmailService(db) result = service.render_template( "Hello {{ name }}!", {} # No name provided ) # Jinja2 renders missing vars as empty string by default assert "Hello" in result def test_render_template_error_returns_original(self, db): """Test template rendering error returns original string.""" service = EmailService(db) # Invalid Jinja2 syntax template = "Hello {{ name" result = service.render_template(template, {"name": "World"}) assert result == template def test_get_template_not_found(self, db): """Test get_template returns None for non-existent template.""" service = EmailService(db) result = service.get_template("nonexistent_template", "en") assert result is None def test_get_template_with_language_fallback(self, db): """Test get_template falls back to English.""" # Create English template only template = EmailTemplate( code="test_template", language="en", name="Test Template", subject="Test", body_html="

Test

", category=EmailCategory.SYSTEM.value, ) db.add(template) db.commit() service = EmailService(db) # Request German, should fallback to English result = service.get_template("test_template", "de") assert result is not None assert result.language == "en" # Cleanup db.delete(template) db.commit() def test_get_template_specific_language(self, db): """Test get_template returns specific language if available.""" # Create templates in both languages template_en = EmailTemplate( code="test_lang_template", language="en", name="Test Template EN", subject="English Subject", body_html="

English

", category=EmailCategory.SYSTEM.value, ) template_fr = EmailTemplate( code="test_lang_template", language="fr", name="Test Template FR", subject="French Subject", body_html="

Français

", category=EmailCategory.SYSTEM.value, ) db.add(template_en) db.add(template_fr) db.commit() service = EmailService(db) # Request French result = service.get_template("test_lang_template", "fr") assert result is not None assert result.language == "fr" assert result.subject == "French Subject" # Cleanup db.delete(template_en) db.delete(template_fr) db.commit() @pytest.mark.unit @pytest.mark.email class TestEmailSending: """Test suite for email sending functionality.""" @patch("app.modules.messaging.services.email_service.get_platform_provider") @patch("app.modules.messaging.services.email_service.get_platform_email_config") def test_send_raw_success(self, mock_get_config, mock_get_platform_provider, db): """Test successful raw email sending.""" # Setup mocks mock_get_config.return_value = { "enabled": True, "debug": False, "provider": "smtp", "from_email": "noreply@test.com", "from_name": "Test", "reply_to": "", } mock_provider = MagicMock() mock_provider.send.return_value = (True, "msg-123", None) mock_get_platform_provider.return_value = mock_provider service = EmailService(db) log = service.send_raw( to_email="user@example.com", to_name="User", subject="Test Subject", body_html="

Hello

", ) assert log.status == EmailStatus.SENT.value assert log.recipient_email == "user@example.com" assert log.subject == "Test Subject" assert log.provider_message_id == "msg-123" @patch("app.modules.messaging.services.email_service.get_platform_provider") @patch("app.modules.messaging.services.email_service.get_platform_email_config") def test_send_raw_failure(self, mock_get_config, mock_get_platform_provider, db): """Test failed raw email sending.""" # Setup mocks mock_get_config.return_value = { "enabled": True, "debug": False, "provider": "smtp", "from_email": "noreply@test.com", "from_name": "Test", "reply_to": "", } mock_provider = MagicMock() mock_provider.send.return_value = (False, None, "Connection refused") mock_get_platform_provider.return_value = mock_provider service = EmailService(db) log = service.send_raw( to_email="user@example.com", subject="Test Subject", body_html="

Hello

", ) assert log.status == EmailStatus.FAILED.value assert log.error_message == "Connection refused" @patch("app.modules.messaging.services.email_service.settings") def test_send_raw_email_disabled(self, mock_settings, db): """Test email sending when disabled.""" mock_settings.email_enabled = False mock_settings.email_from_address = "noreply@test.com" mock_settings.email_from_name = "Test" mock_settings.email_reply_to = "" mock_settings.email_provider = "smtp" mock_settings.email_debug = False service = EmailService(db) log = service.send_raw( to_email="user@example.com", subject="Test Subject", body_html="

Hello

", ) assert log.status == EmailStatus.FAILED.value assert "disabled" in log.error_message.lower() @patch("app.modules.messaging.services.email_service.get_platform_provider") @patch("app.modules.messaging.services.email_service.get_platform_email_config") def test_send_template_success(self, mock_get_config, mock_get_platform_provider, db): """Test successful template email sending.""" # Create test template template = EmailTemplate( code="test_send_template", language="en", name="Test Send Template", subject="Hello {{ first_name }}", body_html="

Welcome {{ first_name }} to {{ merchant }}

", body_text="Welcome {{ first_name }} to {{ merchant }}", category=EmailCategory.SYSTEM.value, ) db.add(template) db.commit() # Setup mocks mock_get_config.return_value = { "enabled": True, "debug": False, "provider": "smtp", "from_email": "noreply@test.com", "from_name": "Test", "reply_to": "", } mock_provider = MagicMock() mock_provider.send.return_value = (True, "msg-456", None) mock_get_platform_provider.return_value = mock_provider service = EmailService(db) log = service.send_template( template_code="test_send_template", to_email="user@example.com", language="en", variables={ "first_name": "John", "merchant": "ACME Corp" }, ) assert log.status == EmailStatus.SENT.value assert log.template_code == "test_send_template" assert log.subject == "Hello John" # Cleanup - delete log first due to FK constraint db.delete(log) db.delete(template) db.commit() def test_send_template_not_found(self, db): """Test sending with non-existent template.""" service = EmailService(db) log = service.send_template( template_code="nonexistent_template", to_email="user@example.com", ) assert log.status == EmailStatus.FAILED.value assert "not found" in log.error_message.lower() @pytest.mark.unit @pytest.mark.email class TestEmailLog: """Test suite for EmailLog model methods.""" def test_mark_sent(self, db): """Test EmailLog.mark_sent method.""" log = EmailLog( recipient_email="test@example.com", subject="Test", from_email="noreply@test.com", status=EmailStatus.PENDING.value, ) db.add(log) db.flush() log.mark_sent("provider-msg-id") assert log.status == EmailStatus.SENT.value assert log.sent_at is not None assert log.provider_message_id == "provider-msg-id" db.rollback() def test_mark_failed(self, db): """Test EmailLog.mark_failed method.""" log = EmailLog( recipient_email="test@example.com", subject="Test", from_email="noreply@test.com", status=EmailStatus.PENDING.value, retry_count=0, ) db.add(log) db.flush() log.mark_failed("Connection timeout") assert log.status == EmailStatus.FAILED.value assert log.error_message == "Connection timeout" assert log.retry_count == 1 db.rollback() def test_mark_delivered(self, db): """Test EmailLog.mark_delivered method.""" log = EmailLog( recipient_email="test@example.com", subject="Test", from_email="noreply@test.com", status=EmailStatus.SENT.value, ) db.add(log) db.flush() log.mark_delivered() assert log.status == EmailStatus.DELIVERED.value assert log.delivered_at is not None db.rollback() def test_mark_opened(self, db): """Test EmailLog.mark_opened method.""" log = EmailLog( recipient_email="test@example.com", subject="Test", from_email="noreply@test.com", status=EmailStatus.DELIVERED.value, ) db.add(log) db.flush() log.mark_opened() assert log.status == EmailStatus.OPENED.value assert log.opened_at is not None db.rollback() @pytest.mark.unit @pytest.mark.email class TestEmailTemplate: """Test suite for EmailTemplate model.""" def test_variables_list_property(self, db): """Test EmailTemplate.variables_list property.""" template = EmailTemplate( code="test_vars", language="en", name="Test", subject="Test", body_html="

Test

", category=EmailCategory.SYSTEM.value, variables=json.dumps(["first_name", "last_name", "email"]), ) db.add(template) db.flush() assert template.variables_list == ["first_name", "last_name", "email"] db.rollback() def test_variables_list_empty(self, db): """Test EmailTemplate.variables_list with no variables.""" template = EmailTemplate( code="test_no_vars", language="en", name="Test", subject="Test", body_html="

Test

", category=EmailCategory.SYSTEM.value, variables=None, ) db.add(template) db.flush() assert template.variables_list == [] db.rollback() def test_variables_list_invalid_json(self, db): """Test EmailTemplate.variables_list with invalid JSON.""" template = EmailTemplate( code="test_invalid_json", language="en", name="Test", subject="Test", body_html="

Test

", category=EmailCategory.SYSTEM.value, variables="not valid json", ) db.add(template) db.flush() assert template.variables_list == [] db.rollback() def test_template_repr(self, db): """Test EmailTemplate string representation.""" template = EmailTemplate( code="signup_welcome", language="en", name="Welcome", subject="Welcome", body_html="

Welcome

", category=EmailCategory.AUTH.value, ) assert "signup_welcome" in repr(template) assert "en" in repr(template) @pytest.mark.unit @pytest.mark.email class TestSignupWelcomeEmail: """Test suite for signup welcome email integration.""" @pytest.fixture def welcome_template(self, db): """Create a welcome template for testing.""" import json template = EmailTemplate( code="signup_welcome", language="en", name="Signup Welcome", subject="Welcome {{ first_name }}!", body_html="

Welcome {{ first_name }} to {{ merchant_name }}

", body_text="Welcome {{ first_name }} to {{ merchant_name }}", category=EmailCategory.AUTH.value, variables=json.dumps([ "first_name", "merchant_name", "email", "store_code", "login_url", "trial_days", "tier_name" ]), ) db.add(template) db.commit() yield template # Cleanup - delete email logs referencing this template first db.query(EmailLog).filter(EmailLog.template_id == template.id).delete() db.delete(template) db.commit() def test_welcome_template_rendering(self, db, welcome_template): """Test that welcome template renders correctly.""" service = EmailService(db) template = service.get_template("signup_welcome", "en") assert template is not None assert template.code == "signup_welcome" # Test rendering rendered = service.render_template( template.subject, {"first_name": "John"} ) assert rendered == "Welcome John!" def test_welcome_template_has_required_variables(self, db, welcome_template): """Test welcome template has all required variables.""" template = ( db.query(EmailTemplate) .filter( EmailTemplate.code == "signup_welcome", EmailTemplate.language == "en", ) .first() ) assert template is not None required_vars = [ "first_name", "merchant_name", "store_code", "login_url", "trial_days", "tier_name", ] for var in required_vars: assert var in template.variables_list, f"Missing variable: {var}" # test_welcome_email_send removed — depends on subscription service methods that were refactored