"""Integration tests for the file management API.""" from pathlib import Path import pytest from agenteval.config import get_settings from agenteval.web.app import app from agenteval.web.deps import get_db from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine @pytest.fixture() def files_client(tmp_path: Path, monkeypatch): from agenteval.storage.db import ( # noqa: F401 EvalResultDB, EvalRunDB, EvalTargetDB, FileCategoryDB, FileRecordDB, ScenarioDB, TurnDB, ) from agenteval.web.routers import files as files_module engine = create_engine( f"sqlite:///{tmp_path / 'files_api.db'}", connect_args={"check_same_thread": False}, ) SQLModel.metadata.create_all(engine) session = Session(engine) files_dir = tmp_path / "files" files_dir.mkdir() monkeypatch.setattr(files_module, "FILES_DIR", files_dir) settings = get_settings() monkeypatch.setattr(settings, "allowed_extensions", "txt,json") monkeypatch.setattr(settings, "max_upload_size_mb", 1) def override_get_db(): yield session app.dependency_overrides[get_db] = override_get_db with TestClient(app) as client: yield client, session, files_dir app.dependency_overrides.clear() session.close() engine.dispose() def _create_category(client: TestClient, name: str, parent_id: str | None = None) -> dict: response = client.post( "/api/files/categories", json={"name": name, "parent_id": parent_id}, ) assert response.status_code == 200 return response.json() def test_upload_config_uses_server_settings(files_client): client, _, _ = files_client response = client.get("/api/files/config") assert response.status_code == 200 assert response.json() == { "allowed_extensions": ["json", "txt"], "max_upload_size_mb": 1, } def test_category_crud_returns_domain_fields_and_deep_tree(files_client): client, _, _ = files_client root = _create_category(client, "根分类") child = _create_category(client, "二级", root["id"]) leaf = _create_category(client, "三级", child["id"]) tree = client.get("/api/files/categories").json() assert tree[0]["id"] == root["id"] assert tree[0]["name"] == "根分类" assert tree[0]["children"][0]["id"] == child["id"] assert tree[0]["children"][0]["children"][0]["id"] == leaf["id"] updated = client.put( f"/api/files/categories/{leaf['id']}", json={"name": "三级分类"}, ) assert updated.status_code == 200 assert updated.json()["name"] == "三级分类" def test_create_category_rejects_missing_parent(files_client): client, _, _ = files_client response = client.post( "/api/files/categories", json={"name": "孤立分类", "parent_id": "missing"}, ) assert response.status_code == 400 assert response.json()["detail"] == "父分类不存在" def test_list_files_includes_all_descendant_categories(files_client): client, _, _ = files_client root = _create_category(client, "根分类") child = _create_category(client, "二级", root["id"]) leaf = _create_category(client, "三级", child["id"]) upload = client.post( "/api/files/upload", data={"category_id": leaf["id"]}, files={"file": ("deep.txt", b"deep content", "text/plain")}, ) assert upload.status_code == 200 response = client.get("/api/files", params={"category_id": root["id"]}) assert response.status_code == 200 assert [item["original_name"] for item in response.json()] == ["deep.txt"] def test_upload_download_and_delete_file(files_client): client, _, files_dir = files_client uploaded = client.post( "/api/files/upload", files={"file": ("notes.txt", b"hello", "text/plain")}, ) assert uploaded.status_code == 200 record = uploaded.json() assert record["original_name"] == "notes.txt" assert record["file_size"] == 5 assert record["storage_directory"] == "data/files" assert client.get("/api/files").json()[0]["storage_directory"] == "data/files" assert len(list(files_dir.iterdir())) == 1 downloaded = client.get(f"/api/files/{record['id']}/download") assert downloaded.status_code == 200 assert downloaded.content == b"hello" deleted = client.delete(f"/api/files/{record['id']}") assert deleted.status_code == 200 assert list(files_dir.iterdir()) == [] assert client.get("/api/files").json() == [] def test_upload_rejects_invalid_category_and_extension(files_client): client, _, files_dir = files_client invalid_category = client.post( "/api/files/upload", data={"category_id": "missing"}, files={"file": ("notes.txt", b"hello", "text/plain")}, ) invalid_extension = client.post( "/api/files/upload", files={"file": ("notes.exe", b"hello", "application/octet-stream")}, ) assert invalid_category.status_code == 400 assert invalid_category.json()["detail"] == "分类不存在" assert invalid_extension.status_code == 400 assert list(files_dir.iterdir()) == [] def test_upload_rejects_oversized_file_without_disk_artifact(files_client): client, _, files_dir = files_client response = client.post( "/api/files/upload", files={"file": ("large.txt", b"x" * (1024 * 1024 + 1), "text/plain")}, ) assert response.status_code == 413 assert list(files_dir.iterdir()) == [] assert client.get("/api/files").json() == [] def test_delete_category_removes_descendant_files(files_client): client, _, files_dir = files_client root = _create_category(client, "根分类") child = _create_category(client, "子分类", root["id"]) uploaded = client.post( "/api/files/upload", data={"category_id": child["id"]}, files={"file": ("child.txt", b"content", "text/plain")}, ) assert uploaded.status_code == 200 assert len(list(files_dir.iterdir())) == 1 response = client.delete(f"/api/files/categories/{root['id']}") assert response.status_code == 200 assert client.get("/api/files/categories").json() == [] assert client.get("/api/files").json() == [] assert list(files_dir.iterdir()) == [] # ── Edge-case error branches ────────────────────────────────────────────────── def test_update_nonexistent_category_returns_404(files_client): """update_category catches CategoryNotFoundError → 404.""" client, _, _ = files_client response = client.put( "/api/files/categories/nonexistent", json={"name": "新名称"}, ) assert response.status_code == 404 assert response.json()["detail"] == "分类不存在" def test_delete_nonexistent_category_returns_404(files_client): """delete_category catches CategoryNotFoundError → 404.""" client, _, _ = files_client response = client.delete("/api/files/categories/nonexistent") assert response.status_code == 404 assert response.json()["detail"] == "分类不存在" def test_list_files_with_invalid_category_returns_404(files_client): """list_files catches CategoryNotFoundError → 404.""" client, _, _ = files_client response = client.get("/api/files", params={"category_id": "nonexistent"}) assert response.status_code == 404 assert response.json()["detail"] == "分类不存在" def test_download_nonexistent_file_returns_404(files_client): """download_file catches FileRecordNotFoundError → 404.""" client, _, _ = files_client response = client.get("/api/files/nonexistent/download") assert response.status_code == 404 assert response.json()["detail"] == "文件不存在" def test_delete_nonexistent_file_returns_404(files_client): """delete_file catches FileRecordNotFoundError → 404.""" client, _, _ = files_client response = client.delete("/api/files/nonexistent") assert response.status_code == 404 assert response.json()["detail"] == "文件不存在" def test_download_file_with_missing_physical_file_returns_404(files_client, monkeypatch): """download_file catches PhysicalFileNotFoundError → 404 when DB record exists but physical file is missing.""" client, session, files_dir = files_client from agenteval.storage.db import FileRecordDB # Upload a file first uploaded = client.post( "/api/files/upload", files={"file": ("notes.txt", b"hello", "text/plain")}, ) assert uploaded.status_code == 200 record = uploaded.json() file_id = record["id"] # Get storage_name from DB (not exposed in API response) db_record = session.get(FileRecordDB, file_id) assert db_record is not None physical_path = files_dir / db_record.storage_name # Delete the physical file but keep the DB record physical_path.unlink() # Now try to download — should get 404 "物理文件不存在" response = client.get(f"/api/files/{file_id}/download") assert response.status_code == 404 assert response.json()["detail"] == "物理文件不存在" def test_unknown_file_management_error_returns_500(files_client, monkeypatch): """_raise_http_error fallback: unknown FileManagementError subclass → 500.""" client, _, _ = files_client from agenteval.services.files import FileManagementError, FileManagementService class UnknownError(FileManagementError): pass def raise_unknown(self, category_id, name): raise UnknownError("未知错误") # Use update_category which has try/except FileManagementError monkeypatch.setattr(FileManagementService, "update_category", raise_unknown) response = client.put( "/api/files/categories/some-id", json={"name": "新名称"}, ) assert response.status_code == 500 assert response.json()["detail"] == "文件操作失败"