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
271 lines
9.6 KiB
Python
Executable File
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)
|