170 lines
6.4 KiB
Python
170 lines
6.4 KiB
Python
|
|
"""Tests for email service (T05)."""
|
||
|
|
import pytest
|
||
|
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||
|
|
from email.mime.multipart import MIMEMultipart
|
||
|
|
|
||
|
|
from app.services.email_service import EmailService
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_send_contact_email_success():
|
||
|
|
"""send_contact_email sends correct email via aiosmtplib."""
|
||
|
|
service = EmailService()
|
||
|
|
contact_data = {
|
||
|
|
"name": "Test User",
|
||
|
|
"email": "test@example.com",
|
||
|
|
"phone": "+49 123 456",
|
||
|
|
"message": "Hallo Welt",
|
||
|
|
}
|
||
|
|
with patch("app.services.email_service.aiosmtplib.send", new_callable=AsyncMock) as mock_send:
|
||
|
|
result = await service.send_contact_email(contact_data)
|
||
|
|
assert result is True
|
||
|
|
mock_send.assert_awaited_once()
|
||
|
|
sent_msg = mock_send.call_args[0][0]
|
||
|
|
assert isinstance(sent_msg, MIMEMultipart)
|
||
|
|
assert "Hallo Welt" in str(sent_msg)
|
||
|
|
assert "Test User" in str(sent_msg)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_send_rental_confirmation_success():
|
||
|
|
"""send_rental_confirmation sends email with reference_number and items."""
|
||
|
|
service = EmailService()
|
||
|
|
items = [
|
||
|
|
{"equipment_name": "L-Acoustics K2", "quantity": 2},
|
||
|
|
{"equipment_name": "d&b T10", "quantity": 4},
|
||
|
|
]
|
||
|
|
with patch("app.services.email_service.aiosmtplib.send", new_callable=AsyncMock) as mock_send:
|
||
|
|
result = await service.send_rental_confirmation(
|
||
|
|
to_addr="customer@example.com",
|
||
|
|
reference_number="HMS-2026-00042",
|
||
|
|
event_name="Rock Festival",
|
||
|
|
items=items,
|
||
|
|
)
|
||
|
|
assert result is True
|
||
|
|
mock_send.assert_awaited_once()
|
||
|
|
sent_msg = mock_send.call_args[0][0]
|
||
|
|
msg_str = str(sent_msg)
|
||
|
|
assert "HMS-2026-00042" in msg_str
|
||
|
|
assert "Rock Festival" in msg_str
|
||
|
|
assert "L-Acoustics K2" in msg_str
|
||
|
|
assert "d&b T10" in msg_str
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_send_email_smtp_failure_returns_false():
|
||
|
|
"""SMTP failure returns False, no crash, graceful handling."""
|
||
|
|
service = EmailService()
|
||
|
|
with patch("app.services.email_service.aiosmtplib.send", new_callable=AsyncMock, side_effect=Exception("SMTP refused")):
|
||
|
|
result = await service.send_email(
|
||
|
|
to_addr="fail@example.com",
|
||
|
|
subject="Test Subject",
|
||
|
|
html_body="<p>Test</p>",
|
||
|
|
text_body="Test",
|
||
|
|
)
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_send_contact_email_smtp_failure_returns_false():
|
||
|
|
"""send_contact_email returns False on SMTP failure without crashing."""
|
||
|
|
service = EmailService()
|
||
|
|
with patch("app.services.email_service.aiosmtplib.send", new_callable=AsyncMock, side_effect=Exception("Connection refused")):
|
||
|
|
result = await service.send_contact_email({
|
||
|
|
"name": "Fail User",
|
||
|
|
"email": "fail@example.com",
|
||
|
|
"phone": "+49 000",
|
||
|
|
"message": "Soll fehlschlagen",
|
||
|
|
})
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_send_rental_confirmation_smtp_failure_returns_false():
|
||
|
|
"""send_rental_confirmation returns False on SMTP failure without crashing."""
|
||
|
|
service = EmailService()
|
||
|
|
with patch("app.services.email_service.aiosmtplib.send", new_callable=AsyncMock, side_effect=Exception("Timeout")):
|
||
|
|
result = await service.send_rental_confirmation(
|
||
|
|
to_addr="fail@example.com",
|
||
|
|
reference_number="HMS-2026-99999",
|
||
|
|
event_name="Fail Event",
|
||
|
|
items=[{"equipment_name": "Speaker", "quantity": 1}],
|
||
|
|
)
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_send_email_failure_saves_to_queue():
|
||
|
|
"""send_email saves failed email to email_queue when db is provided."""
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||
|
|
from app.database import Base
|
||
|
|
from app.models.email_queue import EmailQueue
|
||
|
|
from sqlalchemy import select
|
||
|
|
|
||
|
|
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
||
|
|
async with engine.begin() as conn:
|
||
|
|
await conn.run_sync(Base.metadata.create_all)
|
||
|
|
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||
|
|
|
||
|
|
service = EmailService()
|
||
|
|
with patch("app.services.email_service.aiosmtplib.send", new_callable=AsyncMock, side_effect=Exception("SMTP down")):
|
||
|
|
async with session_maker() as db:
|
||
|
|
result = await service.send_email(
|
||
|
|
to_addr="queue@example.com",
|
||
|
|
subject="Queued Email",
|
||
|
|
html_body="<p>Body</p>",
|
||
|
|
text_body="Body",
|
||
|
|
reply_to="reply@example.com",
|
||
|
|
db=db,
|
||
|
|
)
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
async with session_maker() as db:
|
||
|
|
queued = await db.execute(select(EmailQueue).where(EmailQueue.to_addr == "queue@example.com"))
|
||
|
|
item = queued.scalar_one_or_none()
|
||
|
|
assert item is not None
|
||
|
|
assert item.subject == "Queued Email"
|
||
|
|
assert item.status == "pending"
|
||
|
|
assert item.attempts == 0
|
||
|
|
assert item.reply_to == "reply@example.com"
|
||
|
|
|
||
|
|
await engine.dispose()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_retry_failed_emails_resends_pending():
|
||
|
|
"""retry_failed_emails retries pending queue items and marks sent on success."""
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||
|
|
from app.database import Base
|
||
|
|
from app.models.email_queue import EmailQueue
|
||
|
|
from sqlalchemy import select
|
||
|
|
|
||
|
|
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
||
|
|
async with engine.begin() as conn:
|
||
|
|
await conn.run_sync(Base.metadata.create_all)
|
||
|
|
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||
|
|
|
||
|
|
async with session_maker() as db:
|
||
|
|
db.add(EmailQueue(
|
||
|
|
to_addr="retry@example.com",
|
||
|
|
subject="Retry Test",
|
||
|
|
html_body="<p>Retry</p>",
|
||
|
|
text_body="Retry",
|
||
|
|
status="pending",
|
||
|
|
attempts=0,
|
||
|
|
))
|
||
|
|
await db.commit()
|
||
|
|
|
||
|
|
with patch("app.services.email_service.aiosmtplib.send", new_callable=AsyncMock):
|
||
|
|
async with session_maker() as db:
|
||
|
|
sent_count = await EmailService.retry_failed_emails(db)
|
||
|
|
assert sent_count == 1
|
||
|
|
|
||
|
|
async with session_maker() as db:
|
||
|
|
result = await db.execute(select(EmailQueue).where(EmailQueue.to_addr == "retry@example.com"))
|
||
|
|
item = result.scalar_one_or_none()
|
||
|
|
assert item.status == "sent"
|
||
|
|
assert item.attempts == 1
|
||
|
|
|
||
|
|
await engine.dispose()
|