1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
|
#!/usr/bin/env python
import re
import uuid
from datetime import datetime
from datetime import timedelta
from unittest import TestCase
import icalendar
import pytest
import pytz
import vobject
from caldav.lib import vcal
from caldav.lib.python_utilities import to_normal_str
from caldav.lib.python_utilities import to_wire
from caldav.lib.vcal import create_ical
from caldav.lib.vcal import fix
# from datetime import timezone
# utc = timezone.utc
utc = pytz.utc
# example from http://www.rfc-editor.org/rfc/rfc5545.txt
ev = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:19970901T130000Z-123403@example.com
DTSTAMP:19970901T130000Z
DTSTART;VALUE=DATE:19971102
SUMMARY:Our Blissful Anniversary
TRANSP:TRANSPARENT
CLASS:CONFIDENTIAL
CATEGORIES:ANNIVERSARY,PERSONAL,SPECIAL OCCASION
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR"""
class TestVcal(TestCase):
def assertSameICal(self, ical1, ical2, ignore_uid=False):
"""helper method"""
def normalize(s, ignore_uid):
s = to_wire(s).replace(b"\r\n", b"\n").strip().split(b"\n")
s.sort()
if ignore_uid:
s = [x for x in s if not x.startswith(b"UID:")]
return b"\n".join(s)
self.assertEqual(normalize(ical1, ignore_uid), normalize(ical2, ignore_uid))
return ical2
def verifyICal(self, ical):
"""
Does a best effort on verifying that the ical is correct, by
pushing it through the vobject and icalendar library
"""
vobj = vobject.readOne(to_normal_str(ical))
icalobj = icalendar.Calendar.from_ical(ical)
self.assertSameICal(icalobj.to_ical(), ical)
self.assertSameICal(vobj.serialize(), ical)
return icalobj.to_ical()
## TODO: create a test_fix, should be fairly simple - for each
## "fix" that's done in the code, make up some broken ical data
## that demonstrates the brokenness we're dealing with (preferably
## real-world examples). Then ...
# for bical in broken_ical:
# verifyICal(vcal.fix(bical))
def test_create_ical(self):
def create_and_validate(**args):
return self.verifyICal(create_ical(**args))
## First, a fully valid ical_fragment should go through as is
self.assertSameICal(create_and_validate(ical_fragment=ev), ev)
## One may add stuff to a fully valid ical_fragment
self.assertSameICal(
create_and_validate(ical_fragment=ev, priority=3), ev + "\nPRIORITY:3\n"
)
## binary string or unicode string ... shouldn't matter
self.assertSameICal(
create_and_validate(ical_fragment=ev.encode("utf-8"), priority=3),
ev + "\nPRIORITY:3\n",
)
## The returned ical_fragment should always contain BEGIN:VCALENDAR and END:VCALENDAR
ical_fragment = ev.replace("BEGIN:VCALENDAR", "").replace("END:VCALENDAR", "")
self.assertSameICal(create_and_validate(ical_fragment=ical_fragment), ev)
## Create something with a dtstart and verify that we get it back in the ical
some_ical0 = create_and_validate(
summary="gobledok",
dtstart=datetime(2032, 10, 10, 10, 10, 10, tzinfo=utc),
duration=timedelta(hours=5),
)
some_ical1 = create_and_validate(
summary=b"gobledok",
dtstart=datetime(2032, 10, 10, 10, 10, 10, tzinfo=utc),
duration=timedelta(hours=5),
)
assert re.search(b"DTSTART(;VALUE=DATE-TIME)?:20321010T101010Z", some_ical0)
self.assertSameICal(some_ical0, some_ical1, ignore_uid=True)
## Verify that ical_fragment works as intended
some_ical = create_and_validate(
summary="gobledok",
ical_fragment="PRIORITY:3",
dtstart=datetime(2032, 10, 10, 10, 10, 10, tzinfo=utc),
duration=timedelta(hours=5),
)
assert re.search(b"DTSTART(;VALUE=DATE-TIME)?:20321010T101010Z", some_ical)
assert some_ical.count(b"PRIORITY") == 1
some_ical = create_and_validate(
summary="gobledok",
ical_fragment=b"PRIORITY:3",
dtstart=datetime(2032, 10, 10, 10, 10, 10, tzinfo=utc),
duration=timedelta(hours=5),
)
assert re.search(b"DTSTART(;VALUE=DATE-TIME)?:20321010T101010Z", some_ical)
some_ical = create_and_validate(
summary=b"gobledok",
ical_fragment="",
dtstart=datetime(2032, 10, 10, 10, 10, 10, tzinfo=utc),
duration=timedelta(hours=5),
)
assert re.search(b"DTSTART(;VALUE=DATE-TIME)?:20321010T101010Z", some_ical)
def test_vcal_fixups(self):
"""
There is an obscure function lib.vcal that attempts to fix up
known ical standard breaches from various calendar servers.
"""
non_broken_ical = [
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//python-caldav//caldav//en_DK
BEGIN:VEVENT
SUMMARY:Foo bar blah
blub
DTSTART:20230313T084500Z
DURATION:PT30M
DTSTAMP:20230312T215907Z
UID:1c9bba3e-c121-11ed-bf96-982cbcdd642c
CATEGORIES:oslo
END:VEVENT
END:VCALENDAR
""",
## Next one contains a DTSTAMP before BEGIN:VEVENT
## Doesn’t make sense, but valid, and more importantly,
## not failing during the `fix` call.
"""DTSTAMP:20210205T101751Z
BEGIN:VEVENT
UID:20200516T060000Z-123401@example.com
SUMMARY:Do the needful
DTSTART:20210517T060000Z
END:VEVENT
""",
]
broken_ical = [
## This first one contains duplicated DTSTAMP in the event data
"""BEGIN:VCALENDAR
X-EXPANDED:True
X-MASTER-DTSTART:20200517T060000Z
X-MASTER-RRULE:FREQ=YEARLY
BEGIN:VEVENT
DTSTAMP:20210205T101751Z
UID:20200516T060000Z-123401@example.com
DTSTAMP:20200516T060000Z
SUMMARY:Do the needful
DTSTART:20210517T060000Z
DTEND:20210517T230000Z
RECURRENCE-ID:20210517T060000Z
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20210205T101751Z
UID:20200516T060000Z-123401@example.com
DTSTAMP:20200516T060000Z
SUMMARY:Do the needful
DTSTART:20220517T060000Z
DTEND:20220517T230000Z
RECURRENCE-ID:20220517T060000Z
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20210205T101751Z
UID:20200516T060000Z-123401@example.com
DTSTAMP:20200516T060000Z
SUMMARY:Do the needful
DTSTART:20230517T060000Z
DTEND:20230517T230000Z
RECURRENCE-ID:20230517T060000Z
END:VEVENT
END:VCALENDAR""", ## Next one contains DTEND and DURATION.
"""BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTAMP:20210205T101751Z
UID:20200516T060000Z-123401@example.com
SUMMARY:Do the needful
DTSTART:20210517T060000Z
DURATION:PT15M
DTEND:20210517T230000Z
END:VEVENT
END:VCALENDAR""", ## Same, but real example:
"""BEGIN:VCALENDAR
PRODID:Zimbra-Calendar-Provider
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Brussels
BEGIN:STANDARD
DTSTART:16010101T030000
TZOFFSETTO:+0100
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=10;BYDAY=-1SU
TZNAME:CET
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010101T020000
TZOFFSETTO:+0200
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=-1SU
TZNAME:CEST
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:e42481ad-aabf-43c1-bbc0-04754373678d
RRULE:FREQ=WEEKLY;UNTIL=20221222T225959Z;BYDAY=WE
SUMMARY:Competence Lunch
DESCRIPTION: *removed*
ATTENDEE;CN=Tobias Brox;PARTSTAT=TENTATIVE:mailto:tobias@redpill-linpro.com
PRIORITY:9
ORGANIZER;CN=Someone:mailto:noreply@redpill-linpro.com
DTSTART;TZID="Europe/Brussels":20220817T110000
DTEND;TZID="Europe/Brussels":20220817T113000
DURATION:PT30M
STATUS:CONFIRMED
CLASS:PUBLIC
TRANSP:OPAQUE
LAST-MODIFIED:20220916T120601Z
DTSTAMP:20220906T125002Z
SEQUENCE:1
EXDATE;TZID="Europe/Brussels":20221005T110000
EXDATE;TZID="Europe/Brussels":20221116T110000
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;RELATED=START:-PT15M
DESCRIPTION:Reminder
END:VALARM
END:VEVENT
END:VCALENDAR""",
] ## todo: add more broken ical here
for ical in broken_ical:
## This should raise error
with pytest.raises(vobject.base.ValidateError):
vobject.readOne(ical).serialize()
## This should not raise error
vobject.readOne(vcal.fix(ical)).serialize()
for ical in non_broken_ical:
assert vcal.fix(ical) == ical
|