"""管理页面挂载测试(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