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:
290
tests/integration/api/v1/admin/test_email_settings.py
Normal file
290
tests/integration/api/v1/admin/test_email_settings.py
Normal file
@@ -0,0 +1,290 @@
|
||||
# tests/integration/api/v1/admin/test_email_settings.py
|
||||
"""Integration tests for admin email settings API."""
|
||||
|
||||
import pytest
|
||||
|
||||
from models.database.admin import AdminSetting
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET EMAIL STATUS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestGetAdminEmailStatus:
|
||||
"""Test suite for GET /admin/settings/email/status endpoint."""
|
||||
|
||||
def test_get_status_unauthenticated(self, client):
|
||||
"""Test getting status without auth fails."""
|
||||
response = client.get("/api/v1/admin/settings/email/status")
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_get_status_non_admin(self, client, auth_headers):
|
||||
"""Test getting status as non-admin fails."""
|
||||
response = client.get(
|
||||
"/api/v1/admin/settings/email/status",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_get_status_admin(self, client, admin_headers):
|
||||
"""Test getting status as admin succeeds."""
|
||||
response = client.get(
|
||||
"/api/v1/admin/settings/email/status",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "provider" in data
|
||||
assert "from_email" in data
|
||||
assert "enabled" in data
|
||||
assert "is_configured" in data
|
||||
|
||||
def test_get_status_has_db_overrides_flag(self, client, admin_headers):
|
||||
"""Test status includes has_db_overrides flag."""
|
||||
response = client.get(
|
||||
"/api/v1/admin/settings/email/status",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "has_db_overrides" in data
|
||||
# Initially should be False (no DB settings)
|
||||
assert data["has_db_overrides"] is False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UPDATE EMAIL SETTINGS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestUpdateAdminEmailSettings:
|
||||
"""Test suite for PUT /admin/settings/email/settings endpoint."""
|
||||
|
||||
def test_update_unauthenticated(self, client):
|
||||
"""Test updating settings without auth fails."""
|
||||
response = client.put(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
json={"from_email": "test@example.com"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_update_non_admin(self, client, auth_headers):
|
||||
"""Test updating settings as non-admin fails."""
|
||||
response = client.put(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=auth_headers,
|
||||
json={"from_email": "test@example.com"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_update_settings_admin(self, client, admin_headers, db):
|
||||
"""Test updating settings as admin succeeds."""
|
||||
response = client.put(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"from_email": "platform@example.com",
|
||||
"from_name": "Test Platform",
|
||||
"provider": "smtp",
|
||||
"smtp_host": "smtp.test.com",
|
||||
"smtp_port": 587,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert "updated_keys" in data
|
||||
assert "from_email" in data["updated_keys"]
|
||||
|
||||
# Verify settings were stored in DB
|
||||
setting = (
|
||||
db.query(AdminSetting)
|
||||
.filter(AdminSetting.key == "email_from_address")
|
||||
.first()
|
||||
)
|
||||
assert setting is not None
|
||||
assert setting.value == "platform@example.com"
|
||||
|
||||
def test_update_partial_settings(self, client, admin_headers):
|
||||
"""Test updating only some settings."""
|
||||
response = client.put(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"enabled": False,
|
||||
"debug": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert "enabled" in data["updated_keys"]
|
||||
assert "debug" in data["updated_keys"]
|
||||
|
||||
def test_status_shows_db_overrides(self, client, admin_headers):
|
||||
"""Test that status shows DB overrides after update."""
|
||||
# First, set a DB override
|
||||
client.put(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=admin_headers,
|
||||
json={"from_email": "override@example.com"},
|
||||
)
|
||||
|
||||
# Check status
|
||||
response = client.get(
|
||||
"/api/v1/admin/settings/email/status",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["has_db_overrides"] is True
|
||||
assert data["from_email"] == "override@example.com"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RESET EMAIL SETTINGS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestResetAdminEmailSettings:
|
||||
"""Test suite for DELETE /admin/settings/email/settings endpoint."""
|
||||
|
||||
def test_reset_unauthenticated(self, client):
|
||||
"""Test resetting settings without auth fails."""
|
||||
response = client.delete("/api/v1/admin/settings/email/settings")
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_reset_non_admin(self, client, auth_headers):
|
||||
"""Test resetting settings as non-admin fails."""
|
||||
response = client.delete(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_reset_settings_admin(self, client, admin_headers, db):
|
||||
"""Test resetting settings as admin."""
|
||||
# First, create some DB overrides
|
||||
client.put(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"from_email": "tobereset@example.com",
|
||||
"provider": "sendgrid",
|
||||
},
|
||||
)
|
||||
|
||||
# Verify they exist
|
||||
setting = (
|
||||
db.query(AdminSetting)
|
||||
.filter(AdminSetting.key == "email_from_address")
|
||||
.first()
|
||||
)
|
||||
assert setting is not None
|
||||
|
||||
# Reset
|
||||
response = client.delete(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
# Verify they're gone
|
||||
db.expire_all()
|
||||
setting = (
|
||||
db.query(AdminSetting)
|
||||
.filter(AdminSetting.key == "email_from_address")
|
||||
.first()
|
||||
)
|
||||
assert setting is None
|
||||
|
||||
def test_status_after_reset(self, client, admin_headers):
|
||||
"""Test status after reset shows no DB overrides."""
|
||||
# Set an override
|
||||
client.put(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=admin_headers,
|
||||
json={"from_email": "override@example.com"},
|
||||
)
|
||||
|
||||
# Reset
|
||||
client.delete(
|
||||
"/api/v1/admin/settings/email/settings",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
# Check status
|
||||
response = client.get(
|
||||
"/api/v1/admin/settings/email/status",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["has_db_overrides"] is False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TEST EMAIL TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestSendAdminTestEmail:
|
||||
"""Test suite for POST /admin/settings/email/test endpoint."""
|
||||
|
||||
def test_send_test_unauthenticated(self, client):
|
||||
"""Test sending test email without auth fails."""
|
||||
response = client.post(
|
||||
"/api/v1/admin/settings/email/test",
|
||||
json={"to_email": "test@example.com"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_send_test_non_admin(self, client, auth_headers):
|
||||
"""Test sending test email as non-admin fails."""
|
||||
response = client.post(
|
||||
"/api/v1/admin/settings/email/test",
|
||||
headers=auth_headers,
|
||||
json={"to_email": "test@example.com"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_send_test_invalid_email(self, client, admin_headers):
|
||||
"""Test sending to invalid email format fails."""
|
||||
response = client.post(
|
||||
"/api/v1/admin/settings/email/test",
|
||||
headers=admin_headers,
|
||||
json={"to_email": "not-an-email"},
|
||||
)
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
def test_send_test_admin(self, client, admin_headers):
|
||||
"""Test sending test email as admin."""
|
||||
response = client.post(
|
||||
"/api/v1/admin/settings/email/test",
|
||||
headers=admin_headers,
|
||||
json={"to_email": "test@example.com"},
|
||||
)
|
||||
|
||||
# May fail if email not configured, but should not be 401/403
|
||||
assert response.status_code in (200, 500)
|
||||
data = response.json()
|
||||
assert "success" in data
|
||||
assert "message" in data
|
||||
347
tests/integration/api/v1/vendor/test_email_settings.py
vendored
Normal file
347
tests/integration/api/v1/vendor/test_email_settings.py
vendored
Normal file
@@ -0,0 +1,347 @@
|
||||
# tests/integration/api/v1/vendor/test_email_settings.py
|
||||
"""Integration tests for vendor email settings API."""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from models.database import VendorEmailSettings
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FIXTURES
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vendor_email_settings(db, test_vendor_with_vendor_user):
|
||||
"""Create email settings for vendor owned by test vendor user."""
|
||||
settings = VendorEmailSettings(
|
||||
vendor_id=test_vendor_with_vendor_user.id,
|
||||
from_email="vendor@example.com",
|
||||
from_name="Vendor Test",
|
||||
provider="smtp",
|
||||
smtp_host="smtp.example.com",
|
||||
smtp_port=587,
|
||||
smtp_username="vendoruser",
|
||||
smtp_password="vendorpass",
|
||||
smtp_use_tls=True,
|
||||
is_configured=True,
|
||||
is_verified=False,
|
||||
)
|
||||
db.add(settings)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
return settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def verified_vendor_email_settings(db, test_vendor_with_vendor_user):
|
||||
"""Create verified email settings."""
|
||||
settings = VendorEmailSettings(
|
||||
vendor_id=test_vendor_with_vendor_user.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
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET EMAIL SETTINGS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestGetEmailSettings:
|
||||
"""Test suite for GET /email-settings endpoint."""
|
||||
|
||||
def test_get_settings_not_configured(
|
||||
self, client, vendor_auth_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test getting settings when not configured."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/email-settings",
|
||||
headers=vendor_auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["configured"] is False
|
||||
assert data["settings"] is None
|
||||
|
||||
def test_get_settings_configured(
|
||||
self, client, vendor_auth_headers, vendor_email_settings
|
||||
):
|
||||
"""Test getting configured settings."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/email-settings",
|
||||
headers=vendor_auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["configured"] is True
|
||||
assert data["settings"]["from_email"] == "vendor@example.com"
|
||||
# Password should be masked
|
||||
assert "vendorpass" not in str(data)
|
||||
|
||||
def test_get_settings_unauthenticated(self, client):
|
||||
"""Test getting settings without auth fails."""
|
||||
response = client.get("/api/v1/vendor/email-settings")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET STATUS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestGetEmailStatus:
|
||||
"""Test suite for GET /email-settings/status endpoint."""
|
||||
|
||||
def test_get_status_not_configured(
|
||||
self, client, vendor_auth_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test status when not configured."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/email-settings/status",
|
||||
headers=vendor_auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["is_configured"] is False
|
||||
assert data["is_verified"] is False
|
||||
|
||||
def test_get_status_configured_unverified(
|
||||
self, client, vendor_auth_headers, vendor_email_settings
|
||||
):
|
||||
"""Test status when configured but not verified."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/email-settings/status",
|
||||
headers=vendor_auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["is_configured"] is True
|
||||
assert data["is_verified"] is False
|
||||
|
||||
def test_get_status_verified(
|
||||
self, client, vendor_auth_headers, verified_vendor_email_settings
|
||||
):
|
||||
"""Test status when verified."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/email-settings/status",
|
||||
headers=vendor_auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["is_configured"] is True
|
||||
assert data["is_verified"] is True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET PROVIDERS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestGetProviders:
|
||||
"""Test suite for GET /email-settings/providers endpoint."""
|
||||
|
||||
def test_get_providers(self, client, vendor_auth_headers):
|
||||
"""Test getting available providers."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/email-settings/providers",
|
||||
headers=vendor_auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "providers" in data
|
||||
assert len(data["providers"]) >= 1
|
||||
|
||||
# SMTP should always be available
|
||||
smtp = next((p for p in data["providers"] if p["code"] == "smtp"), None)
|
||||
assert smtp is not None
|
||||
assert smtp["available"] is True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UPDATE EMAIL SETTINGS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestUpdateEmailSettings:
|
||||
"""Test suite for PUT /email-settings endpoint."""
|
||||
|
||||
def test_create_settings(
|
||||
self, client, vendor_auth_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test creating new email settings."""
|
||||
response = client.put(
|
||||
"/api/v1/vendor/email-settings",
|
||||
headers=vendor_auth_headers,
|
||||
json={
|
||||
"from_email": "new@example.com",
|
||||
"from_name": "New Vendor",
|
||||
"provider": "smtp",
|
||||
"smtp_host": "smtp.example.com",
|
||||
"smtp_port": 587,
|
||||
"smtp_username": "user",
|
||||
"smtp_password": "pass",
|
||||
"smtp_use_tls": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["settings"]["from_email"] == "new@example.com"
|
||||
|
||||
def test_update_existing_settings(
|
||||
self, client, vendor_auth_headers, vendor_email_settings
|
||||
):
|
||||
"""Test updating existing settings."""
|
||||
response = client.put(
|
||||
"/api/v1/vendor/email-settings",
|
||||
headers=vendor_auth_headers,
|
||||
json={
|
||||
"from_email": "updated@example.com",
|
||||
"from_name": "Updated Name",
|
||||
"provider": "smtp",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["settings"]["from_email"] == "updated@example.com"
|
||||
assert data["settings"]["from_name"] == "Updated Name"
|
||||
|
||||
def test_premium_provider_rejected_for_basic_tier(
|
||||
self, client, vendor_auth_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test premium provider rejected without Business tier."""
|
||||
response = client.put(
|
||||
"/api/v1/vendor/email-settings",
|
||||
headers=vendor_auth_headers,
|
||||
json={
|
||||
"from_email": "test@example.com",
|
||||
"from_name": "Test",
|
||||
"provider": "sendgrid",
|
||||
"sendgrid_api_key": "test-key",
|
||||
},
|
||||
)
|
||||
|
||||
# Should fail with 403 (AuthorizationException)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_invalid_email_rejected(self, client, vendor_auth_headers):
|
||||
"""Test invalid email format rejected."""
|
||||
response = client.put(
|
||||
"/api/v1/vendor/email-settings",
|
||||
headers=vendor_auth_headers,
|
||||
json={
|
||||
"from_email": "not-an-email",
|
||||
"from_name": "Test",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DELETE EMAIL SETTINGS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestDeleteEmailSettings:
|
||||
"""Test suite for DELETE /email-settings endpoint."""
|
||||
|
||||
def test_delete_settings(
|
||||
self, client, vendor_auth_headers, vendor_email_settings, db
|
||||
):
|
||||
"""Test deleting email settings."""
|
||||
response = client.delete(
|
||||
"/api/v1/vendor/email-settings",
|
||||
headers=vendor_auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
# Verify deletion
|
||||
settings = (
|
||||
db.query(VendorEmailSettings)
|
||||
.filter(VendorEmailSettings.vendor_id == vendor_email_settings.vendor_id)
|
||||
.first()
|
||||
)
|
||||
assert settings is None
|
||||
|
||||
def test_delete_settings_not_found(
|
||||
self, client, vendor_auth_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test deleting non-existent settings returns 404."""
|
||||
response = client.delete(
|
||||
"/api/v1/vendor/email-settings",
|
||||
headers=vendor_auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VERIFY EMAIL SETTINGS TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.email
|
||||
class TestVerifyEmailSettings:
|
||||
"""Test suite for POST /email-settings/verify endpoint."""
|
||||
|
||||
def test_verify_not_configured(
|
||||
self, client, vendor_auth_headers, test_vendor_with_vendor_user
|
||||
):
|
||||
"""Test verification fails when settings not configured."""
|
||||
response = client.post(
|
||||
"/api/v1/vendor/email-settings/verify",
|
||||
headers=vendor_auth_headers,
|
||||
json={"test_email": "test@example.com"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_verify_invalid_email(
|
||||
self, client, vendor_auth_headers, vendor_email_settings
|
||||
):
|
||||
"""Test verification with invalid email address."""
|
||||
response = client.post(
|
||||
"/api/v1/vendor/email-settings/verify",
|
||||
headers=vendor_auth_headers,
|
||||
json={"test_email": "not-an-email"},
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
388
tests/unit/services/test_vendor_email_settings_service.py
Normal file
388
tests/unit/services/test_vendor_email_settings_service.py
Normal 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
|
||||
Reference in New Issue
Block a user