test(T05): contact router, rental router, email service, rate limiting tests + retry queue
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user