diff --git a/docs/source/NEWS.rst b/docs/source/NEWS.rst index 99fff9d2..60cf2a35 100644 --- a/docs/source/NEWS.rst +++ b/docs/source/NEWS.rst @@ -59,6 +59,11 @@ Fixes server accessed over TLS (or any other framing mismatch) fails fast rather than hanging the client. Also removes a stray ``print`` from production code (#170, #240, #243). +- ``SearchByTreeWalkingMixin.search`` now waits for the underlying + subtree iterator's Deferred before firing its own. Previously the + iterator's Deferred was silently discarded, so back ends whose + ``children`` / ``subtree`` methods actually behaved asynchronously + returned an empty result set (#62). 21.2.0 (2021-02-28) diff --git a/ldaptor/entryhelpers.py b/ldaptor/entryhelpers.py index ddba2096..4cad5ee8 100644 --- a/ldaptor/entryhelpers.py +++ b/ldaptor/entryhelpers.py @@ -302,9 +302,9 @@ def _tryMatch(entry): if entry.match(filterObject): matchCallback(entry) - iterator(callback=_tryMatch) - + d = defer.maybeDeferred(iterator, callback=_tryMatch) if callback is None: - return defer.succeed(results) + d.addCallback(lambda _: results) else: - return defer.succeed(None) + d.addCallback(lambda _: None) + return d diff --git a/ldaptor/test/test_inmemory.py b/ldaptor/test/test_inmemory.py index fc1f2bcf..4c23bdb6 100644 --- a/ldaptor/test/test_inmemory.py +++ b/ldaptor/test/test_inmemory.py @@ -827,3 +827,47 @@ def testDeleteSubtree(self): ], ) return d + + +class AsyncChildrenEntry(inmemory.ReadOnlyInMemoryLDAPEntry): + """ + An entry whose ``children`` returns a Deferred that fires later, so the + surrounding subtree walk is genuinely asynchronous (issue #62). + """ + + def children(self, callback=None): + from twisted.internet import defer as _defer + + self.pending = _defer.Deferred() + base = inmemory.ReadOnlyInMemoryLDAPEntry + self.pending.addCallback(lambda _: base.children(self, callback=callback)) + return self.pending + + +class TestSearchAsync(unittest.TestCase): + def test_search_waitsForAsyncIterator(self): + """ + SearchByTreeWalkingMixin.search must return a Deferred that fires + only after the underlying subtree iterator's Deferred has fired. + Previously the iterator's Deferred was discarded, so asynchronous + back ends silently returned an empty result set. + """ + root = AsyncChildrenEntry( + dn=distinguishedname.DistinguishedName("dc=example,dc=com"), + attributes={"objectClass": ["a"], "dc": ["example"]}, + ) + root.addChild( + rdn="cn=foo", + attributes={"objectClass": ["a"], "cn": ["foo"]}, + ) + + d = root.search() + fired = [] + d.addCallback(fired.append) + + self.assertEqual(fired, [], "search Deferred fired before children") + root.pending.callback(None) + self.assertEqual(len(fired), 1) + dns = [str(e.dn) for e in fired[0]] + self.assertIn("dc=example,dc=com", dns) + self.assertIn("cn=foo,dc=example,dc=com", dns)