#!/usr/bin/env python # -*- encoding: utf-8 -*- """ Rule: None of the tests in this file should initiate any internet communication, and there should be no dependencies on a working caldav server for the tests in this file. We use the Mock class when needed to emulate server communication. """ from six import PY3 from nose.tools import assert_equal, assert_not_equal, assert_raises, assert_true import caldav from caldav.davclient import DAVClient, DAVResponse from caldav.objects import (Principal, Calendar, Journal, Event, DAVObject, CalendarSet, FreeBusy, Todo, CalendarObjectResource) from caldav.lib.url import URL from caldav.lib import url, error, vcal from caldav.elements import dav, cdav, ical from caldav.lib.python_utilities import to_local, to_str import vobject, icalendar from datetime import datetime if PY3: from urllib.parse import urlparse from unittest import mock else: from urlparse import urlparse import mock ## Some example icalendar data copied from test_caldav.py ev1 = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN BEGIN:VEVENT UID:20010712T182145Z-123401@example.com DTSTAMP:20060712T182145Z DTSTART:20060714T170000Z DTEND:20060715T040000Z SUMMARY:Bastille Day Party END:VEVENT END:VCALENDAR """ todo = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN BEGIN:VTODO UID:20070313T123432Z-456553@example.com DTSTAMP:20070313T123432Z DUE;VALUE=DATE:20070501 SUMMARY:Submit Quebec Income Tax Return for 2006 CLASS:CONFIDENTIAL CATEGORIES:FAMILY,FINANCE STATUS:NEEDS-ACTION END:VTODO END:VCALENDAR""" journal = """ BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN BEGIN:VJOURNAL UID:19970901T130000Z-123405@example.com DTSTAMP:19970901T130000Z DTSTART;VALUE=DATE:19970317 SUMMARY:Staff meeting minutes DESCRIPTION:1. Staff meeting: Participants include Joe\, Lisa and Bob. Aurora project plans were reviewed. There is currently no budget reserves for this project. Lisa will escalate to management. Next meeting on Tuesday.\n END:VJOURNAL END:VCALENDAR """ def MockedDAVResponse(text): """ For unit testing - a mocked DAVResponse with some specific content """ resp = mock.MagicMock() resp.status_code = 207 resp.reason = 'multistatus' resp.headers = {} resp.content = text return DAVResponse(resp) def MockedDAVClient(xml_returned): """ For unit testing - a mocked DAVClient returning some specific content every time a request is performed """ client = DAVClient(url='https://somwhere.in.the.universe.example/some/caldav/root') client.request = mock.MagicMock(return_value=MockedDAVResponse(xml_returned)) return client class TestCalDAV: """ Test class for "pure" unit tests (small internal tests, testing that a small unit of code works as expected, without any third party dependencies, without accessing any caldav server) """ @mock.patch('caldav.davclient.requests.Session.request') def testRequestNonAscii(self, mocked): """ ref https://github.com/python-caldav/caldav/issues/83 """ mocked().status_code=200 mocked().headers = {} cal_url = "http://me:hunter2@calendar.møøh.example:80/" client = DAVClient(url=cal_url) response = client.put('/foo/møøh/bar', 'bringebærsyltetøy 北京 пиво', {}) assert_equal(response.status, 200) assert(response.tree is None) if PY3: response = client.put('/foo/møøh/bar'.encode('utf-8'), 'bringebærsyltetøy 北京 пиво'.encode('utf-8'), {}) else: response = client.put(u'/foo/møøh/bar', u'bringebærsyltetøy 北京 пиво', {}) assert_equal(response.status, 200) assert(response.tree is None) def testPathWithEscapedCharacters(self): xml=b""" /some/caldav/root/133bahgr6ohlo9ungq0it45vf8%40group.calendar.google.com/events/ HTTP/1.1 200 OK """ client = MockedDAVClient(xml) assert_equal(client.calendar(url="https://somwhere.in.the.universe.example/some/caldav/root/133bahgr6ohlo9ungq0it45vf8%40group.calendar.google.com/events/").get_supported_components(), ['VEVENT']) def testAbsoluteURL(self): """Version 0.7.0 does not handle responses with absolute URLs very well, ref https://github.com/python-caldav/caldav/pull/103""" ## none of this should initiate any communication client = DAVClient(url='http://cal.example.com/') principal = Principal(client=client, url='http://cal.example.com/home/bernard/') ## now, ask for the calendar_home_set, but first we need to mock up client.propfind mocked_response = mock.MagicMock() mocked_response.status_code = 207 mocked_response.reason = 'multistatus' mocked_response.headers = {} mocked_response.content = """ http://cal.example.com/home/bernard/ http://cal.example.com/home/bernard/calendars/ HTTP/1.1 200 OK """ mocked_davresponse = DAVResponse(mocked_response) client.propfind = mock.MagicMock(return_value=mocked_davresponse) bernards_calendars = principal.calendar_home_set assert_equal(bernards_calendars.url, URL('http://cal.example.com/home/bernard/calendars/')) def testDateSearch(self): """ ## ref https://github.com/python-caldav/caldav/issues/133 """ xml = """ /principals/calendar/home@petroski.example.com/963/43B060B3-A023-48ED-B9E7-6FFD38D5073E.ics HTTP/1.1 200 OK HTTP/1.1 404 Not Found /principals/calendar/home@petroski.example.com/963/114A4E50-8835-42E1-8185-8A97567B5C1A.ics HTTP/1.1 200 OK HTTP/1.1 404 Not Found /principals/calendar/home@petroski.example.com/963/C20A8820-7156-4DD2-AD1D-17105D923145.ics HTTP/1.1 200 OK HTTP/1.1 404 Not Found """ client = MockedDAVClient(xml) calendar = Calendar(client, url='/principals/calendar/home@petroski.example.com/963/') results = calendar.date_search(datetime(2021, 2, 1),datetime(2021, 2,7)) assert_equal(len(results), 3) def testCalendar(self): """ Principal.calendar() and CalendarSet.calendar() should create Calendar objects without initiating any communication with the server. Calendar.event() should create Event object without initiating any communication with the server. DAVClient.__init__ also doesn't do any communication Principal.__init__ as well, if the principal_url is given Principal.calendar_home_set needs to be set or the server will be queried """ cal_url = "http://me:hunter2@calendar.example:80/" client = DAVClient(url=cal_url) principal = Principal(client, cal_url + "me/") principal.calendar_home_set = cal_url + "me/calendars/" # calendar_home_set is actually a CalendarSet object assert(isinstance(principal.calendar_home_set, CalendarSet)) calendar1 = principal.calendar(name="foo", cal_id="bar") calendar2 = principal.calendar_home_set.calendar( name="foo", cal_id="bar") calendar3 = principal.calendar(cal_id="bar") assert_equal(calendar1.url, calendar2.url) assert_equal(calendar1.url, calendar3.url) assert_equal( calendar1.url, "http://calendar.example:80/me/calendars/bar/") # principal.calendar_home_set can also be set to an object # This should be noop principal.calendar_home_set = principal.calendar_home_set calendar1 = principal.calendar(name="foo", cal_id="bar") assert_equal(calendar1.url, calendar2.url) # When building a calendar from a relative URL and a client, # the relative URL should be appended to the base URL in the client calendar1 = Calendar(client, 'someoneelse/calendars/main_calendar') calendar2 = Calendar(client, 'http://me:hunter2@calendar.example:80/someoneelse/calendars/main_calendar') assert_equal(calendar1.url, calendar2.url) def test_get_events_icloud(self): """ tests that some XML observed from the icloud returns 0 events found. """ xml = """ /17149682/calendars/testcalendar-485d002e-31b9-4147-a334-1d71503a4e2c/ HTTP/1.1 200 OK HTTP/1.1 404 Not Found """ client = MockedDAVClient(xml) calendar = Calendar(client, url='/17149682/calendars/testcalendar-485d002e-31b9-4147-a334-1d71503a4e2c/') assert_equal(len(calendar.events()), 0) def test_get_calendars(self): xml=""" /dav/tobias%40redpill-linpro.com/ HTTP/1.1 200 OK USER_ROOT /dav/tobias%40redpill-linpro.com/Inbox/ HTTP/1.1 200 OK Inbox /dav/tobias%40redpill-linpro.com/Emailed%20Contacts/ HTTP/1.1 200 OK Emailed Contacts /dav/tobias%40redpill-linpro.com/Calendarc5f1a47c-2d92-11e3-b654-0016eab36bf4.ics HTTP/1.1 200 OK Calendarc5f1a47c-2d92-11e3-b654-0016eab36bf4.ics /dav/tobias%40redpill-linpro.com/Yep/ HTTP/1.1 200 OK Yep """ client=MockedDAVClient(xml) calendar_home_set = CalendarSet(client, url='/dav/tobias%40redpill-linpro.com/') assert_equal(len(calendar_home_set.calendars()), 1) def test_supported_components(self): xml=""" /17149682/calendars/testcalendar-0da571c7-139c-479a-9407-8ce9ed20146d/ HTTP/1.1 200 OK """ client = MockedDAVClient(xml) assert_equal(Calendar(client=client, url="/17149682/calendars/testcalendar-0da571c7-139c-479a-9407-8ce9ed20146d/").get_supported_components(), ['VEVENT']); def test_xml_parsing(self): """ DAVResponse has quite some code to parse the XML received from the server. This test contains real XML received from various caldav servers, and the expected result from the parse methods. """ xml = """ / /17149682/principal/ HTTP/1.1 200 OK """ expected_result = {'/': {'{DAV:}current-user-principal': '/17149682/principal/'}} assert_equal(MockedDAVResponse(xml).expand_simple_props(props=[dav.CurrentUserPrincipal()]), expected_result) ## This duplicated response is observed in the real world - ## see https://github.com/python-caldav/caldav/issues/136 ## (though I suppose there was an email address instead of ## simply "frank", the XML I got was obfuscated) xml = """ /principals/users/frank/ /principals/users/frank/ HTTP/1.1 200 OK /principals/users/frank/ /principals/users/frank/ HTTP/1.1 200 OK """ expected_result = {'/principals/users/frank/': {'{DAV:}current-user-principal': '/principals/users/frank/'}} assert_equal(MockedDAVResponse(xml).expand_simple_props(props=[dav.CurrentUserPrincipal()]), expected_result) xml = """ /17149682/principal/ https://p62-caldav.icloud.com:443/17149682/calendars/ HTTP/1.1 200 OK """ expected_result = {'/17149682/principal/': {'{urn:ietf:params:xml:ns:caldav}calendar-home-set': 'https://p62-caldav.icloud.com:443/17149682/calendars/'}} assert_equal(MockedDAVResponse(xml).expand_simple_props(props=[cdav.CalendarHomeSet()]), expected_result) xml = """ / /17149682/principal/ HTTP/1.1 200 OK """ expected_result = {'/': {'{DAV:}current-user-principal': '/17149682/principal/'}} assert_equal(MockedDAVResponse(xml).expand_simple_props(props=[dav.CurrentUserPrincipal()]), expected_result) xml = """ /17149682/calendars/testcalendar-84439d0b-ce46-4416-b978-7b4009122c64/ HTTP/1.1 200 OK HTTP/1.1 404 Not Found /17149682/calendars/testcalendar-84439d0b-ce46-4416-b978-7b4009122c64/20010712T182145Z-123401%40example.com.ics BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN BEGIN:VEVENT UID:20010712T182145Z-123401@example.com DTSTAMP:20060712T182145Z DTSTART:20060714T170000Z DTEND:20060715T040000Z SUMMARY:Bastille Day Party END:VEVENT END:VCALENDAR HTTP/1.1 200 OK """ expected_result = {'/17149682/calendars/testcalendar-84439d0b-ce46-4416-b978-7b4009122c64/': {'{urn:ietf:params:xml:ns:caldav}calendar-data': None}, '/17149682/calendars/testcalendar-84439d0b-ce46-4416-b978-7b4009122c64/20010712T182145Z-123401@example.com.ics': {'{urn:ietf:params:xml:ns:caldav}calendar-data': 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Example Corp.//CalDAV Client//EN\nBEGIN:VEVENT\nUID:20010712T182145Z-123401@example.com\nDTSTAMP:20060712T182145Z\nDTSTART:20060714T170000Z\nDTEND:20060715T040000Z\nSUMMARY:Bastille Day Party\nEND:VEVENT\nEND:VCALENDAR\n'}} assert_equal(MockedDAVResponse(xml).expand_simple_props(props=[cdav.CalendarData()]), expected_result) xml = """ /17149682/calendars/ Ny Test HTTP/1.1 200 OK /17149682/calendars/06888b87-397f-11eb-943b-3af9d3928d42/ calfoo3 HTTP/1.1 200 OK /17149682/calendars/inbox/ HTTP/1.1 200 OK HTTP/1.1 404 Not Found /17149682/calendars/testcalendar-e2910e0a-feab-4b51-b3a8-55828acaa912/ Yep HTTP/1.1 200 OK """ expected_result = { '/17149682/calendars/': { '{DAV:}resourcetype': ['{DAV:}collection'], '{DAV:}displayname': 'Ny Test'}, '/17149682/calendars/06888b87-397f-11eb-943b-3af9d3928d42/': { '{DAV:}resourcetype': ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar'], '{DAV:}displayname': 'calfoo3'}, '/17149682/calendars/inbox/': { '{DAV:}resourcetype': ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}schedule-inbox'], '{DAV:}displayname': None}, '/17149682/calendars/testcalendar-e2910e0a-feab-4b51-b3a8-55828acaa912/': { '{DAV:}resourcetype': ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar'], '{DAV:}displayname': 'Yep'}} assert_equal(MockedDAVResponse(xml).expand_simple_props(props=[dav.DisplayName()], multi_value_props=[dav.ResourceType()]), expected_result) xml = """ /17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/ "kkkgopik" HTTP/1.1 200 OK /17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/1761bf8c-6363-11eb-8fe4-74e5f9bfd8c1.ics "kkkgorwx" HTTP/1.1 200 OK /17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/20010712T182145Z-123401%40example.com.ics "kkkgoqqu" HTTP/1.1 200 OK HwoQEgwAAAh4yw8ntwAAAAAYAhgAIhUIopml463FieB4EKq9+NSn04DrkQEoAA== """ expected_results = { '/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/': { '{DAV:}getetag': '"kkkgopik"', '{urn:ietf:params:xml:ns:caldav}calendar-data': None}, '/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/1761bf8c-6363-11eb-8fe4-74e5f9bfd8c1.ics': { '{DAV:}getetag': '"kkkgorwx"', '{urn:ietf:params:xml:ns:caldav}calendar-data': None}, '/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/20010712T182145Z-123401@example.com.ics': { '{DAV:}getetag': '"kkkgoqqu"', '{urn:ietf:params:xml:ns:caldav}calendar-data': None} } def testFailedQuery(self): """ ref https://github.com/python-caldav/caldav/issues/54 """ cal_url = "http://me:hunter2@calendar.example:80/" client = DAVClient(url=cal_url) calhome = CalendarSet(client, cal_url + "me/") ## syntesize a failed response class FailedResp: pass failedresp = FailedResp() failedresp.status = 400 failedresp.reason = "you are wrong" failedresp.raw = "your request does not adhere to standards" ## synthesize a new http method calhome.client.unknown_method = lambda url, body, depth: failedresp ## call it. assert_raises(error.DAVError, calhome._query, query_method='unknown_method') def testDefaultClient(self): """When no client is given to a DAVObject, but the parent is given, parent.client will be used""" cal_url = "http://me:hunter2@calendar.example:80/" client = DAVClient(url=cal_url) calhome = CalendarSet(client, cal_url + "me/") calendar = Calendar(parent=calhome) assert_equal(calendar.client, calhome.client) def testInstance(self): cal_url = "http://me:hunter2@calendar.example:80/" client = DAVClient(url=cal_url) my_event = Event(client, data=ev1) my_event.vobject_instance.vevent.summary.value='new summary' assert('new summary' in my_event.data) icalobj = my_event.icalendar_instance icalobj.subcomponents[0]['SUMMARY']='yet another summary' assert_equal(my_event.vobject_instance.vevent.summary.value, 'yet another summary') ## Now the data has been converted from string to vobject to string to icalendar to string to vobject and ... will the string still match the original? lines_now = my_event.data.split('\r\n') lines_orig = ev1.replace('Bastille Day Party', 'yet another summary').split('\n') lines_now.sort() lines_orig.sort() assert_equal(lines_now, lines_orig) def testURL(self): """Exercising the URL class""" long_url = "http://foo:bar@www.example.com:8080/caldav.php/?foo=bar" # 1) URL.objectify should return a valid URL object almost no matter # what's thrown in url0 = URL.objectify(None) url0b= URL.objectify("") url1 = URL.objectify(long_url) url2 = URL.objectify(url1) url3 = URL.objectify("/bar") url4 = URL.objectify(urlparse(str(url1))) url5 = URL.objectify(urlparse("/bar")) # 2) __eq__ works well assert_equal(url1, url2) assert_equal(url1, url4) assert_equal(url3, url5) # 3) str will always return the URL assert_equal(str(url1), long_url) assert_equal(str(url3), "/bar") assert_equal(str(url4), long_url) assert_equal(str(url5), "/bar") ## 3b) repr should also be exercised. Returns URL(/bar) now. assert("/bar" in repr(url5)) assert("URL" in repr(url5)) assert(len(repr(url5)) < 12) # 4) join method url6 = url1.join(url2) url7 = url1.join(url3) url8 = url1.join(url4) url9 = url1.join(url5) urlA = url1.join("someuser/calendar") urlB = url5.join(url1) assert_equal(url6, url1) assert_equal(url7, "http://foo:bar@www.example.com:8080/bar") assert_equal(url8, url1) assert_equal(url9, url7) assert_equal(urlA, "http://foo:bar@www.example.com:8080/caldav.php/someuser/calendar") assert_equal(urlB, url1) assert_raises(ValueError, url1.join, "http://www.google.com") # 4b) join method, with URL as input parameter url6 = url1.join(URL.objectify(url2)) url7 = url1.join(URL.objectify(url3)) url8 = url1.join(URL.objectify(url4)) url9 = url1.join(URL.objectify(url5)) urlA = url1.join(URL.objectify("someuser/calendar")) urlB = url5.join(URL.objectify(url1)) url6b= url6.join(url0) url6c= url6.join(url0b) url6d= url6.join(None) for url6alt in (url6b, url6c, url6d): assert_equal(url6, url6alt) assert_equal(url6, url1) assert_equal(url7, "http://foo:bar@www.example.com:8080/bar") assert_equal(url8, url1) assert_equal(url9, url7) assert_equal(urlA, "http://foo:bar@www.example.com:8080/caldav.php/someuser/calendar") assert_equal(urlB, url1) assert_raises(ValueError, url1.join, "http://www.google.com") # 5) all urlparse methods will work. always. assert_equal(url1.scheme, 'http') assert_equal(url2.path, '/caldav.php/') assert_equal(url7.username, 'foo') assert_equal(url5.path, '/bar') urlC = URL.objectify("https://www.example.com:443/foo") assert_equal(urlC.port, 443) # 6) is_auth returns True if the URL contains a username. assert_equal(urlC.is_auth(), False) assert_equal(url7.is_auth(), True) # 7) unauth() strips username/password assert_equal(url7.unauth(), 'http://www.example.com:8080/bar') # 8) strip_trailing_slash assert_equal(URL('http://www.example.com:8080/bar/').strip_trailing_slash(), URL('http://www.example.com:8080/bar')) assert_equal(URL('http://www.example.com:8080/bar/').strip_trailing_slash(), URL('http://www.example.com:8080/bar').strip_trailing_slash()) def testFilters(self): filter = \ cdav.Filter().append( cdav.CompFilter("VCALENDAR").append( cdav.CompFilter("VEVENT").append( cdav.PropFilter("UID").append( [cdav.TextMatch("pouet", negate=True)])))) # print(filter) crash = cdav.CompFilter() value = None try: value = str(crash) except: pass if value is not None: raise Exception("This should have crashed") 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. """ broken_ical=[ ## This first one contains duplicated DTSTART 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"""] ## todo: add more broken ical here for ical in broken_ical: ## This should raise error assert_raises(vobject.base.ValidateError, vobject.readOne(ical).serialize) ## This should not raise error vobject.readOne(vcal.fix(ical)).serialize() def test_calendar_comp_class_by_data(self): calendar=Calendar() for (ical,class_) in ((ev1, Event), (todo, Todo), (journal, Journal), (None, CalendarObjectResource), ("random rantings", CalendarObjectResource)): ## TODO: freebusy, time zone assert_equal( calendar._calendar_comp_class_by_data(ical), class_) if (ical != "random rantings" and ical): assert_equal( calendar._calendar_comp_class_by_data(icalendar.Calendar.from_ical(ical)), class_) def testContextManager(self): """ ref https://github.com/python-caldav/caldav/pull/175 """ cal_url = "http://me:hunter2@calendar.example:80/" with DAVClient(url=cal_url) as client_ctx_mgr: assert_true(isinstance(client_ctx_mgr, DAVClient))