# ============================================================ # 项目文件上传/下载黄金测试(Web 端持久化到数据目录) # 口径:字节落 project_upload_dir(tenants/<租户>/projects/<项目>/uploads), # 元数据走 ProjectFileRecord;viewer 403;删除记录联动清理字节。 # ============================================================ from __future__ import annotations import io import pytest from fastapi.testclient import TestClient from tests.auth_provider import install_test_auth @pytest.fixture() def secure_app(tmp_path, monkeypatch): monkeypatch.setenv("APS_DB_PATH", str(tmp_path / "tenant.db")) monkeypatch.setenv("APS_WORLD_PATH", str(tmp_path / "world.json")) from server.db.database import reset_engine from server.state import store as world_store install_test_auth(monkeypatch, "tenant-upload-00000000000000000001") world_store._stores.clear() reset_engine() from server.gateway.app import create_app app = create_app() yield app reset_engine() world_store._stores.clear() def _login(client: TestClient, username: str = "planner") -> None: resp = client.post("/api/auth/login", json={ "method": "password", "username": username, "password": "test", }) assert resp.status_code == 200 def _create_project(client: TestClient, name: str = "上传测试项目") -> str: resp = client.post("/api/projects", json={"name": name}) assert resp.status_code == 200 return resp.json()["project"]["id"] def test_upload_download_roundtrip(secure_app): client = TestClient(secure_app) _login(client) pid = _create_project(client) payload = "orderNo,qty\nFO-1,100\n".encode("utf-8") resp = client.post( f"/api/projects/{pid}/files/upload", files=[("files", ("orders.csv", io.BytesIO(payload), "text/csv"))], ) assert resp.status_code == 200, resp.text body = resp.json() assert body["files"][0]["name"] == "orders.csv" assert body["files"][0]["kind"] == "import" assert body["files"][0]["size"] == len(payload) file_id = body["files"][0]["id"] # 字节落数据目录(conftest 隔离的 APS_HOME/data 下) from server.aps_home import project_upload_dir folder = project_upload_dir(pid, "tenant-upload-00000000000000000001") blobs = list(folder.glob(f"{file_id}__*")) assert len(blobs) == 1 assert blobs[0].read_bytes() == payload # 记录出现在工作区快照 snap = client.get("/api/workspace").json() assert any(f["id"] == file_id for f in snap["files"]) # 下载回源字节 dl = client.get(f"/api/project-files/{file_id}/download") assert dl.status_code == 200 assert dl.content == payload # 删除记录 → 字节联动清理 assert client.delete(f"/api/project-files/{file_id}").status_code == 200 assert not list(folder.glob(f"{file_id}__*")) assert client.get(f"/api/project-files/{file_id}/download").status_code == 404 def test_upload_requires_login(secure_app): client = TestClient(secure_app) resp = client.post( "/api/projects/proj_x/files/upload", files=[("files", ("a.csv", io.BytesIO(b"x"), "text/csv"))], ) assert resp.status_code == 401 def test_upload_viewer_forbidden(secure_app): owner = TestClient(secure_app) _login(owner, "planner") pid = _create_project(owner) # owner 邀请 viewer 为只读成员 resp = owner.post(f"/api/projects/{pid}/members", json={"userId": 1003, "role": "viewer"}) assert resp.status_code == 200, resp.text viewer = TestClient(secure_app) _login(viewer, "viewer") resp = viewer.post( f"/api/projects/{pid}/files/upload", files=[("files", ("a.csv", io.BytesIO(b"x"), "text/csv"))], ) assert resp.status_code == 403 def test_upload_rejects_empty_and_cross_tenant_blob(secure_app): client = TestClient(secure_app) _login(client) pid = _create_project(client) resp = client.post( f"/api/projects/{pid}/files/upload", files=[("files", ("empty.csv", io.BytesIO(b""), "text/csv"))], ) assert resp.status_code == 400