import os import sys from decimal import Decimal from pathlib import Path os.environ.setdefault("DATABASE_URL", "sqlite:///./storage/test_api_contract.db") os.environ.setdefault("RUNTIME_MEMORY_BACKEND", "memory") os.environ.setdefault("LLM_MOCK_ENABLED", "true") os.environ.setdefault("AUTH_USER_ME_URL", "http://django-user-center.test/api/user/users/me/") sys.path.insert(0, str(Path(__file__).resolve().parents[1])) Path("storage").mkdir(exist_ok=True) try: from fastapi.testclient import TestClient except ImportError: TestClient = None def run_api_contract_tests() -> None: """API 合约:验证统一响应、user_id 校验、核心接口和跨用户隔离。""" if TestClient is None: print("api contract tests skipped: fastapi is not installed") return from app.main import app from app.services.external_auth_service import AuthenticatedUser, ExternalAuthService from app.db.session import SessionLocal from app.models.source_case import CaseBase, CaseExamItem, ScoringRule, TraditionalCase from app.repositories.case_repository import CaseRepository from scripts.init_demo_db import init_database async def fake_authenticate(self, request): # noqa: ARG001 """测试认证:模拟 Django `/me` 返回 200 后的标准用户解析结果。""" authorization = request.headers.get("Authorization") if not authorization: from app.core.exceptions import AppError raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization header is required", 401) user_id = "api_user_002" if "api_user_002_token" in authorization else "api_user_001" return AuthenticatedUser( user_id=user_id, username=f"{user_id}_name", display_name="Swagger测试", role="student", tenant_id="1", status=1, profile={ "id": user_id, "username": f"{user_id}_name", "real_name": "Swagger测试", "role_type": "student", "institution": 1, "institution_name": "测试机构", "status": 1, }, ) ExternalAuthService.authenticate = fake_authenticate init_database() client = TestClient(app) headers = {"Authorization": "Bearer api_user_001_token", "X-Entry-Scene": "api_test"} live = client.get("/health/live") assert live.status_code == 200 assert live.json()["data"]["status"] == "live" missing_user = client.get("/api/v1/agent/hello") assert missing_user.status_code == 401 assert missing_user.json()["code"] == "AUTH_CREDENTIAL_REQUIRED" hello = client.get("/api/v1/agent/hello", headers=headers) assert hello.status_code == 200 assert hello.json()["code"] == "OK" auth_me = client.get("/api/v1/auth/me", headers=headers) assert auth_me.status_code == 200 assert auth_me.json()["data"]["user_id"] == "api_user_001" assert auth_me.json()["data"]["source"] == "django_user_center" assert auth_me.json()["data"]["display_name"] == "Swagger测试" assert "department_id" in auth_me.json()["data"] openapi = client.get("/openapi.json") assert openapi.status_code == 200 openapi_payload = openapi.json() auth_me_operation = openapi_payload["paths"]["/api/v1/auth/me"]["get"] assert any("HTTPBearer" in item for item in auth_me_operation.get("security", [])) assert "HTTPBearer" in openapi_payload["components"]["securitySchemes"] cases = client.get("/api/v1/cases", headers=headers) assert cases.status_code == 200 case_id = cases.json()["data"]["items"][0]["id"] created = client.post( "/api/v1/sessions", headers=headers, json={"case_id": case_id, "training_type": "diagnosis_treatment", "mode": "practice", "score_type": "percentage"}, ) assert created.status_code == 200 session_id = created.json()["data"]["session_id"] cross_user = client.get( f"/api/v1/sessions/{session_id}/order-items", headers={"Authorization": "Bearer api_user_002_token", "X-Entry-Scene": "api_test"}, ) assert cross_user.status_code == 404 assert cross_user.json()["code"] == "SESSION_NOT_FOUND" invalid_order = client.post(f"/api/v1/sessions/{session_id}/orders", headers=headers, json={"item_code": "bad_item"}) assert invalid_order.status_code == 404 assert invalid_order.json()["code"] == "ORDER_ITEM_NOT_FOUND" order_one = client.post(f"/api/v1/sessions/{session_id}/orders", headers=headers, json={"item_code": "blood_routine"}) assert order_one.status_code == 200 assert order_one.json()["data"]["already_ordered"] is False order_two = client.post(f"/api/v1/sessions/{session_id}/orders", headers=headers, json={"item_code": "blood_routine"}) assert order_two.status_code == 200 assert order_two.json()["data"]["already_ordered"] is True practice_hint_session = client.post( "/api/v1/sessions", headers=headers, json={"case_id": case_id, "training_type": "diagnosis_treatment", "mode": "practice", "score_type": "percentage"}, ) assert practice_hint_session.status_code == 200 practice_hint_session_id = practice_hint_session.json()["data"]["session_id"] hint = client.post( f"/api/v1/sessions/{practice_hint_session_id}/hints", headers=headers, json={"scope": "current_conversation"}, ) assert hint.status_code == 200 assert hint.json()["data"]["hints"] assert "recommended_orders" in hint.json()["data"] teaching = client.post( "/api/v1/sessions", headers=headers, json={"case_id": case_id, "training_type": "diagnosis_treatment", "mode": "teaching", "score_type": "percentage"}, ) assert teaching.status_code == 200 teaching_hint = client.post( f"/api/v1/sessions/{teaching.json()['data']['session_id']}/hints", headers=headers, json={"scope": "current_conversation"}, ) assert teaching_hint.status_code == 400 assert teaching_hint.json()["code"] == "SESSION_STATUS_INVALID" llm_fast = client.post("/api/v1/llm/test/deepseek-fast", headers=headers, json={"message": "hello"}) assert llm_fast.status_code == 200 assert llm_fast.json()["code"] == "OK" assert llm_fast.json()["data"]["stream"] is False llm_reason = client.post("/api/v1/llm/test/deepseek-reason", headers=headers, json={"message": "hello"}) assert llm_reason.status_code == 200 assert llm_reason.json()["code"] == "OK" assert "total_latency_ms" in llm_reason.json()["data"] import_preview = client.post( "/api/v1/imports/case-sql/preview", headers=headers, files={"file": ("bad.sql", b"not sql", "application/sql")}, ) assert import_preview.status_code == 200 assert import_preview.json()["code"] == "OK" assert import_preview.json()["data"]["can_import"] is False assert import_preview.json()["data"]["errors"] import_apply = client.post( "/api/v1/imports/case-sql/apply", headers=headers, files={"file": ("bad.sql", b"not sql", "application/sql")}, ) assert import_apply.status_code == 400 assert import_apply.json()["code"] == "CASE_SQL_IMPORT_INVALID" temp_case_id = 880001 with SessionLocal() as db: CaseRepository(db).delete_case_cascade(temp_case_id) db.add( CaseBase( id=temp_case_id, title="删除测试病例", case_type="diagnosis_treatment", difficulty="medium", chief_complaint="发热、咳嗽", description="用于删除接口测试的临时病例", patient_age=6, patient_gender="male", tags="test", symptom_tags=[], disease_tags=[], competency_tags=[], guideline_tags=[], knowledge_points=[], icd_codes="", osce_enabled=False, rag_enabled=False, ai_prompt_template="", multimodal_assets=[], vector_status=0, publish_status=1, status=1, department_id=1, ) ) db.add( TraditionalCase( id=temp_case_id, case_id=temp_case_id, standard_diagnosis="测试诊断", standard_treatment="测试治疗", guideline_reference="测试指南", ) ) db.add( ScoringRule( id=temp_case_id, case_id=temp_case_id, dimension="信息采集", competency_dimension="问诊完整性", score_weight=Decimal("10.00"), ai_auto_score=True, osce_dimension=False, scoring_standard="测试评分标准", rubric_json={}, ) ) db.add( CaseExamItem( id=temp_case_id, case_id=temp_case_id, item_code="temp_exam", item_name="测试检查", item_type="lab", result_text="测试结果", result_structured={}, is_key=True, is_abnormal=False, score_weight=Decimal("1.00"), display_order=1, ) ) db.commit() delete_preview = client.get(f"/api/v1/cases/{temp_case_id}/delete-preview", headers=headers) assert delete_preview.status_code == 200 assert delete_preview.json()["data"]["affected"]["case_base"] == 1 assert delete_preview.json()["data"]["affected"]["case_exam_item"] == 1 delete_without_confirm = client.request( "DELETE", f"/api/v1/cases/{temp_case_id}", headers=headers, json={"confirm": False, "delete_training_data": True}, ) assert delete_without_confirm.status_code == 400 assert delete_without_confirm.json()["code"] == "CASE_DELETE_CONFIRM_REQUIRED" delete_case = client.request( "DELETE", f"/api/v1/cases/{temp_case_id}", headers=headers, json={"confirm": True, "delete_training_data": True}, ) assert delete_case.status_code == 200 assert delete_case.json()["data"]["deleted_counts"]["case_base"] == 1 deleted_detail = client.get(f"/api/v1/cases/{temp_case_id}", headers=headers) assert deleted_detail.status_code == 404 assert deleted_detail.json()["code"] == "CASE_NOT_FOUND" if __name__ == "__main__": run_api_contract_tests() print("api contract tests passed")