Index: main/waeup.ikoba/trunk/src/waeup/ikoba/tests/sample-datacenter.zcml
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/tests/sample-datacenter.zcml	(revision 12060)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/tests/sample-datacenter.zcml	(revision 12060)
@@ -0,0 +1,13 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:ikoba="http://namespaces.waeup.org/ikoba"
+   i18n_domain="waeup.ikoba"
+   package="waeup.ikoba.tests"
+   >
+  <include package="waeup.ikoba" file="meta.zcml" />
+
+  <!-- Sample for the 'datacenter' ZCML directive: -->
+  <ikoba:datacenter
+      path="/some/path" />
+
+</configure>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/tests/test_zcml.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/tests/test_zcml.py	(revision 12060)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/tests/test_zcml.py	(revision 12060)
@@ -0,0 +1,120 @@
+import os
+import tempfile
+import shutil
+import unittest
+from waeup.ikoba import tests
+from waeup.ikoba.interfaces import IDataCenterConfig, IPayPalConfig
+from zope.component import getUtility, queryUtility, getGlobalSiteManager
+from zope.configuration.exceptions import ConfigurationError
+from zope.configuration.xmlconfig import file as zcfile
+from zope.configuration.xmlconfig import string as zcstring
+
+
+def get_zcml_conf(xml_code):
+    """Get a working ZCML configuration file with `xml_code` embedded.
+
+    The returned ZCML declares an ikoba namespace for local ZCML
+    extensions and includes the `waeup.ikoba.meta` declarations before
+    embedding `xml_code`.
+
+    The returned string can be 'executed' with `string` or `file` from
+    `zope.configuration.xmlconfig.
+    """
+    config = (
+        '<configure '
+        '   xmlns="http://namespaces.zope.org/zope"'
+        '   xmlns:ikoba="http://namespaces.waeup.org/ikoba"'
+        '   i18n_domain="waeup.ikoba"'
+        '   package="waeup.ikoba.tests"'
+        '>'
+        '<include package="waeup.ikoba" file="meta.zcml" />'
+        ''
+        '%s'
+        ''
+        '</configure>'
+        ) % xml_code
+    return config
+
+
+class ZCMLTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        # unregister any remaining utils registered during tests
+        gsm = getGlobalSiteManager()
+        for iface in (IDataCenterConfig, IPayPalConfig):
+            conf = queryUtility(iface)
+            if conf is None:
+                continue
+            gsm.unregisterUtility(conf, iface)
+
+    def create_fake_paypal_configs(self):
+        # create two regular files in workdir
+        self.conf1_path = os.path.join(self.workdir, 'paypal1.conf')
+        self.conf2_path = os.path.join(self.workdir, 'paypal2.conf')
+        open(self.conf1_path, 'w').write('[rest-client]\n')
+        open(self.conf2_path, 'w').write('[rest-client]\n')
+
+    def test_datacenter_config_directive(self):
+        # the 'datacenter' directive can be used in ZCML
+        assert queryUtility(IDataCenterConfig) is None
+        zcfile("sample-datacenter.zcml", tests)  # 'execute' the ZCML
+        result = getUtility(IDataCenterConfig)
+        assert result == {'path': u'/some/path'}
+
+    def test_datacenter_config_singleton(self):
+        # we can define only one datacenter config
+        config = get_zcml_conf(
+            '<ikoba:datacenter path="/some/path" />'
+            '<ikoba:datacenter path="/other/path" />'
+            )
+        assert queryUtility(IDataCenterConfig) is None
+        self.assertRaises(
+            ConfigurationError, zcstring, config)
+
+    def test_paypal_config_directive(self):
+        # the 'paypalconf' ZCML directive works
+        assert queryUtility(IPayPalConfig) is None
+        self.create_fake_paypal_configs()
+        config = get_zcml_conf(
+            '<ikoba:paypalconf path="%s" />' % self.conf1_path
+            )
+        zcstring(config)  # execute ZCML
+        result = getUtility(IPayPalConfig)
+        assert result == {
+            'path': self.conf1_path,
+            'client_id': None,
+            'client_secret': None,
+            'mode': 'sandbox'
+            }
+
+    def test_paypal_config_singleton(self):
+        # we can define only one paypal config
+        assert queryUtility(IPayPalConfig) is None
+        self.create_fake_paypal_configs()
+        config = get_zcml_conf(
+            '<ikoba:paypalconf path="%s" />'
+            '<ikoba:paypalconf path="%s" />'
+            ) % (self.conf1_path, self.conf2_path)
+        assert queryUtility(IPayPalConfig) is None
+        self.assertRaises(
+            ConfigurationError, zcstring, config)
+
+    def test_paypal_config_no_such_file(self):
+        # a set paypal config file must exist
+        config = get_zcml_conf(
+            '<ikoba:paypalconf path="/not/eXiStInG/path" />')
+        self.assertRaises(
+            ConfigurationError, zcstring, config)
+
+    def test_paypal_config_not_a_regular_file(self):
+        # a paypal config file must be a regular file
+        dirpath = os.path.join(self.workdir, 'mydir')
+        os.mkdir(dirpath)
+        config = get_zcml_conf(
+            '<ikoba:paypalconf path="%s" />' % dirpath)
+        self.assertRaises(
+            ConfigurationError, zcstring, config)
