修复高风险预警邮件通知并补充测试

This commit is contained in:
zhangshy
2026-09-12 15:56:50 +08:00
parent 164d55a05a
commit eb4e895e4e
12 changed files with 464 additions and 34 deletions
+4
View File
@@ -56,6 +56,10 @@ class StubRiskScanService:
def __init__(self, _session: Any) -> None:
pass
@classmethod
def from_settings(cls, session: Any) -> "StubRiskScanService":
return cls(session)
async def scan(self, _context: RequestContext) -> dict[str, Any]:
return {"message": "规则扫描完成", "created_count": 2, "high_risk_count": 1}
@@ -0,0 +1,125 @@
"""验证高风险预警扫描会真实调用 SMTP,并正确回写通知状态。"""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any, cast
import pytest
from app.service.risk_scan_service import HIGH_RISK, RiskScanService
from app.service.risk_smtp_mail_service import RiskSmtpSendError
class FakeNested:
async def __aenter__(self) -> FakeNested:
return self
async def __aexit__(self, *_args: object) -> bool:
return False
class FakeSession:
def begin_nested(self) -> FakeNested:
return FakeNested()
class FakeNotificationService:
def __init__(self) -> None:
self.mail_records: list[Any] = []
self.mail_marks: list[tuple[str, str | None]] = []
def create_in_app(self, _alert: Any, **_kwargs: Any) -> None:
return None
def create_mail_record(self, alert: Any, **_kwargs: Any) -> Any:
notification = SimpleNamespace(
alert=alert,
send_status="待发送",
sent_at=None,
fail_reason=None,
)
self.mail_records.append(notification)
return notification
def mark_mail_sent(self, notification: Any) -> None:
self.mail_marks.append(("sent", None))
notification.send_status = "已发送"
def mark_mail_failed(self, notification: Any, reason: str) -> None:
self.mail_marks.append(("failed", reason))
notification.send_status = "发送失败"
def alert() -> Any:
return SimpleNamespace(
alert_level=HIGH_RISK,
alert_no="ALTEST",
alert_type="大额快进快出",
evidence_summary="3 日内入金后大额赎回",
handler_id=9002,
)
def service() -> RiskScanService:
instance = object.__new__(RiskScanService)
instance.session = cast(Any, FakeSession())
instance.notification_enabled = True
instance.notification_email = "risk@example.com"
instance.mail_enabled = True
instance.mail_dry_run = False
instance.notification_service = cast(Any, FakeNotificationService())
return instance
@pytest.mark.asyncio
async def test_high_risk_mail_is_sent_and_marked_successful() -> None:
class SmtpStub:
def __init__(self) -> None:
self.calls: list[tuple[list[str], str, str]] = []
async def send(
self,
recipients: list[str],
subject: str,
content: str,
) -> dict[str, object]:
self.calls.append((recipients, subject, content))
return {"status": "sent", "recipient_count": len(recipients)}
instance = service()
notifier = cast(FakeNotificationService, instance.notification_service)
smtp = SmtpStub()
instance.smtp_mail_service = cast(Any, smtp)
count, failure = await instance._create_notifications([alert()])
assert count == 2
assert failure == ""
assert smtp.calls[0][0] == ["risk@example.com"]
assert "预警编号:ALTEST" in smtp.calls[0][2]
assert notifier.mail_marks == [("sent", None)]
assert notifier.mail_records[0].send_status == "已发送"
@pytest.mark.asyncio
async def test_high_risk_mail_failure_does_not_abort_scan() -> None:
class FailingSmtp:
async def send(
self,
_recipients: list[str],
_subject: str,
_content: str,
) -> dict[str, object]:
raise RiskSmtpSendError("SMTP 身份验证失败")
instance = service()
notifier = cast(FakeNotificationService, instance.notification_service)
instance.smtp_mail_service = cast(Any, FailingSmtp())
count, failure = await instance._create_notifications([alert()])
assert count == 2
assert failure == ""
assert notifier.mail_marks == [("failed", "SMTP 身份验证失败")]
assert notifier.mail_records[0].send_status == "发送失败"
@@ -38,6 +38,8 @@ class _FakeNotificationService:
def __init__(self, *, fail: bool = False) -> None:
self.fail = fail
self.in_app: list[str] = []
self.mail_records: list[Any] = []
self.mail_marks: list[tuple[str, str | None]] = []
def create_in_app(self, alert: Any, **kwargs: Any) -> None:
if self.fail:
@@ -47,6 +49,22 @@ class _FakeNotificationService:
def create_mail_record(self, alert: Any, **kwargs: Any) -> None:
if self.fail:
raise RuntimeError("邮件记录写库失败")
notification = SimpleNamespace(
alert=alert,
send_status="待发送",
sent_at=None,
fail_reason=None,
)
self.mail_records.append(notification)
return notification
def mark_mail_sent(self, notification: Any) -> None:
self.mail_marks.append(("sent", None))
notification.send_status = "已发送"
def mark_mail_failed(self, notification: Any, reason: str) -> None:
self.mail_marks.append(("failed", reason))
notification.send_status = "发送失败"
def _alert(level: str, alert_no: str = "ALTEST") -> Any:
@@ -0,0 +1,71 @@
"""验证风控共用 SMTP 服务的基础发送行为。"""
from __future__ import annotations
from typing import Any
import pytest
from app.service import risk_smtp_mail_service
from app.service.risk_smtp_mail_service import (
RiskSmtpConfigurationError,
RiskSmtpMailService,
)
class SmtpStub:
instances: list[SmtpStub] = []
def __init__(self, host: str, port: int, timeout: float) -> None:
self.host = host
self.port = port
self.timeout = timeout
self.login_args: tuple[str, str] | None = None
self.message: Any = None
self.__class__.instances.append(self)
def __enter__(self) -> SmtpStub:
return self
def __exit__(self, *_args: object) -> None:
return None
def login(self, username: str, password: str) -> None:
self.login_args = (username, password)
def send_message(self, message: Any) -> None:
self.message = message
@pytest.mark.asyncio
async def test_smtp_service_sends_message(monkeypatch: pytest.MonkeyPatch) -> None:
SmtpStub.instances = []
monkeypatch.setattr(risk_smtp_mail_service.smtplib, "SMTP_SSL", SmtpStub)
result = await RiskSmtpMailService(
environment={
"RISK_SMTP_HOST": "smtp.example.com",
"RISK_SMTP_PORT": "465",
"RISK_SMTP_USERNAME": "sender@example.com",
"RISK_SMTP_PASSWORD": "secret",
"RISK_SMTP_SENDER": "sender@example.com",
"RISK_SMTP_USE_SSL": "true",
}
).send(["risk@example.com"], "高风险预警", "请复核")
assert result == {"status": "sent", "recipient_count": 1}
client = SmtpStub.instances[0]
assert (client.host, client.port) == ("smtp.example.com", 465)
assert client.login_args == ("sender@example.com", "secret")
assert client.message["To"] == "risk@example.com"
assert client.message["Subject"] == "高风险预警"
@pytest.mark.asyncio
async def test_smtp_service_rejects_missing_configuration() -> None:
with pytest.raises(RiskSmtpConfigurationError):
await RiskSmtpMailService(environment={}).send(
["risk@example.com"],
"高风险预警",
"请复核",
)