Files
xiaoxia-saas/tests/unit/test_email_service_smtp.py
T
xiaoxia 2972f19954
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m38s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m42s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m27s
CI/CD Pipeline / Unit Tests (push) Failing after 5m54s
CI/CD Pipeline / Integration Tests (push) Successful in 2m26s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 14s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
test(ci): 修复10个ruff错误 + Unit Tests dedup收集失败 (#839 #881)
2026-07-25 17:38:17 +08:00

271 lines
9.6 KiB
Python
Executable File

"""Email Service (SMTP) 单元测试"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from packages.adapters.smtp.email_service import (
EmailService,
NoopEmailService,
get_email_service,
)
from packages.domain.auth.email_service import EmailConfig
@pytest.fixture
def email_config():
return EmailConfig(
smtp_host="smtp.example.com",
smtp_port=587,
from_email="noreply@example.com",
from_name="小虾 SaaS",
smtp_user="user",
smtp_password="pass",
use_tls=True,
)
@pytest.fixture
def email_service(email_config):
return EmailService(email_config)
class TestNoopEmailService:
"""NoopEmailService 测试"""
def test_send_verification_email_returns_false(self):
"""验证邮件返回失败"""
svc = NoopEmailService()
success, msg = svc.send_verification_email(
to_email="test@example.com",
username="testuser",
verification_url="https://example.com/verify",
)
assert success is False
assert "disabled" in msg.lower()
def test_send_password_reset_email_returns_false(self):
"""密码重置邮件返回失败"""
svc = NoopEmailService()
success, msg = svc.send_password_reset_email(
to_email="test@example.com",
username="testuser",
reset_url="https://example.com/reset",
)
assert success is False
assert "disabled" in msg.lower()
class TestEmailServiceInit:
"""初始化测试"""
def test_init_with_config(self, email_config):
"""使用指定配置初始化"""
svc = EmailService(email_config)
assert svc.config is email_config
def test_init_without_config(self):
"""不指定配置使用默认 EmailConfig"""
svc = EmailService()
assert svc.config is not None
assert isinstance(svc.config, EmailConfig)
class TestSendEmail:
"""send_email 方法测试"""
def test_send_success(self, email_service):
"""发送成功"""
with patch("packages.adapters.smtp.email_service.smtplib.SMTP") as mock_smtp:
mock_server = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_server
success, error = email_service.send_email(
to_email="user@example.com",
subject="测试主题",
html_body="<p>测试内容</p>",
)
assert success is True
assert error is None
mock_server.starttls.assert_called_once()
mock_server.login.assert_called_once_with("user", "pass")
mock_server.sendmail.assert_called_once()
def test_send_without_tls(self, email_config):
"""不使用 TLS"""
email_config.use_tls = False
svc = EmailService(email_config)
with patch("packages.adapters.smtp.email_service.smtplib.SMTP") as mock_smtp:
mock_server = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_server
svc.send_email(to_email="u@e.com", subject="s", html_body="body")
mock_server.starttls.assert_not_called()
def test_send_without_auth(self, email_config):
"""不配置用户名密码时不登录"""
email_config.smtp_user = ""
email_config.smtp_password = ""
svc = EmailService(email_config)
with patch("packages.adapters.smtp.email_service.smtplib.SMTP") as mock_smtp:
mock_server = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_server
svc.send_email(to_email="u@e.com", subject="s", html_body="body")
mock_server.login.assert_not_called()
def test_send_with_cc_and_bcc(self, email_service):
"""发送带抄送和密送"""
with patch("packages.adapters.smtp.email_service.smtplib.SMTP") as mock_smtp:
mock_server = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_server
email_service.send_email(
to_email="to@example.com",
subject="s",
html_body="body",
cc=["cc1@example.com", "cc2@example.com"],
bcc=["bcc@example.com"],
)
# 验证 recipients 包含所有收件人
call_args = mock_server.sendmail.call_args
recipients = call_args[0][1]
assert "to@example.com" in recipients
assert "cc1@example.com" in recipients
assert "cc2@example.com" in recipients
assert "bcc@example.com" in recipients
def test_send_with_text_body(self, email_service):
"""带纯文本正文"""
with patch("packages.adapters.smtp.email_service.smtplib.SMTP") as mock_smtp:
mock_server = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_server
email_service.send_email(
to_email="u@e.com",
subject="s",
html_body="<p>html</p>",
text_body="plain text",
)
mock_server.sendmail.assert_called_once()
def test_send_failure_returns_false(self, email_service):
"""发送失败返回 False 和错误信息"""
with patch("packages.adapters.smtp.email_service.smtplib.SMTP") as mock_smtp:
mock_server = MagicMock()
mock_server.sendmail.side_effect = Exception("Connection refused")
mock_smtp.return_value.__enter__.return_value = mock_server
success, error = email_service.send_email(to_email="u@e.com", subject="s", html_body="body")
assert success is False
assert "Connection refused" in error
def test_from_header_exists(self, email_service):
"""From 头存在"""
with patch("packages.adapters.smtp.email_service.smtplib.SMTP") as mock_smtp:
mock_server = MagicMock()
mock_smtp.return_value.__enter__.return_value = mock_server
email_service.send_email(to_email="u@e.com", subject="s", html_body="body")
call_args = mock_server.sendmail.call_args
msg_str = call_args[0][2]
assert "From:" in msg_str
assert "To: u@e.com" in msg_str
class TestSendVerificationEmail:
"""发送验证邮件测试"""
def test_verification_email_contains_url(self, email_service):
"""验证邮件包含验证链接"""
with patch.object(email_service, "send_email", return_value=(True, None)) as mock_send:
email_service.send_verification_email(
to_email="user@example.com",
username="testuser",
verification_url="https://app.example.com/verify?token=abc123",
)
mock_send.assert_called_once()
call_args = mock_send.call_args
# 验证主题
assert "验证" in call_args[0][1]
# HTML 正文包含用户名和链接
assert "testuser" in call_args[0][2]
assert "https://app.example.com/verify?token=abc123" in call_args[0][2]
def test_verification_email_has_text_body(self, email_service):
"""验证邮件有纯文本版"""
with patch.object(email_service, "send_email", return_value=(True, None)) as mock_send:
email_service.send_verification_email(
to_email="u@e.com",
username="u",
verification_url="https://example.com/v",
)
call_args = mock_send.call_args
# 第四个参数是 text_body
assert call_args[0][3] is not None
assert len(call_args[0][3]) > 0
class TestSendPasswordResetEmail:
"""发送密码重置邮件测试"""
def test_reset_email_contains_url(self, email_service):
"""重置邮件包含重置链接"""
with patch.object(email_service, "send_email", return_value=(True, None)) as mock_send:
email_service.send_password_reset_email(
to_email="user@example.com",
username="testuser",
reset_url="https://app.example.com/reset?token=xyz",
)
mock_send.assert_called_once()
call_args = mock_send.call_args
assert "重置" in call_args[0][1]
assert "testuser" in call_args[0][2]
assert "https://app.example.com/reset?token=xyz" in call_args[0][2]
def test_reset_email_has_text_body(self, email_service):
"""重置邮件有纯文本版"""
with patch.object(email_service, "send_email", return_value=(True, None)) as mock_send:
email_service.send_password_reset_email(
to_email="u@e.com",
username="u",
reset_url="https://example.com/r",
)
call_args = mock_send.call_args
assert call_args[0][3] is not None
assert len(call_args[0][3]) > 0
class TestGetEmailService:
"""工厂函数测试"""
def test_disabled_returns_noop(self):
"""禁用时返回 NoopEmailService"""
svc = get_email_service(enabled=False)
assert isinstance(svc, NoopEmailService)
def test_enabled_returns_email_service(self, email_config):
"""启用时返回 EmailService"""
svc = get_email_service(config=email_config, enabled=True)
assert isinstance(svc, EmailService)
def test_singleton_default(self):
"""默认情况下是单例"""
svc1 = get_email_service()
svc2 = get_email_service()
# 两个都可能是 Noop 或 EmailService,取决于环境
assert type(svc1) is type(svc2)