"""calendar_feed.py's fetch/parse/merge against a local HTTP server serving fixture .ics text -- pure functions, no ORM/FastAPI.""" from __future__ import annotations import threading from datetime import date from http.server import BaseHTTPRequestHandler, HTTPServer import pytest from app.calendar_feed import CalendarSource, merge_events _PLAIN_ICS = b"""BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT UID:plain-1@example.com SUMMARY:Dentist DTSTART:20260801T140000Z DTEND:20260801T150000Z END:VEVENT END:VCALENDAR """ _RECURRING_ICS = b"""BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT UID:weekly-1@example.com SUMMARY:Standup DTSTART:20260803T090000Z DTEND:20260803T091500Z RRULE:FREQ=WEEKLY;COUNT=4 END:VEVENT END:VCALENDAR """ _SHARED_EVENT_ICS_A = b"""BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT UID:shared-a@example.com SUMMARY:Family Dinner DTSTART:20260805T230000Z DTEND:20260806T010000Z END:VEVENT END:VCALENDAR """ _SHARED_EVENT_ICS_B = b"""BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT UID:shared-b@example.com SUMMARY:Family Dinner DTSTART:20260805T230000Z DTEND:20260806T010000Z END:VEVENT END:VCALENDAR """ class _Handler(BaseHTTPRequestHandler): def do_GET(self): body = self.server.feeds.get(self.path) # type: ignore[attr-defined] if body is None: self.send_response(404) self.end_headers() return self.send_response(200) self.send_header("Content-Type", "text/calendar") self.end_headers() self.wfile.write(body) def log_message(self, *args): pass @pytest.fixture def ics_server(): server = HTTPServer(("127.0.0.1", 0), _Handler) server.feeds = { "/plain.ics": _PLAIN_ICS, "/recurring.ics": _RECURRING_ICS, "/shared-a.ics": _SHARED_EVENT_ICS_A, "/shared-b.ics": _SHARED_EVENT_ICS_B, } thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() port = server.server_address[1] try: yield f"http://127.0.0.1:{port}" finally: server.shutdown() thread.join() _WINDOW_START = date(2026, 7, 1) _WINDOW_END = date(2026, 9, 1) def test_single_source_fetches_its_event(ics_server): sources = [CalendarSource("Alice", "ics", f"{ics_server}/plain.ics")] events, summary = merge_events(sources, _WINDOW_START, _WINDOW_END) assert summary == "" assert len(events) == 1 assert events[0]["summary"] == "Dentist" assert events[0]["sources"] == [{"owner_display_name": "Alice", "color_index": None}] def test_recurring_event_expands_within_window(ics_server): sources = [CalendarSource("Bob", "ics", f"{ics_server}/recurring.ics")] events, summary = merge_events(sources, _WINDOW_START, _WINDOW_END) assert summary == "" assert len(events) == 4 # COUNT=4 assert all(e["summary"] == "Standup" for e in events) # distinct occurrences, not the same one repeated assert len({e["start"] for e in events}) == 4 def test_unreachable_source_does_not_blank_others(ics_server): sources = [ CalendarSource("Alice", "ics", f"{ics_server}/plain.ics"), CalendarSource("Bob", "ics", f"{ics_server}/does-not-exist.ics"), ] events, summary = merge_events(sources, _WINDOW_START, _WINDOW_END) assert len(events) == 1 assert events[0]["summary"] == "Dentist" assert summary == "1 of 2 calendars unavailable" def test_all_sources_unreachable(ics_server): sources = [ CalendarSource("Alice", "ics", f"{ics_server}/nope1.ics"), CalendarSource("Bob", "ics", f"{ics_server}/nope2.ics"), ] events, summary = merge_events(sources, _WINDOW_START, _WINDOW_END) assert events == [] assert summary == "2 of 2 calendars unavailable" def test_duplicate_event_across_calendars_collapses_with_both_sources(ics_server): """A shared event synced onto two people's calendars (same summary/ start/end/all_day) should show up once, but carry both owners in its `sources` list -- see merge_events' own docstring.""" sources = [ CalendarSource("Alice", "ics", f"{ics_server}/shared-a.ics"), CalendarSource("Bob", "ics", f"{ics_server}/shared-b.ics"), ] events, summary = merge_events(sources, _WINDOW_START, _WINDOW_END) assert summary == "" assert len(events) == 1 owners = {s["owner_display_name"] for s in events[0]["sources"]} assert owners == {"Alice", "Bob"} def test_events_outside_window_are_excluded(ics_server): sources = [CalendarSource("Alice", "ics", f"{ics_server}/plain.ics")] events, _ = merge_events(sources, date(2020, 1, 1), date(2020, 2, 1)) assert events == []