feat: 迁移奶龙风控业务模块与演示文档
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from types import MappingProxyType
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.schemas.risk import RiskAlertPageQuery, RiskEvidencePageQuery
|
||||
from app.core.contracts import RequestContext
|
||||
from app.core.errors import GenericResourceNotFoundError, InvalidCursorError
|
||||
from app.core.risk_cursor import decode_offset_cursor, encode_offset_cursor
|
||||
from app.repository.fund_query_repository import CustomerScope, FundPage, FundRecord
|
||||
from app.service.risk_query_service import RiskQueryService, scope_from_context
|
||||
|
||||
|
||||
class FakeRepository:
|
||||
def __init__(self) -> None:
|
||||
self.page = FundPage(
|
||||
entity="risk_alert",
|
||||
items=(
|
||||
FundRecord(
|
||||
entity="risk_alert",
|
||||
values=MappingProxyType(
|
||||
{
|
||||
"alert_no": "ALERT-001",
|
||||
"customer_id": 9,
|
||||
"risk_level": "高",
|
||||
"amount": Decimal("100.00"),
|
||||
"created_at": datetime(2026, 9, 10, 8, 0),
|
||||
"rule_codes": ("RW-007",),
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
limit=5,
|
||||
offset=0,
|
||||
next_offset=5,
|
||||
)
|
||||
|
||||
async def overview(self) -> dict:
|
||||
return {
|
||||
"total": 2,
|
||||
"levels": {"低": 1, "中": 0, "高": 1},
|
||||
"pending": 1,
|
||||
"overdue": 0,
|
||||
"high_priority": list(self.page.items),
|
||||
}
|
||||
|
||||
async def list_alerts(self, **_kwargs) -> FundPage:
|
||||
return self.page
|
||||
|
||||
async def get_alert_detail(self, _alert_no: str) -> FundRecord | None:
|
||||
return None
|
||||
|
||||
|
||||
def context(**updates) -> RequestContext:
|
||||
values = {
|
||||
"user_id": "990000002",
|
||||
"trace_id": "trace",
|
||||
"permissions": ("risk:alert:read",),
|
||||
"data_scope": "all",
|
||||
}
|
||||
values.update(updates)
|
||||
return RequestContext(**values)
|
||||
|
||||
|
||||
def test_cursor_round_trip_and_invalid_values() -> None:
|
||||
assert decode_offset_cursor(encode_offset_cursor(20)) == 20
|
||||
assert decode_offset_cursor(None) == 0
|
||||
with pytest.raises(InvalidCursorError):
|
||||
decode_offset_cursor("not-a-cursor")
|
||||
with pytest.raises(InvalidCursorError):
|
||||
decode_offset_cursor(encode_offset_cursor(1) + "x")
|
||||
|
||||
|
||||
def test_query_schema_enforces_business_page_sizes() -> None:
|
||||
assert RiskAlertPageQuery(limit=5).limit == 5
|
||||
assert RiskEvidencePageQuery(limit=10).limit == 10
|
||||
with pytest.raises(ValueError):
|
||||
RiskAlertPageQuery(limit=6)
|
||||
with pytest.raises(ValueError):
|
||||
RiskEvidencePageQuery(limit=11)
|
||||
|
||||
|
||||
def test_scope_from_context_is_fail_closed() -> None:
|
||||
assert scope_from_context(context()).is_unrestricted
|
||||
assert scope_from_context(context(data_scope="self")).is_denied
|
||||
scope = scope_from_context(
|
||||
context(data_scope="own_customers", customer_ids=("9", "10"))
|
||||
)
|
||||
assert scope == CustomerScope.for_customers({9, 10})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_overview_maps_levels_and_serializes_high_priority() -> None:
|
||||
service = RiskQueryService(None, repository=FakeRepository())
|
||||
|
||||
result = await service.overview(context())
|
||||
|
||||
assert result["levels"] == {"高风险": 1, "中风险": 0, "低风险": 1}
|
||||
assert result["high_priority"][0]["customer_id"] == "9"
|
||||
assert result["high_priority"][0]["amount"] == "100.00"
|
||||
assert result["high_priority"][0]["created_at"] == "2026-09-10T08:00:00Z"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alert_page_returns_opaque_cursor() -> None:
|
||||
service = RiskQueryService(None, repository=FakeRepository())
|
||||
|
||||
result = await service.list_alerts(context(), RiskAlertPageQuery(limit=5))
|
||||
|
||||
assert result["has_more"] is True
|
||||
assert decode_offset_cursor(result["next_cursor"]) == 5
|
||||
assert result["items"][0]["rule_codes"] == ["RW-007"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_alert_detail_is_hidden() -> None:
|
||||
service = RiskQueryService(None, repository=FakeRepository())
|
||||
|
||||
with pytest.raises(GenericResourceNotFoundError):
|
||||
await service.get_alert_detail(context(), "ALERT-NOT-FOUND")
|
||||
Reference in New Issue
Block a user