From 3bb992da6d0526449c8794ecf72ea659e87878ed Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 5 Dec 2017 13:53:40 +0100 Subject: [PATCH 1/2] Fix error reporting of LDAPObject.set_option() The method LDAPObject.set_option() did not signal an exception in case the set option failed. The bug was caused by checking for the wrong error value. The internal C function LDAP_set_option() returns ``1`` for sucess and ``0`` for error, but LDAPObject.set_option() was checking for ``-1``. Add some tests for LDAPObject.set_option() and LDAPObject.get_option(). Closes: https://github.com/python-ldap/python-ldap/issues/100 Signed-off-by: Christian Heimes --- Modules/LDAPObject.c | 2 +- Tests/t_ldap_options.py | 89 +++++++++++++++++++++++++++++------------ 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c index 815fe426..cd816553 100644 --- a/Modules/LDAPObject.c +++ b/Modules/LDAPObject.c @@ -1273,7 +1273,7 @@ l_ldap_set_option(PyObject* self, PyObject *args) if (!PyArg_ParseTuple(args, "iO:set_option", &option, &value)) return NULL; - if (LDAP_set_option((LDAPObject *)self, option, value) == -1) + if (!LDAP_set_option((LDAPObject *)self, option, value)) return NULL; Py_INCREF(Py_None); return Py_None; diff --git a/Tests/t_ldap_options.py b/Tests/t_ldap_options.py index 798ae463..412610ba 100644 --- a/Tests/t_ldap_options.py +++ b/Tests/t_ldap_options.py @@ -8,8 +8,8 @@ from ldap.controls import RequestControlTuples from ldap.controls.pagedresults import SimplePagedResultsControl from ldap.controls.openldap import SearchNoOpControl -from slapdtest import requires_tls - +from ldap.ldapobject import SimpleLDAPObject +from slapdtest import SlapdTestCase, requires_tls SENTINEL = object() @@ -27,53 +27,62 @@ class TestGlobalOptions(unittest.TestCase): + def get_option(self, option): + return ldap.get_option(option) + + def set_option(self, option, value): + return ldap.set_option(option, value) + def _check_option(self, option, value, expected=SENTINEL, nonevalue=None): - old = ldap.get_option(option) + old = self.get_option(option) try: - ldap.set_option(option, value) - new = ldap.get_option(option) + self.set_option(option, value) + new = self.get_option(option) if expected is SENTINEL: self.assertEqual(new, value) else: self.assertEqual(new, expected) finally: - ldap.set_option(option, old if old is not None else nonevalue) - self.assertEqual(ldap.get_option(option), old) + self.set_option( + option, + old if old is not None else nonevalue + ) + self.assertEqual(self.get_option(option), old) def test_invalid(self): with self.assertRaises(ValueError): - ldap.get_option(-1) + self.get_option(-1) with self.assertRaises(ValueError): - ldap.set_option(-1, '') + self.set_option(-1, '') - def test_timeout(self): - self._check_option(ldap.OPT_TIMEOUT, 0, nonevalue=-1) - self._check_option(ldap.OPT_TIMEOUT, 10.5, nonevalue=-1) + def _test_timeout(self, option): + self._check_option(option, 10.5, nonevalue=-1) + self._check_option(option, 0, nonevalue=-1) with self.assertRaises(ValueError): - self._check_option(ldap.OPT_TIMEOUT, -5, nonevalue=-1) + self._check_option(option, -5, nonevalue=-1) with self.assertRaises(TypeError): - ldap.set_option(ldap.OPT_TIMEOUT, object) + self.set_option(option, object) + + def test_timeout(self): + self._test_timeout(ldap.OPT_TIMEOUT) def test_network_timeout(self): - self._check_option(ldap.OPT_NETWORK_TIMEOUT, 0, nonevalue=-1) - self._check_option(ldap.OPT_NETWORK_TIMEOUT, 10.5, nonevalue=-1) - with self.assertRaises(ValueError): - self._check_option(ldap.OPT_NETWORK_TIMEOUT, -5, nonevalue=-1) + self._test_timeout(ldap.OPT_NETWORK_TIMEOUT) def _test_controls(self, option): self._check_option(option, []) self._check_option(option, TEST_CTRL, TEST_CTRL_EXPECTED) self._check_option(option, tuple(TEST_CTRL), TEST_CTRL_EXPECTED) with self.assertRaises(TypeError): - ldap.set_option(option, object) + self.set_option(option, object) with self.assertRaises(TypeError): # must contain a tuple - ldap.set_option(option, [list(TEST_CTRL[0])]) + self.set_option(option, [list(TEST_CTRL[0])]) with self.assertRaises(TypeError): # data must be bytes or None - ldap.set_option( + self.set_option( option, [TEST_CTRL[0][0], TEST_CTRL[0][1], u'data'] ) @@ -87,20 +96,50 @@ def test_server_controls(self): def test_uri(self): self._check_option(ldap.OPT_URI, "ldapi:///path/to/socket") with self.assertRaises(TypeError): - ldap.set_option(ldap.OPT_URI, object) + self.set_option(ldap.OPT_URI, object) @requires_tls() def test_cafile(self): # None or a distribution or OS-specific path - ldap.get_option(ldap.OPT_X_TLS_CACERTFILE) + self.get_option(ldap.OPT_X_TLS_CACERTFILE) def test_readonly(self): - value = ldap.get_option(ldap.OPT_API_INFO) + value = self.get_option(ldap.OPT_API_INFO) self.assertIsInstance(value, dict) with self.assertRaises(ValueError) as e: - ldap.set_option(ldap.OPT_API_INFO, value) + self.set_option(ldap.OPT_API_INFO, value) self.assertIn('read-only', str(e.exception)) +class TestLDAPObjectOptions(TestGlobalOptions, SlapdTestCase): + ldap_object_class = SimpleLDAPObject + + def setUp(self): + self.conn = self._open_ldap_conn( + who=self.server.root_dn, + cred=self.server.root_pw + ) + + def tearDown(self): + self.conn.unbind_s() + self.conn = None + + def get_option(self, option): + return self.conn.get_option(option) + + def set_option(self, option, value): + return self.conn.set_option(option, value) + + # test is failing with: + # pyasn1.error.SubstrateUnderrunError: Short octet stream on tag decoding + @unittest.expectedFailure + def test_client_controls(self): + self._test_controls(ldap.OPT_CLIENT_CONTROLS) + + @unittest.expectedFailure + def test_server_controls(self): + self._test_controls(ldap.OPT_SERVER_CONTROLS) + + if __name__ == '__main__': unittest.main() From 33fc1027595e74340022d62a500cd9021e7a4195 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 5 Dec 2017 14:48:17 +0100 Subject: [PATCH 2/2] Don't break Liskov substitution principle in option tests Make TestGlobalOptions and TestLDAPObjectOptions peers in the inheritance hierarchy, rather than one deriving from the other. --- Tests/t_ldap_options.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Tests/t_ldap_options.py b/Tests/t_ldap_options.py index 412610ba..f0bcc210 100644 --- a/Tests/t_ldap_options.py +++ b/Tests/t_ldap_options.py @@ -26,12 +26,17 @@ ] -class TestGlobalOptions(unittest.TestCase): +class BaseTestOptions(object): + """Common tests for getting/setting options + + Used in subclasses below + """ + def get_option(self, option): - return ldap.get_option(option) + raise NotImplementedError() def set_option(self, option, value): - return ldap.set_option(option, value) + raise NotImplementedError() def _check_option(self, option, value, expected=SENTINEL, nonevalue=None): @@ -111,7 +116,21 @@ def test_readonly(self): self.assertIn('read-only', str(e.exception)) -class TestLDAPObjectOptions(TestGlobalOptions, SlapdTestCase): +class TestGlobalOptions(BaseTestOptions, unittest.TestCase): + """Test setting/getting options globally + """ + + def get_option(self, option): + return ldap.get_option(option) + + def set_option(self, option, value): + return ldap.set_option(option, value) + + +class TestLDAPObjectOptions(BaseTestOptions, SlapdTestCase): + """Test setting/getting connection-specific options + """ + ldap_object_class = SimpleLDAPObject def setUp(self):