51dc8dc4f6
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
"""管理页面挂载测试(GET /admin)
|
||
|
||
覆盖:
|
||
1. 路由存在性:200 + content-type 为 text/html
|
||
2. 五区块可识别标记(文案与 section id)
|
||
3. 零外部依赖:无 http(s) 外链资源、无 CDN 引用
|
||
4. fetch 调用路径与后端 API 契约一致
|
||
5. 删除操作的 confirm() 二次确认逻辑
|
||
"""
|
||
|
||
import re
|
||
from collections.abc import Iterator
|
||
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
from app.main import app
|
||
from app.services.qdrant import QdrantService
|
||
|
||
|
||
@pytest.fixture
|
||
def client(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestClient]:
|
||
"""TestClient,lifespan 中的 Qdrant 集合初始化替换为空操作"""
|
||
|
||
async def _noop_ensure_collections(self: QdrantService) -> None:
|
||
return None
|
||
|
||
monkeypatch.setattr(QdrantService, "ensure_collections", _noop_ensure_collections)
|
||
with TestClient(app) as test_client:
|
||
yield test_client
|
||
|
||
|
||
@pytest.fixture
|
||
def admin_html(client: TestClient) -> str:
|
||
"""请求 /admin 并返回 HTML 文本(前置断言 200)"""
|
||
resp = client.get("/admin")
|
||
assert resp.status_code == 200
|
||
return resp.text
|
||
|
||
|
||
def test_admin_page_ok(client: TestClient) -> None:
|
||
"""GET /admin → 200,content-type 含 text/html"""
|
||
resp = client.get("/admin")
|
||
|
||
assert resp.status_code == 200
|
||
assert "text/html" in resp.headers["content-type"]
|
||
|
||
|
||
def test_admin_page_has_five_sections(admin_html: str) -> None:
|
||
"""HTML 含五个区块的文案与对应 section id"""
|
||
for section_id in (
|
||
"section-overview",
|
||
"section-docs",
|
||
"section-ingest",
|
||
"section-search",
|
||
"section-categories",
|
||
):
|
||
assert section_id in admin_html
|
||
for label in ("概览", "文档管理", "文档入库", "检索测试台", "类目列表"):
|
||
assert label in admin_html
|
||
|
||
|
||
def test_admin_page_no_external_resources(admin_html: str) -> None:
|
||
"""零外部依赖:无 src/href http(s) 外链,无 CDN 引用"""
|
||
assert re.search(r'src=["\']https?://', admin_html) is None
|
||
assert re.search(r'href=["\']https?://', admin_html) is None
|
||
assert re.search(r"//cdn", admin_html) is None
|
||
|
||
|
||
def test_admin_page_fetch_paths(admin_html: str) -> None:
|
||
"""fetch 调用路径与后端 API 契约一致"""
|
||
for path in (
|
||
"/api/v1/search",
|
||
"/api/v1/documents",
|
||
"/api/v1/knowledge/stats",
|
||
"/api/v1/knowledge/categories",
|
||
):
|
||
assert path in admin_html
|
||
|
||
|
||
def _collect_route_paths(routes: list) -> set[str]:
|
||
"""递归收集路由路径(FastAPI 0.140+ include_router 包装为 _IncludedRouter)"""
|
||
paths: set[str] = set()
|
||
for route in routes:
|
||
path = getattr(route, "path", None)
|
||
if path:
|
||
paths.add(path)
|
||
sub_router = getattr(route, "original_router", None)
|
||
if sub_router is not None:
|
||
paths |= _collect_route_paths(sub_router.routes)
|
||
return paths
|
||
|
||
|
||
def test_admin_page_fetch_paths_in_real_routes(admin_html: str) -> None:
|
||
"""页面 api() 调用路径均在真实后端路由集合内(含新增的 tasks 路径)"""
|
||
route_paths = _collect_route_paths(app.routes)
|
||
assert "/api/v1/documents/tasks/{task_id}" in route_paths
|
||
|
||
prefixes = set(re.findall(r'api\("([^"?]+)', admin_html))
|
||
assert prefixes, "页面应包含 api() 调用"
|
||
for prefix in prefixes:
|
||
assert any(path == prefix or path.startswith(prefix) for path in route_paths), (
|
||
f"页面 fetch 路径 {prefix} 不在后端路由集合内"
|
||
)
|
||
|
||
|
||
def test_admin_page_ingest_polling(admin_html: str) -> None:
|
||
"""入库区块:异步任务轮询逻辑标记"""
|
||
# 轮询任务状态端点
|
||
assert "/api/v1/documents/tasks/" in admin_html
|
||
# 状态中文映射
|
||
for text in ("排队中", "总结中", "分类中", "向量化中", "写入中", "完成", "失败"):
|
||
assert text in admin_html
|
||
# 提交确认与超时提示
|
||
assert "任务已提交" in admin_html
|
||
assert "任务仍在进行,可稍后凭 task_id 查询" in admin_html
|
||
# 5 分钟超时:150 次 × 2s
|
||
assert "150" in admin_html
|
||
# 防重复提交:轮询中禁用提交按钮
|
||
assert "disabled" in admin_html
|
||
|
||
|
||
def test_admin_page_has_confirm(admin_html: str) -> None:
|
||
"""删除操作包含 confirm() 二次确认逻辑"""
|
||
assert "confirm(" in admin_html
|