[CalendarServer-changes] [9235] CalendarServer/trunk/calendarserver/tap/test/test_caldav.py
source_changes at macosforge.org
source_changes at macosforge.org
Tue May 8 15:09:08 PDT 2012
Revision: 9235
http://trac.macosforge.org/projects/calendarserver/changeset/9235
Author: glyph at apple.com
Date: 2012-05-08 15:09:08 -0700 (Tue, 08 May 2012)
Log Message:
-----------
restore accidentally-deleted test code from r8955
Revision Links:
--------------
http://trac.macosforge.org/projects/calendarserver/changeset/8955
Added Paths:
-----------
CalendarServer/trunk/calendarserver/tap/test/test_caldav.py
Added: CalendarServer/trunk/calendarserver/tap/test/test_caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/test/test_caldav.py (rev 0)
+++ CalendarServer/trunk/calendarserver/tap/test/test_caldav.py 2012-05-08 22:09:08 UTC (rev 9235)
@@ -0,0 +1,1161 @@
+##
+# Copyright (c) 2007-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+import os
+import stat
+import grp
+
+from os.path import dirname, abspath
+
+from zope.interface import implements
+
+from twisted.python.threadable import isInIOThread
+from twisted.internet.reactor import callFromThread
+from twisted.python.usage import Options, UsageError
+from twisted.python.reflect import namedAny
+from twisted.python import log
+
+from twisted.internet.interfaces import IProcessTransport, IReactorProcess
+from twisted.internet.protocol import ServerFactory
+from twisted.internet.defer import Deferred
+from twisted.internet.task import Clock
+
+from twisted.application.service import IService, IServiceCollection
+from twisted.application import internet
+
+from twext.web2.dav import auth
+from twext.web2.log import LogWrapperResource
+from twext.python.filepath import CachingFilePath as FilePath
+
+from twext.python.plistlib import writePlist #@UnresolvedImport
+from twext.internet.tcp import MaxAcceptTCPServer, MaxAcceptSSLServer
+
+from twistedcaldav.config import config, ConfigDict
+from twistedcaldav.stdconfig import DEFAULT_CONFIG
+
+from twistedcaldav.directory.aggregate import AggregateDirectoryService
+from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
+from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+
+from twistedcaldav.test.util import TestCase
+
+from calendarserver.tap.caldav import (
+ CalDAVOptions, CalDAVServiceMaker, CalDAVService, GroupOwnedUNIXServer,
+ DelayedStartupProcessMonitor, DelayedStartupLineLogger, TwistdSlaveProcess
+)
+from calendarserver.provision.root import RootResource
+from StringIO import StringIO
+
+
+# Points to top of source tree.
+sourceRoot = dirname(dirname(dirname(dirname(abspath(__file__)))))
+
+
+class NotAProcessTransport(object):
+ """
+ Simple L{IProcessTransport} stub.
+ """
+ implements(IProcessTransport)
+
+ def __init__(self, processProtocol, executable, args, env, path,
+ uid, gid, usePTY, childFDs):
+ """
+ Hold on to all the attributes passed to spawnProcess.
+ """
+ self.processProtocol = processProtocol
+ self.executable = executable
+ self.args = args
+ self.env = env
+ self.path = path
+ self.uid = uid
+ self.gid = gid
+ self.usePTY = usePTY
+ self.childFDs = childFDs
+
+
+class InMemoryProcessSpawner(Clock):
+ """
+ Stub out L{IReactorProcess} and L{IReactorClock} so that we can examine the
+ interaction of L{DelayedStartupProcessMonitor} and the reactor.
+ """
+ implements(IReactorProcess)
+
+ def __init__(self):
+ """
+ Create some storage to hold on to all the fake processes spawned.
+ """
+ super(InMemoryProcessSpawner, self).__init__()
+ self.processTransports = []
+ self.waiting = []
+
+
+ def waitForOneProcess(self, amount=10.0):
+ """
+ Wait for an L{IProcessTransport} to be created by advancing the clock.
+ If none are created in the specified amount of time, raise an
+ AssertionError.
+ """
+ self.advance(amount)
+ if self.processTransports:
+ return self.processTransports.pop(0)
+ else:
+ raise AssertionError(
+ "There were no process transports available. Calls: " +
+ repr(self.calls)
+ )
+
+
+ def spawnProcess(self, processProtocol, executable, args=(), env={},
+ path=None, uid=None, gid=None, usePTY=0,
+ childFDs=None):
+
+ transport = NotAProcessTransport(
+ processProtocol, executable, args, env, path, uid, gid, usePTY,
+ childFDs
+ )
+ transport.startedAt = self.seconds()
+ self.processTransports.append(transport)
+ if self.waiting:
+ self.waiting.pop(0).callback(transport)
+ return transport
+
+
+
+class TestCalDAVOptions (CalDAVOptions):
+ """
+ A fake implementation of CalDAVOptions that provides
+ empty implementations of checkDirectory and checkFile.
+ """
+ def checkDirectory(self, *args, **kwargs):
+ pass
+
+ def checkFile(self, *args, **kwargs):
+ pass
+
+
+ def loadConfiguration(self):
+ """
+ Simple wrapper to avoid printing during test runs.
+ """
+ oldout = sys.stdout
+ newout = sys.stdout = StringIO()
+ try:
+ return CalDAVOptions.loadConfiguration(self)
+ finally:
+ sys.stdout = oldout
+ log.msg(
+ "load configuration console output: %s" % newout.getvalue()
+ )
+
+
+class CalDAVOptionsTest (TestCase):
+ """
+ Test various parameters of our usage.Options subclass
+ """
+ def setUp(self):
+ """
+ Set up our options object, giving it a parent, and forcing the
+ global config to be loaded from defaults.
+ """
+ TestCase.setUp(self)
+ self.config = TestCalDAVOptions()
+ self.config.parent = Options()
+ self.config.parent["uid"] = 0
+ self.config.parent["gid"] = 0
+ self.config.parent["nodaemon"] = False
+
+ def tearDown(self):
+ config.setDefaults(DEFAULT_CONFIG)
+ config.reload()
+
+ def test_overridesConfig(self):
+ """
+ Test that values on the command line's -o and --option options
+ overide the config file
+ """
+ myConfig = ConfigDict(DEFAULT_CONFIG)
+ myConfigFile = self.mktemp()
+ writePlist(myConfig, myConfigFile)
+
+ argv = [
+ "-f", myConfigFile,
+ "-o", "EnableSACLs",
+ "-o", "HTTPPort=80",
+ "-o", "BindAddresses=127.0.0.1,127.0.0.2,127.0.0.3",
+ "-o", "DocumentRoot=/dev/null",
+ "-o", "UserName=None",
+ "-o", "EnableProxyPrincipals=False",
+ ]
+
+ self.config.parseOptions(argv)
+
+ self.assertEquals(config.EnableSACLs, True)
+ self.assertEquals(config.HTTPPort, 80)
+ self.assertEquals(config.BindAddresses,
+ ["127.0.0.1", "127.0.0.2", "127.0.0.3"])
+ self.assertEquals(config.DocumentRoot, "/dev/null")
+ self.assertEquals(config.UserName, None)
+ self.assertEquals(config.EnableProxyPrincipals, False)
+
+ argv = ["-o", "Authentication=This Doesn't Matter"]
+
+ self.assertRaises(UsageError, self.config.parseOptions, argv)
+
+ def test_setsParent(self):
+ """
+ Test that certain values are set on the parent (i.e. twistd's
+ Option's object)
+ """
+ myConfig = ConfigDict(DEFAULT_CONFIG)
+ myConfigFile = self.mktemp()
+ writePlist(myConfig, myConfigFile)
+
+ argv = [
+ "-f", myConfigFile,
+ "-o", "PIDFile=/dev/null",
+ ]
+
+ self.config.parseOptions(argv)
+
+ self.assertEquals(self.config.parent["pidfile"], "/dev/null")
+
+ def test_specifyConfigFile(self):
+ """
+ Test that specifying a config file from the command line
+ loads the global config with those values properly.
+ """
+ myConfig = ConfigDict(DEFAULT_CONFIG)
+
+ myConfig.Authentication.Basic.Enabled = False
+ myConfig.HTTPPort = 80
+ myConfig.ServerHostName = "calendar.calenderserver.org"
+
+ myConfigFile = self.mktemp()
+ writePlist(myConfig, myConfigFile)
+
+ args = ["-f", myConfigFile]
+
+ self.config.parseOptions(args)
+
+ self.assertEquals(config.ServerHostName, myConfig["ServerHostName"])
+ self.assertEquals(config.HTTPPort, myConfig.HTTPPort)
+ self.assertEquals(
+ config.Authentication.Basic.Enabled,
+ myConfig.Authentication.Basic.Enabled
+ )
+
+ def test_specifyDictPath(self):
+ """
+ Test that we can specify command line overrides to leafs using
+ a "/" seperated path. Such as "-o MultiProcess/ProcessCount=1"
+ """
+ myConfig = ConfigDict(DEFAULT_CONFIG)
+ myConfigFile = self.mktemp()
+ writePlist(myConfig, myConfigFile)
+
+ argv = [
+ "-o", "MultiProcess/ProcessCount=102",
+ "-f", myConfigFile,
+ ]
+
+ self.config.parseOptions(argv)
+
+ self.assertEquals(config.MultiProcess["ProcessCount"], 102)
+
+class BaseServiceMakerTests(TestCase):
+ """
+ Utility class for ServiceMaker tests.
+ """
+ configOptions = None
+
+ def setUp(self):
+ TestCase.setUp(self)
+ self.options = TestCalDAVOptions()
+ self.options.parent = Options()
+ self.options.parent["gid"] = None
+ self.options.parent["uid"] = None
+ self.options.parent["nodaemon"] = None
+
+ self.config = ConfigDict(DEFAULT_CONFIG)
+
+ accountsFile = os.path.join(sourceRoot, "twistedcaldav/directory/test/accounts.xml")
+ resourcesFile = os.path.join(sourceRoot, "twistedcaldav/directory/test/resources.xml")
+ augmentsFile = os.path.join(sourceRoot, "twistedcaldav/directory/test/augments.xml")
+ pemFile = os.path.join(sourceRoot, "twistedcaldav/test/data/server.pem")
+
+ self.config["DirectoryService"] = {
+ "params": {"xmlFile": accountsFile},
+ "type": "twistedcaldav.directory.xmlfile.XMLDirectoryService"
+ }
+
+ self.config["ResourceService"] = {
+ "params": {"xmlFile": resourcesFile},
+ }
+
+ self.config["AugmentService"] = {
+ "params": {"xmlFiles": [augmentsFile]},
+ "type": "twistedcaldav.directory.augment.AugmentXMLDB"
+ }
+
+ self.config.UseDatabase = False
+ self.config.ServerRoot = self.mktemp()
+ self.config.ConfigRoot = "config"
+ self.config.ProcessType = "Single"
+ self.config.SSLPrivateKey = pemFile
+ self.config.SSLCertificate = pemFile
+ self.config.EnableSSL = True
+ self.config.Memcached.Pools.Default.ClientEnabled = False
+ self.config.Memcached.Pools.Default.ServerEnabled = False
+ self.config.DirectoryAddressBook.Enabled = False
+
+ self.config.SudoersFile = ""
+
+ if self.configOptions:
+ self.config.update(self.configOptions)
+
+ os.mkdir(self.config.ServerRoot)
+ os.mkdir(os.path.join(self.config.ServerRoot, self.config.DocumentRoot))
+ os.mkdir(os.path.join(self.config.ServerRoot, self.config.DataRoot))
+ os.mkdir(os.path.join(self.config.ServerRoot, self.config.ConfigRoot))
+
+ self.configFile = self.mktemp()
+
+ self.writeConfig()
+
+
+ def tearDown(self):
+ config.setDefaults(DEFAULT_CONFIG)
+ config.reset()
+
+
+ def writeConfig(self):
+ """
+ Flush self.config out to self.configFile
+ """
+ writePlist(self.config, self.configFile)
+
+
+ def makeService(self):
+ """
+ Create a service by calling into CalDAVServiceMaker with
+ self.configFile
+ """
+ self.options.parseOptions(["-f", self.configFile])
+
+ return CalDAVServiceMaker().makeService(self.options)
+
+
+ def getSite(self):
+ """
+ Get the server.Site from the service by finding the HTTPFactory.
+ """
+ service = self.makeService()
+ for listeningService in inServiceHierarchy(
+ service,
+ # FIXME: need a better predicate for 'is this really an HTTP
+ # factory' but this works for now.
+ # NOTE: in a database 'single' configuration, PostgresService
+ # will prevent the HTTP services from actually getting added to
+ # the hierarchy until the hierarchy has started.
+ lambda x: hasattr(x, 'args')
+ ):
+ return listeningService.args[1].protocolArgs['requestFactory']
+ raise RuntimeError("No site found.")
+
+
+
+def inServiceHierarchy(svc, predicate):
+ """
+ Find services in the service collection which satisfy the given predicate.
+ """
+ for subsvc in svc.services:
+ if IServiceCollection.providedBy(subsvc):
+ for value in inServiceHierarchy(subsvc, predicate):
+ yield value
+ if predicate(subsvc):
+ yield subsvc
+
+
+
+def determineAppropriateGroupID():
+ """
+ Determine a secondary group ID which can be used for testing.
+ """
+ return os.getgroups()[1]
+
+
+
+class SocketGroupOwnership(TestCase):
+ """
+ Tests for L{GroupOwnedUNIXServer}.
+ """
+
+ def test_groupOwnedUNIXSocket(self):
+ """
+ When a L{GroupOwnedUNIXServer} is started, it will change the group of
+ its socket.
+ """
+ alternateGroup = determineAppropriateGroupID()
+ socketName = self.mktemp()
+ gous = GroupOwnedUNIXServer(alternateGroup, socketName, ServerFactory(), mode=0660)
+ gous.privilegedStartService()
+ self.addCleanup(gous.stopService)
+ filestat = os.stat(socketName)
+ self.assertTrue(stat.S_ISSOCK(filestat.st_mode))
+ self.assertEquals(filestat.st_gid, alternateGroup)
+ self.assertEquals(filestat.st_uid, os.getuid())
+
+
+
+class CalDAVServiceMakerTests(BaseServiceMakerTests):
+ """
+ Test the service maker's behavior
+ """
+
+ def test_makeServiceDispatcher(self):
+ """
+ Test the default options of the dispatching makeService
+ """
+ validServices = ["Slave", "Combined"]
+
+ self.config["HTTPPort"] = 0
+
+ for service in validServices:
+ self.config["ProcessType"] = service
+ self.writeConfig()
+ self.makeService()
+
+ self.config["ProcessType"] = "Unknown Service"
+ self.writeConfig()
+ self.assertRaises(UsageError, self.makeService)
+
+
+ def test_modesOnUNIXSockets(self):
+ """
+ The logging and stats UNIX sockets that are bound as part of the
+ 'Combined' service hierarchy should have a secure mode specified: only
+ the executing user should be able to open and send to them.
+ """
+
+ self.config["HTTPPort"] = 0 # Don't conflict with the test above.
+ alternateGroup = determineAppropriateGroupID()
+ self.config.GroupName = grp.getgrgid(alternateGroup).gr_name
+
+ self.config["ProcessType"] = "Combined"
+ self.writeConfig()
+ svc = self.makeService()
+ for serviceName in ["logging"]:
+ socketService = svc.getServiceNamed(serviceName)
+ self.assertIsInstance(socketService, GroupOwnedUNIXServer)
+ m = socketService.kwargs.get("mode", 0666)
+ self.assertEquals(
+ m, int("660", 8),
+ "Wrong mode on %s: %s" % (serviceName, oct(m))
+ )
+ self.assertEquals(socketService.gid, alternateGroup)
+ for serviceName in ["stats"]:
+ socketService = svc.getServiceNamed(serviceName)
+ self.assertIsInstance(socketService, GroupOwnedUNIXServer)
+ m = socketService.kwargs.get("mode", 0444)
+ self.assertEquals(
+ m, int("440", 8),
+ "Wrong mode on %s: %s" % (serviceName, oct(m))
+ )
+ self.assertEquals(socketService.gid, alternateGroup)
+
+
+ def test_processMonitor(self):
+ """
+ In the master, there should be exactly one
+ L{DelayedStartupProcessMonitor} in the service hierarchy so that it
+ will be started by startup.
+ """
+ self.config["ProcessType"] = "Combined"
+ self.writeConfig()
+ self.assertEquals(
+ 1,
+ len(
+ list(inServiceHierarchy(
+ self.makeService(),
+ lambda x: isinstance(x, DelayedStartupProcessMonitor)))
+ )
+ )
+
+
+
+
+class SlaveServiceTest(BaseServiceMakerTests):
+ """
+ Test various configurations of the Slave service
+ """
+
+ configOptions = {
+ "HTTPPort": 8008,
+ "SSLPort": 8443,
+ }
+
+ def test_defaultService(self):
+ """
+ Test the value of a Slave service in it's simplest
+ configuration.
+ """
+ service = self.makeService()
+
+ self.failUnless(
+ IService(service),
+ "%s does not provide IService" % (service,)
+ )
+ self.failUnless(
+ service.services,
+ "No services configured"
+ )
+ self.failUnless(
+ isinstance(service, CalDAVService),
+ "%s is not a CalDAVService" % (service,)
+ )
+
+ def test_defaultListeners(self):
+ """
+ Test that the Slave service has sub services with the
+ default TCP and SSL configuration
+ """
+ service = self.makeService()
+
+ expectedSubServices = dict((
+ (MaxAcceptTCPServer, self.config["HTTPPort"]),
+ (MaxAcceptSSLServer, self.config["SSLPort"]),
+ ))
+
+ configuredSubServices = [(s.__class__, getattr(s, 'args', None))
+ for s in service.services]
+ checked = 0
+ for serviceClass, serviceArgs in configuredSubServices:
+ if serviceClass in expectedSubServices:
+ checked += 1
+ self.assertEquals(
+ serviceArgs[0],
+ dict(expectedSubServices)[serviceClass]
+ )
+ # TCP+SSL services for IPv4, TCP+SSL services for IPv6.
+ self.assertEquals(checked, 4)
+
+
+ def test_SSLKeyConfiguration(self):
+ """
+ Test that the configuration of the SSLServer reflect the config file's
+ SSL Private Key and SSL Certificate
+ """
+ service = self.makeService()
+
+ sslService = None
+ for s in service.services:
+ if isinstance(s, internet.SSLServer):
+ sslService = s
+ break
+
+ self.failIf(sslService is None, "No SSL Service found")
+
+ context = sslService.args[2]
+
+ self.assertEquals(
+ self.config["SSLPrivateKey"],
+ context.privateKeyFileName
+ )
+ self.assertEquals(
+ self.config["SSLCertificate"],
+ context.certificateFileName,
+ )
+
+ def test_noSSL(self):
+ """
+ Test the single service to make sure there is no SSL Service when SSL
+ is disabled
+ """
+ del self.config["SSLPort"]
+ self.writeConfig()
+
+ service = self.makeService()
+
+ self.assertNotIn(
+ internet.SSLServer,
+ [s.__class__ for s in service.services]
+ )
+
+ def test_noHTTP(self):
+ """
+ Test the single service to make sure there is no TCPServer when
+ HTTPPort is not configured
+ """
+ del self.config["HTTPPort"]
+ self.writeConfig()
+
+ service = self.makeService()
+
+ self.assertNotIn(
+ internet.TCPServer,
+ [s.__class__ for s in service.services]
+ )
+
+ def test_singleBindAddresses(self):
+ """
+ Test that the TCPServer and SSLServers are bound to the proper address
+ """
+ self.config.BindAddresses = ["127.0.0.1"]
+ self.writeConfig()
+
+ service = self.makeService()
+
+ for s in service.services:
+ if isinstance(s, (internet.TCPServer, internet.SSLServer)):
+ self.assertEquals(s.kwargs["interface"], "127.0.0.1")
+
+ def test_multipleBindAddresses(self):
+ """
+ Test that the TCPServer and SSLServers are bound to the proper
+ addresses.
+ """
+ self.config.BindAddresses = [
+ "127.0.0.1",
+ "10.0.0.2",
+ "172.53.13.123",
+ ]
+
+ self.writeConfig()
+ service = self.makeService()
+
+ tcpServers = []
+ sslServers = []
+
+ for s in service.services:
+ if isinstance(s, internet.TCPServer):
+ tcpServers.append(s)
+ elif isinstance(s, internet.SSLServer):
+ sslServers.append(s)
+
+ self.assertEquals(len(tcpServers), len(self.config.BindAddresses))
+ self.assertEquals(len(sslServers), len(self.config.BindAddresses))
+
+ for addr in self.config.BindAddresses:
+ for s in tcpServers:
+ if s.kwargs["interface"] == addr:
+ tcpServers.remove(s)
+
+ for s in sslServers:
+ if s.kwargs["interface"] == addr:
+ sslServers.remove(s)
+
+ self.assertEquals(len(tcpServers), 0)
+ self.assertEquals(len(sslServers), 0)
+
+ def test_listenBacklog(self):
+ """
+ Test that the backlog arguments is set in TCPServer and SSLServers
+ """
+ self.config.ListenBacklog = 1024
+ self.writeConfig()
+ service = self.makeService()
+
+ for s in service.services:
+ if isinstance(s, (internet.TCPServer, internet.SSLServer)):
+ self.assertEquals(s.kwargs["backlog"], 1024)
+
+
+class ServiceHTTPFactoryTests(BaseServiceMakerTests):
+ """
+ Test the configuration of the initial resource hierarchy of the
+ single service
+ """
+ configOptions = {"HTTPPort": 8008}
+
+ def test_AuthWrapperAllEnabled(self):
+ """
+ Test the configuration of the authentication wrapper
+ when all schemes are enabled.
+ """
+ self.config.Authentication.Digest.Enabled = True
+ self.config.Authentication.Kerberos.Enabled = True
+ self.config.Authentication.Kerberos.ServicePrincipal = "http/hello at bob"
+ self.config.Authentication.Basic.Enabled = True
+
+ self.writeConfig()
+ site = self.getSite()
+
+ self.failUnless(isinstance(
+ site.resource.resource,
+ auth.AuthenticationWrapper))
+
+ authWrapper = site.resource.resource
+
+ expectedSchemes = ["negotiate", "digest", "basic"]
+
+ for scheme in authWrapper.credentialFactories:
+ self.failUnless(scheme in expectedSchemes)
+
+ self.assertEquals(len(expectedSchemes),
+ len(authWrapper.credentialFactories))
+
+ def test_servicePrincipalNone(self):
+ """
+ Test that the Kerberos principal look is attempted if the principal is empty.
+ """
+ self.config.Authentication.Kerberos.ServicePrincipal = ""
+ self.config.Authentication.Kerberos.Enabled = True
+ self.writeConfig()
+ site = self.getSite()
+
+ authWrapper = site.resource.resource
+
+ self.assertFalse(authWrapper.credentialFactories.has_key("negotiate"))
+
+ def test_servicePrincipal(self):
+ """
+ Test that the kerberos realm is the realm portion of a principal
+ in the form proto/host at realm
+ """
+ self.config.Authentication.Kerberos.ServicePrincipal = "http/hello at bob"
+ self.config.Authentication.Kerberos.Enabled = True
+ self.writeConfig()
+ site = self.getSite()
+
+ authWrapper = site.resource.resource
+ ncf = authWrapper.credentialFactories["negotiate"]
+
+ self.assertEquals(ncf.service, "http at HELLO")
+ self.assertEquals(ncf.realm, "bob")
+
+ def test_AuthWrapperPartialEnabled(self):
+ """
+ Test that the expected credential factories exist when
+ only a partial set of authentication schemes is
+ enabled.
+ """
+
+ self.config.Authentication.Basic.Enabled = False
+ self.config.Authentication.Kerberos.Enabled = False
+
+ self.writeConfig()
+ site = self.getSite()
+
+ authWrapper = site.resource.resource
+
+ expectedSchemes = ["digest"]
+
+ for scheme in authWrapper.credentialFactories:
+ self.failUnless(scheme in expectedSchemes)
+
+ self.assertEquals(
+ len(expectedSchemes),
+ len(authWrapper.credentialFactories)
+ )
+
+ def test_LogWrapper(self):
+ """
+ Test the configuration of the log wrapper
+ """
+ site = self.getSite()
+
+ self.failUnless(isinstance(
+ site.resource,
+ LogWrapperResource))
+
+ def test_rootResource(self):
+ """
+ Test the root resource
+ """
+ site = self.getSite()
+ root = site.resource.resource.resource
+
+ self.failUnless(isinstance(root, RootResource))
+
+ def test_principalResource(self):
+ """
+ Test the principal resource
+ """
+ site = self.getSite()
+ root = site.resource.resource.resource
+
+ self.failUnless(isinstance(
+ root.getChild("principals"),
+ DirectoryPrincipalProvisioningResource
+ ))
+
+ def test_calendarResource(self):
+ """
+ Test the calendar resource
+ """
+ site = self.getSite()
+ root = site.resource.resource.resource
+
+ self.failUnless(isinstance(
+ root.getChild("calendars"),
+ DirectoryCalendarHomeProvisioningResource
+ ))
+
+
+sudoersFile = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>users</key>
+ <array>
+ <dict>
+ <key>password</key>
+ <string>superuser</string>
+ <key>username</key>
+ <string>superuser</string>
+ </dict>
+ </array>
+</dict>
+</plist>
+"""
+
+class DirectoryServiceTest(BaseServiceMakerTests):
+ """
+ Tests of the directory service
+ """
+
+ configOptions = {"HTTPPort": 8008}
+
+ def test_sameDirectory(self):
+ """
+ Test that the principal hierarchy has a reference
+ to the same DirectoryService as the calendar hierarchy
+ """
+ site = self.getSite()
+ principals = site.resource.resource.resource.getChild("principals")
+ calendars = site.resource.resource.resource.getChild("calendars")
+
+ self.assertEquals(principals.directory, calendars.directory)
+
+ def test_aggregateDirectory(self):
+ """
+ Assert that the base directory service is actually
+ an AggregateDirectoryService
+ """
+ site = self.getSite()
+ principals = site.resource.resource.resource.getChild("principals")
+ directory = principals.directory
+
+ self.failUnless(isinstance(directory, AggregateDirectoryService))
+
+
+ def test_configuredDirectoryService(self):
+ """
+ Test that the real directory service is the directory service
+ set in the configuration file.
+ """
+ site = self.getSite()
+ principals = site.resource.resource.resource.getChild("principals")
+ directory = principals.directory
+
+ realDirectory = directory.serviceForRecordType("users")
+
+ configuredDirectory = namedAny(self.config.DirectoryService.type)
+
+ self.failUnless(isinstance(realDirectory, configuredDirectory))
+
+
+
+class DummyProcessObject(object):
+ """
+ Simple stub for Process Object API which just has an executable and some
+ arguments.
+
+ This is a stand in for L{TwistdSlaveProcess}.
+ """
+
+ def __init__(self, scriptname, *args):
+ self.scriptname = scriptname
+ self.args = args
+
+
+ def getCommandLine(self):
+ """
+ Simple command line.
+ """
+ return [self.scriptname] + list(self.args)
+
+
+ def getFileDescriptors(self):
+ """
+ Return a dummy, empty mapping of file descriptors.
+ """
+ return {}
+
+
+ def getName(self):
+ """
+ Get a dummy name.
+ """
+ return 'Dummy'
+
+
+class ScriptProcessObject(DummyProcessObject):
+ """
+ Simple stub for the Process Object API that will run a test script.
+ """
+
+ def getCommandLine(self):
+ """
+ Get the command line to invoke this script.
+ """
+ return [
+ sys.executable,
+ FilePath(__file__).sibling(self.scriptname).path
+ ] + list(self.args)
+
+
+
+
+
+class DelayedStartupProcessMonitorTests(TestCase):
+ """
+ Test cases for L{DelayedStartupProcessMonitor}.
+ """
+
+ def test_lineAfterLongLine(self):
+ """
+ A "long" line of output from a monitored process (longer than
+ L{LineReceiver.MAX_LENGTH}) should be logged in chunks rather than all
+ at once, to avoid resource exhaustion.
+ """
+ dspm = DelayedStartupProcessMonitor()
+ dspm.addProcessObject(ScriptProcessObject(
+ 'longlines.py', str(DelayedStartupLineLogger.MAX_LENGTH)),
+ os.environ)
+ dspm.startService()
+ self.addCleanup(dspm.stopService)
+
+ logged = []
+
+ def tempObserver(event):
+ # Probably won't be a problem, but let's not have any intermittent
+ # test issues that stem from multi-threaded log messages randomly
+ # going off...
+ if not isInIOThread():
+ callFromThread(tempObserver, event)
+ return
+ if event.get('isError'):
+ d.errback()
+ m = event.get('message')[0]
+ if m.startswith('[Dummy] '):
+ logged.append(event)
+ if m == '[Dummy] z':
+ d.callback("done")
+
+ log.addObserver(tempObserver)
+ self.addCleanup(log.removeObserver, tempObserver)
+ d = Deferred()
+ def assertions(result):
+ self.assertEquals(["[Dummy] x",
+ "[Dummy] y",
+ "[Dummy] y", # final segment
+ "[Dummy] z"],
+ [''.join(evt['message'])[:len('[Dummy]') + 2]
+ for evt in logged])
+ self.assertEquals([" (truncated, continued)",
+ " (truncated, continued)",
+ "[Dummy] y",
+ "[Dummy] z"],
+ [''.join(evt['message'])[-len(" (truncated, continued)"):]
+ for evt in logged])
+ d.addCallback(assertions)
+ return d
+
+
+ def test_breakLineIntoSegments(self):
+ """
+ Exercise the line-breaking logic with various key lengths
+ """
+ testLogger = DelayedStartupLineLogger()
+ testLogger.MAX_LENGTH = 10
+ for input, output in [
+ ("", []),
+ ("a", ["a"]),
+ ("abcde", ["abcde"]),
+ ("abcdefghij", ["abcdefghij"]),
+ ("abcdefghijk",
+ ["abcdefghij (truncated, continued)",
+ "k"
+ ]
+ ),
+ ("abcdefghijklmnopqrst",
+ ["abcdefghij (truncated, continued)",
+ "klmnopqrst"
+ ]
+ ),
+ ("abcdefghijklmnopqrstuv",
+ ["abcdefghij (truncated, continued)",
+ "klmnopqrst (truncated, continued)",
+ "uv"]
+ ),
+ ]:
+ self.assertEquals(output, testLogger._breakLineIntoSegments(input))
+
+
+ def test_acceptDescriptorInheritance(self):
+ """
+ If a L{TwistdSlaveProcess} specifies some file descriptors to be
+ inherited, they should be inherited by the subprocess.
+ """
+ imps = InMemoryProcessSpawner()
+ dspm = DelayedStartupProcessMonitor(imps)
+
+ # Most arguments here will be ignored, so these are bogus values.
+ slave = TwistdSlaveProcess(
+ twistd = "bleh",
+ tapname = "caldav",
+ configFile = "/does/not/exist",
+ id = 10,
+ interfaces = '127.0.0.1',
+ inheritFDs = [3, 7],
+ inheritSSLFDs = [19, 25],
+ )
+
+ dspm.addProcessObject(slave, {})
+ dspm.startService()
+ # We can easily stub out spawnProcess, because caldav calls it, but a
+ # bunch of callLater calls are buried in procmon itself, so we need to
+ # use the real clock.
+ oneProcessTransport = imps.waitForOneProcess()
+ self.assertEquals(oneProcessTransport.childFDs,
+ {0: 'w', 1: 'r', 2: 'r',
+ 3: 3, 7: 7,
+ 19: 19, 25: 25})
+
+
+ def test_changedArgumentEachSpawn(self):
+ """
+ If the result of C{getCommandLine} changes on subsequent calls,
+ subsequent calls should result in different arguments being passed to
+ C{spawnProcess} each time.
+ """
+ imps = InMemoryProcessSpawner()
+ dspm = DelayedStartupProcessMonitor(imps)
+ slave = DummyProcessObject('scriptname', 'first')
+ dspm.addProcessObject(slave, {})
+ dspm.startService()
+ oneProcessTransport = imps.waitForOneProcess()
+ self.assertEquals(oneProcessTransport.args,
+ ['scriptname', 'first'])
+ slave.args = ['second']
+ oneProcessTransport.processProtocol.processEnded(None)
+ twoProcessTransport = imps.waitForOneProcess()
+ self.assertEquals(twoProcessTransport.args,
+ ['scriptname', 'second'])
+
+
+ def test_metaDescriptorInheritance(self):
+ """
+ If a L{TwistdSlaveProcess} specifies a meta-file-descriptor to be
+ inherited, it should be inherited by the subprocess, and a
+ configuration argument should be passed that indicates to the
+ subprocess.
+ """
+ imps = InMemoryProcessSpawner()
+ dspm = DelayedStartupProcessMonitor(imps)
+ # Most arguments here will be ignored, so these are bogus values.
+ slave = TwistdSlaveProcess(
+ twistd = "bleh",
+ tapname = "caldav",
+ configFile = "/does/not/exist",
+ id = 10,
+ interfaces = '127.0.0.1',
+ metaSocket = FakeDispatcher().addSocket()
+ )
+
+ dspm.addProcessObject(slave, {})
+ dspm.startService()
+ oneProcessTransport = imps.waitForOneProcess()
+ self.assertIn("MetaFD=4", oneProcessTransport.args)
+ self.assertEquals(
+ oneProcessTransport.args[oneProcessTransport.args.index("MetaFD=4")-1],
+ '-o',
+ "MetaFD argument was not passed as an option"
+ )
+ self.assertEquals(oneProcessTransport.childFDs,
+ {0: 'w', 1: 'r', 2: 'r',
+ 4: 4})
+
+
+ def test_startServiceDelay(self):
+ """
+ Starting a L{DelayedStartupProcessMonitor} should result in the process
+ objects that have been added to it being started once per
+ delayInterval.
+ """
+ imps = InMemoryProcessSpawner()
+ dspm = DelayedStartupProcessMonitor(imps)
+ dspm.delayInterval = 3.0
+ sampleCounter = range(0, 5)
+ for counter in sampleCounter:
+ slave = TwistdSlaveProcess(
+ twistd = "bleh",
+ tapname = "caldav",
+ configFile = "/does/not/exist",
+ id = counter * 10,
+ interfaces = '127.0.0.1',
+ metaSocket = FakeDispatcher().addSocket()
+ )
+ dspm.addProcessObject(slave, {"SAMPLE_ENV_COUNTER": str(counter)})
+ dspm.startService()
+
+ # Advance the clock a bunch of times, allowing us to time things with a
+ # comprehensible resolution.
+ imps.pump([0] + [dspm.delayInterval / 2.0] * len(sampleCounter) * 3)
+ expectedValues = [dspm.delayInterval * n for n in sampleCounter]
+ self.assertEquals([x.startedAt for x in imps.processTransports],
+ expectedValues)
+
+
+
+class FakeFD(object):
+
+ def __init__(self, n):
+ self.fd = n
+
+
+ def fileno(self):
+ return self.fd
+
+
+
+class FakeDispatcher(object):
+ n = 3
+
+ def addSocket(self):
+ self.n += 1
+ return FakeFD(self.n)
+
+
+
+class TwistdSlaveProcessTests(TestCase):
+ """
+ Tests for L{TwistdSlaveProcess}.
+ """
+ def test_pidfile(self):
+ """
+ The result of L{TwistdSlaveProcess.getCommandLine} includes an option
+ setting the name of the pidfile to something including the instance id.
+ """
+ slave = TwistdSlaveProcess("/path/to/twistd", "something", "config", 7, [])
+ commandLine = slave.getCommandLine()
+
+ option = 'PIDFile=something-instance-7.pid'
+ self.assertIn(option, commandLine)
+ self.assertEquals(commandLine[commandLine.index(option) - 1], '-o')
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120508/0a08d555/attachment-0001.html>
More information about the calendarserver-changes
mailing list