- Updated test files to import `AgentSessionLocal` from `advisor_db` instead of directly, preventing session binding to the real database during tests. - Fixed 6 test cases to use the new login token utility, ensuring consistency across authentication methods. - Adjusted customer risk codes in `test_convert_confirm.py` to reflect changes in customer classification (C3 to C4). - Verified that changes resulted in zero database pollution during test runs, maintaining integrity of the testing environment. - Documented findings and updates in the relevant test logs and memory files, ensuring clarity on the current state of tests and defects.
207 lines
7.1 KiB
Python
207 lines
7.1 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 import advisor_db
|
|
from app.main import app
|
|
from app.model.entities import AgentSession
|
|
from app.model.entities_advisor import KycSession
|
|
from app.model.advisor_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(advisor_db.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 advisor_db.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_["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 advisor_db.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/auth/login", json={"actor_id": "STAFF-10086", "token_type": "staff"})
|
|
token = login.json()["data"]["access_token"]
|
|
trace_id = f"trace-kyc-api-{uuid4().hex}"
|
|
|
|
created = client.post(
|
|
"/api/advisor-agent/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/advisor-agent/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/advisor-agent/kyc/sessions",
|
|
json={"customer_id": "CUST-1001", "session_type": "new_customer"},
|
|
)
|
|
assert response.status_code == 401
|