feat: complete task management workflow

This commit is contained in:
2026-09-05 11:18:52 +08:00
parent f182c704a4
commit 57145c198b
17 changed files with 1514 additions and 115 deletions
+197
View File
@@ -128,3 +128,200 @@ def test_logout_revokes_current_session(client):
assert client.get("/api/v1/me").status_code == 200
assert client.post("/api/v1/auth/logout").status_code == 204
assert client.get("/api/v1/me").status_code == 401
def initialized_client(client):
client.post(
"/api/v1/setup/initialize",
json={"username": "owner", "password": "correct horse battery staple"},
)
return client
def test_folder_and_list_lifecycle(client):
client = initialized_client(client)
folder = client.post("/api/v1/folders", json={"name": "工作"}).json()
renamed = client.patch(f"/api/v1/folders/{folder['id']}", json={"name": "生活"})
assert renamed.status_code == 200
assert renamed.json()["name"] == "生活"
task_list = client.post(
"/api/v1/lists", json={"name": "采购", "folder_id": folder["id"]}
).json()
renamed_list = client.patch(f"/api/v1/lists/{task_list['id']}", json={"name": "购物"})
assert renamed_list.status_code == 200
assert renamed_list.json()["name"] == "购物"
assert client.delete(f"/api/v1/lists/{task_list['id']}").status_code == 204
assert client.delete(f"/api/v1/folders/{folder['id']}").status_code == 204
def test_task_search_tags_subtasks_and_recycle_bin(client):
client = initialized_client(client)
inbox = client.get("/api/v1/lists").json()[0]
tag = client.post("/api/v1/tags", json={"name": "重要", "color": "#f15a29"}).json()
parent = client.post(
"/api/v1/tasks",
json={"title": "购买咖啡豆", "description": "手冲用", "list_id": inbox["id"], "tag_ids": [tag["id"]]},
).json()
child = client.post(
"/api/v1/tasks",
json={"title": "比较价格", "list_id": inbox["id"], "parent_id": parent["id"]},
)
assert child.status_code == 201
detail = client.get(f"/api/v1/tasks/{parent['id']}")
assert detail.status_code == 200
assert detail.json()["tags"][0]["name"] == "重要"
assert len(detail.json()["subtasks"]) == 1
assert len(client.get("/api/v1/tasks", params={"q": "咖啡"}).json()["items"]) == 1
assert client.delete(f"/api/v1/tasks/{parent['id']}").status_code == 204
trash = client.get("/api/v1/trash").json()["items"]
assert len(trash) == 1
assert client.post(f"/api/v1/tasks/{parent['id']}/restore").status_code == 200
def test_batch_complete_and_move(client):
client = initialized_client(client)
inbox = client.get("/api/v1/lists").json()[0]
other = client.post("/api/v1/lists", json={"name": "稍后"}).json()
ids = [
client.post("/api/v1/tasks", json={"title": f"任务{i}", "list_id": inbox["id"]}).json()["id"]
for i in range(2)
]
response = client.post(
"/api/v1/tasks/batch", json={"task_ids": ids, "completed": True, "list_id": other["id"]}
)
assert response.status_code == 200
assert response.json()["updated"] == 2
rows = client.get("/api/v1/tasks").json()["items"]
assert all(row["completed"] and row["list_id"] == other["id"] for row in rows)
def test_inbox_is_protected_and_deleted_collections_are_hidden(client):
client = initialized_client(client)
inbox = client.get("/api/v1/lists").json()[0]
assert client.patch(f"/api/v1/lists/{inbox['id']}", json={"name": "x"}).status_code == 409
assert client.delete(f"/api/v1/lists/{inbox['id']}").status_code == 409
folder = client.post("/api/v1/folders", json={"name": "项目"}).json()
task_list = client.post(
"/api/v1/lists", json={"name": "待办", "folder_id": folder["id"]}
).json()
task = client.post(
"/api/v1/tasks", json={"title": "保留任务", "list_id": task_list["id"]}
).json()
assert client.delete(f"/api/v1/lists/{task_list['id']}").status_code == 204
assert all(row["id"] != task_list["id"] for row in client.get("/api/v1/lists").json())
moved = client.get(f"/api/v1/tasks/{task['id']}").json()
assert moved["list_id"] == inbox["id"]
assert client.delete(f"/api/v1/folders/{folder['id']}").status_code == 204
assert client.get("/api/v1/folders").json() == []
def test_nested_subtasks_are_rejected(client):
client = initialized_client(client)
inbox = client.get("/api/v1/lists").json()[0]
parent = client.post("/api/v1/tasks", json={"title": "", "list_id": inbox["id"]}).json()
child = client.post(
"/api/v1/tasks",
json={"title": "", "list_id": inbox["id"], "parent_id": parent["id"]},
).json()
nested = client.post(
"/api/v1/tasks",
json={"title": "", "list_id": inbox["id"], "parent_id": child["id"]},
)
assert nested.status_code == 400
def test_tags_are_global_searchable_and_validated(client):
client = initialized_client(client)
inbox = client.get("/api/v1/lists").json()[0]
errands = client.post("/api/v1/lists", json={"name": "跑腿清单"}).json()
tag = client.post("/api/v1/tags", json={"name": "蓝色标签", "color": "#123abc"})
assert tag.status_code == 201
assert client.get("/api/v1/tags").json()[0]["color"] == "#123abc"
task = client.post(
"/api/v1/tasks",
json={"title": "普通标题", "description": "特殊说明", "list_id": errands["id"], "tag_ids": [tag.json()["id"]]},
).json()
for query in ("普通", "特殊", "蓝色", "跑腿"):
assert [row["id"] for row in client.get("/api/v1/tasks", params={"q": query}).json()["items"]] == [task["id"]]
invalid = client.post(
"/api/v1/tasks",
json={"title": "非法标签", "list_id": inbox["id"], "tag_ids": ["00000000-0000-0000-0000-000000000001"]},
)
assert invalid.status_code == 404
def test_recycle_bin_restore_and_permanent_delete_include_subtasks(client):
client = initialized_client(client)
inbox = client.get("/api/v1/lists").json()[0]
parent = client.post("/api/v1/tasks", json={"title": "", "list_id": inbox["id"]}).json()
client.post(
"/api/v1/tasks",
json={"title": "", "list_id": inbox["id"], "parent_id": parent["id"]},
)
client.delete(f"/api/v1/tasks/{parent['id']}")
assert len(client.get("/api/v1/trash").json()["items"]) == 1
restored = client.post(f"/api/v1/tasks/{parent['id']}/restore")
assert restored.status_code == 200
assert len(client.get(f"/api/v1/tasks/{parent['id']}").json()["subtasks"]) == 1
client.delete(f"/api/v1/tasks/{parent['id']}")
assert client.delete(f"/api/v1/trash/{parent['id']}").status_code == 204
assert client.post(f"/api/v1/tasks/{parent['id']}/restore").status_code == 404
def test_batch_supports_due_date_tags_and_soft_delete_atomically(client):
client = initialized_client(client)
inbox = client.get("/api/v1/lists").json()[0]
tag = client.post("/api/v1/tags", json={"name": "批量", "color": "red"}).json()
ids = [
client.post("/api/v1/tasks", json={"title": str(i), "list_id": inbox["id"]}).json()["id"]
for i in range(2)
]
due_at = "2026-09-10T08:00:00+08:00"
response = client.post(
"/api/v1/tasks/batch",
json={"task_ids": ids, "due_at": due_at, "tag_ids": [tag["id"]]},
)
assert response.status_code == 200 and response.json()["updated"] == 2
assert all(client.get(f"/api/v1/tasks/{task_id}").json()["tags"] for task_id in ids)
parent = client.post("/api/v1/tasks", json={"title": "", "list_id": inbox["id"]}).json()
client.post(
"/api/v1/tasks",
json={"title": "", "list_id": inbox["id"], "parent_id": parent["id"]},
)
other = client.post("/api/v1/lists", json={"name": "批量目标"}).json()
assert client.post(
"/api/v1/tasks/batch", json={"task_ids": [parent["id"]], "list_id": other["id"]}
).status_code == 200
moved = client.get(f"/api/v1/tasks/{parent['id']}").json()
assert moved["subtasks"][0]["list_id"] == other["id"]
failed = client.post(
"/api/v1/tasks/batch",
json={"task_ids": [ids[0], "00000000-0000-0000-0000-000000000001"], "completed": True},
)
assert failed.status_code == 404
assert client.get(f"/api/v1/tasks/{ids[0]}").json()["completed"] is False
assert client.post("/api/v1/tasks/batch", json={"task_ids": ids, "soft_delete": True}).status_code == 200
assert len(client.get("/api/v1/trash").json()["items"]) == 2
def test_cursor_pagination_is_stable_and_rejects_bad_cursor(client):
client = initialized_client(client)
inbox = client.get("/api/v1/lists").json()[0]
for i in range(5):
client.post("/api/v1/tasks", json={"title": f"任务{i}", "list_id": inbox["id"]})
first = client.get("/api/v1/tasks", params={"limit": 2}).json()
second = client.get(
"/api/v1/tasks", params={"limit": 2, "cursor": first["next_cursor"]}
).json()
third = client.get(
"/api/v1/tasks", params={"limit": 2, "cursor": second["next_cursor"]}
).json()
ids = [row["id"] for page in (first, second, third) for row in page["items"]]
assert len(ids) == len(set(ids)) == 5
assert third["next_cursor"] is None
assert client.get("/api/v1/tasks", params={"cursor": "broken"}).status_code == 422