"""The HTTP surface, driven the way the clients drive it.""" from __future__ import annotations def test_health_and_info(client): assert client.get("/api/health").json()["status"] == "ok" assert "auth_required" in client.get("/api/info").json() def test_a_whole_pomodoro_through_the_api(client): client.put("/api/settings", json={"pomodoro": {"daily_goal_pomodoros": 1}}) task = client.post("/api/tasks", json={"title": "Write the API"}).json() state = client.post("/api/timer/start", json={"kind": "work", "task_id": task["id"]}).json() assert state["current"]["kind"] == "work" assert state["current"]["task"]["title"] == "Write the API" assert client.post("/api/timer/pause").json()["current"]["status"] == "paused" assert client.post("/api/timer/resume").json()["current"]["status"] == "running" assert client.post("/api/timer/extend", json={"minutes": 5}).json()["current"]["planned_seconds"] > 0 done = client.post("/api/timer/complete", json={"auto_next": False}).json() assert done["finished"]["status"] == "completed" assert done["completed_today"] == 1 report = client.get("/api/reports/daily").json() assert report["report"]["metrics"]["pomodoros"] == 1 assert "Daily report" in report["telegram_preview"] def test_timer_errors_are_400_not_500(client): assert client.post("/api/timer/pause").status_code == 400 assert client.post("/api/timer/complete", json={}).status_code == 400 def test_tasks_cannot_be_ticked_off_here(client): """Completion belongs to Jira / Todoist / Google — we only notice it. 405 rather than 404 because the SPA catch-all claims unknown paths for GET; either way there is no way to close a task through the API. """ task = client.post("/api/tasks", json={"title": "Not yours to close"}).json() response = client.post(f"/api/tasks/{task['id']}/done", json={"done": True}) assert response.status_code in (404, 405) assert client.get("/api/tasks").json()["tasks"][0]["is_done"] is False def test_task_crud(client): created = client.post("/api/tasks", json={"title": "Temporary", "priority": "p2"}).json() listing = client.get("/api/tasks").json() assert any(task["id"] == created["id"] for task in listing["tasks"]) patched = client.patch(f"/api/tasks/{created['id']}", json={"pinned": True}).json() assert patched["pinned"] is True client.delete(f"/api/tasks/{created['id']}") assert all(task["id"] != created["id"] for task in client.get("/api/tasks").json()["tasks"]) def test_refresh_reports_per_source_failures_without_failing(client): client.put( "/api/settings", json={"sources": {"todoist": {"enabled": True, "api_token": "definitely-invalid"}}}, ) result = client.post("/api/tasks/refresh").json() assert result["sources"], "the enabled source should be reported" assert result["sources"][0]["ok"] is False assert client.get("/api/tasks").status_code == 200 def test_blocker_policy_and_events(client): client.put( "/api/settings", json={"blocker": {"enabled": True, "blocked_domains": ["youtube.com"]}}, ) assert client.get("/api/blocker/policy").json()["active"] is False client.post("/api/timer/start", json={"kind": "work"}) policy = client.get("/api/blocker/policy").json() assert policy["active"] is True assert "youtube.com" in policy["domains"] client.post("/api/blocker/events", json={"kind": "domain", "target": "youtube.com"}) events = client.get("/api/blocker/events").json() assert events["total"] == 1 def test_settings_round_trip_masks_secrets(client): client.put("/api/settings", json={"sources": {"todoist": {"api_token": "s3cret"}}}) body = client.get("/api/settings").json() assert body["settings"]["sources"]["todoist"]["api_token"] == "••••••••" assert body["defaults"]["blocked_domains"] # Posting the mask back must not wipe the token. client.put("/api/settings", json=body["settings"]) again = client.get("/api/settings").json() assert again["settings"]["sources"]["todoist"]["api_token"] == "••••••••" def test_invalid_settings_are_rejected(client): assert client.put("/api/settings", json={"pomodoro": {"work_minutes": 0}}).status_code == 422 assert client.put("/api/settings", json={"timezone": "Mars/Olympus"}).status_code == 422 def test_notification_test_endpoint_falls_back_to_the_log(client): body = client.post("/api/notifications/test", json={}).json() assert body["deliveries"][0]["channel"] == "log"