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