Files
group_fqcd_jr/tests/unit/api/test_controller_routing_contract.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

97 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Controller 路由契约测试:鉴权闸门与路由注册。
两类断言:
1. **受保护路由在缺少 `Authorization` 时不得成功**——GET 必须明确 401;POST 因为 FastAPI
先校验请求体,无 body 时会得到 422,因此断言"不是 2xx 且是 401/422 之一",重点是
**未授权不能拿到成功响应**,而不是具体哪一个码。
2. 公开运维路由可达、未注册路径返回 404——防止路由注册被改错却无人发现。
全部用 `httpx.ASGITransport` 进程内调用,不连数据库(鉴权在依赖层就返回)。
"""
from typing import Any
import httpx
import pytest
from app.main import create_app
PROTECTED_GET = [
"/api/v1/agent-runs/run-x",
"/api/v1/agent-runs/run-x/events",
"/api/v1/conversations/session-1",
"/api/v1/conversations/session-1/messages",
"/api/v1/handover-requests/1",
"/api/v1/knowledge-references/token-abcdefghijklmnopqrst",
"/api/v1/users/me/memory-profile",
"/api/v1/admin/config-releases",
"/api/v1/knowledge/list",
]
PROTECTED_POST = [
"/api/v1/agent-runs",
"/api/v1/conversations",
"/api/v1/conversations/session-1/closures",
"/api/v1/conversations/session-1/handover-requests",
"/api/v1/agent-runs/run-x/cancellations",
"/api/v1/conversation-messages/1/feedback",
"/api/v1/knowledge/upload",
"/api/v1/knowledge/1/vector-cleanups",
]
PROTECTED_DELETE = [
"/api/v1/knowledge/1",
]
PUBLIC_GET = ["/internal/health/live"]
async def send(method: str, path: str) -> httpx.Response:
app = create_app()
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
return await client.request(method, path)
@pytest.mark.parametrize("path", PROTECTED_GET)
async def test_protected_get_without_token_is_unauthorized(path: str) -> None:
response = await send("GET", path)
assert response.status_code == 401, f"GET {path} -> {response.status_code}"
@pytest.mark.parametrize("path", PROTECTED_POST)
async def test_protected_post_without_token_never_succeeds(path: str) -> None:
response = await send("POST", path)
assert response.status_code in {401, 422}, f"POST {path} -> {response.status_code}"
@pytest.mark.parametrize("path", PROTECTED_DELETE)
async def test_protected_delete_without_token_is_unauthorized(path: str) -> None:
"""DELETE 没有请求体可校,所以未带令牌必须是明确的 401(知识库删除端点的闸门)。"""
response = await send("DELETE", path)
assert response.status_code == 401, f"DELETE {path} -> {response.status_code}"
@pytest.mark.parametrize("path", PUBLIC_GET)
async def test_public_operational_route_is_reachable(path: str) -> None:
response = await send("GET", path)
assert response.status_code == 200, f"GET {path} -> {response.status_code}"
async def test_unknown_path_is_not_found() -> None:
response = await send("GET", "/api/v1/definitely-not-a-route")
assert response.status_code == 404
async def test_unauthorized_envelope_shape() -> None:
"""401 的错误信封必须与文档一致,否则客户端无法统一处理。"""
response = await send("GET", "/api/v1/agent-runs/run-x")
body: Any = response.json()
assert "detail" in body or "error" in body