Files
govoplan-calendar/tests/test_ical.py
2026-07-07 15:49:06 +02:00

238 lines
8.5 KiB
Python

from __future__ import annotations
import unittest
from datetime import datetime, timezone
from types import SimpleNamespace
from govoplan_calendar.backend.ical import expand_event_occurrences, event_to_ics, parse_vevent
class ICalendarParsingTests(unittest.TestCase):
def test_parse_vevent_preserves_unknown_properties_and_params(self) -> None:
payload = """BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event-1@example.test
DTSTAMP:20260707T080000Z
DTSTART;TZID=Europe/Berlin:20260708T090000
DTEND;TZID=Europe/Berlin:20260708T100000
SUMMARY:Planning
ATTENDEE;CN=Ada;ROLE=REQ-PARTICIPANT:mailto:ada@example.test
RRULE:FREQ=WEEKLY;COUNT=3
X-GOVOPLAN-CUSTOM:kept
END:VEVENT
END:VCALENDAR
"""
event = parse_vevent(payload)
self.assertEqual(event["uid"], "event-1@example.test")
self.assertEqual(event["summary"], "Planning")
self.assertEqual(event["timezone"], "Europe/Berlin")
self.assertEqual(event["rrule"], {"FREQ": "WEEKLY", "COUNT": "3"})
self.assertEqual(event["attendees"][0]["params"]["CN"], ["Ada"])
property_names = [item["name"] for item in event["icalendar"]["properties"]]
self.assertIn("X-GOVOPLAN-CUSTOM", property_names)
def test_parse_all_day_event(self) -> None:
event = parse_vevent(
"""BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event-2@example.test
DTSTART;VALUE=DATE:20260708
DTEND;VALUE=DATE:20260709
SUMMARY:All day
END:VEVENT
END:VCALENDAR
"""
)
self.assertTrue(event["all_day"])
self.assertEqual(event["start_at"].hour, 0)
def test_parse_advanced_vevent_fields_and_valarm(self) -> None:
event = parse_vevent(
"""BEGIN:VCALENDAR
VERSION:2.0
METHOD:REQUEST
BEGIN:VEVENT
UID:event-advanced@example.test
DTSTAMP:20260707T080000Z
CREATED:20260707T070000Z
LAST-MODIFIED:20260707T073000Z
DTSTART;TZID=Europe/Berlin:20260708T090000
DURATION:PT1H30M
SUMMARY:Planning
DESCRIPTION:Line one\\nLine two
LOCATION:Room\\; 1
PRIORITY:5
URL:https://example.test/events/event-advanced
GEO:52.5200;13.4050
COMMENT:First comment
RESOURCES:Projector,Room
REQUEST-STATUS:2.0;Success
ORGANIZER;CN=Orga:mailto:orga@example.test
ATTENDEE;CN=Ada;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED:mailto:ada@example.test
RRULE:FREQ=WEEKLY;COUNT=2
EXDATE;TZID=Europe/Berlin:20260715T090000
RDATE;TZID=Europe/Berlin:20260722T090000
ATTACH;FMTTYPE=application/pdf:https://example.test/a.pdf
RELATED-TO;RELTYPE=PARENT:parent@example.test
X-GOVOPLAN-CUSTOM:kept
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Reminder
TRIGGER:-PT15M
END:VALARM
END:VEVENT
END:VCALENDAR
"""
)
self.assertEqual(event["start_at"], datetime(2026, 7, 8, 7, 0, tzinfo=timezone.utc))
self.assertEqual(event["end_at"], datetime(2026, 7, 8, 8, 30, tzinfo=timezone.utc))
self.assertEqual(event["duration_seconds"], 5400)
self.assertEqual(event["description"], "Line one\nLine two")
self.assertEqual(event["location"], "Room; 1")
self.assertEqual(event["timezone"], "Europe/Berlin")
self.assertEqual(event["organizer"]["params"]["CN"], ["Orga"])
self.assertEqual(event["attendees"][0]["params"]["PARTSTAT"], ["ACCEPTED"])
self.assertEqual(event["icalendar"]["method"], "REQUEST")
self.assertEqual(event["icalendar"]["standard"]["priority"]["value"], "5")
self.assertEqual(event["icalendar"]["standard"]["url"]["value"], "https://example.test/events/event-advanced")
self.assertEqual(event["reminders"][0]["trigger"]["value"], "-PT15M")
self.assertEqual(event["reminders"][0]["trigger"]["decoded"], -900)
property_names = [item["name"] for item in event["icalendar"]["properties"]]
self.assertIn("REQUEST-STATUS", property_names)
self.assertIn("X-GOVOPLAN-CUSTOM", property_names)
def test_generated_ics_preserves_unmapped_properties_and_alarms(self) -> None:
event = SimpleNamespace(
uid="event-export@example.test",
start_at=datetime(2026, 7, 8, 7, 0, tzinfo=timezone.utc),
end_at=datetime(2026, 7, 8, 8, 0, tzinfo=timezone.utc),
duration_seconds=None,
all_day=False,
timezone="Europe/Berlin",
summary="Generated",
sequence=4,
status="CONFIRMED",
transparency="OPAQUE",
classification="PUBLIC",
description=None,
location=None,
categories=["GovOPlaN"],
rrule={"FREQ": "WEEKLY", "COUNT": "2"},
organizer={"value": "mailto:orga@example.test", "params": {"CN": ["Orga"]}},
attendees=[{"value": "mailto:ada@example.test", "params": {"CN": ["Ada"], "ROLE": ["REQ-PARTICIPANT"]}}],
rdate=[],
exdate=[],
reminders=[],
attachments=[{"name": "ATTACH", "value": "https://example.test/a.pdf", "params": {"FMTTYPE": ["application/pdf"]}}],
related_to=[{"name": "RELATED-TO", "value": "parent@example.test", "params": {"RELTYPE": ["PARENT"]}}],
icalendar={
"method": "PUBLISH",
"properties": [
{"name": "URL", "value": "https://example.test/events/event-export", "params": {}},
{"name": "X-GOVOPLAN-CUSTOM", "value": "kept", "params": {}},
{"name": "SUMMARY", "value": "Stale summary", "params": {}},
],
"alarms": [
{
"component": "VALARM",
"properties": [
{"name": "ACTION", "value": "DISPLAY", "params": {}},
{"name": "DESCRIPTION", "value": "Reminder", "params": {}},
{"name": "TRIGGER", "value": "-PT15M", "params": {}},
],
}
],
},
)
ics = event_to_ics(event)
self.assertIn("METHOD:PUBLISH", ics)
self.assertIn("DTSTART;TZID=Europe/Berlin:20260708T090000", ics)
self.assertIn("SUMMARY:Generated", ics)
self.assertNotIn("SUMMARY:Stale summary", ics)
self.assertIn("RRULE:FREQ=WEEKLY;COUNT=2", ics)
self.assertIn("ATTACH;FMTTYPE=application/pdf:https://example.test/a.pdf", ics)
self.assertIn("RELATED-TO;RELTYPE=PARENT:parent@example.test", ics)
self.assertIn("URL:https://example.test/events/event-export", ics)
self.assertIn("X-GOVOPLAN-CUSTOM:kept", ics)
self.assertIn("BEGIN:VALARM", ics)
self.assertIn("TRIGGER:-PT15M", ics)
def test_expand_event_occurrences_applies_rrule_rdate_and_exdate(self) -> None:
parsed = parse_vevent(
"""BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event-recur@example.test
DTSTART;TZID=Europe/Berlin:20260708T090000
DTEND;TZID=Europe/Berlin:20260708T100000
SUMMARY:Recurring
RRULE:FREQ=WEEKLY;COUNT=3
EXDATE;TZID=Europe/Berlin:20260715T090000
RDATE;TZID=Europe/Berlin:20260729T090000
END:VEVENT
END:VCALENDAR
"""
)
event = SimpleNamespace(**parsed)
occurrences = expand_event_occurrences(
event,
datetime(2026, 7, 1, tzinfo=timezone.utc),
datetime(2026, 7, 31, 23, 59, tzinfo=timezone.utc),
)
self.assertEqual(
[item["start_at"] for item in occurrences],
[
datetime(2026, 7, 8, 7, 0, tzinfo=timezone.utc),
datetime(2026, 7, 22, 7, 0, tzinfo=timezone.utc),
datetime(2026, 7, 29, 7, 0, tzinfo=timezone.utc),
],
)
self.assertEqual(occurrences[0]["end_at"], datetime(2026, 7, 8, 8, 0, tzinfo=timezone.utc))
def test_generated_ics_contains_required_vevent_fields(self) -> None:
class Event:
uid = "event-3@example.test"
start_at = parse_vevent(
"""BEGIN:VCALENDAR
BEGIN:VEVENT
UID:x
DTSTART:20260708T090000Z
SUMMARY:x
END:VEVENT
END:VCALENDAR
"""
)["start_at"]
end_at = None
all_day = False
timezone = None
summary = "Generated"
sequence = 0
status = "CONFIRMED"
transparency = "OPAQUE"
classification = "PUBLIC"
description = None
location = None
categories = []
rrule = None
organizer = None
attendees = []
ics = event_to_ics(Event())
self.assertIn("BEGIN:VEVENT", ics)
self.assertIn("UID:event-3@example.test", ics)
self.assertIn("SUMMARY:Generated", ics)
if __name__ == "__main__":
unittest.main()