Add a new test for bug #1837: 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 Monotone-Parent: b532b841f572a64aa9b471da32a51bb939539334 Monotone-Revision: 30b555a484fd45f63e941b88e3afc1f2784480da Monotone-Author: jraby@inverse.ca Monotone-Date: 2012-06-20T20:34:28
942 lines
41 KiB
Python
Executable file
942 lines
41 KiB
Python
Executable file
#!/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
|
|
|
|
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(isinstance(value, xml.etree.ElementTree._ElementInterface),
|
|
"schedule-calendar-transp must be an instance of" \
|
|
" %s, not %s"
|
|
% ("_ElementInterface", transp.__class__.__name__))
|
|
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_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
|
|
|
|
# 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)
|
|
|
|
|
|
def tearDown(self):
|
|
self._deleteEvent(self.client,
|
|
"%stest-delegation.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.attendee1_client,
|
|
"%stest-delegation.ics" % self.attendee1_calendar, None)
|
|
self._deleteEvent(self.attendee1_delegate_client,
|
|
"%stest-delegation.ics" % self.attendee1_delegate_calendar,
|
|
None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-add-attendee.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.attendee1_client,
|
|
"%stest-add-attendee.ics" % self.attendee1_calendar, None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-no-overbook.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-no-overbook-overlap.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-can-overbook.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-can-overbook-overlap.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-rrule-exception-invitation-dance.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.attendee1_client,
|
|
"%stest-rrule-exception-invitation-dance.ics" % self.attendee1_calendar, None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-rrule-invitation-deleted-exdate-dance.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.attendee1_client,
|
|
"%stest-rrule-invitation-deleted-exdate-dance.ics" % self.attendee1_calendar, None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-organizer-is-attendee.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.attendee1_client,
|
|
"%stest-organizer-is-attendee.ics" % self.attendee1_calendar, None)
|
|
self._deleteEvent(self.client,
|
|
"%stest-remove-attendee.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.attendee1_client,
|
|
"%stest-remove-attendee.ics" % self.attendee1_calendar, 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 _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._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._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 the event doesn't exist
|
|
ics_name = "test-no-overbook.ics"
|
|
self._deleteEvent(self.client,
|
|
"%s%s" % (self.user_calendar,ics_name), None)
|
|
|
|
ob_ics_name = "test-no-overbook-overlap.ics"
|
|
self._deleteEvent(self.client,
|
|
"%s%s" % (self.user_calendar,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 the event doesn't exist
|
|
ics_name = "test-can-overbook.ics"
|
|
self._deleteEvent(self.client,
|
|
"%s%s" % (self.user_calendar,ics_name), None)
|
|
|
|
ob_ics_name = "test-can-overbook-overlap.ics"
|
|
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 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._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._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 #1837 """
|
|
|
|
# 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._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 testInvitationDelegation(self):
|
|
""" invitation delegation """
|
|
|
|
# the invitation must not exist
|
|
self._deleteEvent(self.client,
|
|
"%stest-delegation.ics" % self.user_calendar, None)
|
|
self._deleteEvent(self.attendee1_client,
|
|
"%stest-delegation.ics" % self.attendee1_calendar, None)
|
|
self._deleteEvent(self.attendee1_delegate_client,
|
|
"%stest-delegation.ics" % self.attendee1_delegate_calendar,
|
|
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()
|