prepare backend-only fastapi deployment
This commit is contained in:
+28
-9
@@ -1,23 +1,42 @@
|
||||
# Backend
|
||||
|
||||
医疗问诊 Agent 第一版 Demo 后端工程。
|
||||
医疗问诊 Agent FastAPI 后端工程。
|
||||
|
||||
## 启动流程
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
```powershell
|
||||
cd backend
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
.\.venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
copy ..\.env.example ..\.env
|
||||
python -m scripts.init_demo_db
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
uvicorn app.main:app --host 127.0.0.1 --port 9000
|
||||
```
|
||||
|
||||
Swagger:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:9000/docs
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
后端读取项目根目录 `.env`。
|
||||
|
||||
```env
|
||||
DATABASE_URL=mysql+pymysql://root:<password>@mysql:3306/medical?charset=utf8mb4
|
||||
MYSQL_URL=mysql+aiomysql://root:<password>@mysql:3306/medical?charset=utf8mb4
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
```
|
||||
|
||||
真实密码和 API Key 只写入部署环境或本地 `.env`,不提交 Git。
|
||||
|
||||
## 核心约束
|
||||
|
||||
- 所有业务接口通过 `X-User-Id` 做用户隔离。
|
||||
- 用户身份只来自 `Authorization: Bearer <access_token>`。
|
||||
- 后端转发 token 到 Django 用户中心 `/api/user/users/me/`。
|
||||
- Django 返回的 `id` 是本系统内部用户隔离字段。
|
||||
- 问诊消息进入短期 memory,不作为长期历史保存。
|
||||
- 检查检验结果只从数据库读取。
|
||||
- 完整训练结束后只保存评价记录、PDF 导出记录、学习档案和审计日志。
|
||||
- DeepSeek 调用统一经过 `agents/llm_adapter.py`。
|
||||
- 完整训练结束后只保存评价记录、PDF 路径、学习档案和审计日志。
|
||||
- LLM 调用统一经过 `app/agents/llm_adapter.py`。
|
||||
|
||||
@@ -18,7 +18,14 @@ def hello(ctx: UserContext = Depends(get_user_context), db: Session = Depends(ge
|
||||
db.commit()
|
||||
return ok(
|
||||
AgentHelloResponse(
|
||||
user=AgentHelloUser(user_id=ctx.user_id, tenant_id=ctx.tenant_id, role=ctx.role),
|
||||
user=AgentHelloUser(
|
||||
user_id=ctx.user_id,
|
||||
tenant_id=ctx.tenant_id,
|
||||
role=ctx.role,
|
||||
source=ctx.auth_source,
|
||||
username=ctx.username,
|
||||
display_name=ctx.display_name,
|
||||
),
|
||||
features=settings.as_public_dict(),
|
||||
)
|
||||
)
|
||||
|
||||
+21
-1
@@ -9,7 +9,8 @@ router = APIRouter()
|
||||
|
||||
@router.get("/me", response_model=ApiResponse[AuthMeResponse])
|
||||
async def auth_me(ctx: UserContext = Depends(get_user_context)):
|
||||
"""当前用户:返回经 Django 用户中心或 Demo Header 标准化后的用户信息。"""
|
||||
"""当前用户:转发 Authorization 到 Django 用户中心,并返回标准化后的用户信息。"""
|
||||
profile = ctx.profile or {}
|
||||
return ok(
|
||||
AuthMeResponse(
|
||||
user_id=ctx.user_id,
|
||||
@@ -18,5 +19,24 @@ async def auth_me(ctx: UserContext = Depends(get_user_context)):
|
||||
display_name=ctx.display_name,
|
||||
tenant_id=ctx.tenant_id,
|
||||
role=ctx.role,
|
||||
phone=profile.get("phone"),
|
||||
avatar=profile.get("avatar"),
|
||||
gender=profile.get("gender"),
|
||||
institution=profile.get("institution"),
|
||||
institution_name=profile.get("institution_name"),
|
||||
department=profile.get("department"),
|
||||
department_name=profile.get("department_name"),
|
||||
title_name=profile.get("title_name"),
|
||||
major=profile.get("major"),
|
||||
training_stage=profile.get("training_stage"),
|
||||
learning_target=profile.get("learning_target"),
|
||||
competency_profile=profile.get("competency_profile"),
|
||||
weak_dimensions=profile.get("weak_dimensions"),
|
||||
strong_dimensions=profile.get("strong_dimensions"),
|
||||
ai_preference=profile.get("ai_preference"),
|
||||
total_training_count=profile.get("total_training_count"),
|
||||
total_case_count=profile.get("total_case_count"),
|
||||
current_level=profile.get("current_level"),
|
||||
status=profile.get("status"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -82,12 +82,11 @@ class Settings(BaseModel):
|
||||
report_storage_dir: str = Field(default_factory=lambda: os.getenv("REPORT_STORAGE_DIR", "./storage/reports"))
|
||||
runtime_memory_ttl_seconds: int = Field(default_factory=lambda: int(os.getenv("RUNTIME_MEMORY_TTL_SECONDS", "7200")))
|
||||
runtime_memory_backend: str = Field(default_factory=lambda: os.getenv("RUNTIME_MEMORY_BACKEND", "memory"))
|
||||
redis_url: str = Field(default_factory=lambda: os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0"))
|
||||
auth_validate_enabled: bool = Field(default_factory=lambda: os.getenv("AUTH_VALIDATE_ENABLED", "false").lower() == "true")
|
||||
redis_url: str = Field(default_factory=lambda: os.getenv("REDIS_URL", "redis://redis:6379/0"))
|
||||
auth_validate_enabled: bool = Field(default_factory=lambda: os.getenv("AUTH_VALIDATE_ENABLED", "true").lower() == "true")
|
||||
auth_user_me_url: str = Field(default_factory=lambda: os.getenv("AUTH_USER_ME_URL", ""))
|
||||
auth_timeout_seconds: int = Field(default_factory=lambda: int(os.getenv("AUTH_TIMEOUT_SECONDS", "5")))
|
||||
auth_cache_ttl_seconds: int = Field(default_factory=lambda: int(os.getenv("AUTH_CACHE_TTL_SECONDS", "300")))
|
||||
auth_allow_demo_user_id: bool = Field(default_factory=lambda: os.getenv("AUTH_ALLOW_DEMO_USER_ID", "true").lower() == "true")
|
||||
|
||||
def as_public_dict(self) -> dict[str, Any]:
|
||||
"""配置展示:返回允许暴露给 Demo 前端的功能开关。"""
|
||||
@@ -107,8 +106,8 @@ class Settings(BaseModel):
|
||||
"llm_reasoning_effort": self.llm_reasoning_effort,
|
||||
"llm_fast_max_tokens": self.llm_fast_max_tokens,
|
||||
"runtime_memory_backend": self.runtime_memory_backend,
|
||||
"auth_validate_enabled": self.auth_validate_enabled,
|
||||
"auth_source": "django_user_center" if self.auth_validate_enabled else "demo_header",
|
||||
"auth_validate_enabled": True,
|
||||
"auth_source": "django_user_center",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,4 +15,5 @@ class UserContext:
|
||||
user_agent: str | None = None
|
||||
username: str | None = None
|
||||
display_name: str | None = None
|
||||
auth_source: str = "demo_header"
|
||||
auth_source: str = "django_user_center"
|
||||
profile: dict | None = None
|
||||
|
||||
@@ -16,10 +16,10 @@ def register_exception_handlers(app: FastAPI) -> None:
|
||||
@app.exception_handler(AppError)
|
||||
async def handle_app_error(request: Request, exc: AppError) -> JSONResponse:
|
||||
logger.warning(
|
||||
"business_error code=%s path=%s user_id=%s",
|
||||
"business_error code=%s path=%s request_id=%s",
|
||||
exc.code,
|
||||
request.url.path,
|
||||
request.headers.get("X-User-Id"),
|
||||
request.headers.get("X-Request-Id"),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
@@ -29,9 +29,9 @@ def register_exception_handlers(app: FastAPI) -> None:
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def handle_validation_error(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
logger.warning(
|
||||
"validation_error path=%s user_id=%s errors=%s",
|
||||
"validation_error path=%s request_id=%s errors=%s",
|
||||
request.url.path,
|
||||
request.headers.get("X-User-Id"),
|
||||
request.headers.get("X-Request-Id"),
|
||||
exc.errors(),
|
||||
)
|
||||
return JSONResponse(
|
||||
@@ -42,9 +42,9 @@ def register_exception_handlers(app: FastAPI) -> None:
|
||||
@app.exception_handler(SQLAlchemyError)
|
||||
async def handle_database_error(request: Request, exc: SQLAlchemyError) -> JSONResponse:
|
||||
logger.exception(
|
||||
"database_error path=%s user_id=%s",
|
||||
"database_error path=%s request_id=%s",
|
||||
request.url.path,
|
||||
request.headers.get("X-User-Id"),
|
||||
request.headers.get("X-Request-Id"),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
@@ -54,9 +54,9 @@ def register_exception_handlers(app: FastAPI) -> None:
|
||||
@app.exception_handler(Exception)
|
||||
async def handle_unexpected_error(request: Request, exc: Exception) -> JSONResponse:
|
||||
logger.exception(
|
||||
"unexpected_error path=%s user_id=%s",
|
||||
"unexpected_error path=%s request_id=%s",
|
||||
request.url.path,
|
||||
request.headers.get("X-User-Id"),
|
||||
request.headers.get("X-Request-Id"),
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
|
||||
@@ -1,51 +1,34 @@
|
||||
from fastapi import Header, Request
|
||||
from fastapi import Header, Request, Security
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.context import UserContext
|
||||
from app.core.exceptions import AppError
|
||||
from app.services.external_auth_service import ExternalAuthService
|
||||
|
||||
bearer_scheme = HTTPBearer(auto_error=False, description="Django 用户中心 access token")
|
||||
|
||||
|
||||
async def get_user_context(
|
||||
request: Request,
|
||||
x_user_id: str | None = Header(default=None, alias="X-User-Id"),
|
||||
x_tenant_id: str | None = Header(default=None, alias="X-Tenant-Id"),
|
||||
x_user_role: str | None = Header(default=None, alias="X-User-Role"),
|
||||
x_class_id: str | None = Header(default=None, alias="X-Class-Id"),
|
||||
credentials: HTTPAuthorizationCredentials | None = Security(bearer_scheme),
|
||||
x_entry_scene: str | None = Header(default=None, alias="X-Entry-Scene"),
|
||||
x_request_id: str | None = Header(default=None, alias="X-Request-Id"),
|
||||
) -> UserContext:
|
||||
"""用户校验:正式联调优先调用 Django 用户中心,Demo 模式兼容 X-User-Id。"""
|
||||
if settings.auth_validate_enabled and (request.headers.get("Authorization") or request.headers.get("Cookie")):
|
||||
user = await ExternalAuthService().authenticate(request)
|
||||
return UserContext(
|
||||
user_id=user.user_id,
|
||||
tenant_id=user.tenant_id or x_tenant_id,
|
||||
role=user.role or x_user_role,
|
||||
class_id=x_class_id,
|
||||
entry_scene=x_entry_scene,
|
||||
request_id=x_request_id,
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("User-Agent"),
|
||||
username=user.username,
|
||||
display_name=user.display_name,
|
||||
auth_source=user.source,
|
||||
)
|
||||
|
||||
if settings.auth_validate_enabled and not settings.auth_allow_demo_user_id:
|
||||
raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization or Cookie is required", 401)
|
||||
|
||||
if not x_user_id or not x_user_id.strip():
|
||||
raise AppError("USER_ID_REQUIRED", "X-User-Id header is required", 401)
|
||||
"""用户校验:只接受宿主系统 access token,并转发 Django 用户中心 `/me` 获取真实用户。"""
|
||||
if not credentials or not credentials.credentials.strip():
|
||||
raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization header is required", 401)
|
||||
|
||||
user = await ExternalAuthService().authenticate(request)
|
||||
return UserContext(
|
||||
user_id=x_user_id.strip(),
|
||||
tenant_id=x_tenant_id,
|
||||
role=x_user_role,
|
||||
class_id=x_class_id,
|
||||
user_id=user.user_id,
|
||||
tenant_id=user.tenant_id,
|
||||
role=user.role,
|
||||
entry_scene=x_entry_scene,
|
||||
request_id=x_request_id,
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("User-Agent"),
|
||||
auth_source="demo_header",
|
||||
username=user.username,
|
||||
display_name=user.display_name,
|
||||
auth_source=user.source,
|
||||
profile=user.profile,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,9 @@ class AgentHelloUser(BaseModel):
|
||||
user_id: str
|
||||
tenant_id: str | None = None
|
||||
role: str | None = None
|
||||
source: str | None = None
|
||||
username: str | None = None
|
||||
display_name: str | None = None
|
||||
|
||||
|
||||
class AgentHelloResponse(BaseModel):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -10,3 +12,22 @@ class AuthMeResponse(BaseModel):
|
||||
display_name: str | None = None
|
||||
tenant_id: str | None = None
|
||||
role: str | None = None
|
||||
phone: str | None = None
|
||||
avatar: str | None = None
|
||||
gender: int | None = None
|
||||
institution: int | None = None
|
||||
institution_name: str | None = None
|
||||
department: int | None = None
|
||||
department_name: str | None = None
|
||||
title_name: str | None = None
|
||||
major: str | None = None
|
||||
training_stage: str | None = None
|
||||
learning_target: str | None = None
|
||||
competency_profile: dict[str, Any] | None = None
|
||||
weak_dimensions: list[Any] | None = None
|
||||
strong_dimensions: list[Any] | None = None
|
||||
ai_preference: dict[str, Any] | None = None
|
||||
total_training_count: int | None = None
|
||||
total_case_count: int | None = None
|
||||
current_level: str | None = None
|
||||
status: int | None = None
|
||||
|
||||
@@ -22,6 +22,20 @@ class AuthenticatedUser:
|
||||
display_name: str | None = None
|
||||
tenant_id: str | None = None
|
||||
role: str | None = None
|
||||
phone: str | None = None
|
||||
avatar: str | None = None
|
||||
gender: int | None = None
|
||||
institution_id: int | None = None
|
||||
institution_name: str | None = None
|
||||
department_id: int | None = None
|
||||
department_name: str | None = None
|
||||
title_name: str | None = None
|
||||
major: str | None = None
|
||||
training_stage: str | None = None
|
||||
learning_target: str | None = None
|
||||
current_level: str | None = None
|
||||
status: int | None = None
|
||||
profile: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ExternalAuthService:
|
||||
@@ -36,7 +50,7 @@ class ExternalAuthService:
|
||||
|
||||
outbound_headers = self._build_forward_headers(request)
|
||||
if not outbound_headers:
|
||||
raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization or Cookie is required", 401)
|
||||
raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization header is required", 401)
|
||||
|
||||
cache_key = self._cache_key(outbound_headers)
|
||||
cached = self._read_cache(cache_key)
|
||||
@@ -64,16 +78,14 @@ class ExternalAuthService:
|
||||
return user
|
||||
|
||||
def _build_forward_headers(self, request: Request) -> dict[str, str]:
|
||||
"""认证转发:只透传鉴权必要 Header,避免把无关内部头转发给用户中心。"""
|
||||
"""认证转发:只透传 Bearer token,避免把无关内部头转发给用户中心。"""
|
||||
headers: dict[str, str] = {}
|
||||
authorization = request.headers.get("Authorization")
|
||||
cookie = request.headers.get("Cookie")
|
||||
if authorization:
|
||||
authorization = authorization.strip()
|
||||
if authorization and " " not in authorization:
|
||||
authorization = f"Bearer {authorization}"
|
||||
headers["Authorization"] = authorization
|
||||
if cookie:
|
||||
headers["Cookie"] = cookie
|
||||
if request.headers.get("X-CSRFToken"):
|
||||
headers["X-CSRFToken"] = request.headers["X-CSRFToken"]
|
||||
return headers
|
||||
|
||||
def _parse_user(self, payload: dict[str, Any]) -> AuthenticatedUser:
|
||||
@@ -82,18 +94,69 @@ class ExternalAuthService:
|
||||
user_id = self._first_present(data, ["id", "user_id", "uid", "pk", "uuid"])
|
||||
if user_id is None:
|
||||
raise AppError("AUTH_USER_PARSE_FAILED", "auth user id is missing", 502)
|
||||
status = self._to_int(data.get("status"))
|
||||
if status == 0:
|
||||
raise AppError("AUTH_USER_DISABLED", "current user is disabled", 403)
|
||||
username = self._first_present(data, ["username", "account", "mobile", "phone"])
|
||||
display_name = self._first_present(data, ["display_name", "name", "nickname", "real_name"])
|
||||
role = self._first_present(data, ["role", "user_role"])
|
||||
tenant_id = self._first_present(data, ["tenant_id", "org_id", "organization_id"])
|
||||
role = self._first_present(data, ["role_type", "role", "user_role"])
|
||||
institution_id = self._to_int(data.get("institution"))
|
||||
tenant_id = str(institution_id) if institution_id is not None else None
|
||||
profile = self._build_profile(data)
|
||||
return AuthenticatedUser(
|
||||
user_id=str(user_id),
|
||||
username=str(username) if username is not None else None,
|
||||
display_name=str(display_name) if display_name is not None else None,
|
||||
role=str(role) if role is not None else None,
|
||||
tenant_id=str(tenant_id) if tenant_id is not None else None,
|
||||
phone=self._to_str(data.get("phone")),
|
||||
avatar=self._to_str(data.get("avatar")),
|
||||
gender=self._to_int(data.get("gender")),
|
||||
institution_id=institution_id,
|
||||
institution_name=self._to_str(data.get("institution_name")),
|
||||
department_id=self._to_int(data.get("department")),
|
||||
department_name=self._to_str(data.get("department_name")),
|
||||
title_name=self._to_str(data.get("title_name")),
|
||||
major=self._to_str(data.get("major")),
|
||||
training_stage=self._to_str(data.get("training_stage")),
|
||||
learning_target=self._to_str(data.get("learning_target")),
|
||||
current_level=self._to_str(data.get("current_level")),
|
||||
status=status,
|
||||
profile=profile,
|
||||
)
|
||||
|
||||
def _build_profile(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""用户画像:按 Django `/me` 字段白名单保留学习画像,供前端和后续 Agent 个性化使用。"""
|
||||
keys = [
|
||||
"id",
|
||||
"username",
|
||||
"real_name",
|
||||
"phone",
|
||||
"avatar",
|
||||
"gender",
|
||||
"role_type",
|
||||
"institution",
|
||||
"institution_name",
|
||||
"department",
|
||||
"department_name",
|
||||
"title_name",
|
||||
"major",
|
||||
"training_stage",
|
||||
"learning_target",
|
||||
"competency_profile",
|
||||
"weak_dimensions",
|
||||
"strong_dimensions",
|
||||
"ai_preference",
|
||||
"total_training_count",
|
||||
"total_case_count",
|
||||
"current_level",
|
||||
"status",
|
||||
"last_login_time",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
return {key: data.get(key) for key in keys if key in data}
|
||||
|
||||
@staticmethod
|
||||
def _first_present(data: dict[str, Any], keys: list[str]) -> Any:
|
||||
"""用户解析:按优先级读取第一个非空字段。"""
|
||||
@@ -103,6 +166,23 @@ class ExternalAuthService:
|
||||
return value
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _to_int(value: Any) -> int | None:
|
||||
"""用户解析:把 Django 返回的数字字段稳定转成 int,空值保持 None。"""
|
||||
if value is None or value == "":
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _to_str(value: Any) -> str | None:
|
||||
"""用户解析:把可展示字段转成字符串,空值保持 None。"""
|
||||
if value is None or value == "":
|
||||
return None
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def _cache_key(headers: dict[str, str]) -> str:
|
||||
"""认证缓存:基于鉴权凭证生成不可逆缓存键,避免保存明文 token。"""
|
||||
|
||||
@@ -6,6 +6,7 @@ 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)
|
||||
@@ -23,18 +24,47 @@ def run_api_contract_tests() -> None:
|
||||
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 = {"X-User-Id": "api_user_001", "X-Entry-Scene": "api_test"}
|
||||
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"] == "USER_ID_REQUIRED"
|
||||
assert missing_user.json()["code"] == "AUTH_CREDENTIAL_REQUIRED"
|
||||
|
||||
hello = client.get("/api/v1/agent/hello", headers=headers)
|
||||
assert hello.status_code == 200
|
||||
@@ -43,7 +73,15 @@ def run_api_contract_tests() -> None:
|
||||
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"] == "demo_header"
|
||||
assert auth_me.json()["data"]["source"] == "django_user_center"
|
||||
assert auth_me.json()["data"]["display_name"] == "Swagger测试"
|
||||
|
||||
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
|
||||
@@ -57,7 +95,10 @@ def run_api_contract_tests() -> None:
|
||||
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={"X-User-Id": "api_user_002"})
|
||||
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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user