aps-agent/tests/golden/test_project_file_upload.py

120 lines
4.1 KiB
Python
Raw Normal View History

# ============================================================
# 项目文件上传/下载黄金测试(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