"""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="

Test

", 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="

Body

", 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="

Retry

", 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()