Initial commit: InternalAuditInterprise

This commit is contained in:
freedakgmail
2026-06-16 00:38:57 +08:00
commit 7b1e2b10a8
57 changed files with 4622 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
"""集成测试 fixture:连接本地 PostgreSQL 16,按事务隔离并回滚。
需要可连接的数据库(DATABASE_URL)。无法连接时跳过整组集成测试。
"""
from __future__ import annotations
import pytest
from sqlalchemy import text
from sqlalchemy.exc import OperationalError
from app.db import get_engine
@pytest.fixture(scope="session")
def db_available() -> bool:
try:
with get_engine().connect() as conn:
conn.execute(text("SELECT 1"))
return True
except OperationalError:
return False
@pytest.fixture()
def session(db_available):
if not db_available:
pytest.skip("数据库不可用,跳过集成测试")
engine = get_engine()
connection = engine.connect()
trans = connection.begin()
from sqlalchemy.orm import Session
sess = Session(bind=connection)
try:
yield sess
finally:
sess.close()
if trans.is_active:
trans.rollback()
connection.close()
@@ -0,0 +1,49 @@
"""双时态集成测试(需 PostgreSQL)。
验证 R3:按历史业务时点回放属性值,以及双时态排他约束防止有效期重叠。
"""
from __future__ import annotations
import datetime as dt
import pytest
from sqlalchemy.exc import IntegrityError
from app.datahub import bitemporal_repo as btr
from app.datahub.graph_repo import upsert_entity
from app.datahub.ontology import EntityType
def test_bitemporal_replay(session):
"""不同业务时点回放出不同的属性值。"""
cust = upsert_entity(session, EntityType.CUSTOMER, "CUST_BT", "丁公司")
session.flush()
t1 = dt.datetime(2025, 1, 1, tzinfo=dt.UTC)
t2 = dt.datetime(2025, 6, 1, tzinfo=dt.UTC)
btr.record_fact(session, cust.id, "credit_level", {"v": "A"}, valid_from=t1, valid_to=t2)
btr.record_fact(session, cust.id, "credit_level", {"v": "C"}, valid_from=t2)
session.flush()
early = btr.as_of(session, cust.id, "credit_level", dt.datetime(2025, 3, 1, tzinfo=dt.UTC))
late = btr.as_of(session, cust.id, "credit_level", dt.datetime(2025, 9, 1, tzinfo=dt.UTC))
assert early is not None and early.attr_value["v"] == "A"
assert late is not None and late.attr_value["v"] == "C"
def test_bitemporal_exclusion_constraint(session):
"""同一实体同一属性的业务有效期重叠应被排他约束拒绝。"""
cust = upsert_entity(session, EntityType.CUSTOMER, "CUST_EX", "戊公司")
session.flush()
t1 = dt.datetime(2025, 1, 1, tzinfo=dt.UTC)
t3 = dt.datetime(2025, 12, 1, tzinfo=dt.UTC)
t2 = dt.datetime(2025, 6, 1, tzinfo=dt.UTC)
btr.record_fact(session, cust.id, "status", {"v": "active"}, valid_from=t1, valid_to=t3)
session.flush()
# 与上一条 [t1,t3) 重叠:record_fact 内部 flush 时即触发排他约束
with pytest.raises(IntegrityError):
btr.record_fact(session, cust.id, "status", {"v": "frozen"}, valid_from=t2, valid_to=None)
@@ -0,0 +1,87 @@
"""线索闭环 + 系统自审计集成测试(需 PostgreSQL)。
覆盖 R7/R17/R18/R19:线索生成与分级、状态流转、底稿、审计哈希链、线索不可删。
"""
from __future__ import annotations
import pytest
from sqlalchemy import text
from sqlalchemy.exc import InternalError, ProgrammingError
from app.audit import service as audit
from app.clues import service as clue_svc
from app.clues.models import ClueStatus, ConfidenceTier
def _new_clue(session, score=0.9):
return clue_svc.create_clue(
session,
title="疑似政企拆单",
risk_domain="收入",
scenario_code="R8",
score=score,
rationale="8 个客户金额集中在审批阈值边缘,且法人关联同一实控人",
evidence={"contracts": 8, "threshold": 1000000},
amount_involved=4800000,
actor="system",
)
def test_score_to_confidence_tier():
assert clue_svc.score_to_tier(0.9) == ConfidenceTier.HIGH
assert clue_svc.score_to_tier(0.6) == ConfidenceTier.MEDIUM
assert clue_svc.score_to_tier(0.2) == ConfidenceTier.LOW
def test_clue_full_lifecycle(session):
clue = _new_clue(session)
assert clue.confidence == ConfidenceTier.HIGH
assert clue.status == ClueStatus.NEW
clue_svc.assign(session, clue, assignee="auditor_zhang", actor="manager_li")
assert clue.status == ClueStatus.ASSIGNED
assert clue.assignee == "auditor_zhang"
paper = clue_svc.adjudicate(session, clue, confirmed=True, actor="auditor_zhang", note="属实,移交")
assert clue.status == ClueStatus.CONFIRMED
assert clue.feedback == "confirmed"
assert paper.conclusion == "confirmed"
assert paper.snapshot["score"] == 0.9
# 继续闭环:确认 -> 移交 -> 销项
clue_svc.transition(session, clue, ClueStatus.TRANSFERRED, actor="manager_li")
clue_svc.transition(session, clue, ClueStatus.CLOSED, actor="manager_li")
assert clue.status == ClueStatus.CLOSED
def test_illegal_transition_rejected(session):
clue = _new_clue(session)
with pytest.raises(clue_svc.IllegalTransitionError):
# NEW 不能直接到 CLOSED
clue_svc.transition(session, clue, ClueStatus.CLOSED, actor="x")
def test_audit_hash_chain_integrity(session):
_new_clue(session)
clue = _new_clue(session)
clue_svc.assign(session, clue, "auditor_zhang", "manager_li")
ok, broken = audit.verify_chain(session)
assert ok is True
assert broken is None
def test_clue_cannot_be_deleted(session):
"""R19:数据库触发器禁止物理删除线索。"""
clue = _new_clue(session)
session.flush()
with pytest.raises((InternalError, ProgrammingError)):
session.execute(text("DELETE FROM clue WHERE id = :i"), {"i": clue.id})
session.flush()
def test_list_clues_filters(session):
_new_clue(session, score=0.9)
_new_clue(session, score=0.3)
highs = clue_svc.list_clues(session, confidence=ConfidenceTier.HIGH)
assert all(c.confidence == ConfidenceTier.HIGH for c in highs)
@@ -0,0 +1,63 @@
"""数据中台穿透 API 集成测试(需 PostgreSQL)。
通过 TestClient 调用 /datahub/penetrate,验证统一穿透查询服务端到端可用。
"""
from __future__ import annotations
import uuid
import pytest
from fastapi.testclient import TestClient
from app.datahub.graph_repo import add_relationship, upsert_entity
from app.datahub.ontology import EntityType, RelationshipType
from app.db import get_session
from app.main import app
@pytest.fixture()
def client(session):
# 用集成测试的事务化 session 覆盖应用依赖,保证测试数据回滚
app.dependency_overrides[get_session] = lambda: session
try:
yield TestClient(app)
finally:
app.dependency_overrides.pop(get_session, None)
def test_penetrate_endpoint_detects_related(client, session):
suffix = uuid.uuid4().hex[:8]
controller = upsert_entity(session, EntityType.LEGAL_PERSON, f"CTRL-{suffix}", "实控人")
cust = upsert_entity(session, EntityType.CUSTOMER, f"CUST-{suffix}", "政企客户")
rep = upsert_entity(session, EntityType.LEGAL_PERSON, f"REP-{suffix}", "法人")
add_relationship(session, RelationshipType.LEGAL_REP_OF, rep, cust)
add_relationship(session, RelationshipType.RELATED_TO, rep, controller)
session.flush()
resp = client.post(
"/datahub/penetrate",
json={"start_entity_id": str(controller.id), "max_depth": 3},
)
assert resp.status_code == 200
body = resp.json()
related_ids = {r["entity"]["id"] for r in body["related"]}
assert str(cust.id) in related_ids
assert body["related_count"] >= 2
def test_penetrate_unknown_entity_404(client):
resp = client.post(
"/datahub/penetrate",
json={"start_entity_id": str(uuid.uuid4()), "max_depth": 2},
)
assert resp.status_code == 404
def test_get_entity_endpoint(client, session):
suffix = uuid.uuid4().hex[:8]
e = upsert_entity(session, EntityType.SUPPLIER, f"SUP-{suffix}", "供应商甲")
session.flush()
resp = client.get(f"/datahub/entities/{e.id}")
assert resp.status_code == 200
assert resp.json()["business_key"] == f"SUP-{suffix}"
@@ -0,0 +1,76 @@
"""知识图谱穿透集成测试(需 PostgreSQL)。
验证 R2 关键能力:通过关系边的多跳穿透识别"疑似同一实控人"
以及本体约束对非法关系的拒绝。对应场景一(政企拆单+隐性实控人,R8)的图谱基础。
"""
from __future__ import annotations
import pytest
from app.datahub.graph_repo import (
OntologyViolationError,
add_relationship,
find_related_entities,
upsert_entity,
)
from app.datahub.ontology import EntityType, RelationshipType
def test_upsert_entity_is_idempotent(session):
e1 = upsert_entity(session, EntityType.CUSTOMER, "CUST-001", "客户甲")
e2 = upsert_entity(session, EntityType.CUSTOMER, "CUST-001", "客户甲")
assert e1.id == e2.id
def test_ontology_violation_rejected(session):
contract = upsert_entity(session, EntityType.CONTRACT, "C-1")
customer = upsert_entity(session, EntityType.CUSTOMER, "CUST-2")
# 合同 —签约→ 客户 方向非法
with pytest.raises(OntologyViolationError):
add_relationship(session, RelationshipType.SIGNED, contract, customer)
def test_detect_shared_controller_across_customers(session):
"""模拟"8 个客户疑似同一实控人":多个客户经法人关联到同一实控自然人。
构图:每个客户 <-法定代表人- 各自法人;各法人 -关联-> 同一实控人。
从实控人出发,应能穿透到全部客户。
"""
controller = upsert_entity(session, EntityType.LEGAL_PERSON, "PER-CTRL", "实控人")
customers = []
for i in range(8):
cust = upsert_entity(session, EntityType.CUSTOMER, f"CUST-{i}", f"政企客户{i}")
rep = upsert_entity(session, EntityType.LEGAL_PERSON, f"PER-{i}", f"法人{i}")
# 法人 —法定代表人→ 客户
add_relationship(session, RelationshipType.LEGAL_REP_OF, rep, cust)
# 法人 —关联(亲属/实控)→ 实控人
add_relationship(session, RelationshipType.RELATED_TO, rep, controller)
customers.append(cust)
session.flush()
related = find_related_entities(session, controller.id, max_depth=3)
related_ids = {rid for rid, _ in related}
# 从实控人 3 跳内应能穿透到全部 8 个客户
for cust in customers:
assert cust.id in related_ids, f"未穿透到 {cust.business_key}"
def test_traversal_respects_max_depth(session):
a = upsert_entity(session, EntityType.LEGAL_PERSON, "A")
b = upsert_entity(session, EntityType.LEGAL_PERSON, "B")
c = upsert_entity(session, EntityType.CUSTOMER, "C")
add_relationship(session, RelationshipType.RELATED_TO, a, b)
add_relationship(session, RelationshipType.LEGAL_REP_OF, b, c)
session.flush()
# depth=1:从 A 只能到 B,到不了 C
ids_d1 = {rid for rid, _ in find_related_entities(session, a.id, max_depth=1)}
assert b.id in ids_d1
assert c.id not in ids_d1
# depth=2:能到 C
ids_d2 = {rid for rid, _ in find_related_entities(session, a.id, max_depth=2)}
assert c.id in ids_d2