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
View File
+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
+42
View File
@@ -0,0 +1,42 @@
"""数据零出域红线测试:prod 环境必须禁用公网 LLM Provider。"""
import pytest
from app.config import AppEnv, LLMProviderName, Settings
from app.llm.factory import EgressPolicyError, get_llm_provider
def _settings(env: AppEnv, provider: LLMProviderName) -> Settings:
return Settings(aiaudit_env=env, llm_provider=provider, dashscope_api_key="x")
def test_prod_blocks_public_dashscope():
s = _settings(AppEnv.prod, LLMProviderName.dashscope)
with pytest.raises(EgressPolicyError):
get_llm_provider(s)
def test_prod_allows_local_vllm():
s = _settings(AppEnv.prod, LLMProviderName.vllm)
provider = get_llm_provider(s)
assert provider.name == "vllm"
assert provider.egress is False
def test_dev_allows_dashscope():
s = _settings(AppEnv.dev, LLMProviderName.dashscope)
provider = get_llm_provider(s)
assert provider.name == "dashscope"
assert provider.egress is True
def test_validate_egress_policy_raises_in_prod():
s = _settings(AppEnv.prod, LLMProviderName.dashscope)
with pytest.raises(RuntimeError):
s.validate_egress_policy()
def test_validate_egress_policy_ok_in_dev():
s = _settings(AppEnv.dev, LLMProviderName.dashscope)
# dev 下不应抛出
s.validate_egress_policy()
+21
View File
@@ -0,0 +1,21 @@
"""健康检查端点测试。"""
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_ok():
resp = client.get("/health")
assert resp.status_code == 200
assert resp.json()["status"] == "ok"
def test_health_config():
resp = client.get("/health/config")
assert resp.status_code == 200
body = resp.json()
assert "env" in body
assert "llm_provider" in body
+42
View File
@@ -0,0 +1,42 @@
"""审计本体约束测试(无需数据库)。"""
from app.datahub.ontology import EntityType, RelationshipType, is_valid_relationship
def test_valid_signed_relationship():
assert is_valid_relationship(
RelationshipType.SIGNED, EntityType.CUSTOMER, EntityType.CONTRACT
)
def test_invalid_signed_direction():
# 合同不能"签约"客户(方向反了)
assert not is_valid_relationship(
RelationshipType.SIGNED, EntityType.CONTRACT, EntityType.CUSTOMER
)
def test_legal_rep_relationship():
assert is_valid_relationship(
RelationshipType.LEGAL_REP_OF, EntityType.LEGAL_PERSON, EntityType.SUPPLIER
)
def test_related_to_between_legal_persons():
# 实控人关联识别的基础:法人之间的亲属/关联关系
assert is_valid_relationship(
RelationshipType.RELATED_TO, EntityType.LEGAL_PERSON, EntityType.LEGAL_PERSON
)
def test_invalid_relationship_wrong_target():
assert not is_valid_relationship(
RelationshipType.HOLDS_MSISDN, EntityType.CUSTOMER, EntityType.CONTRACT
)
def test_all_relationship_types_have_domain():
from app.datahub.ontology import RELATIONSHIP_DOMAIN
for rel in RelationshipType:
assert rel in RELATIONSHIP_DOMAIN, f"关系 {rel} 缺少本体域定义"