File: test_vcal.py

package info (click to toggle)
python-caldav 1.3.9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 636 kB
  • sloc: python: 6,824; makefile: 91; sh: 7
file content (264 lines) | stat: -rw-r--r-- 8,300 bytes parent folder | download
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