All checks were successful
CI / lint-and-test (push) Successful in 23s
- Introduced a new configuration option `DUTY_PIN_NOTIFY` to control whether the bot re-pins the duty message when updated, providing notifications to group members. - Updated the architecture documentation to reflect the new functionality of re-pinning duty messages. - Enhanced the `.env.example` file to include the new configuration option with a description. - Added tests to verify the behavior of the new refresh pin command and its integration with the existing group duty pin functionality. - Updated internationalization messages to include help text for the new `/refresh_pin` command.
110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""Unit tests for personal calendar ICS generation."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from icalendar import Calendar as ICalendar
|
|
|
|
from duty_teller.api.personal_calendar_ics import build_personal_ics, build_team_ics
|
|
|
|
|
|
# Minimal Duty-like object for build_personal_ics(duties_with_name: list[tuple[Duty, str]])
|
|
def _duty(id_, start_at, end_at, event_type="duty"):
|
|
return (
|
|
SimpleNamespace(
|
|
id=id_, start_at=start_at, end_at=end_at, event_type=event_type
|
|
),
|
|
"Test User",
|
|
)
|
|
|
|
|
|
def test_build_personal_ics_empty_returns_valid_calendar():
|
|
"""Empty list produces VCALENDAR with no VEVENTs."""
|
|
ics = build_personal_ics([])
|
|
assert ics.startswith(b"BEGIN:VCALENDAR")
|
|
assert ics.rstrip().endswith(b"END:VCALENDAR")
|
|
cal = ICalendar.from_ical(ics)
|
|
assert cal is not None
|
|
events = [c for c in cal.walk() if c.name == "VEVENT"]
|
|
assert len(events) == 0
|
|
|
|
|
|
def test_build_personal_ics_one_duty():
|
|
"""One duty produces one VEVENT with correct DTSTART/DTEND/SUMMARY."""
|
|
duties = [
|
|
_duty(1, "2025-02-20T09:00:00Z", "2025-02-20T18:00:00Z", "duty"),
|
|
]
|
|
ics = build_personal_ics(duties)
|
|
cal = ICalendar.from_ical(ics)
|
|
assert cal is not None
|
|
events = [c for c in cal.walk() if c.name == "VEVENT"]
|
|
assert len(events) == 1
|
|
ev = events[0]
|
|
assert ev.get("summary") == "Duty"
|
|
assert "2025-02-20" in str(ev.get("dtstart").dt)
|
|
assert "2025-02-20" in str(ev.get("dtend").dt)
|
|
assert b"duty-1@duty-teller" in ics or "duty-1@duty-teller" in ics.decode("utf-8")
|
|
|
|
|
|
def test_build_team_ics_vevent_has_description_with_full_name():
|
|
"""Team ICS VEVENT has SUMMARY=full_name and DESCRIPTION=full_name."""
|
|
duties = [
|
|
_duty(1, "2025-02-20T09:00:00Z", "2025-02-20T18:00:00Z", "duty"),
|
|
]
|
|
ics = build_team_ics(duties)
|
|
cal = ICalendar.from_ical(ics)
|
|
assert cal is not None
|
|
events = [c for c in cal.walk() if c.name == "VEVENT"]
|
|
assert len(events) == 1
|
|
ev = events[0]
|
|
assert ev.get("summary") == "Test User"
|
|
assert ev.get("description") == "Test User"
|
|
assert b"Team Calendar" in ics or "Team Calendar" in ics.decode("utf-8")
|
|
|
|
|
|
def test_build_team_ics_empty_full_name_summary_fallback():
|
|
"""When full_name is empty or missing, SUMMARY is 'Duty'."""
|
|
duty_ns = SimpleNamespace(
|
|
id=1,
|
|
start_at="2025-02-20T09:00:00Z",
|
|
end_at="2025-02-20T18:00:00Z",
|
|
event_type="duty",
|
|
)
|
|
for empty_name in ("", " ", None):
|
|
duties = [(duty_ns, empty_name)]
|
|
ics = build_team_ics(duties)
|
|
cal = ICalendar.from_ical(ics)
|
|
events = [c for c in cal.walk() if c.name == "VEVENT"]
|
|
assert len(events) == 1
|
|
assert events[0].get("summary") == "Duty"
|
|
|
|
|
|
def test_build_personal_ics_event_types():
|
|
"""Unavailable and vacation get correct SUMMARY."""
|
|
duties = [
|
|
_duty(1, "2025-02-20T09:00:00Z", "2025-02-21T09:00:00Z", "unavailable"),
|
|
_duty(2, "2025-02-25T00:00:00Z", "2025-03-01T00:00:00Z", "vacation"),
|
|
]
|
|
ics = build_personal_ics(duties)
|
|
cal = ICalendar.from_ical(ics)
|
|
events = [c for c in cal.walk() if c.name == "VEVENT"]
|
|
assert len(events) == 2
|
|
summaries = [str(ev.get("summary")) for ev in events]
|
|
assert "Unavailable" in summaries
|
|
assert "Vacation" in summaries
|
|
|
|
|
|
def test_build_personal_ics_naive_datetime_treated_as_utc():
|
|
"""Duties with start_at/end_at without Z (naive datetime) are treated as UTC."""
|
|
duties = [
|
|
_duty(1, "2025-02-20T09:00:00", "2025-02-20T18:00:00", "duty"),
|
|
]
|
|
ics = build_personal_ics(duties)
|
|
cal = ICalendar.from_ical(ics)
|
|
assert cal is not None
|
|
events = [c for c in cal.walk() if c.name == "VEVENT"]
|
|
assert len(events) == 1
|
|
ev = events[0]
|
|
assert ev.get("summary") == "Duty"
|
|
assert "2025-02-20" in str(ev.get("dtstart").dt)
|
|
assert "2025-02-20" in str(ev.get("dtend").dt)
|