[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