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

150 lines
4.5 KiB
Python
Raw Normal View History

from datetime import datetime
from decimal import Decimal
import pytest
from app.core.contracts import RequestContext
from app.model.audit import InteractionAudit
from app.model.fund import FundCustomerProfile, FundRiskAlert
from app.service.risk_action_service import RiskActionError, RiskActionService
class FakeSession:
def __init__(self, alerts=None, profiles=None):
self.values = list(alerts or [])
self.profiles = list(profiles or [])
self.added = []
self.committed = False
async def scalar(self, _statement):
if self.values:
return self.values.pop(0)
if self.profiles:
return self.profiles.pop(0)
return None
def add(self, value):
self.added.append(value)
async def commit(self):
self.committed = True
async def refresh(self, _value):
return None
def context() -> RequestContext:
return RequestContext(
user_id="990000002",
trace_id="action-trace",
permissions=("risk:alert:write",),
data_scope="all",
)
def alert(
*,
status: str = "待处理",
ack_at: datetime | None = None,
level: str = "高",
) -> FundRiskAlert:
return FundRiskAlert(
id=1,
alert_no="ALERT-001",
customer_id=9,
alert_type="适当性错配",
alert_level=level,
trigger_rule_codes=["RW-007"],
evidence_summary="证据",
evidence_snapshot={},
priority_score=90,
event_status="正在发生",
status=status,
ack_status="已确认" if ack_at else "未确认",
ack_at=ack_at,
is_escalated=0,
created_at=datetime(2026, 9, 10),
updated_at=datetime(2026, 9, 10),
)
def profile(score: int = 20) -> FundCustomerProfile:
return FundCustomerProfile(
customer_id=9,
trade_account="ACC-009",
real_name="张三",
investor_type="C2",
total_asset=Decimal("100000.00"),
behavior_score=score,
updated_at=datetime(2026, 9, 10),
)
@pytest.mark.asyncio
async def test_acknowledge_updates_status_and_audit() -> None:
session = FakeSession(alerts=[alert()])
result = await RiskActionService(session).acknowledge("ALERT-001", context())
assert result["ack_status"] == "已确认"
assert session.committed is True
assert session.added[0].action_type == "risk_alert_acknowledged"
@pytest.mark.asyncio
async def test_investigate_requires_acknowledgement() -> None:
session = FakeSession(alerts=[alert()])
with pytest.raises(RiskActionError, match="确认接收"):
await RiskActionService(session).investigate("ALERT-001", context())
@pytest.mark.asyncio
async def test_exclude_requires_reason_and_closes_false_positive() -> None:
session = FakeSession(alerts=[alert(ack_at=datetime(2026, 9, 10))])
result = await RiskActionService(session).exclude("ALERT-001", "客户本人确认", context())
assert result["status"] == "已排除"
assert result["handle_result"] == "客户本人确认"
@pytest.mark.parametrize(
("level", "before", "deduction", "after"),
[("低", 20, 3, 17), ("中", 20, 5, 15), ("高", 20, 20, 0), ("高", 4, 20, 0)],
)
@pytest.mark.asyncio
async def test_resolve_applies_behavior_score_deduction(
level: str,
before: int,
deduction: int,
after: int,
) -> None:
session = FakeSession(
alerts=[alert(status="调查中", ack_at=datetime(2026, 9, 10), level=level)],
profiles=[profile(before)],
)
result = await RiskActionService(session).resolve("ALERT-001", "已核实", context())
assert result["status"] == "已结案"
assert result["behavior_score_deduction"] == deduction
assert result["behavior_score_after"] == after
assert any(isinstance(item, InteractionAudit) for item in session.added)
@pytest.mark.asyncio
async def test_escalate_is_not_terminal_and_rejects_duplicate() -> None:
session = FakeSession(alerts=[alert(status="调查中", ack_at=datetime(2026, 9, 10))])
result = await RiskActionService(session).escalate("ALERT-001", "需要高级复核", context())
assert result["is_escalated"] is True
assert result["status"] == "调查中"
assert result["escalation_reason"] == "需要高级复核"
session = FakeSession(alerts=[alert(status="调查中", ack_at=datetime(2026, 9, 10))])
session.values[0].is_escalated = 1
with pytest.raises(RiskActionError, match="已经升级"):
await RiskActionService(session).escalate("ALERT-001", "重复", context())