[CalendarServer-changes] [10596] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Mon Jan 28 19:35:09 PST 2013
Revision: 10596
http://trac.calendarserver.org//changeset/10596
Author: glyph at apple.com
Date: 2013-01-28 19:35:09 -0800 (Mon, 28 Jan 2013)
Log Message:
-----------
Un-share a direct shared calendar if external access is revoked. Also, refactor lots of tests and add docstrings to many things to better explain how it works.
Modified Paths:
--------------
CalendarServer/trunk/twext/web2/dav/test/util.py
CalendarServer/trunk/twext/web2/test/test_server.py
CalendarServer/trunk/twistedcaldav/directory/aggregate.py
CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py
CalendarServer/trunk/twistedcaldav/directory/wiki.py
CalendarServer/trunk/twistedcaldav/resource.py
CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py
CalendarServer/trunk/twistedcaldav/sharing.py
CalendarServer/trunk/twistedcaldav/test/test_sharing.py
CalendarServer/trunk/twistedcaldav/test/test_wrapping.py
CalendarServer/trunk/twistedcaldav/test/util.py
Property Changed:
----------------
CalendarServer/trunk/
Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
- /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
+ /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
Modified: CalendarServer/trunk/twext/web2/dav/test/util.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/test/util.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twext/web2/dav/test/util.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -29,20 +29,62 @@
from shutil import copy
from twisted.trial import unittest
+from twisted.internet import address
+
from twisted.internet.defer import Deferred
from twext.python.log import Logger
from twext.web2.http import HTTPError, StatusResponse
-from twext.web2 import responsecode
+from twext.web2 import responsecode, server
+from twext.web2 import http_headers
+from twext.web2 import stream
+
from twext.web2.dav.resource import TwistedACLInheritable
from twext.web2.dav.static import DAVFile
from twext.web2.dav.util import joinURL
from txdav.xml import element
from txdav.xml.base import encodeXMLName
+from twext.web2.http_headers import MimeType
+from twext.web2.dav.util import allDataFromStream
log = Logger()
+
+class SimpleRequest(server.Request):
+ """
+ A L{SimpleRequest} can be used in cases where a L{server.Request} object is
+ necessary but it is beneficial to bypass the concrete transport (and
+ associated logic with the C{chanRequest} attribute).
+ """
+
+ clientproto = (1,1)
+
+ def __init__(self, site, method, uri, headers=None, content=None):
+ if not headers:
+ headers = http_headers.Headers(headers)
+
+ super(SimpleRequest, self).__init__(
+ site=site,
+ chanRequest=None,
+ command=method,
+ path=uri,
+ version=self.clientproto,
+ contentLength=len(content or ''),
+ headers=headers)
+
+ self.stream = stream.MemoryStream(content or '')
+
+ self.remoteAddr = address.IPv4Address('TCP', '127.0.0.1', 0)
+ self._parseURL()
+ self.host = 'localhost'
+ self.port = 8080
+
+ def writeResponse(self, response):
+ return response
+
+
+
class InMemoryPropertyStore (object):
"""
A dead property store for keeping properties in memory
@@ -202,6 +244,17 @@
def send(self, request, callback=None):
+ """
+ Invoke the logic involved in traversing a given L{server.Request} as if
+ a client had sent it; call C{locateResource} to look up the resource to
+ be rendered, and render it by calling its C{renderHTTP} method.
+
+ @param request: A L{server.Request} (generally, to avoid real I/O, a
+ L{SimpleRequest}) already associated with a site.
+
+ @return: asynchronously return a response object or L{None}
+ @rtype: L{Deferred} firing L{Response} or L{None}
+ """
log.msg("Sending %s request for URI %s" % (request.method, request.uri))
d = request.locateResource(request.uri)
@@ -216,6 +269,57 @@
return d
+
+ def simpleSend(self, method, path="/", body="", mimetype="text",
+ subtype="xml", resultcode=responsecode.OK, headers=()):
+ """
+ Assemble and send a simple request using L{SimpleRequest}. This
+ L{SimpleRequest} is associated with this L{TestCase}'s C{site}
+ attribute.
+
+ @param method: the HTTP method
+ @type method: L{bytes}
+
+ @param path: the absolute path portion of the HTTP URI
+ @type path: L{bytes}
+
+ @param body: the content body of the request
+ @type body: L{bytes}
+
+ @param mimetype: the main type of the mime type of the body of the
+ request
+ @type mimetype: L{bytes}
+
+ @param subtype: the subtype of the mimetype of the body of the request
+ @type subtype: L{bytes}
+
+ @param resultcode: The expected result code for the response to the
+ request.
+ @type resultcode: L{int}
+
+ @param headers: An iterable of 2-tuples of C{(header, value)}; headers
+ to set on the outgoing request.
+
+ @return: a L{Deferred} which fires with a L{bytes} if the request was
+ successfully processed and fails with an L{HTTPError} if not; or,
+ if the resultcode does not match the response's code, fails with
+ L{FailTest}.
+ """
+ request = SimpleRequest(self.site, method, path, content=body)
+ if headers is not None:
+ for k, v in headers:
+ request.headers.setHeader(k, v)
+ request.headers.setHeader("content-type", MimeType(mimetype, subtype))
+ def checkResult(response):
+ self.assertEqual(response.code, resultcode)
+ if response.stream is None:
+ return None
+ return allDataFromStream(response.stream)
+ return self.send(request, None).addCallback(checkResult)
+
+
+
+
class Site:
# FIXME: There is no ISite interface; there should be.
# implements(ISite)
Modified: CalendarServer/trunk/twext/web2/test/test_server.py
===================================================================
--- CalendarServer/trunk/twext/web2/test/test_server.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twext/web2/test/test_server.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -10,6 +10,8 @@
from twisted.python import components
from twext.web2 import http, http_headers, iweb, server
from twext.web2 import resource, stream
+from twext.web2.dav.test.util import SimpleRequest
+
from twisted.trial import unittest
from twisted.internet import reactor, defer, address
@@ -114,37 +116,6 @@
-class SimpleRequest(server.Request):
- """I can be used in cases where a Request object is necessary
- but it is beneficial to bypass the chanRequest
- """
-
- clientproto = (1,1)
-
- def __init__(self, site, method, uri, headers=None, content=None):
- if not headers:
- headers = http_headers.Headers(headers)
-
- super(SimpleRequest, self).__init__(
- site=site,
- chanRequest=None,
- command=method,
- path=uri,
- version=self.clientproto,
- contentLength=len(content or ''),
- headers=headers)
-
- self.stream = stream.MemoryStream(content or '')
-
- self.remoteAddr = address.IPv4Address('TCP', '127.0.0.1', 0)
- self._parseURL()
- self.host = 'localhost'
- self.port = 8080
-
- def writeResponse(self, response):
- return response
-
-
class TestChanRequest:
implements(iweb.IChanRequest)
Modified: CalendarServer/trunk/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/aggregate.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/directory/aggregate.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -34,7 +34,12 @@
class AggregateDirectoryService(DirectoryService):
"""
- L{IDirectoryService} implementation which aggregates multiple directory services.
+ L{IDirectoryService} implementation which aggregates multiple directory
+ services.
+
+ @ivar _recordTypes: A map of record types to L{IDirectoryService}s.
+ @type _recordTypes: L{dict} mapping L{bytes} to L{IDirectoryService}
+ provider.
"""
baseGUID = "06FB225F-39E7-4D34-B1D1-29925F5E619B"
@@ -182,24 +187,25 @@
@inlineCallbacks
def recordsMatchingTokens(self, tokens, context=None):
"""
+ Combine the results from the sub-services.
+
+ Each token is searched for within each record's full name and email
+ address; if each token is found within a record that record is returned
+ in the results.
+
+ If context is None, all record types are considered. If context is
+ "location", only locations are considered. If context is "attendee",
+ only users, groups, and resources are considered.
+
@param tokens: The tokens to search on
@type tokens: C{list} of C{str} (utf-8 bytes)
- @param context: An indication of what the end user is searching
- for; "attendee", "location", or None
+
+ @param context: An indication of what the end user is searching for;
+ "attendee", "location", or None
@type context: C{str}
- @return: a deferred sequence of L{IDirectoryRecord}s which
- match the given tokens and optional context.
- Each token is searched for within each record's full name and
- email address; if each token is found within a record that
- record is returned in the results.
-
- If context is None, all record types are considered. If
- context is "location", only locations are considered. If
- context is "attendee", only users, groups, and resources
- are considered.
-
- Combine the results from the sub-services.
+ @return: a deferred sequence of L{IDirectoryRecord}s which match the
+ given tokens and optional context.
"""
services = set(self._recordTypes.values())
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_directory.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -77,13 +77,13 @@
def setUp(self):
super(GroupMembershipTests, self).setUp()
- self.directoryService = XMLDirectoryService(
+ self.directoryFixture.addDirectoryService(XMLDirectoryService(
{
'xmlFile' : xmlFile,
'augmentService' :
augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
}
- )
+ ))
calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
# Set up a principals hierarchy for each service we're testing with
@@ -787,13 +787,13 @@
def setUp(self):
super(RecordsMatchingTokensTests, self).setUp()
- self.directoryService = XMLDirectoryService(
+ self.directoryFixture.addDirectoryService(XMLDirectoryService(
{
'xmlFile' : xmlFile,
'augmentService' :
augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
}
- )
+ ))
calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
# Set up a principals hierarchy for each service we're testing with
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -37,13 +37,13 @@
def setUp(self):
super(ProxyPrincipals, self).setUp()
- self.directoryService = XMLDirectoryService(
+ self.directoryFixture.addDirectoryService(XMLDirectoryService(
{
'xmlFile' : xmlFile,
'augmentService' :
augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
}
- )
+ ))
calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
# Set up a principals hierarchy for each service we're testing with
Modified: CalendarServer/trunk/twistedcaldav/directory/wiki.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/wiki.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/directory/wiki.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -131,6 +131,42 @@
Ask the wiki server we're paired with what level of access the userID has
for the given wikiID. Possible values are "read", "write", and "admin"
(which we treat as "write").
+
+ @param userID: the GUID (UUID) of the user's directory record.
+ @type userID: L{bytes} (UTF-8)
+
+ @param wikiID: the short name of the wiki principal's synthetic directory
+ record. (See L{WikiDirectoryService}).
+ @type wikiID: L{bytes} (UTF-8)
+
+ @return: A string indicating the level of access that the given user has to
+ the given wiki. Possible values are:
+
+ 1. C{b"no-access"} for read-only access
+
+ 2. C{b"no-access"} for read/write access
+
+ 3. C{b"no-access"} for administrative access (which, for calendaring
+ purposes, should be equialent to read/write)
+
+ 4. C{b"no-access"} for a user who is not allowed to see the wiki at
+ all.
+
+ @rtype: L{bytes}
+
+ @raise: L{HTTPError} indicating that there is a problem requesting
+ permission information. This may be raised with a few different status
+ codes, each indicating a different problem:
+
+ 1. L{responsecode.FORBIDDEN}: The user represented by C{userID} did not
+ exist.
+
+ 2. L{responsecode.NOT_FOUND}: The wiki represented by C{wikiID} did not
+ exist.
+
+ 3. L{responsecode.SERVICE_UNAVAILABLE}: The service that we are
+ checking permissions with is currently offline or responding with an
+ unknown fault.
"""
wikiConfig = config.Authentication.Wiki
if method is None:
Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/resource.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -2074,6 +2074,15 @@
class CommonHomeResource(PropfindCacheMixin, SharedHomeMixin, CalDAVResource):
"""
Logic common to Calendar and Addressbook home resources.
+
+ @ivar _provisionedChildren: A map of resource names to built-in children
+ with protocol-level meanings, like C{"attachments"}, C{"inbox"},
+ C{"outbox"}, and so on.
+ @type _provisionedChildren: L{dict} mapping L{bytes} to L{Resource}
+
+ @ivar _provisionedLinks: A map of resource names to built-in links that the
+ server has inserted into this L{CommonHomeResource}.
+ @type _provisionedLinks: L{dict} mapping L{bytes} to L{Resource}
"""
cacheNotifierFactory = DisabledCacheNotifier
@@ -2252,10 +2261,11 @@
returnValue(child)
# get regular or shared child
- child = (yield self.makeRegularChild(name))
+ child = yield self.makeRegularChild(name)
- # add _share attribute if child is shared
- yield self.provisionShare(child)
+ # add _share attribute if child is shared; verify that child should
+ # still be accessible and convert it to None if it's not.
+ child = yield self.provisionShare(child)
returnValue(child)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule/test/test_resource.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -28,7 +28,9 @@
super(iSchedulePOST, self).setUp()
self.createStockDirectoryService()
self.setupCalendars()
- self.site.resource.putChild("ischedule", IScheduleInboxResource(self.site.resource, self._newStore))
+ self.site.resource.putChild(
+ "ischedule", IScheduleInboxResource(self.site.resource,
+ self.createDataStore()))
@inlineCallbacks
Modified: CalendarServer/trunk/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/sharing.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -57,7 +57,15 @@
class SharedCollectionMixin(object):
+ """
+ A mix-in for calendar/addressbook resources that implements sharing-related
+ functionality.
+ @ivar _share: If this L{SharedCollectionMixin} is the sharee's version of a
+ resource, this refers to the L{Share} that describes it.
+ @type _share: L{Share} or L{NoneType}
+ """
+
@inlineCallbacks
def inviteProperty(self, request):
"""
@@ -110,8 +118,10 @@
def upgradeToShare(self):
- """ Upgrade this collection to a shared state """
-
+ """
+ Set the resource-type property on this resource to indicate that this
+ is the owner's version of a resource which has been shared.
+ """
# Change resourcetype
rtype = self.resourceType()
rtype = element.ResourceType(*(rtype.children + (customxml.SharedOwner(),)))
@@ -160,11 +170,16 @@
@inlineCallbacks
def directShare(self, request):
"""
- Directly bind an accessible calendar/address book collection into the current
- principal's calendar/addressbook home.
+ Directly bind an accessible calendar/address book collection into the
+ current principal's calendar/addressbook home.
@param request: the request triggering this action
@type request: L{IRequest}
+
+ @return: the (asynchronous) HTTP result to respond to the direct-share
+ request.
+ @rtype: L{Deferred} firing L{twext.web2.http.Response}, failing with
+ L{HTTPError}
"""
# Need to have at least DAV:read to do this
@@ -228,17 +243,25 @@
@inlineCallbacks
def isShared(self, request):
- """ Return True if this is an owner shared calendar collection """
+ """
+ Return True if this is an owner shared calendar collection.
+ """
returnValue((yield self.isSpecialCollection(customxml.SharedOwner)))
def setShare(self, share):
- self._isShareeCollection = True # _isShareeCollection attr is used by self tests
+ """
+ Set the L{Share} associated with this L{SharedCollectionMixin}. (This
+ is only invoked on the sharee's resource, not the owner's.)
+ """
+ self._isShareeCollection = True
self._share = share
def isShareeCollection(self):
- """ Return True if this is a sharee view of a shared calendar collection """
+ """
+ Return True if this is a sharee view of a shared calendar collection.
+ """
return hasattr(self, "_isShareeCollection")
@@ -289,49 +312,82 @@
@inlineCallbacks
- def shareeAccessControlList(self, request, *args, **kwargs):
+ def _checkAccessControl(self):
"""
- Return WebDAV ACLs appropriate for the current user accessing the shared collection. For
- an "invite" share we take the privilege granted to the sharee in the invite and map that
- to WebDAV ACLs. For a "direct" share, if it is a wiki collection we map the wiki privileges
- into WebDAV ACLs, otherwise we use whatever privileges exist on the underlying shared
- collection.
+ Check the shared access mode of this resource, potentially consulting
+ an external access method if necessary.
- @return: the appropriate WebDAV ACL for the sharee
- @rtype: L{davxml.ACL}
+ @return: a L{Deferred} firing a L{bytes} or L{None}, with one of the
+ potential values: C{"own"}, which means that the home is the owner
+ of the collection and it is not shared; C{"read-only"}, meaning
+ that the home that this collection is bound into has only read
+ access to this collection; C{"read-write"}, which means that the
+ home has both read and write access; C{"original"}, which means
+ that it should inherit the ACLs of the owner's collection, whatever
+ those happen to be, or C{None}, which means that the external
+ access control mechanism has dictate the home should no longer have
+ any access at all.
"""
-
- assert self._isShareeCollection, "Only call this for a sharee collection"
-
- wikiAccessMethod = kwargs.get("wikiAccessMethod", getWikiAccess)
-
- sharee = self.principalForUID(self._share.shareeUID())
-
- # Direct shares use underlying privileges of shared collection
if self._share.direct():
- original = (yield request.locateResource(self._share.url()))
- owner = yield original.ownerPrincipal(request)
+ ownerUID = self._share.ownerUID()
+ owner = self.principalForUID(ownerUID)
if owner.record.recordType == WikiDirectoryService.recordType_wikis:
# Access level comes from what the wiki has granted to the
# sharee
+ sharee = self.principalForUID(self._share.shareeUID())
userID = sharee.record.guid
wikiID = owner.record.shortNames[0]
- access = (yield wikiAccessMethod(userID, wikiID))
+ access = (yield getWikiAccess(userID, wikiID))
if access == "read":
- access = "read-only"
+ returnValue("read-only")
elif access in ("write", "admin"):
- access = "read-write"
+ returnValue("read-write")
else:
- access = None
+ returnValue(None)
else:
- result = (yield original.accessControlList(request, *args,
- **kwargs))
- returnValue(result)
+ returnValue("original")
else:
# Invited shares use access mode from the invite
# Get the access for self
- access = Invitation(self._newStoreObject).access()
+ returnValue(Invitation(self._newStoreObject).access())
+
+ @inlineCallbacks
+ def shareeAccessControlList(self, request, *args, **kwargs):
+ """
+ Return WebDAV ACLs appropriate for the current user accessing the
+ shared collection. For an "invite" share we take the privilege granted
+ to the sharee in the invite and map that to WebDAV ACLs. For a
+ "direct" share, if it is a wiki collection we map the wiki privileges
+ into WebDAV ACLs, otherwise we use whatever privileges exist on the
+ underlying shared collection.
+
+ @param request: the request used to locate the owner resource.
+ @type request: L{twext.web2.iweb.IRequest}
+
+ @param args: The arguments for
+ L{twext.web2.dav.idav.IDAVResource.accessControlList}
+
+ @param kwargs: The keyword arguments for
+ L{twext.web2.dav.idav.IDAVResource.accessControlList}, plus
+ keyword-only arguments.
+
+ @return: the appropriate WebDAV ACL for the sharee
+ @rtype: L{davxml.ACL}
+ """
+
+ assert self._isShareeCollection, "Only call this for a sharee collection"
+
+ sharee = self.principalForUID(self._share.shareeUID())
+ access = yield self._checkAccessControl()
+
+ if access == "original":
+ original = (yield request.locateResource(self._share.url()))
+ result = (yield original.accessControlList(request, *args,
+ **kwargs))
+ returnValue(result)
+
+ # Direct shares use underlying privileges of shared collection
userprivs = [
]
if access in ("read-only", "read-write",):
@@ -991,13 +1047,35 @@
@inlineCallbacks
def provisionShare(self, child, request=None):
+ """
+ If the given child resource (a L{SharedCollectionMixin}) of this
+ L{SharedHomeMixin} is a I{sharee}'s view of a shared calendar object,
+ associate it with a L{Share}.
+ """
share = yield self._shareForHomeChild(child._newStoreObject, request)
if share:
child.setShare(share)
+ access = yield child._checkAccessControl()
+ if access is None:
+ returnValue(None)
+ returnValue(child)
@inlineCallbacks
def _shareForHomeChild(self, child, request=None):
+ """
+ Determine the L{Share} associated with the given child.
+
+ @param child: A calendar or addressbook data store object, a child of
+ the resource represented by this L{SharedHomeMixin} instance, which
+ may be shared.
+ @type child: L{txdav.caldav.icalendarstore.ICalendar} or
+ L{txdav.carddav.iaddressbookstore.IAddressBook}
+
+ @return: a L{Share} if C{child} is not the owner's view of the share,
+ or C{None}.
+ @rtype: L{Share} or L{NoneType}
+ """
# Try to find a matching share
if not child or child.owned():
returnValue(None)
@@ -1008,7 +1086,8 @@
sharer = self.principalForUID(sharerHomeChild.viewerHome().uid())
if not request:
- # FIXEME: Fake up a request that can be used to get the sharer home resource
+ # FIXME: Fake up a request that can be used to get the sharer home
+ # resource
class _FakeRequest(object):
pass
fakeRequest = _FakeRequest()
@@ -1021,7 +1100,8 @@
sharerHomeCollection = yield sharer.addressBookHome(request)
url = joinURL(sharerHomeCollection.url(), sharerHomeChild.name())
- share = Share(shareeHomeChild=child, sharerHomeChild=sharerHomeChild, url=url)
+ share = Share(shareeHomeChild=child, sharerHomeChild=sharerHomeChild,
+ url=url)
returnValue(share)
@@ -1053,20 +1133,25 @@
oldShare = yield self._shareForUID(inviteUID, request)
# Send the invite reply then add the link
- yield self._changeShare(request, "ACCEPTED", hostUrl, inviteUID, displayname)
+ yield self._changeShare(request, "ACCEPTED", hostUrl, inviteUID,
+ displayname)
if oldShare:
share = oldShare
else:
sharedCollection = yield request.locateResource(hostUrl)
shareeHomeChild = yield self._newStoreHome.childWithName(inviteUID)
- share = Share(shareeHomeChild=shareeHomeChild, sharerHomeChild=sharedCollection._newStoreObject, url=hostUrl)
+ share = Share(shareeHomeChild=shareeHomeChild,
+ sharerHomeChild=sharedCollection._newStoreObject,
+ url=hostUrl)
- response = yield self._acceptShare(request, not oldShare, share, displayname)
+ response = yield self._acceptShare(request, not oldShare, share,
+ displayname)
returnValue(response)
@inlineCallbacks
- def acceptDirectShare(self, request, hostUrl, resourceUID, displayname=None):
+ def acceptDirectShare(self, request, hostUrl, resourceUID,
+ displayname=None):
# Just add the link
oldShare = yield self._shareForUID(resourceUID, request)
@@ -1074,21 +1159,47 @@
share = oldShare
else:
sharedCollection = yield request.locateResource(hostUrl)
- sharedName = yield sharedCollection._newStoreObject.shareWith(shareeHome=self._newStoreHome,
- mode=_BIND_MODE_DIRECT,
- status=_BIND_STATUS_ACCEPTED,
- message=displayname)
+ sharedName = yield sharedCollection._newStoreObject.shareWith(
+ shareeHome=self._newStoreHome,
+ mode=_BIND_MODE_DIRECT,
+ status=_BIND_STATUS_ACCEPTED,
+ message=displayname
+ )
shareeHomeChild = yield self._newStoreHome.childWithName(sharedName)
- share = Share(shareeHomeChild=shareeHomeChild, sharerHomeChild=sharedCollection._newStoreObject, url=hostUrl)
+ share = Share(shareeHomeChild=shareeHomeChild,
+ sharerHomeChild=sharedCollection._newStoreObject,
+ url=hostUrl)
- response = yield self._acceptShare(request, not oldShare, share, displayname)
+ response = yield self._acceptShare(request, not oldShare, share,
+ displayname)
returnValue(response)
@inlineCallbacks
def _acceptShare(self, request, isNewShare, share, displayname=None):
+ """
+ Mark a pending shared invitation I{to} this, the owner's collection, as
+ accepted, generating the HTTP response to the request that accepted it.
+ @param request: The HTTP request that is accepting it.
+ @type request: L{twext.web2.iweb.IRequest}
+
+ @param isNewShare: a boolean indicating whether this share is new.
+ @type isNewShare: L{bool}
+
+ @param share: The share referencing the proposed sharer and sharee.
+ @type share: L{Share}
+
+ @param displayname: the UTF-8 encoded contents of the display-name
+ property on the resource to be created while accepting.
+ @type displayname: L{bytes}
+
+ @return: a L{twext.web2.iweb.IResponse} containing a serialized
+ L{customxml.SharedAs} element as its body.
+ @rtype: L{Deferred} firing L{XMLResponse}
+ """
+
# Get shared collection in non-share mode first
sharedCollection = yield request.locateResource(share.url())
@@ -1305,20 +1416,27 @@
-class SharedCollectionRecord(object):
+class Share(object):
+ """
+ A L{Share} represents information about a collection which has been shared
+ from one user to another.
+ """
- def __init__(self, shareuid, sharetype, hosturl, localname, summary):
- self.shareuid = shareuid
- self.sharetype = sharetype
- self.hosturl = hosturl
- self.localname = localname
- self.summary = summary
+ def __init__(self, sharerHomeChild, shareeHomeChild, url):
+ """
+ @param sharerHomeChild: The data store object representing the shared
+ collection as present in the owner's home collection; the owner's
+ reference.
+ @type sharerHomeChild: L{txdav.caldav.icalendarstore.ICalendar}
+ @param shareeHomeChild: The data store object representing the
+ collection as present in the sharee's home collection; the sharee's
+ reference.
+ @type shareeHomeChild: L{txdav.caldav.icalendarstore.ICalendar}
-
-class Share(object):
-
- def __init__(self, sharerHomeChild, shareeHomeChild, url):
+ @param url: The URL referring to the sharer's version of the resource.
+ @type url: L{bytes}
+ """
self._shareeHomeChild = shareeHomeChild
self._sharerHomeChild = sharerHomeChild
self._sharedResourceURL = url
@@ -1326,22 +1444,32 @@
@classmethod
def directUID(cls, shareeHome, sharerHomeChild):
- return "Direct-%s-%s" % (shareeHome._resourceID, sharerHomeChild._resourceID,)
+ return "Direct-%s-%s" % (shareeHome._resourceID,
+ sharerHomeChild._resourceID,)
def uid(self):
# Move to CommonHomeChild shareUID?
if self._shareeHomeChild.shareMode() == _BIND_MODE_DIRECT:
- return self.directUID(shareeHome=self._shareeHomeChild.viewerHome(), sharerHomeChild=self._sharerHomeChild,)
+ return self.directUID(shareeHome=self._shareeHomeChild.viewerHome(),
+ sharerHomeChild=self._sharerHomeChild,)
else:
return self._shareeHomeChild.shareUID()
def direct(self):
+ """
+ Is this L{Share} a "direct" share?
+
+ @return: a boolean indicating whether it's direct.
+ """
return self._shareeHomeChild.shareMode() == _BIND_MODE_DIRECT
def url(self):
+ """
+ @return: The URL to the owner's version of the shared collection.
+ """
return self._sharedResourceURL
@@ -1355,3 +1483,7 @@
def shareeUID(self):
return self._shareeHomeChild.viewerHome().uid()
+
+
+ def ownerUID(self):
+ return self._sharerHomeChild.ownerHome().uid()
Modified: CalendarServer/trunk/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_sharing.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/test/test_sharing.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -14,87 +14,174 @@
# limitations under the License.
##
+from xml.etree.cElementTree import XML
+from txdav.xml import element as davxml
+from txdav.xml.parser import WebDAVDocument
+
from twext.web2 import responsecode
-from txdav.xml import element as davxml
-from twext.web2.http_headers import MimeType
-from twext.web2.iweb import IResource
-from twext.web2.stream import MemoryStream
from twext.web2.test.test_server import SimpleRequest
+
from twisted.internet.defer import inlineCallbacks, returnValue, succeed
from twistedcaldav import customxml
from twistedcaldav.config import config
from twistedcaldav.test.util import HomeTestCase, norequest
-from twistedcaldav.sharing import SharedCollectionMixin, WikiDirectoryService
+from twistedcaldav import sharing
+from twistedcaldav.sharing import WikiDirectoryService
from twistedcaldav.resource import CalDAVResource
+
from txdav.common.datastore.test.util import buildStore, StubNotifierFactory
-from zope.interface import implements
+from txdav.caldav.icalendarstore import BIND_DIRECT
+from twistedcaldav.test.test_cache import StubResponseCacheResource
sharedOwnerType = davxml.ResourceType.sharedownercalendar #@UndefinedVariable
regularCalendarType = davxml.ResourceType.calendar #@UndefinedVariable
-class SharingTests(HomeTestCase):
- class FakePrincipal(object):
- class FakeRecord(object):
+def normalize(x):
+ """
+ Normalize some XML by parsing it, collapsing whitespace, and
+ pretty-printing.
+ """
+ return WebDAVDocument.fromString(x).toxml()
- def __init__(self, name, cuaddr):
- self.fullName = name
- self.guid = name
- self.calendarUserAddresses = set((cuaddr,))
- def __init__(self, cuaddr):
- if cuaddr.startswith("mailto:"):
- name = cuaddr[7:].split('@')[0]
- elif cuaddr.startswith("urn:uuid:"):
- name = cuaddr[9:]
- else:
- name = cuaddr
- self.path = "/principals/__uids__/%s" % (name,)
- self.homepath = "/calendars/__uids__/%s" % (name,)
- self.displayname = name.upper()
- self.record = self.FakeRecord(name, cuaddr)
+class FakeHome(object):
+ def removeShareByUID(self, request, uid):
+ pass
- def calendarHome(self, request):
- class FakeHome(object):
- def removeShareByUID(self, request, uid):
- pass
- return FakeHome()
- def principalURL(self):
- return self.path
+class FakeRecord(object):
- def principalUID(self):
- return self.record.guid
+ def __init__(self, name, cuaddr):
+ self.fullName = name
+ self.guid = name
+ self.calendarUserAddresses = set((cuaddr,))
+ if name.startswith("wiki-"):
+ recordType = WikiDirectoryService.recordType_wikis
+ else:
+ recordType = None
+ self.recordType = recordType
+ self.shortNames = [name]
- def displayName(self):
- return self.displayname
+class FakePrincipal(object):
+
+ def __init__(self, cuaddr, test):
+ if cuaddr.startswith("mailto:"):
+ name = cuaddr[7:].split('@')[0]
+ elif cuaddr.startswith("urn:uuid:"):
+ name = cuaddr[9:]
+ else:
+ name = cuaddr
+
+ self.path = "/principals/__uids__/%s" % (name,)
+ self.homepath = "/calendars/__uids__/%s" % (name,)
+ self.displayname = name.upper()
+ self.record = FakeRecord(name, cuaddr)
+ self._test = test
+ self._name = name
+
+
@inlineCallbacks
+ def calendarHome(self, request):
+ a, seg = yield self._test.homeProvisioner.locateChild(request,
+ ["__uids__"])
+ b, seg = yield a.locateChild(request, [self._name])
+ if b is None:
+ # XXX all tests except test_noWikiAccess currently rely on the
+ # fake thing here.
+ returnValue(FakeHome())
+ returnValue(b)
+
+
+ def principalURL(self):
+ return self.path
+
+
+ def principalUID(self):
+ return self.record.guid
+
+
+ def displayName(self):
+ return self.displayname
+
+
+
+class SharingTests(HomeTestCase):
+
+ def configure(self):
+ """
+ Override configuration hook to turn on sharing.
+ """
+ super(SharingTests, self).configure()
+ self.patch(config.Sharing, "Enabled", True)
+ self.patch(config.Sharing.Calendars, "Enabled", True)
+
+
+ @inlineCallbacks
def setUp(self):
self.calendarStore = yield buildStore(self, StubNotifierFactory())
yield super(SharingTests, self).setUp()
- self.patch(config.Sharing, "Enabled", True)
- self.patch(config.Sharing.Calendars, "Enabled", True)
+ def patched(c):
+ """
+ The decorated method is patched on L{CalDAVResource} for the
+ duration of the test.
+ """
+ self.patch(CalDAVResource, c.__name__, c)
+ return c
- CalDAVResource.sendInviteNotification = lambda self, record, request: succeed(True)
- CalDAVResource.removeInviteNotification = lambda self, record, request: succeed(True)
+ @patched
+ def sendInviteNotification(resourceSelf, record, request):
+ """
+ For testing purposes, sending an invite notification succeeds
+ without doing anything.
+ """
+ return succeed(True)
- self.patch(CalDAVResource, "validUserIDForShare", lambda self, userid, request: None if "bogus" in userid else SharingTests.FakePrincipal(userid).principalURL())
- self.patch(CalDAVResource, "principalForCalendarUserAddress", lambda self, cuaddr: None if "bogus" in cuaddr else SharingTests.FakePrincipal(cuaddr))
- self.patch(CalDAVResource, "principalForUID", lambda self, principalUID: SharingTests.FakePrincipal("urn:uuid:" + principalUID))
+ @patched
+ def removeInviteNotification(resourceSelf, record, request):
+ """
+ For testing purposes, removing an invite notification succeeds
+ without doing anything.
+ """
+ return succeed(True)
+ @patched
+ def principalForCalendarUserAddress(resourceSelf, cuaddr):
+ if "bogus" in cuaddr:
+ return None
+ else:
+ return FakePrincipal(cuaddr, self)
+
+ @patched
+ def validUserIDForShare(resourceSelf, userid, request):
+ """
+ Temporary replacement for L{CalDAVResource.validUserIDForShare}
+ that marks any principal without 'bogus' in its name.
+ """
+ result = principalForCalendarUserAddress(resourceSelf, userid)
+ if result is None:
+ return result
+ return result.principalURL()
+
+ @patched
+ def principalForUID(resourceSelf, principalUID):
+ return FakePrincipal("urn:uuid:" + principalUID, self)
+
+
def createDataStore(self):
return self.calendarStore
+
@inlineCallbacks
def _refreshRoot(self, request=None):
if request is None:
@@ -103,20 +190,16 @@
self.resource = (
yield self.site.resource.locateChild(request, ["calendar"])
)[0]
+ self.site.resource.responseCache = StubResponseCacheResource()
+ self.site.resource.putChild("calendars", self.homeProvisioner)
returnValue(result)
- @inlineCallbacks
def _doPOST(self, body, resultcode=responsecode.OK):
- request = SimpleRequest(self.site, "POST", "/calendar/")
- request.headers.setHeader("content-type", MimeType("text", "xml"))
- request.stream = MemoryStream(body)
+ return self.simpleSend("POST", "/calendar/", body,
+ resultcode=resultcode)
- response = (yield self.send(request, None))
- self.assertEqual(response.code, resultcode)
- returnValue(response)
-
def _clearUIDElementValue(self, xml):
for user in xml.children:
@@ -174,14 +257,14 @@
self.resource.upgradeToShare()
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -204,15 +287,14 @@
def test_POSTaddInviteeNotAlreadyShared(self):
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-"""
- )
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -238,27 +320,27 @@
self.assertFalse(isShared)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
isShared = (yield self.resource.isShared(None))
self.assertTrue(isShared)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read/>
+ </CS:set>
+ </CS:share>
+ """)
isShared = (yield self.resource.isShared(None))
self.assertTrue(isShared)
@@ -282,25 +364,25 @@
self.assertFalse(isShared)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
isShared = (yield self.resource.isShared(None))
self.assertTrue(isShared)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:remove>
- <D:href>mailto:user02 at example.com</D:href>
- </CS:remove>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user02 at example.com</D:href>
+ </CS:remove>
+ </CS:share>
+ """)
isShared = (yield self.resource.isShared(None))
self.assertFalse(isShared)
@@ -315,28 +397,28 @@
self.resource.upgradeToShare()
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user03 at example.com</D:href>
- <CS:summary>Your Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
- <CS:set>
- <D:href>mailto:user04 at example.com</D:href>
- <CS:summary>Your Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>Your Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ <CS:set>
+ <D:href>mailto:user04 at example.com</D:href>
+ <CS:summary>Your Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -370,31 +452,31 @@
self.resource.upgradeToShare()
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
- <CS:set>
- <D:href>mailto:user03 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:remove>
- <D:href>mailto:user03 at example.com</D:href>
- </CS:remove>
- <CS:set>
- <D:href>mailto:user04 at example.com</D:href>
- <CS:summary>Your Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user03 at example.com</D:href>
+ </CS:remove>
+ <CS:set>
+ <D:href>mailto:user04 at example.com</D:href>
+ <CS:summary>Your Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -420,31 +502,31 @@
self.resource.upgradeToShare()
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
- <CS:set>
- <D:href>mailto:user03 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:remove>
- <D:href>mailto:user03 at example.com</D:href>
- </CS:remove>
- <CS:set>
- <D:href>mailto:user03 at example.com</D:href>
- <CS:summary>Your Shared Calendar</CS:summary>
- <CS:read/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user03 at example.com</D:href>
+ </CS:remove>
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>Your Shared Calendar</CS:summary>
+ <CS:read/>
+ </CS:set>
+ </CS:share>
+ """)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -476,36 +558,36 @@
self.resource.upgradeToShare()
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user02 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
- <CS:set>
- <D:href>mailto:user03 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:remove>
- <D:href>mailto:user03 at example.com</D:href>
- </CS:remove>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user03 at example.com</D:href>
+ </CS:remove>
+ </CS:share>
+ """)
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:remove>
- <D:href>mailto:user02 at example.com</D:href>
- </CS:remove>
- <CS:remove>
- <D:href>mailto:user03 at example.com</D:href>
- </CS:remove>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user02 at example.com</D:href>
+ </CS:remove>
+ <CS:remove>
+ <D:href>mailto:user03 at example.com</D:href>
+ </CS:remove>
+ </CS:share>
+ """)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(propInvite, None)
@@ -515,46 +597,53 @@
def test_POSTaddInvalidInvitee(self):
self.resource.upgradeToShare()
- response = (yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:bogus at example.net</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""",
+ data = (yield self._doPOST(
+ """<?xml version="1.0" encoding="utf-8" ?>
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:bogus at example.net</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """,
responsecode.MULTI_STATUS
))
- self.assertEqual(
- str(response.stream.read()).replace("\r\n", "\n"),
+ self.assertXMLEquals(
+ data,
"""<?xml version='1.0' encoding='UTF-8'?>
-<multistatus xmlns='DAV:'>
- <response>
- <href>mailto:bogus at example.net</href>
- <status>HTTP/1.1 403 Forbidden</status>
- </response>
-</multistatus>"""
+ <multistatus xmlns='DAV:'>
+ <response>
+ <href>mailto:bogus at example.net</href>
+ <status>HTTP/1.1 403 Forbidden</status>
+ </response>
+ </multistatus>"""
)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
-
self.assertEquals(propInvite, None)
+ def assertXMLEquals(self, a, b):
+ """
+ Assert two strings are equivalent as XML.
+ """
+ self.assertEquals(normalize(a), normalize(b))
+
+
@inlineCallbacks
def test_POSTremoveInvalidInvitee(self):
self.resource.upgradeToShare()
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:set>
- <D:href>mailto:user01 at example.com</D:href>
- <CS:summary>My Shared Calendar</CS:summary>
- <CS:read-write/>
- </CS:set>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user01 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ </CS:share>
+ """)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
@@ -583,18 +672,39 @@
))
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
-<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
- <CS:remove>
- <D:href>mailto:user01 at example.com</D:href>
- </CS:remove>
-</CS:share>
-""")
+ <CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user01 at example.com</D:href>
+ </CS:remove>
+ </CS:share>
+ """)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(propInvite, None)
@inlineCallbacks
+ def wikiSetup(self):
+ """
+ Create a wiki called C{wiki-testing}, and share it with the user whose
+ home is at /. Return the name of the newly shared calendar in the
+ sharee's home.
+ """
+ wcreate = self.calendarStore.newTransaction("create wiki")
+ yield wcreate.calendarHomeWithUID("wiki-testing", create=True)
+ yield wcreate.commit()
+ self.directoryFixture.addDirectoryService(WikiDirectoryService())
+
+ txn = self.site.resource._associatedTransaction
+ sharee = self.site.resource._newStoreHome
+ sharer = yield txn.calendarHomeWithUID("wiki-testing")
+ cal = yield sharer.calendarWithName("calendar")
+ sharedName = yield cal.shareWith(sharee, BIND_DIRECT)
+ yield self._refreshRoot()
+ returnValue(sharedName)
+
+
+ @inlineCallbacks
def test_wikiACL(self):
"""
Ensure shareeAccessControlList( ) honors the access granted by the wiki
@@ -603,100 +713,54 @@
"""
access = "read"
-
def stubWikiAccessMethod(userID, wikiID):
return access
+ self.patch(sharing, "getWikiAccess", stubWikiAccessMethod)
- class StubCollection(object):
- def __init__(self):
- self._isShareeCollection = True
- self._shareePrincipal = StubUserPrincipal()
- def isCalendarCollection(self):
- return True
+ sharedName = yield self.wikiSetup()
+ request = SimpleRequest(self.site, "GET", "/404")
+ collection = yield request.locateResource("/" + sharedName)
- class StubShare(object):
- def direct(self):
- return True
-
- def url(self):
- return "/wikifoo"
-
- def uid(self):
- return "012345"
-
- def shareeUID(self):
- return StubUserPrincipal().record.guid
-
- class TestCollection(SharedCollectionMixin, StubCollection):
- def principalForUID(self, uid):
- principal = StubUserPrincipal()
- return principal if principal.record.guid == uid else None
-
- class StubRecord(object):
- def __init__(self, recordType, name, guid):
- self.recordType = recordType
- self.shortNames = [name]
- self.guid = guid
-
- class StubUserPrincipal(object):
- def __init__(self):
- self.record = StubRecord(
- "users",
- "testuser",
- "4F364813-0415-45CB-9FD4-DBFEF7A0A8E0"
- )
- def principalURL(self):
- return "/principals/__uids__/%s/" % (self.record.guid,)
-
- class StubWikiPrincipal(object):
- def __init__(self):
- self.record = StubRecord(
- WikiDirectoryService.recordType_wikis,
- "wikifoo",
- "foo"
- )
-
- class StubWikiResource(object):
- implements(IResource)
-
- def locateChild(self, req, segments):
- pass
-
-
- def renderHTTP(self, req):
- pass
-
-
- def ownerPrincipal(self, req):
- return succeed(StubWikiPrincipal())
-
- collection = TestCollection()
- collection._share = StubShare()
- self.site.resource.putChild("wikifoo", StubWikiResource())
- request = SimpleRequest(self.site, "GET", "/wikifoo")
-
# Simulate the wiki server granting Read access
- acl = (yield collection.shareeAccessControlList(request,
- wikiAccessMethod=stubWikiAccessMethod))
+ acl = (yield collection.shareeAccessControlList(request))
self.assertFalse("<write/>" in acl.toxml())
# Simulate the wiki server granting Read-Write access
access = "write"
- acl = (yield collection.shareeAccessControlList(request,
- wikiAccessMethod=stubWikiAccessMethod))
+ acl = (yield collection.shareeAccessControlList(request))
self.assertTrue("<write/>" in acl.toxml())
-'''
-class DatabaseSharingTests(SharingTests):
-
@inlineCallbacks
- def setUp(self):
- self.calendarStore = yield buildStore(self, StubNotifierFactory())
- yield super(DatabaseSharingTests, self).setUp()
+ def test_noWikiAccess(self):
+ """
+ If L{SharedCollectionMixin.shareeAccessControlList} detects missing
+ access controls for a directly shared collection, it will automatically
+ un-share that collection.
+ """
+ sharedName = yield self.wikiSetup()
+ access = "write"
+ def stubWikiAccessMethod(userID, wikiID):
+ return access
+ self.patch(sharing, "getWikiAccess", stubWikiAccessMethod)
+ @inlineCallbacks
+ def listChildrenViaPropfind():
+ data = yield self.simpleSend(
+ "PROPFIND", "/", resultcode=responsecode.MULTI_STATUS,
+ headers=[('Depth', '1')]
+ )
+ tree = XML(data)
+ seq = [e.text for e in tree.findall("{DAV:}response/{DAV:}href")]
+ shortest = min(seq, key=len)
+ seq.remove(shortest)
+ filtered = [elem[len(shortest):].rstrip("/") for elem in seq]
+ returnValue(filtered)
+ childNames = yield listChildrenViaPropfind()
+ self.assertIn(sharedName, childNames)
+ access = "no-access"
+ childNames = yield listChildrenViaPropfind()
+ self.assertNotIn(sharedName, childNames)
- def createDataStore(self):
- return self.calendarStore
-'''
+
Modified: CalendarServer/trunk/twistedcaldav/test/test_wrapping.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_wrapping.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/test/test_wrapping.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -236,6 +236,15 @@
pathTypes = ['calendar', 'addressbook']
+ def checkPrincipalCollections(self, resource):
+ """
+ Verify that the C{_principalCollections} attribute of the given
+ L{Resource} is accurately set.
+ """
+ self.assertEquals(resource._principalCollections,
+ frozenset([self.directoryFixture.principalsResource]))
+
+
@inlineCallbacks
def test_autoRevertUnCommitted(self):
"""
@@ -392,8 +401,7 @@
"calendars/users/wsanchez/calendar/1.ics"
)
yield self.commit()
- self.assertEquals(calDavFileCalendar._principalCollections,
- frozenset([self.principalsResource]))
+ self.checkPrincipalCollections(calDavFileCalendar)
self.assertEquals(calDavFileCalendar._associatedTransaction,
calendarHome._associatedTransaction)
@@ -435,8 +443,7 @@
"calendars/users/wsanchez/calendar/xyzzy.ics"
)
yield self.commit()
- self.assertEquals(calDavFileCalendar._principalCollections,
- frozenset([self.principalsResource]))
+ self.checkPrincipalCollections(calDavFileCalendar)
def test_createAddressBookStore(self):
@@ -468,8 +475,7 @@
"""
calDavFile = yield self.getResource("addressbooks/users/wsanchez/addressbook")
yield self.commit()
- self.assertEquals(calDavFile._principalCollections,
- frozenset([self.principalsResource]))
+ self.checkPrincipalCollections(calDavFile)
@inlineCallbacks
@@ -487,8 +493,7 @@
yield calDavFile.createAddressBookCollection()
self.assertTrue(calDavFile.exists())
yield self.commit()
- self.assertEquals(calDavFile._principalCollections,
- frozenset([self.principalsResource]))
+ self.checkPrincipalCollections(calDavFile)
@inlineCallbacks
@@ -503,8 +508,7 @@
"addressbooks/users/wsanchez/addressbook/1.vcf"
)
yield self.commit()
- self.assertEquals(calDavFileAddressBook._principalCollections,
- frozenset([self.principalsResource]))
+ self.checkPrincipalCollections(calDavFileAddressBook)
@inlineCallbacks
@@ -518,8 +522,7 @@
"addressbooks/users/wsanchez/addressbook/xyzzy.ics"
)
yield self.commit()
- self.assertEquals(calDavFileAddressBook._principalCollections,
- frozenset([self.principalsResource]))
+ self.checkPrincipalCollections(calDavFileAddressBook)
@inlineCallbacks
Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py 2013-01-29 03:29:24 UTC (rev 10595)
+++ CalendarServer/trunk/twistedcaldav/test/util.py 2013-01-29 03:35:09 UTC (rev 10596)
@@ -37,9 +37,12 @@
from twistedcaldav.config import config
from twistedcaldav.directory import augment
from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
-from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
+from twistedcaldav.directory.calendar import (
+ DirectoryCalendarHomeProvisioningResource
+)
from twistedcaldav.directory.principal import (
DirectoryPrincipalProvisioningResource)
+from twistedcaldav.directory.aggregate import AggregateDirectoryService
from twistedcaldav.directory.xmlfile import XMLDirectoryService
from txdav.common.datastore.test.util import deriveQuota
@@ -74,33 +77,81 @@
-class TestCase(twext.web2.dav.test.util.TestCase):
- resource_class = RootResource
+class DirectoryFixture(object):
+ """
+ Test fixture for creating various parts of the resource hierarchy related
+ to directories.
+ """
- def createStockDirectoryService(self):
+ def __init__(self):
+ def _setUpPrincipals(ds):
+ # FIXME: see FIXME in
+ # DirectoryPrincipalProvisioningResource.__init__; this performs a
+ # necessary modification to any directory service object for it to
+ # be fully functional.
+ self.principalsResource = DirectoryPrincipalProvisioningResource(
+ "/principals/", ds
+ )
+ self._directoryChangeHooks = [_setUpPrincipals]
+
+
+ directoryService = None
+ principalsResource = None
+
+ def addDirectoryService(self, newService):
"""
- Create a stock C{directoryService} attribute and assign it.
+ Add an L{IDirectoryService} to this test case.
+
+ If this test case does not have a directory service yet, create it and
+ assign C{directoryService} and C{principalsResource} attributes to this
+ test case.
+
+ If the test case already has a directory service, create an
+ L{AggregateDirectoryService} and re-assign the C{self.directoryService}
+ attribute to point at it instead, while setting the C{realmName} of the
+ new service to match the old one.
+
+ If the test already has an L{AggregateDirectoryService}, create a
+ I{new} L{AggregateDirectoryService} with the same list of services,
+ after adjusting the new service's realm to match the existing ones.
"""
- self.xmlFile = FilePath(config.DataRoot).child("accounts.xml")
- self.xmlFile.setContent(xmlFile.getContent())
+ if self.directoryService is None:
+ directoryService = newService
+ else:
+ newService.realmName = self.directoryService.realmName
+ if isinstance(self.directoryService, AggregateDirectoryService):
+ directories = set(self.directoryService._recordTypes.items())
+ directories.add(newService)
+ else:
+ directories = [newService, self.directoryService]
+ directoryService = AggregateDirectoryService(directories, None)
- self.directoryService = XMLDirectoryService(
- {
- "xmlFile" : "accounts.xml",
- "augmentService" :
- augment.AugmentXMLDB( xmlFiles=(augmentsFile.path,)),
- }
- )
-
+ self.directoryService = directoryService
# FIXME: see FIXME in DirectoryPrincipalProvisioningResource.__init__;
- # this performs a necessary modification to the directory service
- # object for it to be fully functional.
- self.principalsResource = DirectoryPrincipalProvisioningResource(
- "/principals/", self.directoryService
- )
+ # this performs a necessary modification to the directory service object
+ # for it to be fully functional.
+ for hook in self._directoryChangeHooks:
+ hook(directoryService)
+ def whenDirectoryServiceChanges(self, callback):
+ """
+ When the C{directoryService} attribute is changed by
+ L{TestCase.addDirectoryService}, call the given callback in order to
+ update any state which relies upon that service.
+
+ If there's already a directory, invoke the callback immediately.
+ """
+ self._directoryChangeHooks.append(callback)
+ if self.directoryService is not None:
+ callback(self.directoryService)
+
+
+
+class TestCase(twext.web2.dav.test.util.TestCase):
+ resource_class = RootResource
+
def createDataStore(self):
"""
Create an L{IDataStore} that can store calendars (but not
@@ -111,45 +162,88 @@
quota=deriveQuota(self))
+ def createStockDirectoryService(self):
+ """
+ Create a stock C{directoryService} attribute and assign it.
+ """
+ self.xmlFile = FilePath(config.DataRoot).child("accounts.xml")
+ self.xmlFile.setContent(xmlFile.getContent())
+ self.directoryFixture.addDirectoryService(XMLDirectoryService({
+ "xmlFile": "accounts.xml",
+ "augmentService":
+ augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
+ }))
+
+
def setupCalendars(self):
"""
- Set up the resource at /calendars (a
- L{DirectoryCalendarHomeProvisioningResource}), and assign it as
- C{self.calendarCollection}.
+ When a directory service exists, set up the resources at C{/calendars}
+ and C{/addressbooks} (a L{DirectoryCalendarHomeProvisioningResource}
+ and L{DirectoryAddressBookHomeProvisioningResource} respectively), and
+ assign them to the C{self.calendarCollection} and
+ C{self.addressbookCollection} attributes.
+
+ A directory service may be associated with this L{TestCase} with
+ L{TestCase.createStockDirectoryService} or
+ L{TestCase.directoryFixture.addDirectoryService}.
"""
+ newStore = self.createDataStore()
+ @self.directoryFixture.whenDirectoryServiceChanges
+ def putAllChildren(ds):
+ self.calendarCollection = (
+ DirectoryCalendarHomeProvisioningResource(
+ ds, "/calendars/", newStore
+ ))
+ self.site.resource.putChild("calendars", self.calendarCollection)
+ self.addressbookCollection = (
+ DirectoryAddressBookHomeProvisioningResource(
+ ds, "/addressbooks/", newStore
+ ))
+ self.site.resource.putChild("addressbooks",
+ self.addressbookCollection)
- # Need a data store
- self._newStore = self.createDataStore()
- self.calendarCollection = DirectoryCalendarHomeProvisioningResource(
- self.directoryService,
- "/calendars/",
- self._newStore
- )
- self.site.resource.putChild("calendars", self.calendarCollection)
+ def configure(self):
+ """
+ Adjust the global configuration for this test.
+ """
+ config.reset()
- self.addressbookCollection = DirectoryAddressBookHomeProvisioningResource(
- self.directoryService,
- "/addressbooks/",
- self._newStore
- )
- self.site.resource.putChild("addressbooks", self.addressbookCollection)
+ config.ServerRoot = os.path.abspath(self.serverRoot)
+ config.ConfigRoot = "config"
+ config.LogRoot = "logs"
+ config.RunRoot = "logs"
+ config.Memcached.Pools.Default.ClientEnabled = False
+ config.Memcached.Pools.Default.ServerEnabled = False
+ ClientFactory.allowTestCache = True
+ memcacher.Memcacher.allowTestCache = True
+ memcacher.Memcacher.memoryCacheInstance = None
+ config.DirectoryAddressBook.Enabled = False
+
+ @property
+ def directoryService(self):
+ """
+ Read-only alias for L{DirectoryFixture.directoryService} for
+ compatibility with older tests. TODO: remove this.
+ """
+ return self.directoryFixture.directoryService
+
+
def setUp(self):
super(TestCase, self).setUp()
+ self.directoryFixture = DirectoryFixture()
+
# FIXME: this is only here to workaround circular imports
doBind()
- config.reset()
- serverroot = self.mktemp()
- os.mkdir(serverroot)
- config.ServerRoot = os.path.abspath(serverroot)
- config.ConfigRoot = "config"
- config.LogRoot = "logs"
- config.RunRoot = "logs"
+ self.serverRoot = self.mktemp()
+ os.mkdir(self.serverRoot)
+ self.configure()
+
if not os.path.exists(config.DataRoot):
os.makedirs(config.DataRoot)
if not os.path.exists(config.DocumentRoot):
@@ -159,15 +253,7 @@
if not os.path.exists(config.LogRoot):
os.makedirs(config.LogRoot)
- config.Memcached.Pools.Default.ClientEnabled = False
- config.Memcached.Pools.Default.ServerEnabled = False
- ClientFactory.allowTestCache = True
- memcacher.Memcacher.allowTestCache = True
- memcacher.Memcacher.memoryCacheInstance = None
- config.DirectoryAddressBook.Enabled = False
-
-
def createHierarchy(self, structure, root=None):
if root is None:
root = os.path.abspath(self.mktemp())
@@ -184,8 +270,6 @@
# This is a file
with open(childPath, "w") as child:
child.write(childStructure["@contents"])
-
-
else:
# This is a directory
os.mkdir(childPath)
@@ -207,6 +291,7 @@
createChildren(root, structure)
return root
+
def verifyHierarchy(self, root, structure):
def verifyChildren(parent, subStructure):
@@ -341,21 +426,18 @@
def setUp(self):
"""
Replace self.site.resource with an appropriately provisioned
- CalendarHomeResource, and replace self.docroot with a path pointing at that
- file.
+ L{CalendarHomeResource}, and, if the data store backing this test is a
+ file store, replace C{self.docroot} with a path pointing at the path
+ that stores the data for that L{CalendarHomeResource}.
"""
super(HomeTestCase, self).setUp()
-
self.createStockDirectoryService()
+ @self.directoryFixture.whenDirectoryServiceChanges
+ def addHomeProvisioner(ds):
+ self.homeProvisioner = DirectoryCalendarHomeProvisioningResource(
+ ds, "/calendars/", self.createDataStore()
+ )
- # Need a data store
- _newStore = self.createDataStore()
-
- self.homeProvisioner = DirectoryCalendarHomeProvisioningResource(
- self.directoryService, "/calendars/",
- _newStore
- )
-
def _defer(user):
# Commit the transaction
self.addCleanup(self.noRenderCommit)
@@ -434,20 +516,13 @@
file.
"""
super(AddressBookHomeTestCase, self).setUp()
-
- fp = FilePath(self.mktemp())
- fp.createDirectory()
-
self.createStockDirectoryService()
+ @self.directoryFixture.whenDirectoryServiceChanges
+ def addHomeProvisioner(ds):
+ self.homeProvisioner = DirectoryAddressBookHomeProvisioningResource(
+ ds, "/calendars/", self.createDataStore()
+ )
- # Need a data store
- _newStore = CommonDataStore(fp, None, True, False)
-
- self.homeProvisioner = DirectoryAddressBookHomeProvisioningResource(
- self.directoryService, "/addressbooks/",
- _newStore
- )
-
@inlineCallbacks
def _defer(user):
# Commit the transaction
@@ -456,6 +531,7 @@
return self._refreshRoot().addCallback(_defer)
+
@inlineCallbacks
def _refreshRoot(self):
"""
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130128/7a1f99c4/attachment-0001.html>
More information about the calendarserver-changes
mailing list