Files
mdcaldav/tests/test_ical.py
Tyler Perkins 8b5cd370ea
Some checks failed
CI / test (push) Failing after 11s
CI / image (push) Has been skipped
Initial commit
2026-08-01 21:03:35 -04:00

129 lines
4.3 KiB
Python

"""iCalendar mapping tests (SPEC 4)."""
from __future__ import annotations
from datetime import datetime, timezone
from mdcaldav.config import Config
from mdcaldav.ical import apply_to_task, escape, fold, from_ics, to_ics, unescape
from mdcaldav.model import Status
from mdcaldav.parser import parse
def prop(text: str, name: str) -> str | None:
from mdcaldav.ical import unfold
for line in unfold(text):
key = line.split(":", 1)[0].split(";", 1)[0]
if key == name:
return line.split(":", 1)[1]
return None
def one(src: bytes, cfg: Config):
return parse(src, "t.md", cfg)[0]
def test_basic_vtodo(cfg: Config):
text = to_ics(one(b"- [ ] Water the seedlings\n", cfg))
assert "BEGIN:VTODO" in text and "END:VTODO" in text
assert prop(text, "SUMMARY") == "Water the seedlings"
assert prop(text, "STATUS") == "NEEDS-ACTION"
assert prop(text, "PRIORITY") is None
assert text.endswith("\r\n")
def test_completed_maps_percent_and_timestamp(cfg: Config):
when = datetime(2026, 1, 7, 15, 30, tzinfo=timezone.utc)
text = to_ics(one(b"- [x] Rotate the compost\n", cfg), completed_at=when)
assert prop(text, "STATUS") == "COMPLETED"
assert prop(text, "PERCENT-COMPLETE") == "100"
assert prop(text, "COMPLETED") == "20260107T153000Z"
def test_priority_bands(cfg: Config):
for marker, expected in (("A", "1"), ("B", "5"), ("C", "9")):
text = to_ics(one(f"- [ ] [#{marker}] x\n".encode(), cfg))
assert prop(text, "PRIORITY") == expected
def test_description_and_categories(cfg: Config):
src = b"## Garden\n\n- [ ] Prune\n - Use the long loppers\n"
task = parse(src, "t.md", cfg)[0]
text = to_ics(task)
assert prop(text, "DESCRIPTION") == "Use the long loppers"
assert "Garden" in (prop(text, "CATEGORIES") or "")
def test_parent_relation(cfg: Config):
tasks = parse(b"- [ ] parent\n - [ ] child\n", "t.md", cfg)
text = to_ics(tasks[1])
assert f"RELATED-TO;RELTYPE=PARENT:{tasks[0].uid}" in text
def test_text_escaping_round_trip():
raw = "Comma, semi; back\\slash\nnewline"
assert unescape(escape(raw)) == raw
def test_special_characters_survive_serialization(cfg: Config):
src = "- [ ] Buy: milk, eggs; and a note\\here\n".encode()
text = to_ics(one(src, cfg))
assert from_ics(text)["title"] == "Buy: milk, eggs; and a note\\here"
def test_folding_respects_character_boundaries():
line = "SUMMARY:" + "é" * 100
folded = fold(line)
assert all(len(seg.encode()) <= 75 for seg in folded.split("\r\n "))
from mdcaldav.ical import unfold
assert unfold(folded)[0] == line
def test_long_summary_round_trips(cfg: Config):
title = "Plan " + "the very long garden bed layout " * 6
text = to_ics(one(f"- [ ] {title}\n".encode(), cfg))
assert from_ics(text)["title"] == title.strip()
def test_from_ics_ignores_unmodelled_properties():
text = (
"BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nUID:abc\r\nSUMMARY:Task\r\n"
"RRULE:FREQ=WEEKLY\r\nBEGIN:VALARM\r\nTRIGGER:-PT15M\r\n"
"SUMMARY:Alarm text\r\nEND:VALARM\r\nSTATUS:COMPLETED\r\n"
"END:VTODO\r\nEND:VCALENDAR\r\n"
)
fields = from_ics(text)
assert fields["title"] == "Task" # not clobbered by the VALARM's SUMMARY
assert fields["status"] is Status.COMPLETED
assert "rrule" not in fields
def test_percent_complete_implies_completion():
text = (
"BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nUID:abc\r\nSUMMARY:T\r\n"
"PERCENT-COMPLETE:100\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"
)
assert from_ics(text)["status"] is Status.COMPLETED
def test_apply_to_task_reports_only_real_changes(cfg: Config):
task = one(b"- [ ] [#A] Ship it\n", cfg)
same = from_ics(to_ics(task))
assert apply_to_task(task, same, cfg) == {}
changed = dict(same, status=Status.COMPLETED, title="Ship it now")
assert apply_to_task(task, changed, cfg) == {
"status": Status.COMPLETED,
"title": "Ship it now",
}
def test_priority_snaps_to_configured_bands(cfg: Config):
task = one(b"- [ ] [#A] Ship it\n", cfg)
# A client sending 3 stays in the "high" band → still [#A], so no change.
assert apply_to_task(task, {"priority": 3}, cfg) == {}
assert apply_to_task(task, {"priority": 9}, cfg) == {"priority": 9}
assert apply_to_task(task, {"priority": 0}, cfg) == {"priority": None}