219 lines
7.0 KiB
Python
219 lines
7.0 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import inspect
|
|
|
|
import app.api.kyc as kyc_api
|
|
from app.config.database import AgentSessionLocal, agent_engine
|
|
from app.main import app
|
|
from app.model.entities import AgentSession, KycSession
|
|
from app.model.schemas import AuthContext, KycSessionCreate
|
|
from app.repository.kyc_session_repository import KycSessionRepository
|
|
from app.service.kyc_session_service import KycSessionService
|
|
from app.utils.exceptions import AppError
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
class FakeCoreRepository:
|
|
def get_customer_l0(self, customer_id: str) -> dict | None:
|
|
if customer_id == "CUST-KYC-001":
|
|
return {
|
|
"customer_id": customer_id,
|
|
"display_name": "客户·KYC测试",
|
|
"age": 32,
|
|
}
|
|
return None
|
|
|
|
|
|
class AllowOwnership:
|
|
def assert_customer_access(self, auth: AuthContext, customer_id: str) -> None:
|
|
assert auth.advisor_id == "ADV-KYC-001"
|
|
assert customer_id == "CUST-KYC-001"
|
|
|
|
|
|
def advisor_context() -> AuthContext:
|
|
return AuthContext(
|
|
user_id="advisor_test",
|
|
display_name="测试顾问",
|
|
roles=["advisor"],
|
|
permissions=["kyc:create", "kyc:chat"],
|
|
trace_id="trace-kyc-session-unit",
|
|
advisor_id="ADV-KYC-001",
|
|
)
|
|
|
|
|
|
def session_service() -> KycSessionService:
|
|
return KycSessionService(
|
|
repository=KycSessionRepository(),
|
|
core_repository=FakeCoreRepository(),
|
|
ownership_service=AllowOwnership(),
|
|
)
|
|
|
|
|
|
def test_kyc_session_migration_and_creation_associate_agent_session():
|
|
inspector = inspect(agent_engine)
|
|
assert "kyc_session" in inspector.get_table_names()
|
|
columns = {column["name"] for column in inspector.get_columns("kyc_session")}
|
|
assert {
|
|
"session_id",
|
|
"trace_id",
|
|
"advisor_id",
|
|
"customer_id",
|
|
"session_type",
|
|
"status",
|
|
"current_node",
|
|
"collected_fields",
|
|
"missing_fields",
|
|
"progress_pct",
|
|
"dialog_turns",
|
|
"started_at",
|
|
"completed_at",
|
|
"duration_seconds",
|
|
}.issubset(columns)
|
|
|
|
service = session_service()
|
|
result = service.create_session(
|
|
KycSessionCreate(customer_id="CUST-KYC-001", session_type="new_customer"),
|
|
auth=advisor_context(),
|
|
trace_id=f"trace-kyc-create-{uuid4().hex}",
|
|
)
|
|
|
|
assert result.status == "in_progress"
|
|
assert result.current_node == "basic_info"
|
|
assert result.collected_fields == {}
|
|
assert result.progress_pct == 0
|
|
assert result.suggested_question
|
|
assert "年龄" in result.suggested_question
|
|
|
|
with AgentSessionLocal() as session:
|
|
agent_session = (
|
|
session.query(AgentSession)
|
|
.filter(AgentSession.session_id == result.session_id)
|
|
.one()
|
|
)
|
|
assert agent_session.customer_id == "CUST-KYC-001"
|
|
assert agent_session.agent_type == "advisor"
|
|
assert agent_session.metadata_json["kyc_session_id"] == result.session_id
|
|
|
|
|
|
def test_kyc_session_can_resume_and_complete():
|
|
service = session_service()
|
|
auth = advisor_context()
|
|
created = service.create_session(
|
|
KycSessionCreate(customer_id="CUST-KYC-001", session_type="periodic_review"),
|
|
auth=auth,
|
|
trace_id=f"trace-kyc-resume-{uuid4().hex}",
|
|
)
|
|
|
|
resumed = service.get_session(created.session_id, auth=auth)
|
|
assert resumed.session_id == created.session_id
|
|
assert resumed.status == "in_progress"
|
|
assert resumed.current_node == "basic_info"
|
|
|
|
completed = service.complete_session(
|
|
created.session_id,
|
|
auth=auth,
|
|
trace_id=f"trace-kyc-complete-{uuid4().hex}",
|
|
)
|
|
assert completed.status == "completed"
|
|
assert completed.current_node == "profile_generation"
|
|
assert completed.completed_at is not None
|
|
assert completed.duration_seconds is not None
|
|
|
|
|
|
def test_expired_in_progress_session_is_archived_as_abandoned():
|
|
service = session_service()
|
|
auth = advisor_context()
|
|
created = service.create_session(
|
|
KycSessionCreate(customer_id="CUST-KYC-001", session_type="deep_kyc"),
|
|
auth=auth,
|
|
trace_id=f"trace-kyc-expire-create-{uuid4().hex}",
|
|
)
|
|
old_time = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=5)
|
|
with AgentSessionLocal() as session:
|
|
row = session.query(KycSession).filter(KycSession.session_id == created.session_id).one()
|
|
row.started_at = old_time
|
|
row.updated_at = old_time
|
|
session.commit()
|
|
|
|
expired = service.expire_stale_sessions(
|
|
now=datetime.now(timezone.utc),
|
|
ttl_hours=4,
|
|
trace_id=f"trace-kyc-expire-{uuid4().hex}",
|
|
)
|
|
|
|
assert expired == 1
|
|
archived = service.get_session(created.session_id, auth=auth)
|
|
assert archived.status == "abandoned"
|
|
assert archived.completed_at is None
|
|
|
|
|
|
def test_completed_session_cannot_be_completed_again():
|
|
service = session_service()
|
|
auth = advisor_context()
|
|
created = service.create_session(
|
|
KycSessionCreate(customer_id="CUST-KYC-001", session_type="new_customer"),
|
|
auth=auth,
|
|
trace_id=f"trace-kyc-repeat-{uuid4().hex}",
|
|
)
|
|
service.complete_session(
|
|
created.session_id,
|
|
auth=auth,
|
|
trace_id=f"trace-kyc-complete-first-{uuid4().hex}",
|
|
)
|
|
|
|
with pytest.raises(AppError) as exc_info:
|
|
service.complete_session(
|
|
created.session_id,
|
|
auth=auth,
|
|
trace_id=f"trace-kyc-complete-second-{uuid4().hex}",
|
|
)
|
|
|
|
assert exc_info.value.code == "40901"
|
|
assert exc_info.value.status_code == 409
|
|
|
|
|
|
def test_kyc_session_api_creates_and_resumes_session(monkeypatch):
|
|
monkeypatch.setattr(kyc_api, "kyc_session_service", KycSessionService())
|
|
login = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"username": "admin_test", "password": "admin_test"},
|
|
)
|
|
token = login.json()["data"]["access_token"]
|
|
trace_id = f"trace-kyc-api-{uuid4().hex}"
|
|
|
|
created = client.post(
|
|
"/api/v1/kyc/sessions",
|
|
json={
|
|
"customer_id": "CUST-1001",
|
|
"session_type": "new_customer",
|
|
"customer_display_name": "客户·王**",
|
|
},
|
|
headers={"Authorization": f"Bearer {token}", "X-Trace-Id": trace_id},
|
|
)
|
|
|
|
assert created.status_code == 200
|
|
assert created.json()["data"]["status"] == "in_progress"
|
|
assert created.json()["data"]["current_node"] == "basic_info"
|
|
assert created.json()["trace_id"] == trace_id
|
|
|
|
session_id = created.json()["data"]["session_id"]
|
|
resumed = client.get(
|
|
f"/api/v1/kyc/sessions/{session_id}",
|
|
headers={"Authorization": f"Bearer {token}", "X-Trace-Id": trace_id},
|
|
)
|
|
assert resumed.status_code == 200
|
|
assert resumed.json()["data"]["session_id"] == session_id
|
|
assert resumed.json()["data"]["suggested_question"]
|
|
|
|
|
|
def test_kyc_session_api_requires_authentication():
|
|
response = client.post(
|
|
"/api/v1/kyc/sessions",
|
|
json={"customer_id": "CUST-1001", "session_type": "new_customer"},
|
|
)
|
|
assert response.status_code == 401
|