from __future__ import annotations import smtplib from email.message import EmailMessage from app.core.config import Settings from app.service.offsite_smtp_adapter import ( OffsiteMailReplyRequest, OffsiteSmtpSender, SmtpAttachment, ) class FakeSmtpConnection: def __init__(self, *, fail_send: bool = False) -> None: self.fail_send = fail_send self.login_calls: list[tuple[str, str]] = [] self.sent_messages: list[EmailMessage] = [] self.quit_count = 0 def login(self, user: str, password: str) -> object: self.login_calls.append((user, password)) return {} def send_message(self, msg: EmailMessage) -> object: if self.fail_send: raise smtplib.SMTPException("send failed") self.sent_messages.append(msg) return {} def quit(self) -> object: self.quit_count += 1 return {} def test_health_check_reports_disabled_and_dry_run_modes() -> None: disabled = OffsiteSmtpSender(_settings()).health_check() assert disabled == {"status": "disabled", "message": "场外 SMTP 未启用"} dry_run = OffsiteSmtpSender( _settings(offsite_smtp_enabled=True, offsite_smtp_dry_run=True) ).health_check() assert dry_run == {"status": "dry_run", "message": "场外 SMTP 处于 dry-run 模式"} def test_send_reply_requires_operator_confirmation_before_any_send() -> None: connection = FakeSmtpConnection() sender = OffsiteSmtpSender( _settings(offsite_smtp_enabled=True, offsite_smtp_dry_run=False), connection, ) result = sender.send_reply(_request(operator_confirmed=False)) assert result.status == "发送失败" assert result.failure_reason == "邮件发送前必须完成运营确认" assert connection.sent_messages == [] def test_send_reply_defaults_to_dry_run_without_real_smtp_call() -> None: connection = FakeSmtpConnection() sender = OffsiteSmtpSender(_settings(), connection) result = sender.send_reply(_request()) assert result.status == "待发送" assert result.dry_run is True assert result.provider_message_id is None assert result.failure_reason is None assert connection.sent_messages == [] assert result.request_summary["attachment_count"] == 1 def test_enabled_real_send_builds_reply_message_and_attaches_original_file() -> None: connection = FakeSmtpConnection() sender = OffsiteSmtpSender( _settings(offsite_smtp_enabled=True, offsite_smtp_dry_run=False), connection, ) result = sender.send_reply(_request(reply_to_message_id="")) assert result.status == "发送成功" assert result.dry_run is False assert result.retry_count == 0 assert connection.login_calls == [] assert len(connection.sent_messages) == 1 message = connection.sent_messages[0] assert message["From"] == "15273589815@163.com" assert message["To"] == "15008108550@163.com" assert message["In-Reply-To"] == "" assert message["References"] == "" assert message["Message-ID"] == result.provider_message_id attachments = list(message.iter_attachments()) assert len(attachments) == 1 assert attachments[0].get_filename() == "申购申请单.pdf" assert attachments[0].get_payload(decode=True) == b"pdf-bytes" def test_enabled_real_send_reports_failure_and_increments_retry_count() -> None: connection = FakeSmtpConnection(fail_send=True) sender = OffsiteSmtpSender( _settings(offsite_smtp_enabled=True, offsite_smtp_dry_run=False), connection, ) result = sender.send_reply(_request(retry_count=2)) assert result.status == "发送失败" assert result.failure_reason == "SMTPException" assert result.retry_count == 3 assert connection.quit_count == 1 def test_enabled_real_send_fails_closed_when_config_is_missing() -> None: sender = OffsiteSmtpSender( _settings( offsite_smtp_enabled=True, offsite_smtp_dry_run=False, offsite_smtp_host="", offsite_smtp_username="", offsite_smtp_password="", offsite_smtp_sender="", ) ) result = sender.send_reply(_request(retry_count=1)) assert result.status == "发送失败" assert result.retry_count == 2 assert "OFFSITE_SMTP_HOST" in str(result.failure_reason) def _request( *, operator_confirmed: bool = True, retry_count: int = 0, reply_to_message_id: str | None = None, ) -> OffsiteMailReplyRequest: return OffsiteMailReplyRequest( to_address="15008108550@163.com", subject="场外基金申购赎回处理结果", body="已处理。", operator_id="operator-001", operator_confirmed=operator_confirmed, reply_to_message_id=reply_to_message_id, retry_count=retry_count, attachments=( SmtpAttachment( filename="申购申请单.pdf", media_type="application/pdf", payload=b"pdf-bytes", ), ), ) def _settings(**updates: object) -> Settings: values: dict[str, object] = { "jwt_issuer": "jr-local", "jwt_audience": "jr-agent-platform", "mysql_dsn": "sqlite+aiosqlite:///test.db", "redis_url": "redis://127.0.0.1:6379/0", "milvus_uri": "http://127.0.0.1:19530", "neo4j_uri": "bolt://127.0.0.1:7687", "offsite_smtp_host": "smtp.example.local", "offsite_smtp_username": "15273589815@163.com", "offsite_smtp_password": "test-auth-code", "offsite_smtp_sender": "15273589815@163.com", "offsite_smtp_enabled": False, "offsite_smtp_dry_run": True, } values.update(updates) return Settings(**values)