blob: 62b141228b871f9bdad18d30075979777703a9c9 [file]
"""iCalendar rendering."""
from __future__ import annotations
from asfcalendar import ics
from asfcalendar.models import Event
JAN_1 = 1767225600
def make_event(**overrides: object) -> Event:
fields: dict[str, object] = {
"id": 1,
"shortlink": "AbCd2345",
"title": "Board meeting",
"category": "foundation",
"visibility": "public",
"start": JAN_1,
"end": JAN_1 + 3600,
"all_day": False,
"description": "",
"location": "",
"url": "",
"project": None,
"timezone": "UTC",
"owner": "carol",
"created_at": JAN_1,
"updated_at": JAN_1,
}
fields.update(overrides)
return Event(**fields) # type: ignore[arg-type]
def test_escaping() -> None:
assert ics.escape("a;b") == r"a\;b"
assert ics.escape("a,b") == "a\\,b"
assert ics.escape("a\\b") == "a\\\\b"
assert ics.escape("line1\nline2") == "line1\\nline2"
assert ics.escape("line1\r\nline2") == "line1\\nline2"
def test_short_lines_are_left_alone() -> None:
assert ics.fold("SUMMARY:hello") == "SUMMARY:hello"
def test_long_lines_are_folded_to_75_octets() -> None:
folded = ics.fold("SUMMARY:" + "x" * 200)
pieces = folded.split("\r\n")
assert len(pieces) > 1
assert len(pieces[0].encode()) <= 75
for piece in pieces[1:]:
assert piece.startswith(" ")
assert len(piece.encode()) <= 75
assert "".join([pieces[0], *[p[1:] for p in pieces[1:]]]) == "SUMMARY:" + "x" * 200
def test_folding_does_not_split_multibyte_characters() -> None:
folded = ics.fold("SUMMARY:" + "æ" * 100)
for piece in folded.split("\r\n"):
assert len(piece.encode()) <= 75
piece.encode("utf-8").decode("utf-8") # would raise on a split codepoint
def test_calendar_envelope() -> None:
body = ics.render([make_event()], name="My Calendar")
assert body.startswith("BEGIN:VCALENDAR\r\n")
assert body.endswith("END:VCALENDAR\r\n")
assert "VERSION:2.0" in body
assert "X-WR-CALNAME:My Calendar" in body
def test_timed_event() -> None:
body = ics.render([make_event(location="Room 3", description="Agenda")])
assert "DTSTART:20260101T000000Z" in body
assert "DTEND:20260101T010000Z" in body
assert "LOCATION:Room 3" in body
assert "DESCRIPTION:Agenda" in body
assert "CLASS:PUBLIC" in body
def test_all_day_event_uses_dates() -> None:
body = ics.render([make_event(all_day=True, end=JAN_1 + 86400)])
assert "DTSTART;VALUE=DATE:20260101" in body
assert "DTEND;VALUE=DATE:20260102" in body
def test_private_events_are_marked_private() -> None:
assert "CLASS:PRIVATE" in ics.render([make_event(visibility="private")])
def test_categories_include_the_project() -> None:
body = ics.render([make_event(category="project", project="httpd")])
assert "CATEGORIES:project\\,httpd" in body
def test_shortlink_is_appended_to_the_description() -> None:
body = ics.render([make_event(description="Agenda")], shortlinks={1: "https://example.org/e/AbCd2345"})
assert "https://example.org/e/AbCd2345" in body
assert "URL:https://example.org/e/AbCd2345" in body
def test_uid_is_stable_and_unique() -> None:
first = ics.render([make_event(id=1, shortlink="AAAA2345")])
second = ics.render([make_event(id=2, shortlink="BBBB2345")])
assert "UID:event-1-AAAA2345@calendar.apache.org" in first
assert "UID:event-2-BBBB2345@calendar.apache.org" in second
def test_several_events() -> None:
body = ics.render([make_event(id=1), make_event(id=2, title="Another")])
assert body.count("BEGIN:VEVENT") == 2
assert body.count("END:VEVENT") == 2
def test_an_empty_calendar_is_still_valid() -> None:
body = ics.render([])
assert "BEGIN:VEVENT" not in body
assert "END:VCALENDAR" in body
def test_every_line_ends_with_crlf() -> None:
body = ics.render([make_event(description="A really long description " * 10)])
for line in body.split("\r\n"):
assert "\n" not in line
def test_the_organisers_timezone_is_recorded() -> None:
body = ics.render([make_event(timezone="Europe/Berlin")])
# The times themselves stay in UTC so every client reads them correctly.
assert "DTSTART:20260101T000000Z" in body
assert "X-ASF-EVENT-TIMEZONE:Europe/Berlin" in body
def test_a_utc_event_gets_no_extra_property() -> None:
assert "X-ASF-EVENT-TIMEZONE" not in ics.render([make_event(timezone="UTC")])
def test_all_day_events_carry_no_timezone() -> None:
body = ics.render([make_event(all_day=True, end=JAN_1 + 86400, timezone="Europe/Berlin")])
assert "X-ASF-EVENT-TIMEZONE" not in body