"""Phase 9-11 公共 AI 页面测试。""" from fastapi.testclient import TestClient from app.main import app def _client() -> TestClient: return TestClient(app, raise_server_exceptions=False) def _register_and_create_kb(client: TestClient) -> tuple[str, str]: """注册用户并创建知识库,返回 (kb_id, ai_url)。""" client.post("/api/auth/register", json={ "username": "testuser", "email": "test@example.com", "password": "password123", }) resp = client.post("/api/knowledge-bases", json={ "name": "测试知识库", "description": "这是测试描述", }) kb_id = resp.json()["id"] ai_url = resp.json()["ai_url"] return kb_id, ai_url def _get_token_from_url(ai_url: str) -> str: """从 AI URL 中提取 token。""" return ai_url.split("/k/")[-1] # --- HTML 入口 --- def test_kb_index_html() -> None: with _client() as client: _, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) resp = client.get(f"/k/{token}") assert resp.status_code == 200 assert "text/html" in resp.headers["content-type"] assert "测试知识库" in resp.text assert "noindex" in resp.text assert "no-referrer" in resp.text def test_kb_index_html_not_found() -> None: with _client() as client: resp = client.get("/k/nonexistent") assert resp.status_code == 404 def test_kb_index_html_disabled() -> None: with _client() as client: kb_id, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) # 禁用链接 client.post(f"/api/knowledge-bases/{kb_id}/disable") resp = client.get(f"/k/{token}") assert resp.status_code == 404 # --- Markdown 输出 --- def test_kb_index_markdown() -> None: with _client() as client: _, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) resp = client.get(f"/k/{token}.md") assert resp.status_code == 200 assert "text/markdown" in resp.headers["content-type"] assert "测试知识库" in resp.text # --- 纯文本输出 --- def test_kb_index_text() -> None: with _client() as client: _, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) resp = client.get(f"/k/{token}.txt") assert resp.status_code == 200 assert "text/plain" in resp.headers["content-type"] assert "测试知识库" in resp.text # --- JSON 输出 --- def test_kb_index_json() -> None: with _client() as client: _, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) resp = client.get(f"/k/{token}.json") assert resp.status_code == 200 data = resp.json() assert data["name"] == "测试知识库" assert "documents" in data # 安全:不包含内部字段 assert "user_id" not in data assert "token_hash" not in data # --- 搜索 --- def test_search_html() -> None: with _client() as client: _, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) resp = client.get(f"/k/{token}/search?q=test") assert resp.status_code == 200 assert "text/html" in resp.headers["content-type"] def test_search_json() -> None: with _client() as client: _, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) resp = client.get(f"/k/{token}/search.json?q=test") assert resp.status_code == 200 data = resp.json() assert "query" in data assert "total" in data # --- 单文档访问 --- def test_doc_page_html() -> None: """上传文档后通过 token 访问文档页面。""" import io, zipfile with _client() as client: kb_id, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) # 上传一个真实 .docx 文件 buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("[Content_Types].xml", '') zf.writestr("word/document.xml", 'Hello World') # 获取文档 token resp = client.get(f"/api/documents?kb_id={kb_id}") if resp.json()["total"] > 0: doc = resp.json()["items"][0] doc_token_hint = doc.get("doc_token_hint", "") # 访问文档页面(使用 hint 作为 token 的一部分) resp = client.get(f"/k/{token}/doc/{doc_token_hint}") # 可能 404(因为 hint 不是完整 token),但不应 500 assert resp.status_code in (200, 404) # --- 限流 --- def test_rate_limit_not_triggered() -> None: """正常请求不应触发限流。""" with _client() as client: _, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) # 连续请求 5 次 for _ in range(5): resp = client.get(f"/k/{token}") assert resp.status_code == 200 # --- 安全头 --- def test_no_robots_meta() -> None: with _client() as client: _, ai_url = _register_and_create_kb(client) token = _get_token_from_url(ai_url) resp = client.get(f"/k/{token}") assert "noindex" in resp.text assert "nofollow" in resp.text assert "noarchive" in resp.text