"""caldav_client.merge_tasks -- pure-function coverage (sort, per-source color/owner tagging, partial-failure handling), monkeypatching fetch_tasks itself rather than the caldav package's DAVClient/Calendar -- this project has no CalDAV test server fixture (fetch_calendar_events' own caldav branch is likewise only exercised this way, never against a real server -- see test_calendar_feed.py, which only covers the ICS path). fetch_tasks' own VTODO-parsing internals are trusted to the icalendar library, same posture calendar_feed.py already takes for VEVENT parsing.""" from __future__ import annotations from datetime import datetime, timezone from app.caldav_client import CalDavError, TaskSource, merge_tasks def test_single_source_tasks_pass_through_tagged(monkeypatch): monkeypatch.setattr( "app.caldav_client.fetch_tasks", lambda url, username, password, completed_since=None: [ {"summary": "Buy milk", "due": "2026-08-01", "completed_at": None}, ], ) sources = [TaskSource("Alice", "https://example.com/tasks", "alice", "pw", color_index=3)] tasks, summary = merge_tasks(sources) assert summary == "" assert tasks == [ {"summary": "Buy milk", "due": "2026-08-01", "completed_at": None, "owner_display_name": "Alice", "color_index": 3}, ] def test_outstanding_tasks_sort_by_due_date_none_last(monkeypatch): def fake_fetch(url, username, password, completed_since=None): by_url = { "a": [{"summary": "No due date", "due": None, "completed_at": None}], "b": [{"summary": "Due soonest", "due": "2026-08-01", "completed_at": None}], } return by_url[url] monkeypatch.setattr("app.caldav_client.fetch_tasks", fake_fetch) sources = [TaskSource("Alice", "a", "alice", "pw"), TaskSource("Bob", "b", "bob", "pw")] tasks, _ = merge_tasks(sources) assert [t["summary"] for t in tasks] == ["Due soonest", "No due date"] def test_completed_tasks_sort_after_outstanding_most_recent_first(monkeypatch): def fake_fetch(url, username, password, completed_since=None): return [ {"summary": "Outstanding", "due": "2026-08-05", "completed_at": None}, {"summary": "Done yesterday", "due": None, "completed_at": "2026-08-01T09:00:00+00:00"}, {"summary": "Done today", "due": None, "completed_at": "2026-08-02T09:00:00+00:00"}, ] monkeypatch.setattr("app.caldav_client.fetch_tasks", fake_fetch) tasks, _ = merge_tasks([TaskSource("Alice", "a", "alice", "pw")]) assert [t["summary"] for t in tasks] == ["Outstanding", "Done today", "Done yesterday"] def test_one_broken_source_does_not_blank_others(monkeypatch): def fake_fetch(url, username, password, completed_since=None): if url == "broken": raise CalDavError("nope") return [{"summary": "Buy milk", "due": None, "completed_at": None}] monkeypatch.setattr("app.caldav_client.fetch_tasks", fake_fetch) sources = [TaskSource("Alice", "ok", "alice", "pw"), TaskSource("Bob", "broken", "bob", "pw")] tasks, summary = merge_tasks(sources) assert len(tasks) == 1 assert summary == "1 of 2 task lists unavailable" def test_all_sources_unreachable(monkeypatch): monkeypatch.setattr( "app.caldav_client.fetch_tasks", lambda url, username, password, completed_since=None: (_ for _ in ()).throw(CalDavError("nope")), ) sources = [TaskSource("Alice", "a", "alice", "pw"), TaskSource("Bob", "b", "bob", "pw")] tasks, summary = merge_tasks(sources) assert tasks == [] assert summary == "2 of 2 task lists unavailable" def test_completed_since_is_forwarded_to_fetch_tasks(monkeypatch): seen = [] def fake_fetch(url, username, password, completed_since=None): seen.append(completed_since) return [] monkeypatch.setattr("app.caldav_client.fetch_tasks", fake_fetch) cutoff = datetime(2026, 8, 1, tzinfo=timezone.utc) merge_tasks([TaskSource("Alice", "a", "alice", "pw")], completed_since=cutoff) assert seen == [cutoff]