"""Tests for duty_teller.api.calendar_ics (_to_date, _event_date_range, _get_events_from_ics, get_calendar_events).""" from datetime import date, datetime from unittest.mock import patch import pytest from icalendar import Calendar, Event from duty_teller.api import calendar_ics as mod class TestToDate: """Tests for _to_date.""" def test_datetime_returns_date(self): dt = datetime(2025, 3, 15, 10, 0, 0) assert mod._to_date(dt) == date(2025, 3, 15) def test_date_returns_same(self): d = date(2025, 3, 15) assert mod._to_date(d) == date(2025, 3, 15) def test_invalid_returns_none(self): assert mod._to_date("2025-03-15") is None assert mod._to_date(None) is None assert mod._to_date(123) is None class TestEventDateRange: """Tests for _event_date_range. DTEND is exclusive in iCalendar.""" def _vevent(self, dtstart_value, dtend_value=None): """Build a minimal VEVENT component using icalendar.Event.""" cal = Calendar() ev = Event() ev.add("dtstart", dtstart_value) if dtend_value is not None: ev.add("dtend", dtend_value) cal.add_component(ev) for c in cal.walk(): if c.name == "VEVENT": return c raise AssertionError("no vevent") def test_dtstart_and_dtend_exclusive_end(self): start = date(2025, 1, 10) end_exclusive = date(2025, 1, 13) # last day of event = 2025-01-12 comp = self._vevent(start, end_exclusive) s, e = mod._event_date_range(comp) assert s == date(2025, 1, 10) assert e == date(2025, 1, 12) def test_only_dtstart_returns_same_for_start_and_end(self): start = date(2025, 2, 1) comp = self._vevent(start) s, e = mod._event_date_range(comp) assert s == date(2025, 2, 1) assert e == date(2025, 2, 1) def test_no_dtstart_returns_none_none(self): cal = Calendar() ev = Event() ev.add("summary", "No dates") cal.add_component(ev) for c in cal.walk(): if c.name == "VEVENT": s, e = mod._event_date_range(c) assert s is None assert e is None return pytest.fail("no vevent") class TestGetEventsFromIcs: """Tests for _get_events_from_ics.""" def test_minimal_ics_one_event(self): cal = Calendar() ev = Event() ev.add("dtstart", date(2025, 1, 15)) ev.add("summary", "Meeting") cal.add_component(ev) raw = cal.to_ical() result = mod._get_events_from_ics(raw, "2025-01-01", "2025-01-31") assert len(result) == 1 assert result[0]["date"] == "2025-01-15" assert result[0]["summary"] == "Meeting" def test_event_filtered_by_date_range(self): cal = Calendar() ev = Event() ev.add("dtstart", date(2025, 1, 10)) ev.add("dtend", date(2025, 1, 14)) # 10,11,12,13 ev.add("summary", "Multi") cal.add_component(ev) raw = cal.to_ical() result = mod._get_events_from_ics(raw, "2025-01-12", "2025-01-13") assert len(result) == 2 assert result[0]["date"] == "2025-01-12" assert result[1]["date"] == "2025-01-13" def test_empty_ics_returns_empty_list(self): cal = Calendar() raw = cal.to_ical() result = mod._get_events_from_ics(raw, "2025-01-01", "2025-01-31") assert result == [] def test_broken_ics_returns_empty_list(self): result = mod._get_events_from_ics( b"not ical at all", "2025-01-01", "2025-01-31" ) assert result == [] def test_recurring_events_skipped(self): cal = Calendar() ev = Event() ev.add("dtstart", date(2025, 1, 15)) ev.add("rrule", {"freq": "daily", "count": 3}) ev.add("summary", "Recurring") cal.add_component(ev) raw = cal.to_ical() result = mod._get_events_from_ics(raw, "2025-01-01", "2025-01-31") assert result == [] class TestGetCalendarEvents: """Tests for get_calendar_events (with mocked _fetch_ics, no real HTTP).""" def test_empty_url_returns_empty(self): assert mod.get_calendar_events("", "2025-01-01", "2025-01-31") == [] def test_disallowed_url_scheme_returns_empty(self): """get_calendar_events: file:// or ftp:// URL does not call urlopen, returns [].""" result = mod.get_calendar_events( "file:///etc/passwd", "2025-01-01", "2025-01-31" ) assert result == [] result = mod.get_calendar_events( "ftp://example.com/cal.ics", "2025-01-01", "2025-01-31" ) assert result == [] def test_from_after_to_returns_empty(self): assert ( mod.get_calendar_events( "https://example.com/a.ics", "2025-02-01", "2025-01-01" ) == [] ) @patch.object(mod, "_fetch_ics", return_value=None) def test_fetch_returns_none_returns_empty(self, mock_fetch): result = mod.get_calendar_events( "https://example.com/a.ics", "2025-01-01", "2025-01-31" ) assert result == [] mock_fetch.assert_called_once() @patch.object(mod, "_fetch_ics") def test_success_returns_events(self, mock_fetch): cal = Calendar() ev = Event() ev.add("dtstart", date(2025, 1, 20)) ev.add("summary", "Test event") cal.add_component(ev) mock_fetch.return_value = cal.to_ical() result = mod.get_calendar_events( "https://example.com/cal.ics", "2025-01-01", "2025-01-31" ) assert len(result) == 1 assert result[0]["date"] == "2025-01-20" assert result[0]["summary"] == "Test event" mock_fetch.assert_called_once_with("https://example.com/cal.ics")