Files
fastapi/backend/tests/test_api_contract.py
T
2026-06-03 15:51:46 +08:00

275 lines
10 KiB
Python

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"}
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")