Files
QMDSearch/tests/test_document_file_integration.py
T
kplam f92eff6f65 feat: add document file download API and related features
1. add `/api/v1/documents/{doc_id}/file` endpoint for downloading original document files
2. add file field to document detail API response based on metadata
3. add comprehensive tests for file download API and metadata integration
4. update API documentation in CLAUDE.md
2026-07-31 22:46:50 +08:00

162 lines
6.6 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.
"""文件链接集成验证
在真实内存 Qdrant + FakeOllama + 真实临时 upload_dir 环境下,验证文件上传到下载的全链路闭环:
POST /documents/upload → 入库 wait_done → GET /documents/{id}file 字段)→ GET /documents/{id}/file
并覆盖纯文本入库无文件关联的对照路径(详情 file=None、下载端点 1004)。
"""
from pathlib import Path
from urllib.parse import unquote
import pytest
from fastapi.testclient import TestClient
from qdrant_client import AsyncQdrantClient
from app.api.v1 import document as document_module
from app.config import Settings
from app.core.chunker import Chunker
from app.core.classifier import Classifier
from app.core.ingest_tasks import IngestTaskManager, IngestTaskStatus
from app.core.ingestion import Ingester
from app.core.sparse import SparseEncoder
from app.core.summarizer import Summarizer
from app.main import app
from app.models.knowledge import load_taxonomy
from app.services.qdrant import QdrantService
from tests.test_e2e_integration import (
FAKE_CATEGORY,
DeterministicEmbedding,
FakeOllama,
)
async def _make_env() -> tuple[QdrantService, Ingester]:
"""构建集成环境:真实组件 + 内存 Qdrant + FakeOllama + 确定性向量"""
qdrant = QdrantService(client=AsyncQdrantClient(location=":memory:"))
await qdrant.ensure_collections()
ollama = FakeOllama()
taxonomy = load_taxonomy()
embedding = DeterministicEmbedding()
ingester = Ingester(
summarizer=Summarizer(ollama=ollama), # type: ignore[arg-type]
classifier=Classifier(ollama=ollama, taxonomy=taxonomy), # type: ignore[arg-type]
chunker=Chunker(),
embedding=embedding,
sparse=SparseEncoder(),
qdrant=qdrant,
)
return qdrant, ingester
def _patch_app(
monkeypatch: pytest.MonkeyPatch,
manager: IngestTaskManager,
qdrant: QdrantService,
upload_dir: str,
) -> None:
"""替换模块级单例:任务管理器 / Qdrant / upload_dirlifespan 建集合改空操作"""
async def _noop_ensure_collections(self: QdrantService) -> None:
return None
monkeypatch.setattr(QdrantService, "ensure_collections", _noop_ensure_collections)
monkeypatch.setattr(document_module, "_task_manager", manager)
monkeypatch.setattr(document_module, "_qdrant", qdrant)
monkeypatch.setattr(document_module.settings, "upload_dir", upload_dir)
class TestDocumentFileIntegration:
"""文件链接集成:上传闭环 + 文本入库无文件对照"""
async def test_upload_full_loop(
self,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
admin_headers: dict[str, str],
) -> None:
"""文件上传闭环:upload → wait_done done → 详情含 file → 下载返回原文"""
qdrant, ingester = await _make_env()
manager = IngestTaskManager(ingester, None, Settings())
_patch_app(monkeypatch, manager, qdrant, str(tmp_path / "uploads"))
filename = "上传测试.md"
original_content = "上传原文测试内容"
with TestClient(app) as client:
# 1. multipart 上传 .md 文件(变更类端点需 admin 认证头)
resp_upload = client.post(
"/api/v1/documents/upload",
files={"file": (filename, original_content.encode("utf-8"), "text/markdown")},
headers=admin_headers,
)
assert resp_upload.status_code == 202
body_upload = resp_upload.json()
assert body_upload["code"] == 0
assert body_upload["data"]["status"] == "pending"
task_id = body_upload["data"]["task_id"]
assert task_id
# 2. 等待入库终态:done + 结果完整
final = await manager.wait_done(task_id)
assert final["status"] == IngestTaskStatus.DONE
result = final["result"]
document_id = result["document_id"]
assert document_id
assert result["category"] == FAKE_CATEGORY
# 3. 文档详情:file 字段非空,含原文件名 / url / size
resp_detail = client.get(f"/api/v1/documents/{document_id}")
body_detail = resp_detail.json()
assert body_detail["code"] == 0
file_info = body_detail["data"]["file"]
assert file_info is not None
assert filename in file_info["filename"]
assert file_info["url"] == f"/api/v1/documents/{document_id}/file"
assert file_info["size_bytes"] > 0
# 4. 下载:200 + 响应体含上传原文 + Content-Disposition 含原文件名(中文按 RFC 5987 百分号编码,需 unquote
resp_file = client.get(f"/api/v1/documents/{document_id}/file")
assert resp_file.status_code == 200
assert original_content in resp_file.content.decode("utf-8")
disposition = resp_file.headers.get("content-disposition", "")
assert "attachment" in disposition
assert filename in unquote(disposition)
async def test_text_ingest_no_file(
self,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
admin_headers: dict[str, str],
) -> None:
"""纯文本入库(无 metadata):详情 file=None,下载端点返回 1004"""
qdrant, ingester = await _make_env()
manager = IngestTaskManager(ingester, None, Settings())
_patch_app(monkeypatch, manager, qdrant, str(tmp_path / "uploads"))
with TestClient(app) as client:
# 1. 纯文本入库(无 metadata
resp_post = client.post(
"/api/v1/documents",
json={"text": "纯文本入库无附件的对照内容", "title": "无文件文档"},
headers=admin_headers,
)
assert resp_post.status_code == 202
body_post = resp_post.json()
assert body_post["code"] == 0
task_id = body_post["data"]["task_id"]
# 2. 等待终态 done
final = await manager.wait_done(task_id)
assert final["status"] == IngestTaskStatus.DONE
document_id = final["result"]["document_id"]
# 3. 详情:file 为 None
resp_detail = client.get(f"/api/v1/documents/{document_id}")
body_detail = resp_detail.json()
assert body_detail["code"] == 0
assert body_detail["data"]["file"] is None
# 4. 下载端点:文档未关联文件 → 1004
resp_file = client.get(f"/api/v1/documents/{document_id}/file")
body_file = resp_file.json()
assert body_file["code"] == 1004