175 lines
5.7 KiB
Python
175 lines
5.7 KiB
Python
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="<source@example.local>"))
|
|
|
|
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"] == "<source@example.local>"
|
|
assert message["References"] == "<source@example.local>"
|
|
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)
|