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>
594 lines
18 KiB
Python
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
|