Files
group_fqcd_jr/tests/unit/service/test_risk_scan_notification.py
T

129 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""扫描的通知环节:失败必须能被区分出来。
**背景**:`_create_notifications` 原先失败时 `return 0`,而"无需通知"(没有高风险预警、
通知功能关闭)也返回 0 —— 调用方只拿到一个 `notification_count`,分不清两者。
这不是洁癖:**通知没发出去等于处置链路的第一环断了,而扫描依旧报"完成"**。
风控系统里这种"看起来在工作、其实有一环没跑"的状态最难发现(同一批缺陷里,
"定时扫描重启后不再执行"也是这个性质)。
所以这里锁住契约:返回 `(条数, 失败原因)`,失败原因非空即代表出了问题。
"""
import logging
from types import SimpleNamespace
from typing import Any, cast
import pytest
from app.service.risk_scan_service import HIGH_RISK, RiskScanService
class _FakeNested:
async def __aenter__(self) -> "_FakeNested":
return self
async def __aexit__(self, *exc: object) -> bool:
return False
class _FakeSession:
"""只提供 `begin_nested()`;被测方法不碰其它会话能力。"""
def begin_nested(self) -> _FakeNested:
return _FakeNested()
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:
raise RuntimeError("通知写库失败")
self.in_app.append(str(alert.alert_no))
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:
return SimpleNamespace(
alert_level=level, alert_no=alert_no, alert_type="大额频繁交易",
evidence_summary="摘要", handler_id=9002,
)
def _service(*, enabled: bool = True, fail: bool = False,
email: str | None = None) -> RiskScanService:
service = object.__new__(RiskScanService)
service.notification_enabled = enabled
service.notification_email = email
service.mail_enabled = False
service.notification_service = cast(Any, _FakeNotificationService(fail=fail))
service.session = cast(Any, _FakeSession())
return service
@pytest.mark.asyncio
async def test_success_reports_count_and_no_failure() -> None:
service = _service()
count, failure = await service._create_notifications([_alert(HIGH_RISK)])
assert count == 1
assert failure == ""
@pytest.mark.asyncio
async def test_nothing_to_notify_is_not_a_failure() -> None:
"""没有高风险预警,或通知功能关闭——都不算失败,失败原因应为空。"""
service = _service()
assert await service._create_notifications([_alert("低")]) == (0, "")
disabled = _service(enabled=False)
assert await disabled._create_notifications([_alert(HIGH_RISK)]) == (0, "")
@pytest.mark.asyncio
async def test_failure_is_reported_not_swallowed(caplog: pytest.LogCaptureFixture) -> None:
"""通知创建失败时必须带出原因——这正是原先丢失的信号。"""
service = _service(fail=True)
with caplog.at_level(logging.ERROR):
count, failure = await service._create_notifications([_alert(HIGH_RISK)])
assert count == 0
assert "RuntimeError" in failure
assert "通知写库失败" in failure
@pytest.mark.asyncio
async def test_failure_reason_differs_from_nothing_to_notify() -> None:
"""两种 0 必须可区分——这是本次修复的全部意义。"""
quiet, _ = await _service()._create_notifications([_alert("低")])
_count, failure = await _service(fail=True)._create_notifications([_alert(HIGH_RISK)])
assert quiet == 0
assert failure != "", "失败必须能与『无需通知』区分开"