Files
duty-teller/tests/test_calendar_ics.py
Nikolay Tatarinov aa89494bd5
All checks were successful
CI / lint-and-test (push) Successful in 22s
feat: enhance calendar ICS generation with event type filtering
- Added support for filtering calendar events by type in the ICS generation API endpoint, allowing users to specify whether to include only duty shifts or all event types (duty, unavailable, vacation).
- Updated the `get_duties_for_user` function to accept an optional `event_types` parameter, enabling more flexible data retrieval based on user preferences.
- Enhanced unit tests to cover the new event type filtering functionality, ensuring correct behavior and reliability of the ICS generation process.
2026-02-20 17:47:52 +03:00

163 lines
5.4 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")