From 3e3e7cd209c350742f359e630721d6e1d3d9b560 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sun, 14 Jan 2018 13:57:42 +0000 Subject: [PATCH] Use absolute imports and fix all python2 -3 warnings when running tests. (#86) --- CONTRIBUTING.rst | 7 + docs/source/NEWS.rst | 6 +- ldaptor/delta.py | 22 +- ldaptor/dns.py | 9 +- ldaptor/md4.py | 3 +- ldaptor/numberalloc.py | 3 +- ldaptor/protocols/ldap/ldapclient.py | 2 +- ldaptor/protocols/ldap/ldapconnector.py | 2 +- ldaptor/protocols/ldap/ldaperrors.py | 15 +- ldaptor/protocols/ldap/ldapsyntax.py | 17 +- ldaptor/protocols/pureber.py | 30 +- ldaptor/protocols/pureldap.py | 2 +- ldaptor/test/test_autofill_samba.py | 11 +- ldaptor/test/test_delta.py | 445 ++++++++++++++++++------ ldaptor/test/test_ldapsyntax.py | 141 +++++++- ldaptor/test/test_ldifprotocol.py | 5 +- ldaptor/test/test_pureber.py | 36 +- ldaptor/test/test_usage.py | 94 +++++ ldaptor/usage.py | 25 +- setup.py | 1 + tox.ini | 3 +- 21 files changed, 703 insertions(+), 176 deletions(-) create mode 100644 ldaptor/test/test_usage.py diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d316856f..3287cac7 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -25,9 +25,16 @@ The recommended local dev enviroment is `tox -e py27-dev` When running on local dev env, you will get a coverage report for whole code as well as for the changes since `master`. The reports are also produced in HTML at: + * build/coverage-html/index.html * build/coverage-diff.html +You can run a subset of the test by passing the dotted path to the test or +test case, test module or test package:: + + tox -e py27-dev ldaptor.test.test_delta.TestModifyOp.testAsLDIF + tox -e py27-dev ldaptor.test.test_usage + Release notes ------------- diff --git a/docs/source/NEWS.rst b/docs/source/NEWS.rst index 3482b82d..5c3649c5 100644 --- a/docs/source/NEWS.rst +++ b/docs/source/NEWS.rst @@ -2,7 +2,7 @@ Changelog ========= -Release 17.1 (UNRELEASED) +Release 18.0 (UNRELEASED) ------------------------- Features @@ -21,11 +21,13 @@ Changes Twisted trunk branch. - The local development environment was updated to produce overall and diff coverage reports in HTML format. +- `six` package is now a direct dependency in preparation for the Python 3 + port. Bugfixes ^^^^^^^^ -- DN matching now case insensitive. +- DN matching is now case insensitive. Release 16.0 (2016-06-07) diff --git a/ldaptor/delta.py b/ldaptor/delta.py index c586ef74..617b3fa7 100644 --- a/ldaptor/delta.py +++ b/ldaptor/delta.py @@ -185,16 +185,22 @@ def __repr__(self): def __eq__(self, other): if not isinstance(other, self.__class__): - return 0 + return NotImplemented if self.dn != other.dn: return 0 if self.modifications != other.modifications: return 0 return 1 + def __hash__(self): + # We use the LDIF representation as similar objects + # should have the same LDIF. + return hash(self.asLDIF()) + def __ne__(self, other): return not self==other + class AddOp(Operation): def __init__(self, entry): self.entry = entry @@ -220,13 +226,19 @@ def __repr__(self): def __eq__(self, other): if not isinstance(other, self.__class__): - return False + return NotImplemented if self.entry != other.entry: return False return True def __ne__(self, other): - return not self==other + return not self == other + + def __hash__(self): + # Use the LDIF representions as equal operations should + # have the same LDIF. + return hash(self.asLDIF()) + class DeleteOp(Operation): def __init__(self, dn): @@ -254,7 +266,7 @@ def __repr__(self): def __eq__(self, other): if not isinstance(other, self.__class__): - return False + return NotImplemented if self.dn != other.dn: return False return True @@ -262,3 +274,5 @@ def __eq__(self, other): def __ne__(self, other): return not self==other + def __hash__(self): + return hash(self.dn) diff --git a/ldaptor/dns.py b/ldaptor/dns.py index c41e73a6..e327bd40 100644 --- a/ldaptor/dns.py +++ b/ldaptor/dns.py @@ -2,13 +2,14 @@ from socket import inet_aton, inet_ntoa + def aton_octets(ip): - s=inet_aton(ip) - octets=map(None, s) + s = inet_aton(ip) + octets = list(s) n = 0 for o in octets: - n=n<<8 - n+=ord(o) + n = n << 8 + n += ord(o) return n def aton_numbits(num): diff --git a/ldaptor/md4.py b/ldaptor/md4.py index 596c5156..003f9c9b 100644 --- a/ldaptor/md4.py +++ b/ldaptor/md4.py @@ -17,7 +17,7 @@ import struct from warnings import warn # site -from compat import b, bytes, bascii_to_str, irange # , PY3 # twisted isn't Python3, yet +from ldaptor.compat import b, bytes, bascii_to_str, irange, PYPY # local __all__ = ["md4"] @@ -244,7 +244,6 @@ def hexdigest(self): # check if hashlib provides accelarated md4 # ============================================================================= import hashlib -from compat import PYPY def _has_native_md4(): # pragma: no cover -- runtime detection diff --git a/ldaptor/numberalloc.py b/ldaptor/numberalloc.py index 0df91d25..c566ef97 100644 --- a/ldaptor/numberalloc.py +++ b/ldaptor/numberalloc.py @@ -1,4 +1,5 @@ """Find an available uidNumber/gidNumber/other similar number.""" +from __future__ import division from ldaptor.protocols import pureldap @@ -29,7 +30,7 @@ def _nextGuess(self, found, lastGuess): if max is None: max=self.min+1000 - guess=(max+self.min)/2 + guess = (max + self.min) // 2 d=self.makeAGuess(guess) d.addCallback(self._nextGuess, guess) return d diff --git a/ldaptor/protocols/ldap/ldapclient.py b/ldaptor/protocols/ldap/ldapclient.py index 5b0e5aff..4ac084c0 100644 --- a/ldaptor/protocols/ldap/ldapclient.py +++ b/ldaptor/protocols/ldap/ldapclient.py @@ -88,7 +88,7 @@ def _send(self, op): msg = pureldap.LDAPMessage(op) if self.debug: log.msg('C->S %s' % repr(msg)) - assert not self.onwire.has_key(msg.id) + assert msg.id not in self.onwire return msg def _cbSend(self, msg, d): diff --git a/ldaptor/protocols/ldap/ldapconnector.py b/ldaptor/protocols/ldap/ldapconnector.py index 33b18f7a..53dfb2d2 100644 --- a/ldaptor/protocols/ldap/ldapconnector.py +++ b/ldaptor/protocols/ldap/ldapconnector.py @@ -48,7 +48,7 @@ def __getstate__(self): def _findOverRide(self, dn, overrides): while True: - if overrides.has_key(dn): + if dn in overrides: return overrides[dn] if dn == '': break diff --git a/ldaptor/protocols/ldap/ldaperrors.py b/ldaptor/protocols/ldap/ldaperrors.py index 03010e1a..3aac43e3 100644 --- a/ldaptor/protocols/ldap/ldaperrors.py +++ b/ldaptor/protocols/ldap/ldaperrors.py @@ -81,7 +81,7 @@ def __str__(self): else: return codeName -import new + def init(**errors): global reverse reverse = {} @@ -90,11 +90,14 @@ def init(**errors): klass = Success else: classname = 'LDAP'+name[0].upper()+name[1:] - klass = new.classobj(classname, - (LDAPException,), - { 'resultCode': value, - 'name': name, - }) + klass = type( + classname, + (LDAPException,), + { + 'resultCode': value, + 'name': name, + }, + ) globals()[classname] = klass reverse[value] = klass diff --git a/ldaptor/protocols/ldap/ldapsyntax.py b/ldaptor/protocols/ldap/ldapsyntax.py index d93c3987..d321794d 100644 --- a/ldaptor/protocols/ldap/ldapsyntax.py +++ b/ldaptor/protocols/ldap/ldapsyntax.py @@ -1,4 +1,5 @@ """Pythonic API for LDAP operations.""" +import functools from twisted.internet import defer from twisted.python.failure import Failure @@ -247,22 +248,22 @@ def __str__(self): def __eq__(self, other): if not isinstance(other, self.__class__): - return 0 + return NotImplemented if self.dn != other.dn: - return 0 + return False my=self.keys() my.sort() its=other.keys() its.sort() if my != its: - return 0 + return False for key in my: myAttr = self[key] itsAttr = other[key] if myAttr != itsAttr: - return 0 - return 1 + return False + return True def __ne__(self, other): return not self == other @@ -273,6 +274,9 @@ def __len__(self): def __nonzero__(self): return True + def __hash__(self): + return hash(str(self)) + def bind(self, password): r = pureldap.LDAPBindRequest(dn=str(self.dn), auth=password) d = self.client.send(r) @@ -563,7 +567,8 @@ def _passwordChangerPriorityComparison(me, other): prefix = 'setPasswordMaybe_' names = [name[len(prefix):] for name in dir(self) if name.startswith(prefix)] - names.sort(_passwordChangerPriorityComparison) + names.sort( + key=functools.cmp_to_key(_passwordChangerPriorityComparison)) d = defer.maybeDeferred(self._setPasswordAll, [], diff --git a/ldaptor/protocols/pureber.py b/ldaptor/protocols/pureber.py index 44fc718d..9dfd2d14 100644 --- a/ldaptor/protocols/pureber.py +++ b/ldaptor/protocols/pureber.py @@ -30,9 +30,10 @@ # Only some BOOLEAN and INTEGER types have default values in # this protocol definition. - import string +from six.moves import UserList + # xxxxxxxx # |/|\.../ # | | | @@ -70,7 +71,6 @@ def __str__(self): return "BERDecoderContext has no tag 0x%02x: %s" \ % (self.tag, self.context) -import UserList def berDecodeLength(m, offset=0): """ @@ -117,6 +117,7 @@ def ber2int(e, signed=True): v = (v << 8) | ord(e[i]) return v + class BERBase(object): tag = None @@ -137,16 +138,19 @@ def __cmp__(self, other): return -1 def __eq__(self, other): - if isinstance(other, BERBase): - return str(self) == str(other) - else: - return False + if not isinstance(other, BERBase): + return NotImplemented + + return str(self) == str(other) def __ne__(self, other): - if isinstance(other, BERBase): - return str(self) != str(other) - else: - return False + if not isinstance(other, BERBase): + return NotImplemented + + return str(self) != str(other) + + def __hash__(self): + return hash(str(self)) class BERStructured(BERBase): @@ -289,7 +293,8 @@ def __repr__(self): class BEREnumerated(BERInteger): tag = 0x0a -class BERSequence(BERStructured, UserList.UserList): + +class BERSequence(BERStructured, UserList): # TODO __getslice__ calls __init__ with no args. tag = 0x10 @@ -301,9 +306,8 @@ def fromBER(klass, tag, content, berdecoder=None): def __init__(self, value=None, tag=None): BERStructured.__init__(self, tag) - UserList.UserList.__init__(self) assert value is not None - self[:] = value + UserList.__init__(self, value) def __str__(self): r=string.join(map(str, self.data), '') diff --git a/ldaptor/protocols/pureldap.py b/ldaptor/protocols/pureldap.py index 155e706d..d23762c3 100644 --- a/ldaptor/protocols/pureldap.py +++ b/ldaptor/protocols/pureldap.py @@ -17,7 +17,7 @@ import string -from pureber import ( +from ldaptor.protocols.pureber import ( BERBoolean, BERDecoderContext, BEREnumerated, BERInteger, BERNull, BEROctetString, BERSequence, BERSequenceOf, BERSet, BERStructured, diff --git a/ldaptor/test/test_autofill_samba.py b/ldaptor/test/test_autofill_samba.py index 30e71e18..21785b6c 100644 --- a/ldaptor/test/test_autofill_samba.py +++ b/ldaptor/test/test_autofill_samba.py @@ -1,9 +1,8 @@ """ Test cases for ldaptor.protocols.ldap.autofill.sambaAccount module. """ - -import sets from twisted.trial import unittest + from ldaptor.protocols.ldap import ldapsyntax from ldaptor.protocols.ldap.autofill import sambaAccount, sambaSamAccount from ldaptor import testutil @@ -153,7 +152,7 @@ def testDefaultSetting(self): def cb(dummy): client.assertNothingSent() - self.failUnlessEqual(sets.Set(o.keys()), sets.Set([ + self.failUnlessEqual(set(o.keys()), { 'objectClass', 'sambaAcctFlags', 'sambaLogoffTime', @@ -161,7 +160,7 @@ def cb(dummy): 'sambaPwdCanChange', 'sambaPwdLastSet', 'sambaPwdMustChange', - ])) + }) self.failUnlessEqual(o['sambaAcctFlags'], ['[UX ]']) self.failUnlessEqual(o['sambaPwdLastSet'], ['1']) @@ -187,7 +186,7 @@ def testDefaultSetting_fixedPrimaryGroupSID(self): def cb(dummy): client.assertNothingSent() - self.failUnlessEqual(sets.Set(o.keys()), sets.Set([ + self.failUnlessEqual(set(o.keys()), { 'objectClass', 'sambaAcctFlags', 'sambaLogoffTime', @@ -196,7 +195,7 @@ def cb(dummy): 'sambaPwdLastSet', 'sambaPwdMustChange', 'sambaPrimaryGroupSID', - ])) + }) self.failUnlessEqual(o['sambaPrimaryGroupSID'], ['foo-4131312']) self.failUnlessEqual(o['sambaAcctFlags'], ['[UX ]']) diff --git a/ldaptor/test/test_delta.py b/ldaptor/test/test_delta.py index adc5d2a9..fb82f159 100644 --- a/ldaptor/test/test_delta.py +++ b/ldaptor/test/test_delta.py @@ -3,7 +3,6 @@ """ from twisted.trial import unittest -from ldaptor import testutil from ldaptor import delta, entry, attributeset, inmemory from ldaptor.protocols.ldap import ldapsyntax, distinguishedname, ldaperrors @@ -98,6 +97,8 @@ def testReplace_Delete_NonExisting(self): self.failUnlessEqual(self.foo['sn'], ['bar']) self.failUnlessEqual(self.foo['more'], ['junk']) + + class TestModificationOpLDIF(unittest.TestCase): def testAdd(self): m=delta.Add('foo', ['bar', 'baz']) @@ -146,48 +147,260 @@ def testReplaceAll(self): """) -class TestAddOpLDIF(unittest.TestCase): - def testSimple(self): - op=delta.AddOp(entry.BaseLDAPEntry( +class OperationTestCase(unittest.TestCase): + """ + Test case for operations on a LDAP tree. + """ + def getRoot(self): + """ + Returns a new LDAP root for dc=example,dc=com. + """ + return inmemory.ReadOnlyInMemoryLDAPEntry( + dn=distinguishedname.DistinguishedName('dc=example,dc=com')) + + +class TestAddOpLDIF(OperationTestCase): + """ + Unit tests for `AddOp`. + """ + def testAsLDIF(self): + """ + It will return the LDIF representation of the operation. + """ + sut =delta.AddOp(entry.BaseLDAPEntry( dn='dc=example,dc=com', - attributes={'foo': ['bar', 'baz'], - 'quux': ['thud']})) - self.assertEqual(op.asLDIF(), - """\ -dn: dc=example,dc=com + attributes={ + 'foo': ['bar', 'baz'], + 'quux': ['thud'], + }, + )) + + result = sut.asLDIF() + + self.assertEqual("""dn: dc=example,dc=com changetype: add foo: bar foo: baz quux: thud -""") +""", + result) + + def testAddOpEqualitySameEntry(self): + """ + Objects are equal when the have the same LDAP entry. + """ + first_entry = entry.BaseLDAPEntry( + dn='ou=Duplicate Team, dc=example,dc=com', + attributes={'foo': ['same', 'attributes']}) + second_entry = entry.BaseLDAPEntry( + dn='ou=Duplicate Team, dc=example,dc=com', + attributes={'foo': ['same', 'attributes']}) + + first = delta.AddOp(first_entry) + second = delta.AddOp(second_entry) + + self.assertEqual(first, second) + + def testAddOpInequalityDifferentEntry(self): + """ + Objects are not equal when the have different LDAP entries. + """ + first_entry = entry.BaseLDAPEntry( + dn='ou=First Team, dc=example,dc=com', + attributes={'foo': ['same', 'attributes']}) + second_entry = entry.BaseLDAPEntry( + dn='ou=First Team, dc=example,dc=com', + attributes={'foo': ['other', 'attributes']}) + + first = delta.AddOp(first_entry) + second = delta.AddOp(second_entry) + + self.assertNotEqual(first, second) + + def testAddOpInequalityNoEntryObject(self): + """ + Objects is not equal with random objects. + """ + team_entry = entry.BaseLDAPEntry( + dn='ou=Duplicate Team, dc=example,dc=com', + attributes={'foo': ['same', 'attributes']}) + sut = delta.AddOp(team_entry) + + self.assertNotEqual(sut, {'foo': ['same', 'attributes']}) + + def testAddOpHashSimilar(self): + """ + Objects which are equal have the same hash. + """ + first_entry = entry.BaseLDAPEntry( + dn='ou=Duplicate Team, dc=example,dc=com', + attributes={'foo': ['same', 'attributes']}) + second_entry = entry.BaseLDAPEntry( + dn='ou=Duplicate Team, dc=example,dc=com', + attributes={'foo': ['same', 'attributes']}) + + first = delta.AddOp(first_entry) + second = delta.AddOp(second_entry) + + self.assertEqual(hash(first), hash(second)) + + def testAddOpHashDifferent(self): + """ + Objects which are not equal have different hash. + """ + first_entry = entry.BaseLDAPEntry( + dn='ou=Duplicate Team, dc=example,dc=com', + attributes={'foo': ['one', 'attributes']}) + second_entry = entry.BaseLDAPEntry( + dn='ou=Duplicate Team, dc=example,dc=com', + attributes={'foo': ['other', 'attributes']}) + + first = delta.AddOp(first_entry) + second = delta.AddOp(second_entry) + + self.assertNotEqual(hash(first), hash(second)) + def testAddOp_DNExists(self): + """ + It fails to perform the `add` operation for an existing entry. + """ + root = self.getRoot() + root.addChild( + rdn='ou=Existing Team', + attributes={ + 'objectClass': ['a', 'b'], + 'ou': ['HR'], + }) -class TestDeleteOpLDIF(unittest.TestCase): - def testSimple(self): - op=delta.DeleteOp('dc=example,dc=com') - self.assertEqual(op.asLDIF(), - """\ -dn: dc=example,dc=com + hr_entry = entry.BaseLDAPEntry( + dn='ou=Existing Team, dc=example,dc=com', + attributes={'foo': ['dont', 'care']}) + sut = delta.AddOp(hr_entry) + + deferred = sut.patch(root) + + failure = self.failureResultOf(deferred) + self.assertIsInstance(failure.value, ldaperrors.LDAPEntryAlreadyExists) + + +class TestDeleteOpLDIF(OperationTestCase): + """ + Unit tests for DeleteOp. + """ + def testAsLDIF(self): + """ + It return the LDIF representation of the delete operation. + """ + sut = delta.DeleteOp('dc=example,dc=com') + + result = sut.asLDIF() + self.assertEqual("""dn: dc=example,dc=com changetype: delete -""") +""", + result) + def testDeleteOpEqualitySameDN(self): + """ + Objects are equal when the have the same DN. + """ + first_entry = entry.BaseLDAPEntry(dn='ou=Team, dc=example,dc=com') + second_entry = entry.BaseLDAPEntry(dn='ou=Team, dc=example,dc=com') + first = delta.DeleteOp(first_entry) + second = delta.DeleteOp(second_entry) -class TestOperationLDIF(unittest.TestCase): - def testModify(self): - op=delta.ModifyOp('cn=Paula Jensen, ou=Product Development, dc=airius, dc=com', - [ - delta.Add('postaladdress', - ['123 Anystreet $ Sunnyvale, CA $ 94086']), - delta.Delete('description'), - delta.Replace('telephonenumber', ['+1 408 555 1234', '+1 408 555 5678']), - delta.Delete('facsimiletelephonenumber', ['+1 408 555 9876']), - ]) - self.assertEqual(op.asLDIF(), - """\ -dn: cn=Paula Jensen,ou=Product Development,dc=airius,dc=com + self.assertEqual(first, second) + + def testDeleteOpInequalityDifferentEntry(self): + """ + DeleteOp objects are not equal when the have different LDAP entries. + """ + first_entry = entry.BaseLDAPEntry(dn='ou=Team, dc=example,dc=com') + second_entry = entry.BaseLDAPEntry(dn='ou=Cowboys, dc=example,dc=com') + + first = delta.DeleteOp(first_entry) + second = delta.DeleteOp(second_entry) + + self.assertNotEqual(first, second) + + def testDeleteOpInequalityNoEntryObject(self): + """ + DeleteOp objects is not equal with random objects. + """ + team_entry = entry.BaseLDAPEntry(dn='ou=Team, dc=example,dc=com') + + sut = delta.DeleteOp(team_entry) + + self.assertNotEqual(sut, 'ou=Team, dc=example,dc=com') + + def testDeleteOpHashSimilar(self): + """ + Objects which are equal have the same hash. + """ + first_entry = entry.BaseLDAPEntry(dn='ou=Team, dc=example,dc=com') + second_entry = entry.BaseLDAPEntry(dn='ou=Team, dc=example,dc=com') + + first = delta.DeleteOp(first_entry) + second = delta.DeleteOp(second_entry) + + self.assertEqual(hash(first), hash(second)) + + def testDeleteOpHashDifferent(self): + """ + Objects which are not equal have different hash. + """ + first_entry = entry.BaseLDAPEntry(dn='ou=Team, dc=example,dc=com') + second_entry = entry.BaseLDAPEntry(dn='ou=Cowboys, dc=example,dc=com') + + first = delta.DeleteOp(first_entry) + second = delta.DeleteOp(second_entry) + + self.assertNotEqual(hash(first), hash(second)) + + def testDeleteOp_DNNotFound(self): + """ + If fail to delete when the RDN does not exists. + """ + root = self.getRoot() + sut = delta.DeleteOp('cn=nope,dc=example,dc=com') + + deferred = sut.patch(root) + + failure = self.failureResultOf(deferred) + self.assertIsInstance(failure.value, ldaperrors.LDAPNoSuchObject) + + +class TestModifyOp(OperationTestCase): + """ + Unit tests for ModifyOp. + """ + + def testAsLDIF(self): + """ + It will return a LDIF representation of the contained operations. + """ + sut = delta.ModifyOp( + 'cn=Paula Jensen, ou=Dev Ops, dc=airius, dc=com', + [ + delta.Add( + 'postaladdress', + ['123 Anystreet $ Sunnyvale, CA $ 94086'], + ), + delta.Delete('description'), + delta.Replace( + 'telephonenumber', + ['+1 408 555 1234', '+1 408 555 5678'], + ), + delta.Delete( + 'facsimiletelephonenumber', ['+1 408 555 9876']), + ] + ) + + result = sut.asLDIF() + + self.assertEqual("""dn: cn=Paula Jensen,ou=Dev Ops,dc=airius,dc=com changetype: modify add: postaladdress postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086 @@ -202,7 +415,106 @@ def testModify(self): facsimiletelephonenumber: +1 408 555 9876 - -""") +""", + result, + ) + + def testInequalityDiffertnDN(self): + """ + Modify operations for different DN are not equal. + """ + first = delta.ModifyOp( + 'cn=john,dc=example,dc=com', + [delta.Delete('description')] + ) + + second = delta.ModifyOp( + 'cn=doe,dc=example,dc=com', + [delta.Delete('description')] + ) + + self.assertNotEqual(first, second) + + def testInequalityNotModifyOP(self): + """ + Modify operations are not equal with other object types. + """ + sut = delta.ModifyOp( + 'cn=john,dc=example,dc=com', + [delta.Delete('description')] + ) + + self.assertNotEqual('cn=john,dc=example,dc=com', sut) + + def testInequalityDiffertnOperations(self): + """ + Modify operations for same DN but different operations are not equal. + """ + first = delta.ModifyOp( + 'cn=john,dc=example,dc=com', + [delta.Delete('description')] + ) + second = delta.ModifyOp( + 'cn=doe,dc=example,dc=com', + [delta.Delete('homeDirectory')] + ) + + self.assertNotEqual(first, second) + + def testHashEquality(self): + """ + Modify operations can be hashed and equal objects have the same + hash. + """ + first = delta.ModifyOp( + 'cn=john,dc=example,dc=com', + [delta.Delete('description')] + ) + + second = delta.ModifyOp( + 'cn=john,dc=example,dc=com', + [delta.Delete('description')] + ) + + self.assertEqual(first, second) + self.assertEqual( + first.asLDIF(), second.asLDIF(), + 'LDIF equality is a precondition for valid hash values', + ) + self.assertEqual(hash(first), hash(second)) + + def testHashInequality(self): + """ + Different modify operations have different hash values. + """ + first = delta.ModifyOp( + 'cn=john,dc=example,dc=com', + [delta.Delete('description')] + ) + + second = delta.ModifyOp( + 'cn=john,dc=example,dc=com', + [delta.Delete('homeDirectory')] + ) + + self.assertNotEqual(first.asLDIF(), second.asLDIF()) + self.assertNotEqual(hash(first), hash(second)) + + def testModifyOp_DNNotFound(self): + """ + If fail to modify when the RDN does not exists. + """ + root = self.getRoot() + sut = delta.ModifyOp( + 'cn=nope,dc=example,dc=com', + [delta.Add('foo', ['bar'])], + ) + + deferred = sut.patch(root) + + failure = self.failureResultOf(deferred) + self.assertIsInstance(failure.value, ldaperrors.LDAPNoSuchObject) + class TestModificationComparison(unittest.TestCase): def testEquality_Add_True(self): @@ -225,74 +537,3 @@ def testEquality_List_False(self): b = ['b', 'c', 'd'] self.assertNotEquals(a, b) -class TestOperations(unittest.TestCase): - def setUp(self): - self.root = inmemory.ReadOnlyInMemoryLDAPEntry( - dn=distinguishedname.DistinguishedName('dc=example,dc=com')) - self.meta=self.root.addChild( - rdn='ou=metasyntactic', - attributes={ - 'objectClass': ['a', 'b'], - 'ou': ['metasyntactic'], - }) - self.foo=self.meta.addChild( - rdn='cn=foo', - attributes={ - 'objectClass': ['a', 'b'], - 'cn': ['foo'], - }) - self.bar=self.meta.addChild( - rdn='cn=bar', - attributes={ - 'objectClass': ['a', 'b'], - 'cn': ['bar'], - }) - - self.empty=self.root.addChild( - rdn='ou=empty', - attributes={ - 'objectClass': ['a', 'b'], - 'ou': ['empty'], - }) - - self.oneChild=self.root.addChild( - rdn='ou=oneChild', - attributes={ - 'objectClass': ['a', 'b'], - 'ou': ['oneChild'], - }) - self.theChild=self.oneChild.addChild( - rdn='cn=theChild', - attributes={ - 'objectClass': ['a', 'b'], - 'cn': ['theChild'], - }) - - def testAddOp_DNExists(self): - foo2 = entry.BaseLDAPEntry( - dn='cn=foo,ou=metasyntactic,dc=example,dc=com', - attributes={'foo': ['bar', 'baz'], - 'quux': ['thud']}) - op = delta.AddOp(foo2) - d = op.patch(self.root) - def eb(fail): - fail.trap(ldaperrors.LDAPEntryAlreadyExists) - d.addCallbacks(testutil.mustRaise, eb) - return d - - def testDeleteOp_DNNotFound(self): - op = delta.DeleteOp('cn=nope,dc=example,dc=com') - d = op.patch(self.root) - def eb(fail): - fail.trap(ldaperrors.LDAPNoSuchObject) - d.addCallbacks(testutil.mustRaise, eb) - return d - - def testModifyOp_DNNotFound(self): - op = delta.ModifyOp('cn=nope,dc=example,dc=com', - [delta.Add('foo', ['bar'])]) - d = op.patch(self.root) - def eb(fail): - fail.trap(ldaperrors.LDAPNoSuchObject) - d.addCallbacks(testutil.mustRaise, eb) - return d diff --git a/ldaptor/test/test_ldapsyntax.py b/ldaptor/test/test_ldapsyntax.py index e2a186f2..09b752bf 100755 --- a/ldaptor/test/test_ldapsyntax.py +++ b/ldaptor/test/test_ldapsyntax.py @@ -11,7 +11,11 @@ from twisted.python import failure from ldaptor.testutil import LDAPClientTestDriver -class LDAPSyntaxBasics(unittest.TestCase): +class LDAPEntryTests(unittest.TestCase): + """ + Unit tests for LDAPEntry. + """ + def testCreation(self): """Creating an LDAP object should succeed.""" client = LDAPClientTestDriver() @@ -40,7 +44,7 @@ def testKeys(self): }) seen={} for k in o.keys(): - assert not seen.has_key(k) + assert k not in seen seen[k]=1 assert seen == {'objectClass': 1, 'aValue': 1, @@ -59,7 +63,7 @@ def testItems(self): }) seen={} for k,vs in o.items(): - assert not seen.has_key(k) + assert k not in seen seen[k]=vs assert seen == {'objectClass': ['a', 'b'], 'aValue': ['a'], @@ -94,6 +98,131 @@ def testIn(self): assert '' not in o['aValue'] assert None not in o['aValue'] + def testInequalityOtherObject(self): + """ + It is not equal with non LDAPEntry objects. + """ + client = LDAPClientTestDriver() + sut = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + ) + + self.assertNotEqual('dc=example,dc=com', sut) + + def testInequalityDN(self): + """ + Entries with different DN are not equal. + """ + client = LDAPClientTestDriver() + first = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + ) + second = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=org', + ) + + self.assertNotEqual(first, second) + + def testInequalityAttributes(self): + """ + Entries with same DN but different attributes are not equal. + """ + client = LDAPClientTestDriver() + first = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + attributes={'attr_key1': ['some-value']}, + ) + second = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + attributes={'attr_key2': ['some-value']}, + ) + + self.assertNotEqual(first, second) + + def testInequalityValues(self): + """ + Entries with same DN same attributes, but different + values for attributes are not equal. + """ + client = LDAPClientTestDriver() + first = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + attributes={'attr_key1': ['some-value']}, + ) + second = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + attributes={'attr_key1': ['other-value']}, + ) + + self.assertNotEqual(first, second) + + def testEquality(self): + """ + Entries with same DN, same attributes, and same values for + attributes equal, regardless of the order of the attributes. + """ + client = LDAPClientTestDriver() + first = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + attributes={ + 'attr_key1': ['some-value'], + 'attr_key2': ['second-value'], + }, + ) + second = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + attributes={ + 'attr_key2': ['second-value'], + 'attr_key1': ['some-value'], + }, + ) + + self.assertEqual(first, second) + + def testHashEqual(self): + """ + Entries which are equal have the same hash. + """ + client = LDAPClientTestDriver() + first = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + ) + second = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + ) + + self.assertEqual(first, second) + self.assertEqual(hash(first), hash(second)) + + def testHashNotEqual(self): + """ + Entries which are not equal have different hash values. + """ + client = LDAPClientTestDriver() + first = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=com', + ) + second = ldapsyntax.LDAPEntry( + client=client, + dn='dc=example,dc=org', + ) + + self.assertNotEqual(first, second) + self.assertNotEqual(hash(first), hash(second)) + + class LDAPSyntaxAttributes(unittest.TestCase): def testAttributeSetting(self): client=LDAPClientTestDriver() @@ -126,8 +255,8 @@ def testAttributeDelete(self): o['aValue']=['quux'] del o['aValue'] del o['bValue'] - self.failIf(o.has_key('aValue')) - self.failIf(o.has_key('bValue')) + self.failIf('aValue' in o) + self.failIf('bValue' in o) def testAttributeAdd(self): client=LDAPClientTestDriver() @@ -229,7 +358,7 @@ def cb(dummy): o.undo() self.failUnlessEqual(o['aValue'], ['foo', 'bar']) self.failUnlessEqual(o['bValue'], ['quux']) - self.failIf(o.has_key('cValue')) + self.failIf('cValue' in o) d.addCallback(cb) return d diff --git a/ldaptor/test/test_ldifprotocol.py b/ldaptor/test/test_ldifprotocol.py index d0c82a33..c9daa506 100644 --- a/ldaptor/test/test_ldifprotocol.py +++ b/ldaptor/test/test_ldifprotocol.py @@ -3,7 +3,6 @@ """ from twisted.trial import unittest -import sets from ldaptor.protocols.ldap import ldifprotocol, distinguishedname class LDIFDriver(ldifprotocol.LDIF): @@ -297,8 +296,8 @@ def testExamples(self): o = proto.listOfCompleted.pop(0) self.failUnlessEqual(o.dn, distinguishedname.DistinguishedName(dn)) - got = sets.Set([x.lower() for x in o.keys()]) - want = sets.Set([x.lower() for x in attr.keys()]) + got = set([x.lower() for x in o.keys()]) + want = set([x.lower() for x in attr.keys()]) self.failUnlessEqual(got, want) for k, v in attr.items(): diff --git a/ldaptor/test/test_pureber.py b/ldaptor/test/test_pureber.py index 0bb8a4e6..e75b79ff 100644 --- a/ldaptor/test/test_pureber.py +++ b/ldaptor/test/test_pureber.py @@ -74,7 +74,11 @@ def testPartialBER(self): assert len(m)==101 self.assertRaises(pureber.BERExceptionInsufficientData, pureber.berDecodeLength, m[:100]) -class BERBaseEquality(unittest.TestCase): + +class BERBaseTests(unittest.TestCase): + """ + Unit tests for generic BERBase. + """ valuesToTest=( (pureber.BERInteger, [0]), (pureber.BERInteger, [1]), @@ -85,16 +89,20 @@ class BERBaseEquality(unittest.TestCase): (pureber.BEROctetString, ["b"+chr(0xe4)+chr(0xe4)]), ) - def testBERBaseEquality(self): - """BER objects equal BER objects with same type and content""" + def testEquality(self): + """ + BER objects equal BER objects with same type and content + """ for class_, args in self.valuesToTest: x=class_(*args) y=class_(*args) assert x==x assert x==y - def testBERBaseInEquality(self): - """BER objects do not equal BER objects with different type or content""" + def testInequalityWithBER(self): + """ + BER objects do not equal BER objects with different type or content + """ for i in xrange(len(self.valuesToTest)): for j in xrange(len(self.valuesToTest)): if i!=j: @@ -104,6 +112,24 @@ def testBERBaseInEquality(self): y=j_class(*j_args) assert x!=y + def testInequalityWithNonBER(self): + """ + BER objects are equal with non-BER objects. + """ + sut = pureber.BERInteger([0]) + + self.assertFalse(0 == sut) + self.assertNotEqual(0, sut) + + def testHashEquality(self): + """ + Objects which are equal have the same hash. + """ + for klass, arguments in self.valuesToTest: + first = klass(*arguments) + second = klass(*arguments) + self.assertEqual(hash(first), hash(second)) + class BERIntegerKnownValues(unittest.TestCase): knownValues=( diff --git a/ldaptor/test/test_usage.py b/ldaptor/test/test_usage.py new file mode 100644 index 00000000..51d25418 --- /dev/null +++ b/ldaptor/test/test_usage.py @@ -0,0 +1,94 @@ +""" +Test cases for ldaptor.usage +""" + +from twisted.python.usage import UsageError +from twisted.trial.unittest import TestCase + +from ldaptor.protocols.ldap.distinguishedname import DistinguishedName +from ldaptor.usage import Options, Options_service_location + +class ServiceLocationOptionsImplementation(Options, Options_service_location): + """ + Minimal implementation for a command line using `Options_service_location`. + """ + +class TestOptions_service_location(TestCase): + """ + Unit tests for Options_service_location. + """ + + def test_parseOptions_default(self): + """ + When no explicit options is provided it will set an empty dict. + """ + sut = ServiceLocationOptionsImplementation() + self.assertNotIn('service-location', sut.opts) + + sut.parseOptions(options=[]) + + self.assertEqual({}, sut.opts['service-location']) + + def test_parseOptions_single(self): + """ + It can have a single --service-location option. + """ + sut = ServiceLocationOptionsImplementation() + + sut.parseOptions(options=[ + '--service-location', 'dc=example,dc=com:127.0.0.1:1234']) + + base = DistinguishedName('dc=example,dc=com') + value = sut.opts['service-location'][base] + self.assertEqual(('127.0.0.1', '1234'), value) + + def test_parseOptions_invalid_DN(self): + """ + It fails to parse the option when the base DN is not valid. + """ + sut = ServiceLocationOptionsImplementation() + + exception = self.assertRaises( + UsageError, + sut.parseOptions, + options=['--service-location', 'example.com:1.2.3.4'], + ) + + self.assertEqual( + "Invalid relative distinguished name 'example.com'.", + exception.message) + + def test_parseOptions_no_server(self): + """ + It fails to parse the option when no host is defined, but only + a base DN. + """ + sut = ServiceLocationOptionsImplementation() + + exception = self.assertRaises( + UsageError, + sut.parseOptions, + options=['--service-location', 'dc=example,dc=com'], + ) + + self.assertEqual( + 'service-location must specify host', exception.message) + + def test_parseOptions_multiple(self): + """ + It can have have multiple --service-location options and they are + indexed using the base DN. + """ + sut = ServiceLocationOptionsImplementation() + + sut.parseOptions(options=[ + '--service-location', 'dc=example,dc=com:127.0.0.1', + '--service-location', 'dc=example,dc=org:172.0.0.1', + ]) + + base_com = DistinguishedName('dc=example,dc=com') + base_org = DistinguishedName('dc=example,dc=org') + value_com = sut.opts['service-location'][base_com] + value_org = sut.opts['service-location'][base_org] + self.assertEqual(('127.0.0.1', None), value_com) + self.assertEqual(('172.0.0.1', None), value_org) diff --git a/ldaptor/usage.py b/ldaptor/usage.py index b8b18cc0..7cc274b4 100644 --- a/ldaptor/usage.py +++ b/ldaptor/usage.py @@ -1,3 +1,6 @@ +""" +Command line argument/options available to various ldaptor tools. +""" from twisted.python import usage, reflect from twisted.python.usage import UsageError from ldaptor.protocols import pureldap @@ -24,11 +27,18 @@ def postOptions(self): method() class Options_service_location: + """ + Mixing for providing the --service-location option. + """ + def opt_service_location(self, value): """Service location, in the form BASEDN:HOST[:PORT]""" - if not self.opts.has_key('service-location'): - self.opts['service-location']={} + if 'service-location' not in self.opts: + self.opts['service-location'] = {} + + if ':' not in value: + raise usage.UsageError("service-location must specify host") base, location = value.split(':', 1) try: @@ -36,24 +46,15 @@ def opt_service_location(self, value): except distinguishedname.InvalidRelativeDistinguishedName as e: raise usage.UsageError(str(e)) - if not location: - raise usage.UsageError("service-location must specify host") - if ':' in location: host, port = location.split(':', 1) else: host, port = location, None - if not host: - host = None - - if not port: - port = None - self.opts['service-location'][dn] = (host, port) def postOptions_service_location(self): - if not self.opts.has_key('service-location'): + if 'service-location' not in self.opts: self.opts['service-location']={} class Options_base_optional: diff --git a/setup.py b/setup.py index c5955071..59f1c3c9 100755 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ def find_version(*file_paths): 'Twisted', 'pyOpenSSL', 'pyparsing', + 'six', 'zope.interface', ], classifiers=[ diff --git a/tox.ini b/tox.ini index 9081999f..2fecd033 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ deps = pyparsing pycrypto service_identity + six twlatest: Twisted twtrunk: https://github.com/twisted/twisted/archive/trunk.zip tw162: Twisted==16.2 @@ -39,7 +40,7 @@ passenv = * commands = {envpython} --version trial --version - coverage run --rcfile={toxinidir}/.coveragerc -m twisted.trial ldaptor + coverage run --rcfile={toxinidir}/.coveragerc -m twisted.trial {posargs:ldaptor} ; Only run on local dev env. dev: coverage report --show-missing dev: coverage xml -o {toxinidir}/build/coverage.xml