#!/usr/bin/python # setup: 4 users are needed: username, attendee1_username, # attendee1_delegate_username and superuser. # when writing new tests, avoid using superuser when not absolutely needed # TODO # - Individual tests should set the ACLs themselves on Resources tests from config import hostname, port, username, password, \ superuser, superuser_password, \ attendee1, attendee1_username, \ attendee1_password, \ attendee1_delegate, attendee1_delegate_username, \ attendee1_delegate_password, \ resource_no_overbook, resource_can_overbook import datetime import dateutil.tz import sogotests import sys import time import unittest import utilities import vobject import vobject.base import vobject.icalendar import webdavlib import StringIO import xml.etree.ElementTree class CalDAVPropertiesTest(unittest.TestCase): def setUp(self): self.client = webdavlib.WebDAVClient(hostname, port, username, password) self.test_calendar \ = "/SOGo/dav/%s/Calendar/test-dav-properties/" % username mkcol = webdavlib.WebDAVMKCOL(self.test_calendar) self.client.execute(mkcol) def tearDown(self): delete = webdavlib.WebDAVDELETE(self.test_calendar) self.client.execute(delete) def testDavScheduleCalendarTransparency(self): """{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp""" ## PROPFIND propfind = webdavlib.WebDAVPROPFIND(self.test_calendar, ["{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp"], 0) self.client.execute(propfind) response = propfind.response["document"].find('{DAV:}response') propstat = response.find('{DAV:}propstat') status = propstat.find('{DAV:}status').text[9:12] self.assertEquals(status, "200", "schedule-calendar-transp marked as 'Not Found' in response") transp = propstat.find('{DAV:}prop/{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp') values = transp.getchildren() self.assertEquals(len(values), 1, "one and only one element expected") value = values[0] self.assertTrue(xml.etree.ElementTree.iselement(value)) ns = value.tag[0:31] tag = value.tag[31:] self.assertTrue(ns == "{urn:ietf:params:xml:ns:caldav}", "schedule-calendar-transp must have a value in"\ " namespace '%s', not '%s'" % ("urn:ietf:params:xml:ns:caldav", ns)) self.assertTrue(tag == "opaque", "schedule-calendar-transp must be 'opaque' on new" \ " collections, not '%s'" % tag) ## PROPPATCH newValueNode = "{urn:ietf:params:xml:ns:caldav}thisvaluedoesnotexist" proppatch = webdavlib.WebDAVPROPPATCH(self.test_calendar, {"{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": \ { newValueNode: True }}) self.client.execute(proppatch) self.assertEquals(proppatch.response["status"], 400, "expecting failure when setting transparency to" \ " an invalid value") newValueNode = "{urn:ietf:params:xml:ns:caldav}transparent" proppatch = webdavlib.WebDAVPROPPATCH(self.test_calendar, {"{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": \ { newValueNode: True }}) self.client.execute(proppatch) self.assertEquals(proppatch.response["status"], 207, "failure (%s) setting transparency to" \ " 'transparent': '%s'" % (proppatch.response["status"], proppatch.response["body"])) newValueNode = "{urn:ietf:params:xml:ns:caldav}opaque" proppatch = webdavlib.WebDAVPROPPATCH(self.test_calendar, {"{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": \ { newValueNode: True }}) self.client.execute(proppatch) self.assertEquals(proppatch.response["status"], 207, "failure (%s) setting transparency to" \ " 'transparent': '%s'" % (proppatch.response["status"], proppatch.response["body"])) class CalDAVSchedulingTest(unittest.TestCase): def setUp(self): self.superuser_client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password) self.client = webdavlib.WebDAVClient(hostname, port, username, password) self.attendee1_client = webdavlib.WebDAVClient(hostname, port, attendee1_username, attendee1_password) self.attendee1_delegate_client = webdavlib.WebDAVClient(hostname, port, attendee1_delegate_username, attendee1_delegate_password) utility = utilities.TestUtility(self, self.client) (self.user_name, self.user_email) = utility.fetchUserInfo(username) (self.attendee1_name, self.attendee1_email) = utility.fetchUserInfo(attendee1) (self.attendee1_delegate_name, self.attendee1_delegate_email) = utility.fetchUserInfo(attendee1_delegate) (self.res_no_ob_name, self.res_no_ob_email) = utility.fetchUserInfo(resource_no_overbook) (self.res_can_ob_name, self.res_can_ob_email) = utility.fetchUserInfo(resource_can_overbook) self.user_calendar = "/SOGo/dav/%s/Calendar/personal/" % username self.attendee1_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1 self.attendee1_delegate_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1_delegate self.res_calendar = "/SOGo/dav/%s/Calendar/personal/" % resource_no_overbook self.res_ob_calendar = "/SOGo/dav/%s/Calendar/personal/" % resource_can_overbook # fetch non existing event to let sogo create the calendars in the db self._getEvent(self.client, "%snonexistent" % self.user_calendar, exp_status=404) self._getEvent(self.attendee1_client, "%snonexistent" % self.attendee1_calendar, exp_status=404) self._getEvent(self.attendee1_delegate_client, "%snonexistent" % self.attendee1_delegate_calendar, exp_status=404) # list of ics used by the test. # tearDown will loop over this and wipe them in all users' calendar self.ics_list = [] def tearDown(self): # delete all created events from all users' calendar for ics in self.ics_list: self._deleteEvent(self.superuser_client, "%s%s" % (self.user_calendar, ics), None) self._deleteEvent(self.superuser_client, "%s%s" % (self.attendee1_calendar, ics), None) self._deleteEvent(self.superuser_client, "%s%s" % (self.attendee1_delegate_calendar, ics), None) self._deleteEvent(self.superuser_client, "%s%s" % (self.res_calendar, ics), None) self._deleteEvent(self.superuser_client, "%s%s" % (self.res_ob_calendar, ics), None) def _newEvent(self, summary="test event", uid="test", transp=0): transparency = ("OPAQUE", "TRANSPARENT") newCal = vobject.iCalendar() vevent = newCal.add('vevent') vevent.add('summary').value = summary vevent.add('transp').value = transparency[transp] now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) startdate = vevent.add('dtstart') startdate.value = now enddate = vevent.add('dtend') enddate.value = now + datetime.timedelta(0, 3600) vevent.add('uid').value = uid vevent.add('dtstamp').value = now vevent.add('last-modified').value = now vevent.add('created').value = now vevent.add('class').value = "PUBLIC" vevent.add('sequence').value = "0" return newCal def _putEvent(self, client, filename, event, exp_status = 201): put = webdavlib.HTTPPUT(filename, event.serialize()) put.content_type = "text/calendar; charset=utf-8" client.execute(put) if exp_status is not None: self.assertEquals(put.response["status"], exp_status) def _postEvent(self, client, outbox, event, originator, recipients, exp_status = 200): post = webdavlib.CalDAVPOST(outbox, event.serialize(), originator, recipients) client.execute(post) if exp_status is not None: self.assertEquals(post.response["status"], exp_status) def _getEvent(self, client, filename, exp_status = 200): get = webdavlib.HTTPGET(filename) client.execute(get) if exp_status is not None: self.assertEquals(get.response["status"], exp_status) if get.response["headers"]["content-type"].startswith("text/calendar"): stream = StringIO.StringIO(get.response["body"]) event = vobject.base.readComponents(stream).next() else: event = None return event def _deleteEvent(self, client, filename, exp_status = 204): delete = webdavlib.WebDAVDELETE(filename) client.execute(delete) if exp_status is not None: self.assertEquals(delete.response["status"], exp_status) def _getAllEvents(self, client, collection, exp_status = 207): propfind = webdavlib.WebDAVPROPFIND(collection, None) client.execute(propfind) if exp_status is not None: self.assertEquals(propfind.response["status"], exp_status) content = [] nodes = propfind.response["document"].findall('{DAV:}response') for node in nodes: responseHref = node.find('{DAV:}href').text content += [responseHref] return content def _deleteAllEvents(self, client, collection, exp_status = 204): content = self._getAllEvents(client, collection) for item in content: self._deleteEvent(client, item) def _eventAttendees(self, event): attendees = {} event_component = event.vevent for child in event_component.getChildren(): if child.name == "ATTENDEE": try: delegated_to = child.delegated_to_param except: delegated_to = "(none)" try: delegated_from = child.delegated_from_param except: delegated_from = "(none)" attendees[child.value] = ("%s/%s/%s" % (child.partstat_param, delegated_to, delegated_from)) return attendees def _compareAttendees(self, compared_event, event): compared_attendees = self._eventAttendees(compared_event) compared_emails = compared_attendees.keys() self.assertTrue(len(compared_emails) > 0, "no attendee found") compared_emails.sort() attendees = self._eventAttendees(event) emails = attendees.keys() emails.sort() self.assertEquals(len(compared_emails), len(emails), "number of attendees is not equal" + " (actual: %d, exp: %d)" % (len(compared_emails), len(emails))) for email in emails: self.assertEquals(compared_attendees[email], attendees[email], "partstat for attendee '%s' does not match" " (actual: '%s', expected: '%s')" % (email, compared_attendees[email], attendees[email])) def testAddAttendee(self): """ add attendee after event creation """ # make sure the event doesn't exist ics_name = "test-add-attendee.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ics_name), None) self._deleteEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar,ics_name), None) # 1. create an event in the organiser's calendar event = self._newEvent(summary="Test add attendee", uid="Test add attendee") organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # 2. add an attendee event.add("method").value = "REQUEST" attendee = event.vevent.add('attendee') attendee.cn_param = self.attendee1_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.attendee1_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event, exp_status=204) # 3. verify that the attendee has the event attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) # 4. make sure the received event match the original one # XXX is this enough? self.assertEquals(event.vevent.uid, attendee_event.vevent.uid) def testUninviteAttendee(self): """ Remove attendee after event creation """ # make sure the event doesn't exist ics_name = "test-remove-attendee.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ics_name), None) self._deleteEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar,ics_name), None) # 1. create an event in the organiser's calendar event = self._newEvent(summary="Test uninvite attendee", uid="Test uninvite attendee") organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # keep a copy around for updates without other attributes noAttendeeEvent = vobject.iCalendar() noAttendeeEvent.copy(event) # 2. add an attendee event.add("method").value = "REQUEST" attendee = event.vevent.add('attendee') attendee.cn_param = self.attendee1_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.attendee1_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event, exp_status=204) # 3. verify that the attendee has the event attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) # 4. make sure the received event match the original one self.assertEquals(event.vevent.uid, attendee_event.vevent.uid) # 5. uninvite the attendee - put the event back without the attendee now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) noAttendeeEvent.vevent.last_modified.value = now self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), noAttendeeEvent, exp_status=204) # 6. verify that the attendee doesn't have the event anymore attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), 404) def testResourceNoOverbook(self): """ try to overbook a resource """ # make sure there are no events in the resource calendar self._deleteAllEvents(self.superuser_client, self.res_calendar) # make sure the event doesn't exist ics_name = "test-no-overbook.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ics_name), None) ob_ics_name = "test-no-overbook-overlap.ics" self.ics_list += [ob_ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ob_ics_name), None) # 1. create an event in the organiser's calendar event = self._newEvent(summary="Test no overbook", uid="test no overbook") organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email attendee = event.vevent.add('attendee') attendee.cn_param = self.res_no_ob_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.res_no_ob_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # 2. create a second event overlapping the first one event = self._newEvent(summary="Test no overbook - overlap", uid="test no overbook - overlap") organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email attendee = event.vevent.add('attendee') attendee.cn_param = self.res_no_ob_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.res_no_ob_email # put the event - should trigger a 403 self._putEvent(self.client, "%s%s" % (self.user_calendar, ob_ics_name), event, exp_status=403) def testResourceCanOverbook(self): """ try to overbook a resource - multiplebookings=0""" # make sure there are no events in the resource calendar self._deleteAllEvents(self.superuser_client, self.res_ob_calendar) # make sure the event doesn't exist ics_name = "test-can-overbook.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ics_name), None) ob_ics_name = "test-can-overbook-overlap.ics" self.ics_list += [ob_ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ob_ics_name), None) # 1. create an event in the organiser's calendar event = self._newEvent(summary="Test can overbook", uid="test can overbook") organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email attendee = event.vevent.add('attendee') attendee.cn_param = self.res_can_ob_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.res_can_ob_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # 2. create a second event overlapping the first one event = self._newEvent(summary="Test can overbook - overlap", uid="test can overbook - overlap") organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email attendee = event.vevent.add('attendee') attendee.cn_param = self.res_can_ob_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.res_can_ob_email # put the event - should be fine since we can overbook this one self._putEvent(self.client, "%s%s" % (self.user_calendar, ob_ics_name), event) def testResourceBookingOverlapDetection(self): """ Resource booking overlap detection - bug #1837""" # There used to be some problems with recurring events and resources booking # This test implements these edge cases # 1. Create recurring event (with resource) # 2. Create single event overlaping one instance for the previous event # (should fail) # 3. Create recurring event which _doesn't_ overlap the first event # (should be OK, used to fail pre1.3.17) # 4. Create recurring event overlapping the previous recurring event # (should fail) # make sure there are no events in the resource calendar self._deleteAllEvents(self.superuser_client, self.res_calendar) # make sure the event doesn't exist ics_name = "test-res-overlap-detection.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ics_name), None) overlap_ics_name = "test-res-overlap-detection-overlap.ics" self.ics_list += [overlap_ics_name] self._deleteEvent(self.client, "%s%s" % (self.attendee1_calendar,overlap_ics_name), None) nooverlap_recurring_ics_name = "test-res-overlap-detection-nooverlap.ics" self.ics_list += [nooverlap_recurring_ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,nooverlap_recurring_ics_name), None) overlap_recurring_ics_name = "test-res-overlap-detection-overlap-recurring.ics" self.ics_list += [overlap_recurring_ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,overlap_recurring_ics_name), None) # 1. create recurring event with resource event = self._newEvent(summary="recurring event with resource", uid="recurring event w resource") event.vevent.add('rrule').value = "FREQ=DAILY;COUNT=5" organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email attendee = event.vevent.add('attendee') attendee.cn_param = self.res_no_ob_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.res_no_ob_email # keep a copy around for #3 nooverlap_event = vobject.iCalendar() nooverlap_event.copy(event) self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # 2. Create single event overlaping one instance for the previous event event = self._newEvent(summary="recurring event with resource", uid="recurring event w resource - overlap") organizer = event.vevent.add('organizer') organizer.cn_param = self.attendee1_name organizer.value = self.attendee1_email attendee = event.vevent.add('attendee') attendee.cn_param = self.res_no_ob_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.res_no_ob_email # should fail self._putEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, overlap_ics_name), event, exp_status=403) # 3. Create recurring event which _doesn't_ overlap the first event # (should be OK, used to fail pre1.3.17) # shift the start date to one hour after the original event end time nstartdate = nooverlap_event.vevent.dtend.value + datetime.timedelta(0, 3600) nooverlap_event.vevent.dtstart.value = nstartdate nooverlap_event.vevent.dtend.value = nstartdate + datetime.timedelta(0, 3600) nooverlap_event.vevent.uid.value = "recurring - nooverlap" self._putEvent(self.client, "%s%s" % (self.user_calendar, nooverlap_recurring_ics_name), nooverlap_event) # 4. Create recurring event overlapping the previous recurring event # should fail nstartdate = nooverlap_event.vevent.dtstart.value + datetime.timedelta(0, 300) nooverlap_event.vevent.dtstart.value = nstartdate nooverlap_event.vevent.dtend.value = nstartdate + datetime.timedelta(0, 3600) nooverlap_event.vevent.uid.value = "recurring - overlap" self._putEvent(self.client, "%s%s" % (self.user_calendar, overlap_recurring_ics_name), nooverlap_event, exp_status=403) def testRruleExceptionInvitationDance(self): """ RRULE exception invitation dance """ # This workflow is based on what lightning 1.2.1 does # create a reccurring event # add an exception # invite bob to the exception: # bob is declined in the master event # bob needs-action in the exception # bob accepts # bob is declined in the master event # bob is accepted in the exception # the organizer 'uninvites' bob # the event disappears from bob's calendar # bob isn't in the master+exception event ics_name = "test-rrule-exception-invitation-dance.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar, ics_name), None) self._deleteEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), None) # 1. create a recurring event in the organiser's calendar summary="Test reccuring exception invite cancel" uid="Test-recurring-exception-invite-cancel" event = self._newEvent(summary, uid) event.vevent.add('rrule').value = "FREQ=DAILY;COUNT=5" self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # read the event back from the server org_ev = self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) # 2. Add an exception to the master event and invite attendee1 to it now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) org_ev.vevent.last_modified.value = now orig_dtstart = org_ev.vevent.dtstart.value orig_dtend = org_ev.vevent.dtend.value ev_exception = org_ev.add("vevent") ev_exception.add('created').value = now ev_exception.add('last-modified').value = now ev_exception.add('dtstamp').value = now ev_exception.add('uid').value = uid ev_exception.add('summary').value = summary # out of laziness, add the exception for the first occurence of the event recurrence_id = orig_dtstart ev_exception.add('recurrence-id').value = recurrence_id ev_exception.add('transp').value = "OPAQUE" ev_exception.add('description').value = "Exception" ev_exception.add('sequence').value = "1" ev_exception.add('dtstart').value = orig_dtstart ev_exception.add('dtend').value = orig_dtend # 2.1 Add attendee1 and organizer to the exception organizer = ev_exception.add('organizer') organizer.cn_param = self.user_name organizer.partstat_param = "ACCEPTED" organizer.value = self.user_email attendee = ev_exception.add('attendee') attendee.cn_param = self.attendee1_name attendee.rsvp_param = "TRUE" attendee.role_param = "REQ-PARTICIPANT" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.attendee1_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), org_ev, exp_status=204) # 3. Make sure the attendee got the event attendee_ev = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) for ev in attendee_ev.vevent_list: try: if (ev.recurrence_id.value): attendee_ev_exception = ev except: attendee_ev_master = ev # make sure sogo doesn't duplicate attendees - yes, we've seen that self.assertEquals(len(attendee_ev_master.attendee_list), 1) self.assertEquals(len(attendee_ev_exception.attendee_list), 1) # 4. The master event must contain the invitation, declined self.assertEquals(attendee_ev_master.attendee.partstat_param, "DECLINED") # 5. The exception event contain the invitation, NEEDS-ACTION self.assertEquals(attendee_ev_exception.attendee.partstat_param, "NEEDS-ACTION") # 6. attendee accepts invitation attendee_ev_exception.attendee.partstat_param = "ACCEPTED" self._putEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), attendee_ev, exp_status=204) # fetch the organizer's event org_ev = self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) for ev in org_ev.vevent_list: try: if (ev.recurrence_id.value): org_ev_exception = ev except: org_ev_master = ev # make sure sogo doesn't duplicate attendees self.assertEquals(len(org_ev_master.attendee_list), 1) self.assertEquals(len(org_ev_exception.attendee_list), 1) # 7. Make sure organizer got the accept for the exception and # that the attendee is still declined in the master self.assertEquals(org_ev_exception.attendee.partstat_param, "ACCEPTED") self.assertEquals(org_ev_master.attendee.partstat_param, "DECLINED") # 8. delete the attendee from the master event (uninvite) # The event should be deleted from the attendee's calendar del org_ev_exception.attendee self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), org_ev, exp_status=204) del org_ev_master.attendee self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), org_ev, exp_status=204) self._getEvent(self.client, "%s%s" % (self.attendee1_calendar, ics_name), exp_status=404) # now be happy def testRruleInvitationDeleteExdate(self): """RRULE invitation delete exdate dance""" # Workflow: # Create an recurring event and invite Bob # Add an exdate to the master event # Verify that the exdate has propagated to Bob's calendar # Add an exdate to bob's version of the event # Verify that an exception has been created in the org's calendar # and that bob is 'declined' ics_name = "test-rrule-invitation-deleted-exdate-dance.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar, ics_name), None) self._deleteEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), None) # 1. create a recurring event in the organiser's calendar summary="Test-rrule-invitation-deleted-exdate-dance" uid=summary event = self._newEvent(summary, uid) event.vevent.add('rrule').value = "FREQ=DAILY;COUNT=5" organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.partstat_param = "ACCEPTED" organizer.value = self.user_email attendee = event.vevent.add('attendee') attendee.cn_param = self.attendee1_name attendee.rsvp_param = "TRUE" attendee.role_param = "REQ-PARTICIPANT" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.attendee1_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # 2. Make sure the attendee got it self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) # 3. Add exdate to master event org_ev=self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) orig_dtstart = org_ev.vevent.dtstart.value # exdate is a list in vobject.icalendar org_exdate = [orig_dtstart.astimezone(dateutil.tz.gettz("UTC"))] org_ev.vevent.add('exdate').value = org_exdate self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), org_ev, exp_status=204) # 4. make sure the attendee has the exdate attendee_ev = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) self.assertEqual(org_exdate, attendee_ev.vevent.exdate.value) # 5. Create an exdate in the attendee's calendar new_exdate = orig_dtstart + datetime.timedelta(days=2) attendee_exdate = [new_exdate.astimezone(dateutil.tz.gettz("UTC"))] attendee_ev.vevent.add('exdate').value = attendee_exdate now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal")) attendee_ev.vevent.last_modified.value = now self._putEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), attendee_ev, exp_status=204) # 6. Make sure the attendee is: # needs-action in master event # declined in the new exception created by the exdate above org_ev=self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) for ev in org_ev.vevent_list: try: if (ev.recurrence_id.value == attendee_exdate[0]): org_ev_exception = ev except: org_ev_master = ev self.assertTrue(org_ev_exception) # make sure sogo doesn't duplicate attendees self.assertEquals(len(org_ev_master.attendee_list), 1) self.assertEquals(len(org_ev_exception.attendee_list), 1) self.assertEqual(org_ev_master.attendee.partstat_param, "NEEDS-ACTION"); self.assertEqual(org_ev_exception.attendee.partstat_param, "DECLINED"); def testOrganizerIsAttendee(self): """ iCal organizer is attendee - bug #1839 """ # This tries to have the same behavior as iCal # 1. create an event, add an attendee and add the organizer as an attendee # 2. SOGo should remove the organizer from the attendee list ics_name = "test-organizer-is-attendee.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar, ics_name), None) self._deleteEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), None) # 1. create a recurring event in the organiser's calendar summary="org is attendee" uid=summary event = self._newEvent(summary, uid) organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.partstat_param = "ACCEPTED" organizer.value = self.user_email attendee = event.vevent.add('attendee') attendee.cn_param = self.attendee1_name attendee.rsvp_param = "TRUE" attendee.role_param = "REQ-PARTICIPANT" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.attendee1_email # 1.1 add the organizer as an attendee attendee = event.vevent.add('attendee') attendee.cn_param = self.user_name attendee.rsvp_param = "TRUE" attendee.role_param = "REQ-PARTICIPANT" attendee.partstat_param = "ACCEPTED" attendee.value = self.user_email self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # 2. Fetch the event and make sure the organizer is not in the attendee list anymore org_ev = self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) for attendee in org_ev.vevent.attendee_list: self.assertNotEqual(self.user_email, attendee.value) def testEventsWithSameUID(self): """ PUT 2 events with the same UID - bug #1853 """ ics_name = "test-same-uid.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar, ics_name), None) conflict_ics_name = "test-same-uid-conflict.ics" self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar, conflict_ics_name), None) # 1. create simple event summary="same uid" uid=summary event = self._newEvent(summary, uid) self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # PUT the same event with a new filename - should trigger a 403 self._putEvent(self.client, "%s%s" % (self.user_calendar, conflict_ics_name), event, exp_status=403) def testInvitationDelegation(self): """ invitation delegation """ ics_name = "test-delegation.ics" self.ics_list += [ics_name] # the invitation must not exist self._deleteEvent(self.client, "%s%s" % (self.user_calendar, ics_name), None) self._deleteEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), None) self._deleteEvent(self.attendee1_delegate_client, "%s%s" % (self.attendee1_delegate_calendar, ics_name), None) # 1. org -> attendee => org: 1, attendee: 1 (pst=N-A), delegate: 0 invitation = self._newEvent() invitation.add("method").value = "REQUEST" organizer = invitation.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email attendee = invitation.vevent.add('attendee') attendee.cn_param = self.attendee1_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.attendee1_email self._postEvent(self.client, self.user_calendar, invitation, self.user_email, [self.attendee1_email]) del invitation.method self._putEvent(self.client, "%stest-delegation.ics" % self.user_calendar, invitation) att_inv = self._getEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar) self._compareAttendees(att_inv, invitation) # 2. attendee delegates to delegate # => org: 1 (updated), attendee: 1 (updated,pst=D), # delegate: 1 (new,pst=N-A) invitation.add("method").value = "REQUEST" attendee1 = invitation.vevent.attendee attendee1.partstat_param = "DELEGATED" attendee1.delegated_to_param = self.attendee1_delegate_email delegate = invitation.vevent.add('attendee') delegate.delegated_from_param = self.attendee1_email delegate.cn_param = self.attendee1_delegate_name delegate.rsvp_param = "TRUE" delegate.partstat_param = "NEEDS-ACTION" delegate.value = self.attendee1_delegate_email self._postEvent(self.attendee1_client, self.attendee1_calendar, invitation, self.attendee1_email, [self.attendee1_delegate_email]) invitation.method.value = "REPLY" self._postEvent(self.attendee1_client, self.attendee1_calendar, invitation, self.attendee1_email, [self.user_email]) del invitation.method self._putEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar, invitation, 204) del_inv = self._getEvent(self.attendee1_delegate_client, "%stest-delegation.ics" % self.attendee1_delegate_calendar) self._compareAttendees(del_inv, invitation) org_inv = self._getEvent(self.client, "%stest-delegation.ics" % self.user_calendar) self._compareAttendees(org_inv, invitation) # 3. delegate accepts # => org: 1 (updated), attendee: 1 (updated,pst=D), # delegate: 1 (accepted,pst=A) invitation.add("method").value = "REPLY" delegate.partstat_param = "ACCEPTED" self._postEvent(self.attendee1_delegate_client, self.attendee1_delegate_calendar, invitation, self.attendee1_delegate_email, [self.user_email, self.attendee1_email]) del invitation.method self._putEvent(self.attendee1_delegate_client, "%stest-delegation.ics" % self.attendee1_delegate_calendar, invitation, 204) org_inv = self._getEvent(self.client, "%stest-delegation.ics" % self.user_calendar) self._compareAttendees(org_inv, invitation) att_inv = self._getEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar) self._compareAttendees(att_inv, invitation) # 4. attendee accepts # => org: 1 (updated), attendee: 1 (updated,pst=A), # delegate: 0 (cancelled, deleted) cancellation = vobject.iCalendar() cancellation.copy(invitation) cancellation.add("method").value = "CANCEL" cancellation.vevent.sequence.value = "1" self._postEvent(self.attendee1_client, self.attendee1_calendar, cancellation, self.attendee1_email, [self.attendee1_delegate_email]) attendee1 = invitation.vevent.attendee attendee1.partstat_param = "ACCEPTED" del attendee1.delegated_to_param invitation.add("method").value = "REPLY" invitation.vevent.remove(delegate) self._postEvent(self.attendee1_client, self.attendee1_calendar, invitation, self.attendee1_email, [self.user_email]) del invitation.method self._putEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar, invitation, 204) org_inv = self._getEvent(self.client, "%stest-delegation.ics" % self.user_calendar) self._compareAttendees(org_inv, invitation) del_inv = self._getEvent(self.attendee1_delegate_client, "%stest-delegation.ics" % self.attendee1_delegate_calendar, 404) # 5. org updates inv. # => org: 1 (updated), attendee: 1 (updated), delegate: 0 invitation.add("method").value = "REQUEST" invitation.vevent.summary.value = "Updated invitation" invitation.vevent.sequence.value = "1" attendee.partstat_param = "NEEDS-ACTION" now = datetime.datetime.now() invitation.vevent.last_modified.value = now invitation.vevent.dtstamp.value = now self._postEvent(self.client, self.user_calendar, invitation, self.user_email, [self.attendee1_email]) del invitation.method self._putEvent(self.client, "%stest-delegation.ics" % self.user_calendar, invitation, 204) att_inv = self._getEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar) self._compareAttendees(att_inv, invitation) # 6. attendee delegates to delegate # => org: 1 (updated), attendee: 1 (updated), delegate: 1 (new) invitation.add("method").value = "REQUEST" attendee1.partstat_param = "DELEGATED" attendee1.delegated_to_param = self.attendee1_delegate_email delegate = invitation.vevent.add('attendee') delegate.delegated_from_param = self.attendee1_email delegate.cn_param = self.attendee1_delegate_name delegate.rsvp_param = "TRUE" delegate.partstat_param = "NEEDS-ACTION" delegate.value = self.attendee1_delegate_email self._postEvent(self.attendee1_client, self.attendee1_calendar, invitation, self.attendee1_email, [self.attendee1_delegate_email]) invitation.method.value = "REPLY" self._postEvent(self.attendee1_client, self.attendee1_calendar, invitation, self.attendee1_email, [self.user_email]) del invitation.method self._putEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar, invitation, 204) org_inv = self._getEvent(self.client, "%stest-delegation.ics" % self.user_calendar) self._compareAttendees(org_inv, invitation) del_inv = self._getEvent(self.attendee1_delegate_client, "%stest-delegation.ics" % self.attendee1_delegate_calendar) self._compareAttendees(del_inv, invitation) # 7. delegate accepts # => org: 1 (updated), attendee: 1 (updated), delegate: 1 (accepted) invitation.add("method").value = "REPLY" delegate.partstat_param = "ACCEPTED" self._postEvent(self.attendee1_delegate_client, self.attendee1_delegate_calendar, invitation, self.attendee1_delegate_email, [self.user_email, self.attendee1_email]) del invitation.method self._putEvent(self.attendee1_delegate_client, "%stest-delegation.ics" % self.attendee1_delegate_calendar, invitation, 204) org_inv = self._getEvent(self.client, "%stest-delegation.ics" % self.user_calendar) self._compareAttendees(org_inv, invitation) att_inv = self._getEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar) self._compareAttendees(att_inv, invitation) # 8. org updates inv. # => org: 1 (updated), attendee: 1 (updated,partstat unchanged), # delegate: 1 (updated,partstat reset) invitation.add("method").value = "REQUEST" now = datetime.datetime.now() invitation.vevent.last_modified.value = now invitation.vevent.dtstamp.value = now invitation.vevent.summary.value = "Updated invitation (again)" invitation.vevent.sequence.value = "2" delegate.partstat_param = "NEEDS-ACTION" self._postEvent(self.client, self.user_calendar, invitation, self.user_email, [self.attendee1_email, self.attendee1_delegate_email]) del invitation.method self._putEvent(self.client, "%stest-delegation.ics" % self.user_calendar, invitation, 204) att_inv = self._getEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar) self._compareAttendees(att_inv, invitation) del_inv = self._getEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar) self._compareAttendees(del_inv, invitation) # 9. org cancels invitation # => org: 1 (updated), attendee: 0 (cancelled, deleted), # delegate: 0 (cancelled, deleted) invitation.add("method").value = "CANCEL" now = datetime.datetime.now() invitation.vevent.last_modified.value = now invitation.vevent.dtstamp.value = now invitation.vevent.summary.value = "Cancelled invitation (again)" invitation.vevent.sequence.value = "3" self._postEvent(self.client, self.user_calendar, invitation, self.user_email, [self.attendee1_email, self.attendee1_delegate_email]) del invitation.method invitation.vevent.remove(attendee) invitation.vevent.remove(delegate) self._putEvent(self.client, "%stest-delegation.ics" % self.user_calendar, invitation, 204) att_inv = self._getEvent(self.attendee1_client, "%stest-delegation.ics" % self.attendee1_calendar, 404) del_inv = self._getEvent(self.attendee1_delegate_client, "%stest-delegation.ics" % self.attendee1_delegate_calendar, 404) if __name__ == "__main__": sogotests.runTests()