Files
group_xinghuo_jinrong/tests/test_sprint0_infrastructure.py
T
zhanghongyu_0626 70aa861983 feat(advisor-agent): Introduce advisor agent functionalities with compliance, KYC, and script templates
- Added new modules for advisor compliance, KYC sessions, and script templates, enhancing the advisor agent's capabilities.
- Implemented a comprehensive API structure under the `/api/advisor-agent` prefix, ensuring clear organization and access to new features.
- Established database models and repositories for compliance rules and KYC sessions, facilitating robust data management.
- Integrated exception handling and response models to improve error management and user feedback.
- Updated settings to include new configurations for compliance and KYC features, ensuring flexibility and adaptability.

This update significantly expands the advisor agent's functionality, providing essential tools for compliance and customer interaction while maintaining a structured API design.
2026-09-12 16:33:07 +08:00

209 lines
6.9 KiB
Python

from pathlib import Path
from uuid import uuid4
import pytest
from app.advisor_db import AgentSessionLocal
from app.advisor_exceptions import OwnershipDeniedError
from app.api.advisor_auth_adapter import AdvisorAuthContext
from app.model.entities import AuditLog
from app.service.audit_service import AuditService, InMemoryAuditRepository
from app.service.ownership_service import OwnershipService
ROOT = Path(__file__).resolve().parents[1]
def test_requirements_include_sprint0_approved_dependencies():
requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8")
for package_name in [
"pytest",
"httpx",
]:
assert package_name in requirements
def test_advisor_agent_migration_sql_exists():
path = ROOT / "scripts" / "agent" / "migrate-advisor-agent-sprint1-3.sql"
assert path.exists()
text = path.read_text(encoding="utf-8")
assert "compliance_rule" in text
assert "kyc_session" in text
def test_core_reset_script_runs_core_seed_and_optional_neo4j_sync():
reset_script = (ROOT / "scripts" / "core" / "reset.ps1").read_text(encoding="utf-8")
assert "scripts/core/01-ddl.sql" in reset_script
assert "python scripts/sync/sync_advisor_rel.py" in reset_script
assert "[switch]$SkipNeo4j" in reset_script
assert "if (-not $SkipNeo4j)" in reset_script
assert "python scripts/sync/sync_neo4j.py" in reset_script
def test_core_reset_script_uses_run_sql_file_helper():
reset_script = (ROOT / "scripts" / "core" / "reset.ps1").read_text(encoding="utf-8")
assert "scripts/dev/run_sql_file.py" in reset_script
assert "fix_utf8_seed.py" in reset_script
def test_sprint0_test_suite_covers_required_quality_gates():
tests_text = "\n".join(
path.read_text(encoding="utf-8")
for path in sorted((ROOT / "tests").glob("test_sprint0_*.py"))
)
required_checks = [
"test_requirements_include_sprint0_approved_dependencies",
"test_advisor_agent_migration_sql_exists",
"test_core_reset_script_runs_core_seed_and_optional_neo4j_sync",
"test_sqlalchemy_audit_repository_persists_append_only_event",
"test_trace_id_header_is_reused_in_response_and_rbac_audit_event",
"test_missing_trace_id_generates_response_header_on_unauthenticated_request",
"test_platform_compliance_suitability_unchanged",
]
for check_name in required_checks:
assert check_name in tests_text
class FakeCoreRepo:
def __init__(self, assigned: bool) -> None:
self.assigned = assigned
def is_advisor_assigned(self, advisor_id: str, customer_id: str) -> bool:
return self.assigned and advisor_id == "ADV-TEST-001" and customer_id == "CUST-001"
def test_ownership_guard_allows_assigned_advisor_customer():
service = OwnershipService(FakeCoreRepo(assigned=True))
auth = AdvisorAuthContext(
user_id="STAFF-10086",
token_type="staff",
roles=["advisor"],
permissions=["advisor:workspace"],
advisor_id="ADV-TEST-001",
trace_id="trace-owner-ok",
)
service.assert_customer_access(auth, "CUST-001")
def test_ownership_guard_denies_unassigned_advisor_customer():
service = OwnershipService(FakeCoreRepo(assigned=False))
auth = AdvisorAuthContext(
user_id="STAFF-10086",
token_type="staff",
roles=["advisor"],
permissions=["advisor:workspace"],
advisor_id="ADV-TEST-001",
trace_id="trace-owner-deny",
)
with pytest.raises(OwnershipDeniedError):
service.assert_customer_access(auth, "CUST-999")
def test_audit_service_appends_events_without_mutating_prior_records():
repository = InMemoryAuditRepository()
service = AuditService(repository)
service.record(
trace_id="trace-audit-001",
event_type="auth_login",
actor_id="advisor_test",
decision="success",
)
service.record(
trace_id="trace-audit-002",
event_type="rbac_denied",
actor_id="advisor_test",
decision="audit:read",
)
events = service.list_events()
assert len(events) == 2
assert events[0].trace_id == "trace-audit-001"
assert events[1].trace_id == "trace-audit-002"
def test_sqlalchemy_audit_repository_persists_append_only_event():
from app.service import audit_service as audit_module
trace_id = f"trace-db-{uuid4().hex}"
repository_class = getattr(audit_module, "SqlAlchemyAuditRepository", None)
assert repository_class is not None
repository = repository_class(AgentSessionLocal)
service = AuditService(repository)
service.record(
trace_id=trace_id,
event_type="audit_repository_test",
actor_id="test_runner",
decision="persisted",
input_summary={"source": "pytest"},
)
with AgentSessionLocal() as session:
event = session.query(AuditLog).filter(AuditLog.trace_id == trace_id).one()
assert event.event_type == "audit_repository_test"
assert event.actor_id == "test_runner"
assert event.decision == "persisted"
assert event.input_summary == {"source": "pytest"}
def test_unknown_staff_login_is_rejected():
from fastapi.testclient import TestClient
from app.main import app
response = TestClient(app).post(
"/api/auth/login",
json={"actor_id": "STAFF-UNKNOWN-ACTOR", "token_type": "staff"},
)
assert response.status_code == 401
def test_trace_id_header_is_reused_in_response_and_rbac_audit_event():
from fastapi.testclient import TestClient
from app.main import app
trace_id = f"trace-rbac-db-{uuid4().hex}"
login = TestClient(app).post(
"/api/auth/login",
json={"actor_id": "STAFF-10086", "token_type": "staff"},
)
token = login.json()["data"]["access_token"]
response = TestClient(app).post(
"/api/advisor-agent/compliance/rules",
json={
"rule_type": "keyword",
"pattern": "trace-id-rbac-test",
"severity": "block",
"category": "return_promise",
"suggestion": "no promise",
},
headers={"Authorization": f"Bearer {token}", "X-Trace-Id": trace_id},
)
assert response.status_code == 403
assert response.headers["X-Trace-Id"] == trace_id
assert response.json()["trace_id"] == trace_id
with AgentSessionLocal() as session:
event = (
session.query(AuditLog)
.filter(AuditLog.trace_id == trace_id, AuditLog.event_type == "rbac_denied")
.one()
)
assert event.actor_id == "STAFF-10086"
assert event.decision == "compliance:rule:write"
def test_missing_trace_id_generates_response_header_on_unauthenticated_request():
from fastapi.testclient import TestClient
from app.main import app
response = TestClient(app).get("/api/advisor-agent/compliance/ping")
assert response.status_code == 401
generated_trace_id = response.headers["X-Trace-Id"]
assert generated_trace_id
assert response.json()["trace_id"] == generated_trace_id