import os import tempfile os.environ["DB_PATH"] = os.path.join(tempfile.mkdtemp(), "test.db") os.environ["APP_PASSWORD"] = "test-password" os.environ["SECRET_KEY"] = "test-secret" import pytest # noqa: E402 from fastapi.testclient import TestClient # noqa: E402 from app import main # noqa: E402 H = {"X-WhatToDo": "1"} @pytest.fixture(scope="module") def client(): with TestClient(main.app) as c: assert c.post("/api/login", json={"password": "test-password"}, headers=H).status_code == 200 yield c def test_requires_login(): with TestClient(main.app) as anon: assert anon.get("/api/tasks").status_code == 401 assert anon.get("/api/health").status_code == 200 assert anon.get("/").status_code == 200 # static shell is public def test_wrong_password(): with TestClient(main.app) as anon: assert anon.post("/api/login", json={"password": "nope"}, headers=H).status_code == 401 def test_write_requires_header(client): assert client.post("/api/tasks", json={"title": "x"}).status_code == 403 def test_default_categories(client): names = [c["name"] for c in client.get("/api/categories").json()] assert names[:4] == ["Work", "Life", "Personal", "YouTube"] def test_task_crud(client): cat = client.get("/api/categories").json()[0] r = client.post("/api/tasks", headers=H, json={ "title": " Write script ", "category_id": cat["id"], "priority": 3, "due_date": "2026-10-10", "tags": ["#Video", "video", " edit "], "subtasks": [{"title": "outline"}, {"title": "draft"}], }) assert r.status_code == 201, r.text t = r.json() assert t["title"] == "Write script" assert t["tags"] == ["video", "edit"] assert [s["title"] for s in t["subtasks"]] == ["outline", "draft"] r = client.patch(f"/api/tasks/{t['id']}", headers=H, json={ "due_date": None, "subtasks": [{"title": "outline", "done": True}], }) t2 = r.json() assert t2["due_date"] is None and t2["priority"] == 3 assert t2["subtasks"] == [{"id": t2["subtasks"][0]["id"], "title": "outline", "done": True}] assert client.post("/api/tasks", headers=H, json={"title": " "}).status_code == 422 assert client.post("/api/tasks", headers=H, json={"title": "x", "category_id": 9999}).status_code == 400 assert client.delete(f"/api/tasks/{t['id']}", headers=H).status_code == 204 assert client.patch(f"/api/tasks/{t['id']}", headers=H, json={"title": "x"}).status_code == 404 def test_recurring_completion(client): t = client.post("/api/tasks", headers=H, json={ "title": "Upload video", "due_date": "2026-10-02", "recurrence": "weekly", "subtasks": [{"title": "thumbnail", "done": True}], }).json() r = client.post(f"/api/tasks/{t['id']}/complete", headers=H, json={"today": "2026-10-07"}).json() assert r["task"]["done"] is True # Oct 2 + 1 week = Oct 9, which is after "today" (Oct 7). assert r["next"]["due_date"] == "2026-10-09" assert r["next"]["done"] is False assert r["next"]["subtasks"][0]["done"] is False reopened = client.post(f"/api/tasks/{t['id']}/reopen", headers=H).json() assert reopened["done"] is False def test_next_due_skips_past_and_clamps_month(): from datetime import date assert main.next_due(date(2026, 9, 1), "daily", 1, date(2026, 10, 7)) == date(2026, 10, 8) assert main.next_due(date(2026, 1, 31), "monthly", 1, date(2026, 1, 31)) == date(2026, 2, 28) assert main.next_due(None, "yearly", 1, date(2026, 10, 7)) == date(2027, 10, 7) def test_category_crud(client): c = client.post("/api/categories", headers=H, json={"name": "Gym", "color": "#123456"}).json() assert client.post("/api/categories", headers=H, json={"name": "gym", "color": "#123456"}).status_code == 409 t = client.post("/api/tasks", headers=H, json={"title": "Leg day", "category_id": c["id"]}).json() assert client.patch(f"/api/categories/{c['id']}", headers=H, json={"name": "Fitness"}).json()["name"] == "Fitness" client.delete(f"/api/categories/{c['id']}", headers=H) tasks = {x["id"]: x for x in client.get("/api/tasks").json()} assert tasks[t["id"]]["category_id"] is None def test_export_import_roundtrip(client): client.post("/api/tasks", headers=H, json={"title": "Export me", "category_id": 1, "tags": ["a"]}) exported = client.get("/api/export") assert "attachment" in exported.headers["content-disposition"] data = exported.json() n = len(data["tasks"]) r = client.post("/api/import?mode=merge", headers=H, json=data).json() assert r["tasks"] == n assert len(client.get("/api/tasks").json()) == 2 * n assert len(client.get("/api/categories").json()) == len(data["categories"]) # matched by name client.post("/api/import?mode=replace", headers=H, json=data) after = client.get("/api/tasks").json() assert len(after) == n cats = {c["id"]: c["name"] for c in client.get("/api/categories").json()} exported_task = next(t for t in after if t["title"] == "Export me") assert cats[exported_task["category_id"]] == "Work" assert client.post("/api/import", headers=H, json={"app": "other", "version": 1}).status_code == 400