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"]]