Files
dodo/tests/test_list_organization.py
T
bboysoul 0e291ddb9c
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 3m22s
feat: organize lists with drag and drop
2026-09-09 20:28:29 +08:00

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