Some checks failed
CI / lint-and-test (push) Failing after 11s
- Upgraded `pytest-asyncio` to version 1.0 to ensure compatibility with the latest features and improvements. - Increased the coverage threshold in pytest configuration to 80%, enhancing the quality assurance process. - Added a new `conftest.py` file to manage shared fixtures and improve test organization. - Introduced multiple new test files to cover various components, ensuring comprehensive test coverage across the application. - Updated the `.coverage` file to reflect the latest coverage metrics.
154 lines
5.3 KiB
Python
154 lines
5.3 KiB
Python
"""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_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")
|