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

122 lines
4.1 KiB
Python
Raw Normal View History

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")