"""GET /api/v1/documents/{doc_id}/file 文件下载端点测试(TestClient + FakeQdrant,不真实联网) 覆盖分支:成功下载、文档不存在、无关联文件、文件已从磁盘删除、路径越界、免登录访问。 """ from collections.abc import Iterator from pathlib import Path import pytest from fastapi.testclient import TestClient from app.api.v1 import document as document_module from app.main import app from app.services.qdrant import QdrantService class FakeQdrant: """假 QdrantService:仅实现 get_l1_metadata,按 doc_id 返回固定 metadata 或 None""" def __init__(self, meta: dict[str, str] | None = None) -> None: self.meta = meta async def get_l1_metadata(self, doc_id: str) -> dict[str, str] | None: return self.meta @pytest.fixture def client(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Iterator[TestClient]: """TestClient,Qdrant 集合初始化空操作;upload_dir 指向临时目录 不挂 admin_headers:下载端点免登录,验证无 token 可访问。 """ async def _noop_ensure_collections(self: QdrantService) -> None: return None monkeypatch.setattr(QdrantService, "ensure_collections", _noop_ensure_collections) monkeypatch.setattr(document_module.settings, "upload_dir", str(tmp_path / "uploads")) with TestClient(app) as test_client: yield test_client def _install_fake(monkeypatch: pytest.MonkeyPatch, fake: FakeQdrant) -> None: """将 _get_qdrant 单例替换为假服务""" monkeypatch.setattr(document_module, "_get_qdrant", lambda: fake) def test_download_file_success( client: TestClient, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """文档存在且有关联文件 → 200 + 文件内容正确 + content-disposition 含文件名""" upload_dir = tmp_path / "uploads" upload_dir.mkdir(parents=True, exist_ok=True) target = upload_dir / "doc-1_notes.txt" content = b"hello world file content" target.write_bytes(content) fake = FakeQdrant( meta={ "raw_file_path": str(target), "original_filename": "notes.txt", "original_size_bytes": str(len(content)), } ) _install_fake(monkeypatch, fake) resp = client.get("/api/v1/documents/doc-1/file") assert resp.status_code == 200 assert resp.content == content disposition = resp.headers.get("content-disposition", "") assert "attachment" in disposition assert "notes.txt" in disposition def test_download_file_doc_not_found( client: TestClient, monkeypatch: pytest.MonkeyPatch ) -> None: """文档不存在(metadata 为 None)→ 1004""" fake = FakeQdrant(meta=None) _install_fake(monkeypatch, fake) resp = client.get("/api/v1/documents/missing/file") body = resp.json() assert body["code"] == 1004 assert "文件" in body["message"] def test_download_file_no_associated_file( client: TestClient, monkeypatch: pytest.MonkeyPatch ) -> None: """文档存在但 raw_file_path 为空 → 1004""" fake = FakeQdrant(meta={"raw_file_path": "", "original_filename": "x.txt"}) _install_fake(monkeypatch, fake) resp = client.get("/api/v1/documents/doc-1/file") body = resp.json() assert body["code"] == 1004 assert "未关联文件" in body["message"] def test_download_file_missing_on_disk( client: TestClient, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """文件已从磁盘删除(path.is_file() False)→ 1004""" missing = tmp_path / "uploads" / "gone.txt" fake = FakeQdrant(meta={"raw_file_path": str(missing), "original_filename": "gone.txt"}) _install_fake(monkeypatch, fake) resp = client.get("/api/v1/documents/doc-1/file") body = resp.json() assert body["code"] == 1004 assert "文件不存在" in body["message"] def test_download_file_path_traversal_rejected( client: TestClient, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """raw_file_path 指向 upload_dir 之外 → 1004(路径越界校验)""" outside = tmp_path / "secret.txt" outside.write_bytes(b"secret") fake = FakeQdrant(meta={"raw_file_path": str(outside), "original_filename": "secret.txt"}) _install_fake(monkeypatch, fake) resp = client.get("/api/v1/documents/doc-1/file") body = resp.json() assert body["code"] == 1004 assert "文件不存在" in body["message"] def test_download_file_no_auth_required( client: TestClient, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """免登录:不携带 Authorization 头可正常下载(端点不挂鉴权依赖)""" upload_dir = tmp_path / "uploads" upload_dir.mkdir(parents=True, exist_ok=True) target = upload_dir / "doc-1_open.txt" target.write_bytes(b"open content") fake = FakeQdrant(meta={"raw_file_path": str(target), "original_filename": "open.txt"}) _install_fake(monkeypatch, fake) resp = client.get("/api/v1/documents/doc-1/file") assert resp.status_code == 200 assert resp.content == b"open content" def test_download_file_uses_original_filename_when_missing( client: TestClient, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """metadata 缺少 original_filename 时回退到 path.name 作为下载文件名""" upload_dir = tmp_path / "uploads" upload_dir.mkdir(parents=True, exist_ok=True) target = upload_dir / "fallback_name.txt" target.write_bytes(b"fb") fake = FakeQdrant(meta={"raw_file_path": str(target)}) _install_fake(monkeypatch, fake) resp = client.get("/api/v1/documents/doc-1/file") assert resp.status_code == 200 assert "fallback_name.txt" in resp.headers.get("content-disposition", "")