120 lines
5.3 KiB
Python
120 lines
5.3 KiB
Python
from datetime import date
|
|
|
|
from backend.mvp import countdown_occurrence, countdown_status
|
|
|
|
|
|
def boot(client):
|
|
response = client.post(
|
|
"/api/v1/setup/initialize",
|
|
json={"username": "owner", "password": "correct horse battery staple"},
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
|
|
def create_countdown(client, **overrides):
|
|
payload = {
|
|
"title": "旅行",
|
|
"event_date": "2026-09-10",
|
|
"kind": "countdown",
|
|
"repeat_rule": "none",
|
|
"icon": "🧳",
|
|
}
|
|
payload.update(overrides)
|
|
return client.post("/api/v1/countdowns", json=payload)
|
|
|
|
|
|
def test_countdown_date_math_uses_date_only_recurrence_semantics():
|
|
assert countdown_status(date(2026, 9, 10), date(2026, 9, 6)) == (4, "还有 4 天")
|
|
assert countdown_status(date(2026, 9, 6), date(2026, 9, 6)) == (0, "就是今天")
|
|
assert countdown_status(date(2026, 9, 1), date(2026, 9, 6)) == (-5, "已经 5 天")
|
|
assert countdown_occurrence(date(2026, 8, 31), "monthly", date(2026, 9, 6)) == date(2026, 9, 30)
|
|
assert countdown_occurrence(date(2024, 2, 29), "yearly", date(2026, 2, 1)) == date(2026, 2, 28)
|
|
assert countdown_occurrence(date(2026, 9, 1), "weekly", date(2026, 9, 8)) == date(2026, 9, 8)
|
|
|
|
|
|
def test_countdown_crud_single_pin_archive_restore_and_purge(client):
|
|
boot(client)
|
|
first = create_countdown(client).json()
|
|
second = create_countdown(
|
|
client,
|
|
title="生日",
|
|
event_date="1990-02-28",
|
|
kind="birthday",
|
|
repeat_rule="yearly",
|
|
icon="🎂",
|
|
).json()
|
|
|
|
assert client.post(f"/api/v1/countdowns/{first['id']}/pin").status_code == 200
|
|
assert client.post(f"/api/v1/countdowns/{second['id']}/pin").status_code == 200
|
|
active = client.get("/api/v1/countdowns").json()
|
|
assert [item["title"] for item in active] == ["生日", "旅行"]
|
|
assert [item["pinned"] for item in active] == [True, False]
|
|
|
|
updated = client.patch(
|
|
f"/api/v1/countdowns/{first['id']}",
|
|
json={"title": "海边旅行", "event_date": "2026-09-12", "kind": "anniversary", "repeat_rule": "none", "icon": "🌊"},
|
|
)
|
|
assert updated.status_code == 200
|
|
assert updated.json()["title"] == "海边旅行"
|
|
|
|
assert client.delete(f"/api/v1/countdowns/{first['id']}").status_code == 204
|
|
assert [item["id"] for item in client.get("/api/v1/countdowns", params={"archived": True}).json()] == [first["id"]]
|
|
assert client.delete(f"/api/v1/countdowns/{second['id']}/purge").status_code == 409
|
|
assert client.post(f"/api/v1/countdowns/{first['id']}/restore").status_code == 200
|
|
assert client.delete(f"/api/v1/countdowns/{first['id']}").status_code == 204
|
|
assert client.delete(f"/api/v1/countdowns/{first['id']}/purge").status_code == 204
|
|
|
|
|
|
def test_countdown_validation_and_user_isolation(client):
|
|
boot(client)
|
|
item = create_countdown(client).json()
|
|
assert create_countdown(client, kind="festival").status_code == 422
|
|
assert create_countdown(client, repeat_rule="daily").status_code == 422
|
|
|
|
client.post("/api/v1/auth/logout")
|
|
from backend.auth import hash_password
|
|
from backend.db import get_db
|
|
from backend.models import TaskList, User
|
|
|
|
async def add_other_user():
|
|
db_gen = get_db()
|
|
db = await anext(db_gen)
|
|
try:
|
|
other = User(username="other", password_hash=hash_password("correct horse battery staple"))
|
|
db.add(other)
|
|
await db.flush()
|
|
db.add(TaskList(user_id=other.id, name="收集箱", is_inbox=True))
|
|
await db.commit()
|
|
finally:
|
|
await db_gen.aclose()
|
|
|
|
client.portal.call(add_other_user)
|
|
assert client.post("/api/v1/auth/login", json={"username": "other", "password": "correct horse battery staple"}).status_code == 200
|
|
assert client.get("/api/v1/countdowns").json() == []
|
|
assert client.patch(f"/api/v1/countdowns/{item['id']}", json={"title": "偷改"}).status_code == 404
|
|
|
|
|
|
def test_countdowns_backup_replace_and_merge_round_trip(client):
|
|
boot(client)
|
|
active = create_countdown(client, title="周年", kind="anniversary", repeat_rule="yearly", pinned=True).json()
|
|
archived = create_countdown(client, title="旧日", event_date="2020-01-02").json()
|
|
client.delete(f"/api/v1/countdowns/{archived['id']}")
|
|
|
|
exported = client.get("/api/v1/export").json()
|
|
assert {item["title"] for item in exported["countdowns"]} == {"周年", "旧日"}
|
|
assert next(item for item in exported["countdowns"] if item["title"] == "周年")["pinned"] is True
|
|
assert next(item for item in exported["countdowns"] if item["title"] == "旧日")["archived_at"]
|
|
|
|
create_countdown(client, title="干扰数据")
|
|
restored = client.post("/api/v1/restore", params={"mode": "replace"}, json=exported)
|
|
assert restored.status_code == 200
|
|
assert {item["title"] for item in client.get("/api/v1/countdowns").json()} == {"周年"}
|
|
assert {item["title"] for item in client.get("/api/v1/countdowns", params={"archived": True}).json()} == {"旧日"}
|
|
|
|
merged = client.post("/api/v1/restore", params={"mode": "merge"}, json=exported)
|
|
assert merged.status_code == 200
|
|
all_active = client.get("/api/v1/countdowns").json()
|
|
assert [item["title"] for item in all_active].count("周年") == 1
|
|
assert sum(item["pinned"] for item in all_active) == 1
|
|
assert active["title"] == "周年"
|