Files
group_xinghuo_jinrong/tests/test_demo_scripts.py
T

153 lines
5.5 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.
"""B9a 演示/运维脚本单测(开发计划 B9a · sqlite)。
rebuild_alerts:补偿重放出单并推送、幂等跳过(防重复 append/aml 重复出单)、
missing 不落库;subscribe_alerts:payload → 单行可读文本。
脚本目录非包,动态入 sys.path 后按模块名导入。
"""
import sys
from datetime import datetime
from decimal import Decimal
from pathlib import Path
import pytest
from sqlalchemy import text
from _ddl import create_sqlite_engine
DEMO_DIR = Path(__file__).resolve().parents[1] / "scripts" / "demo"
sys.path.insert(0, str(DEMO_DIR))
from rebuild_alerts import rebuild_trade # noqa: E402
from subscribe_alerts import format_alert # noqa: E402
from app.repository.core_ro import CoreReadOnlyRepository
from app.repository.risk_repository import RiskRepository
from app.service.risk import alert_service
class FakePublisher:
def __init__(self):
self.messages = []
def publish(self, channel, payload):
self.messages.append((channel, payload))
@pytest.fixture()
def env():
engine = create_sqlite_engine()
with engine.begin() as conn:
conn.execute(
text(
"INSERT INTO core_customer (customer_id, display_name, age, is_active) VALUES"
" ('C1', '张某某', 40, 1), ('C2', '李四', 35, 1)"
)
)
conn.execute(
text(
"INSERT INTO core_product (product_id, product_name, min_risk_code, product_type)"
" VALUES ('P1', '测试混合基金', 'R3', 'mixed')"
)
)
conn.execute(
text(
"INSERT INTO risk_aml_list (list_id, list_type, full_name, match_threshold,"
" source, list_version, is_active) VALUES ('PEP-1', 'pep', '李四', 0.85, 'mock', 'v1', 1)"
)
)
core = CoreReadOnlyRepository(engine=engine)
repo = RiskRepository(engine=engine)
pub = FakePublisher()
alert_service.set_publisher(pub)
yield core, repo, pub, engine
alert_service.set_publisher(None)
engine.dispose()
def _seed_trade(conn, trade_id, amount, customer="C1", at=datetime(2026, 9, 6, 14, 0, 0)):
"""直接落 core_trade 不调引擎(engine_error 补偿场景:交易已成立、预警缺失)。"""
conn.execute(
text(
"INSERT INTO core_trade (trade_id, customer_id, product_id, trade_type, amount,"
" trade_status, traded_at) VALUES (:tid, :cid, 'P1', 'subscribe', :amt, 'confirmed', :at)"
),
{"tid": trade_id, "cid": customer, "amt": amount, "at": at},
)
def _counts(engine, table, where="1=1"):
with engine.connect() as conn:
return conn.execute(text(f"SELECT COUNT(*) FROM {table} WHERE {where}")).scalar_one()
def test_rebuild_creates_alert_and_publishes(env):
core, repo, pub, engine = env
with engine.begin() as conn:
_seed_trade(conn, "TRD-TEST-RB1", "600000")
out = rebuild_trade("TRD-TEST-RB1", core, repo)
assert out["state"] == "rebuilt"
assert out["triggered_rules"] == ["RISK-001", "RISK-002"]
assert len(out["alert_ids"]) == 1 and out["aml_hit"] is False
alert = repo.get_alert(out["alert_ids"][0])
assert alert["status"] == "pending_review" and alert["risk_score"] == 70
(channel, _), = pub.messages
assert channel == "risk:pub:alert"
def test_rebuild_idempotent_skips_second_run(env):
core, repo, pub, engine = env
with engine.begin() as conn:
_seed_trade(conn, "TRD-TEST-RB2", "600000")
first = rebuild_trade("TRD-TEST-RB2", core, repo)
second = rebuild_trade("TRD-TEST-RB2", core, repo)
assert first["state"] == "rebuilt" and second["state"] == "skipped"
assert second["alert_ids"] == first["alert_ids"]
assert _counts(engine, "risk_alert") == 1
assert len(repo.get_alert(first["alert_ids"][0])["payload"]["events"]) == 1
assert len(pub.messages) == 1 # 重放不重复推送
def test_rebuild_missing_trade_touches_nothing(env):
core, repo, pub, engine = env
out = rebuild_trade("TRD-NO-SUCH", core, repo)
assert out["state"] == "missing" and out["alert_ids"] == []
assert _counts(engine, "risk_alert") == 0
assert _counts(engine, "audit_log") == 0
assert pub.messages == []
def test_rebuild_aml_hit_then_idempotent(env):
"""aml 单幂等是 LIKE 检查的关键价值:record_aml_alert 本身无去重,重放防二次出单。"""
core, repo, pub, engine = env
with engine.begin() as conn:
_seed_trade(conn, "TRD-TEST-RB3", "1000", customer="C2")
first = rebuild_trade("TRD-TEST-RB3", core, repo)
assert first["state"] == "rebuilt" and first["aml_hit"] is True
aml_ids = [
aid
for aid in first["alert_ids"]
if repo.get_alert(aid)["alert_type"] == "aml"
]
assert aml_ids
second = rebuild_trade("TRD-TEST-RB3", core, repo)
assert second["state"] == "skipped" and second["alert_ids"] == first["alert_ids"]
assert _counts(engine, "risk_alert", "alert_type='aml'") == 1
def test_format_alert_renders_payload_fields():
line = format_alert(
{
"alert_id": "ALT-20260906-ABC",
"alert_type": "aml",
"customer_id_mask": "CUST-9**",
"risk_score": 95,
"trace_id": "tr-1",
"notify_role": ["risk_officer", "compliance"],
}
)
for frag in ("aml", "ALT-20260906-ABC", "score=95", "CUST-9**", "tr-1",
"risk_officer,compliance"):
assert frag in line
assert "CUST-9527" not in line # payload 只有脱敏掩码,无原始 id