Files
orion/app/modules/messaging/tests/unit/test_email_service.py
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

594 lines
18 KiB
Python

# 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="<h1>Hello</h1>",
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="<h1>Hello</h1>",
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="<p>Test</p>",
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="<p>English</p>",
category=EmailCategory.SYSTEM.value,
)
template_fr = EmailTemplate(
code="test_lang_template",
language="fr",
name="Test Template FR",
subject="French Subject",
body_html="<p>Français</p>",
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="<h1>Hello</h1>",
)
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="<h1>Hello</h1>",
)
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="<h1>Hello</h1>",
)
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="<p>Welcome {{ first_name }} to {{ merchant }}</p>",
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="<p>Test</p>",
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="<p>Test</p>",
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="<p>Test</p>",
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="<p>Welcome</p>",
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="<p>Welcome {{ first_name }} to {{ merchant_name }}</p>",
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