修复高风险预警邮件通知并补充测试
This commit is contained in:
@@ -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"],
|
||||
"高风险预警",
|
||||
"请复核",
|
||||
)
|
||||
Reference in New Issue
Block a user