fix: resolve email settings architecture violations and add tests/docs

- Fix API-002 in admin/settings.py: use service layer for DB delete
- Fix API-001/API-003 in vendor/email_settings.py: add Pydantic response
  models, remove HTTPException raises
- Fix SVC-002/SVC-006 in vendor_email_settings_service.py: use domain
  exceptions, change db.commit() to db.flush()
- Add unit tests for VendorEmailSettingsService
- Add integration tests for vendor and admin email settings APIs
- Add user guide (docs/guides/email-settings.md)
- Add developer guide (docs/implementation/email-settings.md)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-05 22:38:10 +01:00
parent 36603178c3
commit 84a523cd7b
9 changed files with 1765 additions and 85 deletions

View File

@@ -0,0 +1,388 @@
# tests/unit/services/test_vendor_email_settings_service.py
"""Unit tests for VendorEmailSettingsService."""
import pytest
from datetime import datetime, timezone
from unittest.mock import patch, MagicMock
from app.exceptions import (
AuthorizationException,
ResourceNotFoundException,
ValidationException,
)
from app.services.vendor_email_settings_service import VendorEmailSettingsService
from models.database import VendorEmailSettings, TierCode
# =============================================================================
# FIXTURES
# =============================================================================
@pytest.fixture
def test_email_settings(db, test_vendor):
"""Create test email settings for a vendor."""
settings = VendorEmailSettings(
vendor_id=test_vendor.id,
from_email="test@example.com",
from_name="Test Sender",
provider="smtp",
smtp_host="smtp.example.com",
smtp_port=587,
smtp_username="testuser",
smtp_password="testpass",
smtp_use_tls=True,
smtp_use_ssl=False,
is_configured=True,
is_verified=False,
)
db.add(settings)
db.commit()
db.refresh(settings)
return settings
@pytest.fixture
def test_verified_email_settings(db, test_vendor):
"""Create verified email settings."""
settings = VendorEmailSettings(
vendor_id=test_vendor.id,
from_email="verified@example.com",
from_name="Verified Sender",
provider="smtp",
smtp_host="smtp.example.com",
smtp_port=587,
smtp_username="testuser",
smtp_password="testpass",
smtp_use_tls=True,
is_configured=True,
is_verified=True,
last_verified_at=datetime.now(timezone.utc),
)
db.add(settings)
db.commit()
db.refresh(settings)
return settings
# =============================================================================
# READ OPERATION TESTS
# =============================================================================
@pytest.mark.unit
@pytest.mark.email
class TestVendorEmailSettingsRead:
"""Test suite for reading email settings."""
def test_get_settings_exists(self, db, test_email_settings):
"""Test getting settings when they exist."""
service = VendorEmailSettingsService(db)
settings = service.get_settings(test_email_settings.vendor_id)
assert settings is not None
assert settings.from_email == "test@example.com"
assert settings.provider == "smtp"
def test_get_settings_not_exists(self, db, test_vendor):
"""Test getting settings when they don't exist."""
service = VendorEmailSettingsService(db)
settings = service.get_settings(test_vendor.id)
assert settings is None
def test_get_settings_or_404_exists(self, db, test_email_settings):
"""Test get_settings_or_404 when settings exist."""
service = VendorEmailSettingsService(db)
settings = service.get_settings_or_404(test_email_settings.vendor_id)
assert settings is not None
assert settings.id == test_email_settings.id
def test_get_settings_or_404_not_exists(self, db, test_vendor):
"""Test get_settings_or_404 raises exception when not found."""
service = VendorEmailSettingsService(db)
with pytest.raises(ResourceNotFoundException) as exc:
service.get_settings_or_404(test_vendor.id)
assert "vendor_email_settings" in str(exc.value)
def test_is_configured_true(self, db, test_email_settings):
"""Test is_configured returns True for configured settings."""
service = VendorEmailSettingsService(db)
result = service.is_configured(test_email_settings.vendor_id)
assert result is True
def test_is_configured_false_not_exists(self, db, test_vendor):
"""Test is_configured returns False when settings don't exist."""
service = VendorEmailSettingsService(db)
result = service.is_configured(test_vendor.id)
assert result is False
def test_get_status_configured(self, db, test_email_settings):
"""Test get_status for configured settings."""
service = VendorEmailSettingsService(db)
status = service.get_status(test_email_settings.vendor_id)
assert status["is_configured"] is True
assert status["is_verified"] is False
assert status["provider"] == "smtp"
assert status["from_email"] == "test@example.com"
def test_get_status_not_configured(self, db, test_vendor):
"""Test get_status when settings don't exist."""
service = VendorEmailSettingsService(db)
status = service.get_status(test_vendor.id)
assert status["is_configured"] is False
assert status["is_verified"] is False
assert status["provider"] is None
# =============================================================================
# WRITE OPERATION TESTS
# =============================================================================
@pytest.mark.unit
@pytest.mark.email
class TestVendorEmailSettingsWrite:
"""Test suite for writing email settings."""
def test_create_settings(self, db, test_vendor):
"""Test creating new email settings."""
service = VendorEmailSettingsService(db)
data = {
"from_email": "new@example.com",
"from_name": "New Sender",
"provider": "smtp",
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"smtp_username": "user",
"smtp_password": "pass",
}
settings = service.create_or_update(
vendor_id=test_vendor.id,
data=data,
current_tier=TierCode.ESSENTIAL,
)
assert settings.from_email == "new@example.com"
assert settings.provider == "smtp"
assert settings.smtp_host == "smtp.example.com"
def test_update_existing_settings(self, db, test_email_settings):
"""Test updating existing settings."""
service = VendorEmailSettingsService(db)
data = {
"from_email": "updated@example.com",
"from_name": "Updated Sender",
}
settings = service.create_or_update(
vendor_id=test_email_settings.vendor_id,
data=data,
current_tier=TierCode.ESSENTIAL,
)
assert settings.from_email == "updated@example.com"
assert settings.from_name == "Updated Sender"
# Other fields should remain unchanged
assert settings.smtp_host == "smtp.example.com"
def test_premium_provider_requires_business_tier(self, db, test_vendor):
"""Test that premium providers require Business tier."""
service = VendorEmailSettingsService(db)
data = {
"from_email": "test@example.com",
"from_name": "Test",
"provider": "sendgrid",
"sendgrid_api_key": "test-key",
}
with pytest.raises(AuthorizationException) as exc:
service.create_or_update(
vendor_id=test_vendor.id,
data=data,
current_tier=TierCode.ESSENTIAL,
)
assert "Business or Enterprise" in str(exc.value)
def test_premium_provider_allowed_for_business(self, db, test_vendor):
"""Test that premium providers work with Business tier."""
service = VendorEmailSettingsService(db)
data = {
"from_email": "test@example.com",
"from_name": "Test",
"provider": "sendgrid",
"sendgrid_api_key": "test-key",
}
settings = service.create_or_update(
vendor_id=test_vendor.id,
data=data,
current_tier=TierCode.BUSINESS,
)
assert settings.provider == "sendgrid"
def test_provider_change_resets_verification(self, db, test_verified_email_settings):
"""Test that changing provider resets verification status."""
service = VendorEmailSettingsService(db)
assert test_verified_email_settings.is_verified is True
data = {"smtp_host": "new-smtp.example.com"}
settings = service.create_or_update(
vendor_id=test_verified_email_settings.vendor_id,
data=data,
current_tier=TierCode.ESSENTIAL,
)
assert settings.is_verified is False
def test_delete_settings(self, db, test_email_settings):
"""Test deleting email settings."""
service = VendorEmailSettingsService(db)
vendor_id = test_email_settings.vendor_id
service.delete(vendor_id)
db.commit()
# Verify deletion
settings = service.get_settings(vendor_id)
assert settings is None
def test_delete_settings_not_found(self, db, test_vendor):
"""Test deleting non-existent settings raises exception."""
service = VendorEmailSettingsService(db)
with pytest.raises(ResourceNotFoundException):
service.delete(test_vendor.id)
# =============================================================================
# VERIFICATION TESTS
# =============================================================================
@pytest.mark.unit
@pytest.mark.email
class TestVendorEmailSettingsVerification:
"""Test suite for email verification."""
def test_verify_settings_not_configured(self, db, test_vendor):
"""Test verification fails for non-existent settings."""
service = VendorEmailSettingsService(db)
with pytest.raises(ResourceNotFoundException):
service.verify_settings(test_vendor.id, "test@example.com")
def test_verify_settings_incomplete(self, db, test_vendor):
"""Test verification fails for incomplete settings."""
# Create incomplete settings
settings = VendorEmailSettings(
vendor_id=test_vendor.id,
from_email="test@example.com",
from_name="Test",
provider="smtp",
# Missing SMTP config
is_configured=False,
)
db.add(settings)
db.commit()
service = VendorEmailSettingsService(db)
with pytest.raises(ValidationException) as exc:
service.verify_settings(test_vendor.id, "test@example.com")
assert "incomplete" in str(exc.value).lower()
@patch("smtplib.SMTP")
def test_verify_smtp_success(self, mock_smtp, db, test_email_settings):
"""Test successful SMTP verification."""
# Mock SMTP connection
mock_server = MagicMock()
mock_smtp.return_value = mock_server
service = VendorEmailSettingsService(db)
result = service.verify_settings(
test_email_settings.vendor_id,
"recipient@example.com",
)
assert result["success"] is True
assert "successfully" in result["message"].lower()
@patch("smtplib.SMTP")
def test_verify_smtp_failure(self, mock_smtp, db, test_email_settings):
"""Test SMTP verification failure."""
# Mock SMTP error
mock_smtp.side_effect = Exception("Connection refused")
service = VendorEmailSettingsService(db)
result = service.verify_settings(
test_email_settings.vendor_id,
"recipient@example.com",
)
assert result["success"] is False
assert "failed" in result["message"].lower()
# =============================================================================
# PROVIDER AVAILABILITY TESTS
# =============================================================================
@pytest.mark.unit
@pytest.mark.email
class TestVendorEmailProvidersAvailability:
"""Test suite for provider availability checking."""
def test_get_providers_essential_tier(self, db):
"""Test available providers for Essential tier."""
service = VendorEmailSettingsService(db)
providers = service.get_available_providers(TierCode.ESSENTIAL)
# Find SMTP provider
smtp = next((p for p in providers if p["code"] == "smtp"), None)
assert smtp is not None
assert smtp["available"] is True
# Find SendGrid provider
sendgrid = next((p for p in providers if p["code"] == "sendgrid"), None)
assert sendgrid is not None
assert sendgrid["available"] is False
def test_get_providers_business_tier(self, db):
"""Test available providers for Business tier."""
service = VendorEmailSettingsService(db)
providers = service.get_available_providers(TierCode.BUSINESS)
# All providers should be available
for provider in providers:
assert provider["available"] is True
def test_get_providers_no_tier(self, db):
"""Test available providers with no subscription."""
service = VendorEmailSettingsService(db)
providers = service.get_available_providers(None)
# Only SMTP should be available
smtp = next((p for p in providers if p["code"] == "smtp"), None)
assert smtp["available"] is True
sendgrid = next((p for p in providers if p["code"] == "sendgrid"), None)
assert sendgrid["available"] is False