165 lines
6.3 KiB
Python
165 lines
6.3 KiB
Python
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from backend.db import get_engine
|
|
from backend.models import Folder, User
|
|
|
|
|
|
def initialized_client(client):
|
|
response = client.post(
|
|
"/api/v1/setup/initialize",
|
|
json={"username": "owner", "password": "correct horse battery staple"},
|
|
)
|
|
assert response.status_code == 201
|
|
return client
|
|
|
|
|
|
def create_list(client, name, folder_id=None):
|
|
response = client.post("/api/v1/lists", json={"name": name, "folder_id": folder_id})
|
|
assert response.status_code == 201
|
|
return response.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_move_list_validates_folder_ownership_and_is_atomic(client):
|
|
client = initialized_client(client)
|
|
task_list = create_list(client, "项目")
|
|
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
|
|
session_factory = async_sessionmaker(get_engine(), expire_on_commit=False)
|
|
async with session_factory() as db:
|
|
other = User(username="other", password_hash="not-used")
|
|
db.add(other)
|
|
await db.flush()
|
|
foreign_folder = Folder(user_id=other.id, name="别人的文件夹")
|
|
db.add(foreign_folder)
|
|
await db.commit()
|
|
foreign_folder_id = str(foreign_folder.id)
|
|
|
|
response = client.put(
|
|
f"/api/v1/lists/{task_list['id']}/move",
|
|
json={"folder_id": foreign_folder_id, "list_ids": [task_list["id"]]},
|
|
)
|
|
assert response.status_code == 404
|
|
unchanged = next(row for row in client.get("/api/v1/lists").json() if row["id"] == task_list["id"])
|
|
assert unchanged["folder_id"] is None
|
|
|
|
|
|
def test_move_list_between_folder_and_root_applies_target_order_atomically(client):
|
|
client = initialized_client(client)
|
|
folder = client.post("/api/v1/folders", json={"name": "工作"}).json()
|
|
root_a = create_list(client, "根 A")
|
|
root_b = create_list(client, "根 B")
|
|
folder_a = create_list(client, "文件夹 A", folder["id"])
|
|
|
|
moved = client.put(
|
|
f"/api/v1/lists/{root_a['id']}/move",
|
|
json={"folder_id": folder["id"], "list_ids": [root_a["id"], folder_a["id"]]},
|
|
)
|
|
assert moved.status_code == 200
|
|
assert moved.json()["folder_id"] == folder["id"]
|
|
folder_rows = [row for row in client.get("/api/v1/lists").json() if row["folder_id"] == folder["id"]]
|
|
assert [row["id"] for row in folder_rows] == [root_a["id"], folder_a["id"]]
|
|
|
|
moved_to_root = client.put(
|
|
f"/api/v1/lists/{root_a['id']}/move",
|
|
json={"folder_id": None, "list_ids": [root_b["id"], root_a["id"]]},
|
|
)
|
|
assert moved_to_root.status_code == 200
|
|
assert moved_to_root.json()["folder_id"] is None
|
|
root_rows = [
|
|
row for row in client.get("/api/v1/lists").json()
|
|
if not row["is_inbox"] and row["folder_id"] is None
|
|
]
|
|
assert [row["id"] for row in root_rows] == [root_b["id"], root_a["id"]]
|
|
|
|
|
|
def test_move_list_rejects_invalid_target_order_without_moving(client):
|
|
client = initialized_client(client)
|
|
folder = client.post("/api/v1/folders", json={"name": "工作"}).json()
|
|
root_a = create_list(client, "根 A")
|
|
folder_a = create_list(client, "文件夹 A", folder["id"])
|
|
|
|
response = client.put(
|
|
f"/api/v1/lists/{root_a['id']}/move",
|
|
json={"folder_id": folder["id"], "list_ids": [root_a["id"]]},
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
rows = client.get("/api/v1/lists").json()
|
|
unchanged = next(row for row in rows if row["id"] == root_a["id"])
|
|
assert unchanged["folder_id"] is None
|
|
assert [row["id"] for row in rows if row["folder_id"] == folder["id"]] == [folder_a["id"]]
|
|
|
|
|
|
def test_inbox_cannot_be_moved_or_reordered(client):
|
|
client = initialized_client(client)
|
|
inbox = client.get("/api/v1/lists").json()[0]
|
|
folder = client.post("/api/v1/folders", json={"name": "工作"}).json()
|
|
|
|
moved = client.put(
|
|
f"/api/v1/lists/{inbox['id']}/move",
|
|
json={"folder_id": folder["id"], "list_ids": [inbox["id"]]},
|
|
)
|
|
assert moved.status_code == 409
|
|
|
|
reordered = client.put(
|
|
"/api/v1/lists/reorder", json={"folder_id": None, "list_ids": [inbox["id"]]}
|
|
)
|
|
assert reordered.status_code == 409
|
|
|
|
|
|
def test_reorder_lists_is_scoped_and_persists(client):
|
|
client = initialized_client(client)
|
|
folder = client.post("/api/v1/folders", json={"name": "工作"}).json()
|
|
root_a = create_list(client, "根 A")
|
|
root_b = create_list(client, "根 B")
|
|
folder_a = create_list(client, "文件夹 A", folder["id"])
|
|
folder_b = create_list(client, "文件夹 B", folder["id"])
|
|
|
|
assert client.put(
|
|
"/api/v1/lists/reorder",
|
|
json={"folder_id": None, "list_ids": [root_b["id"], root_a["id"]]},
|
|
).status_code == 204
|
|
assert client.put(
|
|
"/api/v1/lists/reorder",
|
|
json={"folder_id": folder["id"], "list_ids": [folder_b["id"], folder_a["id"]]},
|
|
).status_code == 204
|
|
|
|
rows = client.get("/api/v1/lists").json()
|
|
root_rows = [row for row in rows if not row["is_inbox"] and row["folder_id"] is None]
|
|
folder_rows = [row for row in rows if row["folder_id"] == folder["id"]]
|
|
assert [row["id"] for row in root_rows] == [root_b["id"], root_a["id"]]
|
|
assert [row["id"] for row in folder_rows] == [folder_b["id"], folder_a["id"]]
|
|
assert [row["position"] for row in root_rows] == [0, 1]
|
|
assert [row["position"] for row in folder_rows] == [0, 1]
|
|
|
|
|
|
def test_reorder_rejects_stale_or_mixed_scope_without_partial_write(client):
|
|
client = initialized_client(client)
|
|
folder = client.post("/api/v1/folders", json={"name": "工作"}).json()
|
|
root_a = create_list(client, "根 A")
|
|
root_b = create_list(client, "根 B")
|
|
folder_a = create_list(client, "文件夹 A", folder["id"])
|
|
|
|
stale = client.put(
|
|
"/api/v1/lists/reorder", json={"folder_id": None, "list_ids": [root_b["id"]]}
|
|
)
|
|
assert stale.status_code == 409
|
|
mixed = client.put(
|
|
"/api/v1/lists/reorder",
|
|
json={"folder_id": None, "list_ids": [root_b["id"], folder_a["id"]]},
|
|
)
|
|
assert mixed.status_code == 409
|
|
missing = client.put(
|
|
"/api/v1/lists/reorder",
|
|
json={"folder_id": str(uuid4()), "list_ids": [root_a["id"], root_b["id"]]},
|
|
)
|
|
assert missing.status_code == 404
|
|
|
|
rows = client.get("/api/v1/lists").json()
|
|
root_rows = [row for row in rows if not row["is_inbox"] and row["folder_id"] is None]
|
|
assert [row["id"] for row in root_rows] == [root_a["id"], root_b["id"]]
|