[CalendarServer-changes] [13543] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Tue May 27 18:49:09 PDT 2014
Revision: 13543
http://trac.calendarserver.org//changeset/13543
Author: cdaboo at apple.com
Date: 2014-05-27 18:49:09 -0700 (Tue, 27 May 2014)
Log Message:
-----------
Fix SCHEDULE-AGENT=CLIENT issues so that the server does its best to fix previously broken state.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/ical.py
CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_implicit.py
Added Paths:
-----------
CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_processing.py
Removed Paths:
-------------
CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_pocessing.py
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2014-05-27 21:47:15 UTC (rev 13542)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2014-05-28 01:49:09 UTC (rev 13543)
@@ -2229,6 +2229,26 @@
return is_server
+ def cleanOrganizerScheduleAgent(self):
+ """
+ Remove components whose ORGANIZER property does not have
+ SCHEDULE-AGENT=SERVER.
+ """
+
+ changed = False
+ for component in tuple(self.subcomponents()):
+ if component.name() in ignoredComponents:
+ continue
+
+ organizerProp = component.getOrganizerProperty()
+ if organizerProp is not None:
+ if organizerProp.parameterValue("SCHEDULE-AGENT", "SERVER") != "SERVER":
+ self.removeComponent(component)
+ changed = True
+
+ return changed
+
+
def recipientPropertyName(self):
return "VOTER" if self.name() == "VPOLL" else "ATTENDEE"
Modified: CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_icalendar.py 2014-05-27 21:47:15 UTC (rev 13542)
+++ CalendarServer/trunk/twistedcaldav/test/test_icalendar.py 2014-05-28 01:49:09 UTC (rev 13543)
@@ -10620,3 +10620,163 @@
changed = component.hasDuplicatePrivateComments(doFix=True)
self.assertEqual(sorted(normalize_iCalStr(component).splitlines()), sorted(normalize_iCalStr(result).splitlines()), msg=title)
self.assertEqual(changed, result_changed, msg=title)
+
+
+ def test_cleanOrganizerScheduleAgent(self):
+ """
+ Test that L{Component.cleanOrganizerScheduleAgent} correctly removes components.
+ """
+
+ data = (
+ (
+ "All SERVER - master only",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ False,
+ ),
+ (
+ "All SERVER - master and overrides",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ False,
+ ),
+ (
+ "Master CLIENT and override SERVER",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER;SCHEDULE-AGENT=CLIENT:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DURATION:PT1H
+DTSTAMP:20080601T120000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ True,
+ ),
+ )
+
+ for title, txt, result, result_changed in data:
+ component = Component.fromString(txt)
+ changed = component.cleanOrganizerScheduleAgent()
+ self.assertEqual(sorted(normalize_iCalStr(component).splitlines()), sorted(normalize_iCalStr(result).splitlines()), msg=title)
+ self.assertEqual(changed, result_changed, msg=title)
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py 2014-05-27 21:47:15 UTC (rev 13542)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py 2014-05-28 01:49:09 UTC (rev 13543)
@@ -149,6 +149,7 @@
# If the new data has no organizer, then there must also be no attendees
if self.organizer is None and self.attendees:
+ log.error("organizer-allowed: Organizer removal also requires attendees to be removed for UID: {uid}", uid=self.uid)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "organizer-allowed"),
@@ -171,6 +172,12 @@
(existing_type != new_type) and
existing_resource
):
+ log.error(
+ "valid-attendee-change: Cannot change scheduling object mode from {old} to {new} for UID: {uid}",
+ old=existing_type,
+ new=new_type,
+ uid=self.uid,
+ )
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -179,6 +186,7 @@
# Organizer events must have a master component
if self.state == "organizer" and self.calendar.masterComponent() is None:
+ log.error("organizer-allowed: Organizer cannot schedule without a master component for UID: {uid}", uid=self.uid)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "organizer-allowed"),
@@ -260,7 +268,7 @@
# to create new scheduling resources.
if self.action == "create":
if self.organizerAddress.hosted() and not self.organizerAddress.record.enabledAsOrganizer():
- log.error("ORGANIZER not allowed to be an Organizer: {organizer}", organizer=self.organizer)
+ log.error("organizer-allowed: ORGANIZER not allowed to be an Organizer: {organizer}", organizer=self.organizer)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "organizer-allowed"),
@@ -499,7 +507,7 @@
self.organizer = self.calendar.validOrganizerForScheduling()
except ValueError:
# We have different ORGANIZERs in the same iCalendar object - this is an error
- log.error("Only one ORGANIZER is allowed in an iCalendar object:\n{calendar}", calendar=self.calendar)
+ log.error("single-organizer: Only one ORGANIZER is allowed in an iCalendar object:\n{calendar}", calendar=self.calendar)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "single-organizer"),
@@ -546,7 +554,7 @@
# Check for matching resource somewhere else in the home
foundElsewhere = (yield self.calendar_home.hasCalendarResourceUIDSomewhereElse(self.uid, check_resource, mode))
if foundElsewhere is not None:
- log.debug("Implicit - found component with same UID in a different collection: {resource}", resource=check_resource)
+ log.debug("unique-scheduling-object-resource: Found component with same UID in a different collection: {resource}", resource=check_resource)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "unique-scheduling-object-resource"),
@@ -823,7 +831,7 @@
oldOrganizer = self.oldcalendar.getOrganizer()
newOrganizer = self.calendar.getOrganizer()
if oldOrganizer != newOrganizer:
- log.error("Cannot change ORGANIZER: UID:{uid}", uid=self.uid)
+ log.error("valid-organizer-change: Cannot change ORGANIZER: UID:{uid}", uid=self.uid)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-organizer-change"),
@@ -1389,7 +1397,7 @@
oldOrganizer = self.oldcalendar.getOrganizer()
newOrganizer = self.calendar.getOrganizer()
if oldOrganizer != newOrganizer:
- log.error("Cannot change ORGANIZER: UID:{uid}", uid=self.uid)
+ log.error("valid-attendee-change: Cannot change ORGANIZER: UID:{uid}", uid=self.uid)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1404,13 +1412,24 @@
# If Organizer copy exists we cannot allow SCHEDULE-AGENT=CLIENT or NONE
if not doScheduling:
- log.error("Attendee '{attendee}' is not allowed to change SCHEDULE-AGENT on organizer: UID:{uid}", attendee=self.attendeeAddress.record, uid=self.uid)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-attendee-change"),
- "Cannot alter organizer",
- ))
+ # If an existing resource is present and it does not have SCHEDULE-AGENT=SERVER, then
+ # try and fix the situation by using the organizer's copy of the event and stripping
+ # the incoming attendee copy of any SCHEDULE-AGENT=CLIENT components. That should allow
+ # a fixed version of the data to be stored and proper scheduling to occur.
+ if self.oldcalendar is not None and not self.oldcalendar.getOrganizerScheduleAgent():
+ self.oldcalendar = self.organizer_calendar.duplicate()
+ self.oldcalendar.attendeesView((self.attendee,), onlyScheduleAgentServer=True)
+ self.calendar.cleanOrganizerScheduleAgent()
+ doScheduling = True
+ if not doScheduling:
+ log.error("valid-attendee-change: Attendee '{attendee}' is not allowed to change SCHEDULE-AGENT on organizer: UID:{uid}", attendee=self.attendeeAddress.record, uid=self.uid)
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-attendee-change"),
+ "Cannot alter organizer",
+ ))
+
# Determine whether the current change is allowed
changeAllowed, doITipReply, changedRids, newCalendar = self.isAttendeeChangeInsignificant()
if changeAllowed:
@@ -1422,7 +1441,7 @@
self.return_status = ImplicitScheduler.STATUS_ORPHANED_EVENT
returnValue(None)
else:
- log.error("Attendee '{attendee}' is not allowed to make an unauthorized change to an organized event: UID:{uid}", attendee=self.attendeeAddress.record, uid=self.uid)
+ log.error("valid-attendee-change: Attendee '{attendee}' is not allowed to make an unauthorized change to an organized event: UID:{uid}", attendee=self.attendeeAddress.record, uid=self.uid)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1442,7 +1461,7 @@
log.debug("Attendee '{attendee}' is allowed to update UID: '{uid}' with local organizer '{organizer}'", attendee=self.attendee, uid=self.uid, organizer=self.organizer)
elif isinstance(self.organizerAddress, LocalCalendarUser):
- # If Organizer copy does not exists we cannot allow SCHEDULE-AGENT=SERVER
+ # If Organizer copy does not exist we cannot allow SCHEDULE-AGENT=SERVER
if doScheduling:
# Check to see whether all instances are CANCELLED
if self.calendar.hasPropertyValueInAllComponents(Property("STATUS", "CANCELLED")):
@@ -1454,7 +1473,7 @@
if self.oldcalendar:
oldScheduling = self.oldcalendar.getOrganizerScheduleAgent()
if not oldScheduling:
- log.error("Attendee '{attendee}' is not allowed to set SCHEDULE-AGENT=SERVER on organizer: UID:{uid}", attendee=self.attendeeAddress.record, uid=self.uid)
+ log.error("valid-attendee-change: Attendee '{attendee}' is not allowed to set SCHEDULE-AGENT=SERVER on organizer: UID:{uid}", attendee=self.attendeeAddress.record, uid=self.uid)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1511,7 +1530,7 @@
oldOrganizer = self.oldcalendar.getOrganizer()
newOrganizer = self.calendar.getOrganizer()
if oldOrganizer != newOrganizer and self.oldcalendar.getOrganizerScheduleAgent():
- log.error("Cannot change ORGANIZER: UID:{uid}", uid=self.uid)
+ log.error("valid-attendee-change: Cannot change ORGANIZER: UID:{uid}", uid=self.uid)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1531,7 +1550,7 @@
break
if found_old:
- log.error("Cannot remove ATTENDEE: UID:%s" % (self.uid,))
+ log.error("valid-attendee-change: Cannot remove ATTENDEE: UID:%s" % (self.uid,))
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
@@ -1601,7 +1620,7 @@
oldcalendar = self.organizer_calendar
oldcalendar.attendeesView((self.attendee,), onlyScheduleAgentServer=True)
if oldcalendar.mainType() is None:
- log.debug("Implicit - attendee '{attendee}' cannot use an event they are not an attendee of, UID: '{uid}'", attendee=self.attendee, uid=self.uid)
+ log.debug("valid-attendee-change: Attendee '{attendee}' cannot use an event they are not an attendee of, UID: '{uid}'", attendee=self.attendee, uid=self.uid)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-attendee-change"),
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py 2014-05-27 21:47:15 UTC (rev 13542)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py 2014-05-28 01:49:09 UTC (rev 13543)
@@ -351,8 +351,11 @@
new_organizer = normalizeCUAddr(self.message.getOrganizer())
new_organizer = normalizeCUAddr(new_organizer) if new_organizer else ""
if existing_organizer != new_organizer:
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- raise ImplicitProcessorException("5.3;Organizer change not allowed")
+ # Additional check - if the existing organizer is missing and the originator
+ # is local to the server - then allow the change
+ if not (existing_organizer == "" and self.originator.hosted()):
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ raise ImplicitProcessorException("5.3;Organizer change not allowed")
# Handle splitting of data early so we can preserve per-attendee data
if self.message.hasProperty("X-CALENDARSERVER-SPLIT-OLDER-UID"):
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_implicit.py 2014-05-27 21:47:15 UTC (rev 13542)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_implicit.py 2014-05-28 01:49:09 UTC (rev 13543)
@@ -1617,3 +1617,317 @@
self.assertEqual(len(list2), 2)
self.assertTrue(list2[0].startswith(hashlib.md5("12345-67890").hexdigest()))
self.assertTrue(list2[1].startswith(hashlib.md5("12345-67890").hexdigest()))
+
+
+
+class ScheduleAgentFixBase(CommonCommonTests, TestCase):
+ """
+ Test txdav.caldav.datastore.scheduling.implicit.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(ScheduleAgentFixBase, self).setUp()
+ yield self.buildStoreAndDirectory()
+ yield self.populate()
+ self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 0)
+
+
+ @inlineCallbacks
+ def populate(self):
+ yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+ self.notifierFactory.reset()
+
+ metadata = {
+ "accessMode": "PUBLIC",
+ "isScheduleObject": True,
+ "scheduleTag": "abc",
+ "scheduleEtags": (),
+ "hasPrivateComment": False,
+ }
+
+ @classproperty(cache=False)
+ def requirements(cls): #@NoSelf
+ return {
+ "user01": {
+ "calendar_1": {
+ "organizer.ics": (cls.organizer_data, cls.metadata),
+ },
+ "inbox": {
+ },
+ },
+ "user02": {
+ "calendar_1": {
+ "attendee2.ics": (cls.attendee2_data, cls.metadata),
+ },
+ "inbox": {
+ },
+ },
+ "user03": {
+ "calendar_1": {
+ "attendee3.ics": (cls.attendee3_data, cls.metadata),
+ },
+ "inbox": {
+ },
+ },
+ }
+
+
+
+class ScheduleAgentFix(ScheduleAgentFixBase):
+ """
+ Test that implicit scheduling where an attendee has S-A=CLIENT and S-A=SERVER is
+ corrected when the attendee updates.
+ """
+
+ organizer_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user03
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+RECURRENCE-ID:20140102T100000Z
+DTSTART:20140102T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user02
+ATTENDEE:urn:x-uid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee2_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER;SCHEDULE-AGENT=CLIENT:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user03
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+RECURRENCE-ID:20140102T100000Z
+DTSTART:20140102T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER;SCHEDULE-AGENT=SERVER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user02
+ATTENDEE:urn:x-uid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee2_update_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER;SCHEDULE-AGENT=CLIENT:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user03
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+RECURRENCE-ID:20140102T100000Z
+DTSTART:20140102T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER;SCHEDULE-AGENT=SERVER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:user02
+ATTENDEE:urn:x-uid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee3_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user03
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+RECURRENCE-ID:20140102T100000Z
+DTSTART:20140102T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user02
+ATTENDEE:urn:x-uid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+
+ @inlineCallbacks
+ def test_doImplicitScheduling(self):
+ """
+ Test that doImplicitScheduling fixes an inconsistent schedule-agent state when an
+ attendee stores their data.
+ """
+
+ cobj = yield self.calendarObjectUnderTest(home="user02", name="attendee2.ics")
+ yield cobj.setComponent(Component.fromString(self.attendee2_update_data))
+ yield self.commit()
+
+ cobj = yield self.calendarObjectUnderTest(home="user02", name="attendee2.ics")
+ comp = yield cobj.component()
+ self.assertTrue(comp.masterComponent() is None)
+ self.assertTrue(comp.getOrganizerScheduleAgent())
+
+ inbox = yield self.calendarUnderTest(home="user01", name="inbox")
+ cobjs = yield inbox.calendarObjects()
+ self.assertTrue(len(cobjs) == 1)
+
+
+
+class MissingOrganizerFix(ScheduleAgentFixBase):
+ """
+ Test that an attendee with a copy of an event without any organizer or attendee
+ properties is corrected when the organizer updates.
+ """
+
+ organizer_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ organizer_update_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user02
+ATTENDEE:urn:x-uid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee2_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+END:VEVENT
+END:VCALENDAR
+"""
+
+ attendee3_data = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20140101T100000Z
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user01
+ATTENDEE:urn:x-uid:user03
+END:VEVENT
+END:VCALENDAR
+"""
+
+
+ @inlineCallbacks
+ def test_doImplicitScheduling(self):
+ """
+ Test that doImplicitScheduling fixes an inconsistent schedule-agent state when an
+ attendee stores their data.
+ """
+
+ cobj = yield self.calendarObjectUnderTest(home="user02", name="attendee2.ics")
+ comp = yield cobj.component()
+ self.assertTrue(comp.getOrganizer() is None)
+ yield self.commit()
+
+ cobj = yield self.calendarObjectUnderTest(home="user01", name="organizer.ics")
+ yield cobj.setComponent(Component.fromString(self.organizer_update_data))
+ yield self.commit()
+
+ cobj = yield self.calendarObjectUnderTest(home="user02", name="attendee2.ics")
+ comp = yield cobj.component()
+ self.assertTrue(comp.getOrganizer() is not None)
+
+ inbox = yield self.calendarUnderTest(home="user02", name="inbox")
+ cobjs = yield inbox.calendarObjects()
+ self.assertTrue(len(cobjs) == 1)
Deleted: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_pocessing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_pocessing.py 2014-05-27 21:47:15 UTC (rev 13542)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_pocessing.py 2014-05-28 01:49:09 UTC (rev 13543)
@@ -1,234 +0,0 @@
-##
-# Copyright (c) 2005-2014 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, succeed
-from twisted.trial import unittest
-
-from twistedcaldav import memcacher
-from twistedcaldav.ical import Component
-from twistedcaldav.stdconfig import config
-
-from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
-from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
-
-
-class FakeImplicitProcessor(ImplicitProcessor):
- """
- A fake ImplicitProcessor that tracks batch refreshes.
- """
-
- def __init__(self):
- self.batches = 0
-
-
- def _enqueueBatchRefresh(self, exclude_attendees):
- self.batches += 1
-
-
- def writeCalendarResource(self, collection, resource, calendar):
- return succeed(FakeResource())
-
-
-
-class FakePrincipal(object):
-
- def __init__(self, cuaddr):
- self.cuaddr = cuaddr
-
-
- def calendarUserAddresses(self):
- return (self.cuaddr,)
-
-
-
-class FakeResource(object):
-
- def parentCollection(self):
- return self
-
-
- def ownerHome(self):
- return self
-
-
- def uid(self):
- return None
-
-
- def id(self):
- return 1
-
-
-
-class BatchRefresh (unittest.TestCase):
- """
- iCalendar support tests
- """
-
- def setUp(self):
- super(BatchRefresh, self).setUp()
- config.Memcached.Pools.Default.ClientEnabled = False
- config.Memcached.Pools.Default.ServerEnabled = False
- memcacher.Memcacher.allowTestCache = True
- memcacher.Memcacher.memoryCacheInstance = None
-
-
- @inlineCallbacks
- def test_queueAttendeeUpdate_no_refresh(self):
-
- self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
-
- calendar = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE:urn:uuid:user01
-ATTENDEE:urn:uuid:user02
-END:VEVENT
-END:VCALENDAR
-""")
- processor = FakeImplicitProcessor()
- processor.txn = ""
- processor.uid = "12345-67890"
- processor.recipient_calendar_resource = FakeResource()
- processor.recipient_calendar = calendar
- yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
- self.assertEqual(processor.batches, 0)
-
-
- @inlineCallbacks
- def test_queueAttendeeUpdate_with_refresh(self):
-
- self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
-
- calendar = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE:urn:uuid:user01
-ATTENDEE:urn:uuid:user02
-ATTENDEE:urn:uuid:user03
-END:VEVENT
-END:VCALENDAR
-""")
- processor = FakeImplicitProcessor()
- processor.txn = ""
- processor.uid = "12345-67890"
- processor.recipient_calendar_resource = FakeResource()
- processor.recipient_calendar = calendar
- yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
- self.assertEqual(processor.batches, 1)
-
-
- @inlineCallbacks
- def test_queueAttendeeUpdate_count_suppressed(self):
-
- self.patch(config.Scheduling.Options, "AttendeeRefreshCountLimit", 5)
- self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
-
- calendar_small = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE:urn:uuid:user01
-ATTENDEE:urn:uuid:user02
-ATTENDEE:urn:uuid:user03
-ATTENDEE:urn:uuid:user04
-END:VEVENT
-END:VCALENDAR
-""")
- itip_small = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-METHOD:REPLY
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
-END:VEVENT
-END:VCALENDAR
-""")
- calendar_large = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE:urn:uuid:user01
-ATTENDEE:urn:uuid:user02
-ATTENDEE:urn:uuid:user03
-ATTENDEE:urn:uuid:user04
-ATTENDEE:urn:uuid:user05
-ATTENDEE:urn:uuid:user06
-ATTENDEE:urn:uuid:user07
-ATTENDEE:urn:uuid:user08
-ATTENDEE:urn:uuid:user09
-ATTENDEE:urn:uuid:user10
-END:VEVENT
-END:VCALENDAR
-""")
- itip_large = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-METHOD:REPLY
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER:urn:uuid:user01
-ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
-END:VEVENT
-END:VCALENDAR
-""")
-
- for count, calendar, itip, result, msg in (
- (5, calendar_small, itip_small, 1, "Small, count=5"),
- (5, calendar_large, itip_large, 0, "Large, count=5"),
- (0, calendar_small, itip_small, 1, "Small, count=0"),
- (0, calendar_large, itip_large, 1, "Large, count=0"),
- ):
- config.Scheduling.Options.AttendeeRefreshCountLimit = count
- processor = FakeImplicitProcessor()
- processor.txn = ""
- processor.recipient_calendar = calendar.duplicate()
- processor.uid = processor.recipient_calendar.newUID()
- processor.recipient_calendar_resource = FakeResource()
- processor.message = itip.duplicate()
- processor.message.newUID(processor.uid)
- processor.originator = LocalCalendarUser(None, None)
- processor.recipient = LocalCalendarUser(None, None)
- processor.uid = calendar.resourceUID()
- processor.noAttendeeRefresh = False
-
- processed = yield processor.doImplicitOrganizerUpdate()
- self.assertTrue(processed[3] is not None, msg=msg)
- self.assertEqual(processor.batches, result, msg=msg)
Copied: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_processing.py (from rev 13521, CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_pocessing.py)
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_processing.py (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_processing.py 2014-05-28 01:49:09 UTC (rev 13543)
@@ -0,0 +1,234 @@
+##
+# Copyright (c) 2005-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.trial import unittest
+
+from twistedcaldav import memcacher
+from twistedcaldav.ical import Component
+from twistedcaldav.stdconfig import config
+
+from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
+
+
+class FakeImplicitProcessor(ImplicitProcessor):
+ """
+ A fake ImplicitProcessor that tracks batch refreshes.
+ """
+
+ def __init__(self):
+ self.batches = 0
+
+
+ def _enqueueBatchRefresh(self, exclude_attendees):
+ self.batches += 1
+
+
+ def writeCalendarResource(self, collection, resource, calendar):
+ return succeed(FakeResource())
+
+
+
+class FakePrincipal(object):
+
+ def __init__(self, cuaddr):
+ self.cuaddr = cuaddr
+
+
+ def calendarUserAddresses(self):
+ return (self.cuaddr,)
+
+
+
+class FakeResource(object):
+
+ def parentCollection(self):
+ return self
+
+
+ def ownerHome(self):
+ return self
+
+
+ def uid(self):
+ return None
+
+
+ def id(self):
+ return 1
+
+
+
+class BatchRefresh (unittest.TestCase):
+ """
+ iCalendar support tests
+ """
+
+ def setUp(self):
+ super(BatchRefresh, self).setUp()
+ config.Memcached.Pools.Default.ClientEnabled = False
+ config.Memcached.Pools.Default.ServerEnabled = False
+ memcacher.Memcacher.allowTestCache = True
+ memcacher.Memcacher.memoryCacheInstance = None
+
+
+ @inlineCallbacks
+ def test_queueAttendeeUpdate_no_refresh(self):
+
+ self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
+
+ calendar = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+ processor = FakeImplicitProcessor()
+ processor.txn = ""
+ processor.uid = "12345-67890"
+ processor.recipient_calendar_resource = FakeResource()
+ processor.recipient_calendar = calendar
+ yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
+ self.assertEqual(processor.batches, 0)
+
+
+ @inlineCallbacks
+ def test_queueAttendeeUpdate_with_refresh(self):
+
+ self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
+
+ calendar = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+END:VEVENT
+END:VCALENDAR
+""")
+ processor = FakeImplicitProcessor()
+ processor.txn = ""
+ processor.uid = "12345-67890"
+ processor.recipient_calendar_resource = FakeResource()
+ processor.recipient_calendar = calendar
+ yield processor.queueAttendeeUpdate(("urn:uuid:user02", "urn:uuid:user01",))
+ self.assertEqual(processor.batches, 1)
+
+
+ @inlineCallbacks
+ def test_queueAttendeeUpdate_count_suppressed(self):
+
+ self.patch(config.Scheduling.Options, "AttendeeRefreshCountLimit", 5)
+ self.patch(config.Scheduling.Options, "AttendeeRefreshBatch", 5)
+
+ calendar_small = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+ATTENDEE:urn:uuid:user04
+END:VEVENT
+END:VCALENDAR
+""")
+ itip_small = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+ calendar_large = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE:urn:uuid:user01
+ATTENDEE:urn:uuid:user02
+ATTENDEE:urn:uuid:user03
+ATTENDEE:urn:uuid:user04
+ATTENDEE:urn:uuid:user05
+ATTENDEE:urn:uuid:user06
+ATTENDEE:urn:uuid:user07
+ATTENDEE:urn:uuid:user08
+ATTENDEE:urn:uuid:user09
+ATTENDEE:urn:uuid:user10
+END:VEVENT
+END:VCALENDAR
+""")
+ itip_large = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER:urn:uuid:user01
+ATTENDEE;PARTSTAT="ACCEPTED":urn:uuid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+ for count, calendar, itip, result, msg in (
+ (5, calendar_small, itip_small, 1, "Small, count=5"),
+ (5, calendar_large, itip_large, 0, "Large, count=5"),
+ (0, calendar_small, itip_small, 1, "Small, count=0"),
+ (0, calendar_large, itip_large, 1, "Large, count=0"),
+ ):
+ config.Scheduling.Options.AttendeeRefreshCountLimit = count
+ processor = FakeImplicitProcessor()
+ processor.txn = ""
+ processor.recipient_calendar = calendar.duplicate()
+ processor.uid = processor.recipient_calendar.newUID()
+ processor.recipient_calendar_resource = FakeResource()
+ processor.message = itip.duplicate()
+ processor.message.newUID(processor.uid)
+ processor.originator = LocalCalendarUser(None, None)
+ processor.recipient = LocalCalendarUser(None, None)
+ processor.uid = calendar.resourceUID()
+ processor.noAttendeeRefresh = False
+
+ processed = yield processor.doImplicitOrganizerUpdate()
+ self.assertTrue(processed[3] is not None, msg=msg)
+ self.assertEqual(processor.batches, result, msg=msg)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140527/ba7eaae2/attachment-0001.html>
More information about the calendarserver-changes
mailing list