diff options
Diffstat (limited to 'passlib/tests/test_ext_django.py')
-rw-r--r-- | passlib/tests/test_ext_django.py | 209 |
1 files changed, 171 insertions, 38 deletions
diff --git a/passlib/tests/test_ext_django.py b/passlib/tests/test_ext_django.py index f9fd689..d522386 100644 --- a/passlib/tests/test_ext_django.py +++ b/passlib/tests/test_ext_django.py @@ -89,7 +89,8 @@ if has_django: finally: del self.saved_passwords[:] - def save(self): + def save(self, update_fields=None): + # NOTE: ignoring update_fields for test purposes self.saved_passwords.append(self.password) def create_mock_setter(): @@ -107,12 +108,17 @@ def create_mock_setter(): #============================================================================= # work up stock django config #============================================================================= +sample_hashes = {} # override sample hashes used in test cases if has_django14: # have to modify this a little - # all but pbkdf2_sha256 will be deprecated here, # whereas preconfigured passlib policy is more permissive stock_config = django14_context.to_dict() stock_config['deprecated'] = ["django_pbkdf2_sha1", "django_bcrypt"] + stock_config['deprecated'] + if DJANGO_VERSION >= (1,6): + sample_hashes.update( + django_pbkdf2_sha256=("not a password", "pbkdf2_sha256$12000$rpUPFQOVetrY$cEcWG4DjjDpLrDyXnduM+XJUz25U63RcM3//xaFnBnw="), + ) elif has_django1: stock_config = django10_context.to_dict() else: @@ -239,7 +245,9 @@ class _ExtensionSupport(object): # eoc #=================================================================== +# XXX: rename to ExtensionFixture? class _ExtensionTest(TestCase, _ExtensionSupport): + def setUp(self): super(_ExtensionTest, self).setUp() @@ -273,12 +281,23 @@ class DjangoBehaviorTest(_ExtensionTest): return CryptContext._norm_source(self.config) def assert_unusable_password(self, user): - self.assertEqual(user.password, "!") + """check that user object is set to 'unusable password' constant""" + if DJANGO_VERSION >= (1,6): + # 1.6 on adds a random(?) suffix + self.assertTrue(user.password.startswith("!")) + else: + self.assertEqual(user.password, "!") if has_django1 or self.patched: self.assertFalse(user.has_usable_password()) self.assertEqual(user.pop_saved_passwords(), []) def assert_valid_password(self, user, hash=UNSET, saved=None): + """check that user object has a usuable password hash. + + :param hash: optionally check it has this exact hash + :param saved: check that mock commit history + for user.password matches this list + """ if hash is UNSET: self.assertNotEqual(user.password, "!") self.assertNotEqual(user.password, None) @@ -318,11 +337,19 @@ class DjangoBehaviorTest(_ExtensionTest): PASS1 = "toomanysecrets" WRONG1 = "letmein" + has_hashers = False + has_identify_hasher = False if has_django14: from passlib.ext.django.utils import hasher_to_passlib_name, passlib_to_hasher_name from django.contrib.auth.hashers import check_password, make_password, is_password_usable - if patched: + if patched or DJANGO_VERSION > (1,5): + # identify_hasher() + # django 1.4 -- not present + # django 1.5 -- present (added in django ticket 18184) + # passlib integration -- present even under 1.4 from django.contrib.auth.hashers import identify_hasher + has_identify_hasher = True + hash_hashers = True else: from django.contrib.auth.models import check_password @@ -361,8 +388,8 @@ class DjangoBehaviorTest(_ExtensionTest): #======================================================= # empty password behavior #======================================================= - if has_django14: - # NOTE: django 1.4 treats empty password as invalid + if (1,4) <= DJANGO_VERSION < (1,6): + # NOTE: django 1.4-1.5 treat empty password as invalid # User.set_password() should set unusable flag user = FakeUser() @@ -408,21 +435,36 @@ class DjangoBehaviorTest(_ExtensionTest): user.set_unusable_password() self.assert_unusable_password(user) - # ensure User.set_password() sets flag + # ensure User.set_password() sets unusable flag user = FakeUser() user.set_password(None) - self.assert_unusable_password(user) + if DJANGO_VERSION < (1,2): + # would set password to hash of "None" + self.assert_valid_password(user) + else: + self.assert_unusable_password(user) # User.check_password() should always fail - self.assertFalse(user.check_password(None)) - self.assertFalse(user.check_password('')) - self.assertFalse(user.check_password(PASS1)) - self.assertFalse(user.check_password(WRONG1)) - self.assert_unusable_password(user) + if DJANGO_VERSION < (1,2): + self.assertTrue(user.check_password(None)) + self.assertTrue(user.check_password('None')) + self.assertFalse(user.check_password('')) + self.assertFalse(user.check_password(PASS1)) + self.assertFalse(user.check_password(WRONG1)) + else: + self.assertFalse(user.check_password(None)) + self.assertFalse(user.check_password('None')) + self.assertFalse(user.check_password('')) + self.assertFalse(user.check_password(PASS1)) + self.assertFalse(user.check_password(WRONG1)) + self.assert_unusable_password(user) # make_password() should also set flag if has_django14: - self.assertEqual(make_password(None), "!") + if DJANGO_VERSION >= (1,6): + self.assertTrue(make_password(None).startswith("!")) + else: + self.assertEqual(make_password(None), "!") # check_password() should return False (didn't handle disabled under 1.3) if has_django14 or patched: @@ -431,7 +473,7 @@ class DjangoBehaviorTest(_ExtensionTest): # identify_hasher() and is_password_usable() should reject it if has_django14: self.assertFalse(is_password_usable(user.password)) - if has_django14 and patched: + if has_identify_hasher: self.assertRaises(ValueError, identify_hasher, user.password) #======================================================= @@ -447,7 +489,10 @@ class DjangoBehaviorTest(_ExtensionTest): else: self.assertRaises(TypeError, user.check_password, PASS1) if has_django1 or patched: - self.assertFalse(user.has_usable_password()) + if DJANGO_VERSION < (1,2): + self.assertTrue(user.has_usable_password()) + else: + self.assertFalse(user.has_usable_password()) # make_password() - n/a @@ -458,32 +503,65 @@ class DjangoBehaviorTest(_ExtensionTest): self.assertRaises(AttributeError, check_password, PASS1, None) # identify_hasher() - error - if has_django14 and patched: + if has_identify_hasher: self.assertRaises(TypeError, identify_hasher, None) #======================================================= - # invalid hash values + # empty & invalid hash values + # NOTE: django 1.5 behavior change due to django ticket 18453 + # NOTE: passlib integration tries to match current django version #======================================================= - for hash in ("", "$789$foo"): + for hash in ("", # empty hash + "$789$foo", # empty identifier + ): # User.set_password() - n/a - # User.check_password() - blank hash causes error + # User.check_password() + # empty + # ----- + # django 1.3 and earlier -- blank hash returns False + # django 1.4 -- blank threw error (fixed in 1.5) + # django 1.5 -- blank hash returns False + # + # invalid + # ------- + # django 1.4 and earlier -- invalid hash threw error (fixed in 1.5) + # django 1.5 -- invalid hash returns False user = FakeUser() user.password = hash - if has_django14 or patched or hash: - self.assertRaises(ValueError, user.check_password, PASS1) - else: - # django 1.3 returns False for empty hashes + if DJANGO_VERSION >= (1,5) or (not hash and DJANGO_VERSION < (1,4)): + # returns False for hash self.assertFalse(user.check_password(PASS1)) - self.assert_valid_password(user, hash) # '' counts as valid for some reason + else: + # throws error for hash + self.assertRaises(ValueError, user.check_password, PASS1) + + # verify hash wasn't changed/upgraded during check_password() call + self.assertEqual(user.password, hash) + self.assertEqual(user.pop_saved_passwords(), []) + + # User.has_usable_password() + # passlib shim for django 0.x -- invalid/empty usable, to match 1.0-1.4 + # django 1.0-1.4 -- invalid/empty usable (fixed in 1.5) + # django 1.5 -- invalid/empty no longer usable + if has_django1 or self.patched: + if DJANGO_VERSION < (1,5): + self.assertTrue(user.has_usable_password()) + else: + self.assertFalse(user.has_usable_password()) # make_password() - n/a - # check_password() - error - self.assertRaises(ValueError, check_password, PASS1, hash) + # check_password() + # django 1.4 and earlier -- invalid/empty hash threw error (fixed in 1.5) + # django 1.5 -- invalid/empty hash now returns False + if DJANGO_VERSION < (1,5): + self.assertRaises(ValueError, check_password, PASS1, hash) + else: + self.assertFalse(check_password(PASS1, hash)) - # identify_hasher() - error - if has_django14 and patched: + # identify_hasher() - throws error + if has_identify_hasher: self.assertRaises(ValueError, identify_hasher, hash) #======================================================= @@ -508,11 +586,14 @@ class DjangoBehaviorTest(_ExtensionTest): if not has_active_backend(handler): assert scheme == "django_bcrypt" continue - while True: - secret, hash = testcase('setUp').get_sample_hash() - if secret: # don't select blank passwords, special under django - break - other = 'letmein' + try: + secret, hash = sample_hashes[scheme] + except KeyError: + while True: + secret, hash = testcase('setUp').get_sample_hash() + if secret: # don't select blank passwords, especially under django 1.4/1.5 + break + other = 'dontletmein' # User.set_password() - n/a @@ -582,7 +663,7 @@ class DjangoBehaviorTest(_ExtensionTest): #------------------------------------------------------- # identify_hasher() recognizes known hash #------------------------------------------------------- - if has_django14 and patched: + if has_identify_hasher: self.assertTrue(is_password_usable(hash)) name = hasher_to_passlib_name(identify_hasher(hash).algorithm) self.assertEqual(name, scheme) @@ -808,18 +889,70 @@ class DjangoExtensionTest(_ExtensionTest): # eoc #=================================================================== +from passlib.context import CryptContext +class ContextWithHook(CryptContext): + """subclass which invokes update_hook(self) before major actions""" + + @staticmethod + def update_hook(self): + pass + + def encrypt(self, *args, **kwds): + self.update_hook(self) + return super(ContextWithHook, self).encrypt(*args, **kwds) + + def verify(self, *args, **kwds): + self.update_hook(self) + return super(ContextWithHook, self).verify(*args, **kwds) + # hack up the some of the real django tests to run w/ extension loaded, # to ensure we mimic their behavior. if has_django14: - from django.contrib.auth.tests.hashers import TestUtilsHashPass as _TestHashers - class HashersTest(_TestHashers, _ExtensionSupport): + from passlib.tests.utils import patchAttr + if DJANGO_VERSION >= (1,6): + from django.contrib.auth.tests import test_hashers as _thmod + else: + from django.contrib.auth.tests import hashers as _thmod + + class HashersTest(_thmod.TestUtilsHashPass, _ExtensionSupport): + """run django's hasher unittests against passlib's extension + and workalike implementations""" def setUp(self): - # omitted orig setup, loading hashers our own way + # NOTE: omitted orig setup, want to install our extension, + # and load hashers through it instead. self.load_extension(PASSLIB_CONTEXT=stock_config, check=False) + from passlib.ext.django.models import password_context + + # update test module to use our versions of some hasher funcs + from django.contrib.auth import hashers + for attr in ["make_password", + "check_password", + "identify_hasher", + "get_hasher"]: + patchAttr(self, _thmod, attr, getattr(hashers, attr)) + + # django 1.5 tests expect empty django_des_crypt salt field + if DJANGO_VERSION > (1,4): + from passlib.hash import django_des_crypt + patchAttr(self, django_des_crypt, "use_duplicate_salt", False) + + # hack: need password_context to keep up to date with hasher.iterations + if DJANGO_VERSION >= (1,6): + def update_hook(self): + rounds = _thmod.get_hasher("pbkdf2_sha256").iterations + self.update( + django_pbkdf2_sha256__min_rounds=rounds, + django_pbkdf2_sha256__max_rounds=rounds, + ) + patchAttr(self, password_context, "__class__", ContextWithHook) + patchAttr(self, password_context, "update_hook", update_hook) + + # omitting this test, since it depends on updated to django hasher settings + test_pbkdf2_upgrade_new_hasher = lambda self: self.skipTest("omitted by passlib") + def tearDown(self): self.unload_extension() super(HashersTest, self).tearDown() - del _TestHashers HashersTest = skipUnless(TEST_MODE("default"), "requires >= 'default' test mode")(HashersTest) |