Initial commit
This commit is contained in:
128
tests/test_ical.py
Normal file
128
tests/test_ical.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user