summaryrefslogtreecommitdiff
path: root/passlib/tests
diff options
context:
space:
mode:
Diffstat (limited to 'passlib/tests')
-rw-r--r--passlib/tests/test_ext_django.py209
-rw-r--r--passlib/tests/test_handlers.py268
-rw-r--r--passlib/tests/test_handlers_django.py297
-rw-r--r--passlib/tests/utils.py16
4 files changed, 498 insertions, 292 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)
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index 4845824..293a5da 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -7,6 +7,7 @@ from __future__ import with_statement
import hashlib
import logging; log = logging.getLogger(__name__)
import os
+import sys
import warnings
# site
# pkg
@@ -37,7 +38,19 @@ def get_handler_case(scheme):
name = "%s_%s_test" % (scheme, backend)
else:
name = "%s_test" % scheme
- return globals()[name]
+ try:
+ return globals()[name]
+ except KeyError:
+ pass
+ for suffix in ("handlers_django",):
+ modname = "passlib.tests.test_" + suffix
+ __import__(modname)
+ mod = sys.modules[modname]
+ try:
+ return getattr(mod, name)
+ except AttributeError:
+ pass
+ raise KeyError("test case %r not found" % name)
#=============================================================================
# apr md5 crypt
@@ -689,259 +702,6 @@ des_crypt_os_crypt_test, des_crypt_builtin_test = \
_des_crypt_test.create_backend_cases(["os_crypt","builtin"])
#=============================================================================
-# django
-#=============================================================================
-class _DjangoHelper(object):
- # NOTE: not testing against Django < 1.0 since it doesn't support
- # most of these hash formats.
-
- # flag if hash wasn't added until Django 1.4
- requires14 = False
-
- def fuzz_verifier_django(self):
- from passlib.tests.test_ext_django import has_django1, has_django14
- if not has_django1:
- return None
- if self.requires14 and not has_django14:
- return None
- from django.contrib.auth.models import check_password
- def verify_django(secret, hash):
- "django/check_password"
- if has_django14 and not secret:
- return "skip"
- if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"):
- hash = hash.replace("$$2y$", "$$2a$")
- return check_password(secret, hash)
- return verify_django
-
- def test_90_django_reference(self):
- "run known correct hashes through Django's check_password()"
- from passlib.tests.test_ext_django import has_django1, has_django14
- if self.requires14 and not has_django14:
- raise self.skipTest("Django >= 1.4 not installed")
- if not has_django1:
- raise self.skipTest("Django >= 1.0 not installed")
- from django.contrib.auth.models import check_password
- assert self.known_correct_hashes
- for secret, hash in self.iter_known_hashes():
- if has_django14 and not secret:
- # django 1.4 rejects empty passwords
- self.assertFalse(check_password(secret, hash),
- "empty string should not have verified")
- continue
- self.assertTrue(check_password(secret, hash),
- "secret=%r hash=%r failed to verify" %
- (secret, hash))
- self.assertFalse(check_password('x' + secret, hash),
- "mangled secret=%r hash=%r incorrect verified" %
- (secret, hash))
-
- def test_91_django_generation(self):
- "test against output of Django's make_password()"
- from passlib.tests.test_ext_django import has_django14
- if not has_django14:
- raise self.skipTest("Django >= 1.4 not installed")
- from passlib.utils import tick
- from django.contrib.auth.hashers import make_password
- name = self.handler.django_name # set for all the django_* handlers
- end = tick() + self.max_fuzz_time/2
- while tick() < end:
- secret, other = self.get_fuzz_password_pair()
- if not secret: # django 1.4 rejects empty passwords.
- continue
- hash = make_password(secret, hasher=name)
- self.assertTrue(self.do_identify(hash))
- self.assertTrue(self.do_verify(secret, hash))
- self.assertFalse(self.do_verify(other, hash))
-
-class django_disabled_test(HandlerCase):
- "test django_disabled"
- handler = hash.django_disabled
- is_disabled_handler = True
-
- known_correct_hashes = [
- # *everything* should hash to "!", and nothing should verify
- ("password", "!"),
- ("", "!"),
- (UPASS_TABLE, "!"),
- ]
-
-class django_des_crypt_test(HandlerCase, _DjangoHelper):
- "test django_des_crypt"
- handler = hash.django_des_crypt
- secret_size = 8
-
- known_correct_hashes = [
- # ensures only first two digits of salt count.
- ("password", 'crypt$c2$c2M87q...WWcU'),
- ("password", 'crypt$c2e86$c2M87q...WWcU'),
- ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'),
-
- # ensures utf-8 used for unicode
- (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'),
- (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'),
- (u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"),
-
- # prevent regression of issue 22
- ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'),
- ]
-
- known_alternate_hashes = [
- # ensure django 1.4 empty salt field is accepted;
- # but that salt field is re-filled (for django 1.0 compatibility)
- ('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'),
- ]
-
- known_unidentified_hashes = [
- 'sha1$aa$bb',
- ]
-
- known_malformed_hashes = [
- # checksum too short
- 'crypt$c2$c2M87q',
-
- # salt must be >2
- 'crypt$f$c2M87q...WWcU',
-
- # make sure first 2 chars of salt & chk field agree.
- 'crypt$ffe86$c2M87q...WWcU',
- ]
-
-class django_salted_md5_test(HandlerCase, _DjangoHelper):
- "test django_salted_md5"
- handler = hash.django_salted_md5
-
- known_correct_hashes = [
- # test extra large salt
- ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'),
-
- # test django 1.4 alphanumeric salt
- ("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'),
-
- # ensures utf-8 used for unicode
- (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'),
- (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'),
- ]
-
- known_unidentified_hashes = [
- 'sha1$aa$bb',
- ]
-
- known_malformed_hashes = [
- # checksum too short
- 'md5$aa$bb',
- ]
-
- def fuzz_setting_salt_size(self):
- # workaround for django14 regression --
- # 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier.
- # looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144
- # for now, we avoid salt_size==0 under 1.4
- handler = self.handler
- from passlib.tests.test_ext_django import has_django14
- default = handler.default_salt_size
- assert handler.min_salt_size == 0
- lower = 1 if has_django14 else 0
- upper = handler.max_salt_size or default*4
- return randintgauss(lower, upper, default, default*.5)
-
-class django_salted_sha1_test(HandlerCase, _DjangoHelper):
- "test django_salted_sha1"
- handler = hash.django_salted_sha1
-
- known_correct_hashes = [
- # test extra large salt
- ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'),
-
- # test django 1.4 alphanumeric salt
- ("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'),
-
- # ensures utf-8 used for unicode
- (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'),
- (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'),
-
- # generic password
- ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'),
- ]
-
- known_unidentified_hashes = [
- 'md5$aa$bb',
- ]
-
- known_malformed_hashes = [
- # checksum too short
- 'sha1$c2e86$0f75',
- ]
-
- fuzz_setting_salt_size = get_method_function(django_salted_md5_test.fuzz_setting_salt_size)
-
-class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper):
- "test django_pbkdf2_sha256"
- handler = hash.django_pbkdf2_sha256
- requires14 = True
-
- known_correct_hashes = [
- #
- # custom - generated via django 1.4 hasher
- #
- ('not a password',
- 'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='),
- (UPASS_TABLE,
- 'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='),
- ]
-
-class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper):
- "test django_pbkdf2_sha1"
- handler = hash.django_pbkdf2_sha1
- requires14 = True
-
- known_correct_hashes = [
- #
- # custom - generated via django 1.4 hashers
- #
- ('not a password',
- 'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='),
- (UPASS_TABLE,
- 'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='),
- ]
-
-class django_bcrypt_test(HandlerCase, _DjangoHelper):
- "test django_bcrypt"
- handler = hash.django_bcrypt
- secret_size = 72
- requires14 = True
-
- known_correct_hashes = [
- #
- # just copied and adapted a few test vectors from bcrypt (above),
- # since django_bcrypt is just a wrapper for the real bcrypt class.
- #
- ('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
- ('abcdefghijklmnopqrstuvwxyz',
- 'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
- (UPASS_TABLE,
- 'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
- ]
-
- # NOTE: the following have been cloned from _bcrypt_test()
-
- def populate_settings(self, kwds):
- # speed up test w/ lower rounds
- kwds.setdefault("rounds", 4)
- super(django_bcrypt_test, self).populate_settings(kwds)
-
- def fuzz_setting_rounds(self):
- # decrease default rounds for fuzz testing to speed up volume.
- return randintgauss(5, 8, 6, 1)
-
- def fuzz_setting_ident(self):
- # omit multi-ident tests, only $2a$ counts for this class
- return None
-
-django_bcrypt_test = skipUnless(hash.bcrypt.has_backend(),
- "no bcrypt backends available")(django_bcrypt_test)
-
-#=============================================================================
# fshp
#=============================================================================
class fshp_test(HandlerCase):
diff --git a/passlib/tests/test_handlers_django.py b/passlib/tests/test_handlers_django.py
new file mode 100644
index 0000000..ac5d1f9
--- /dev/null
+++ b/passlib/tests/test_handlers_django.py
@@ -0,0 +1,297 @@
+"""passlib.tests.test_handlers_django - tests for passlib hash algorithms"""
+#=============================================================================
+# imports
+#=============================================================================
+from __future__ import with_statement
+# core
+import hashlib
+import logging; log = logging.getLogger(__name__)
+import os
+import warnings
+# site
+# pkg
+from passlib import hash
+from passlib.utils import repeat_string
+from passlib.utils.compat import irange, PY3, u, get_method_function
+from passlib.tests.utils import TestCase, HandlerCase, skipUnless, \
+ TEST_MODE, b, catch_warnings, UserHandlerMixin, randintgauss, EncodingHandlerMixin
+from passlib.tests.test_handlers import UPASS_WAV, UPASS_USD, UPASS_TABLE
+# module
+
+#=============================================================================
+# django
+#=============================================================================
+class _DjangoHelper(object):
+ # NOTE: not testing against Django < 1.0 since it doesn't support
+ # most of these hash formats.
+
+ # flag if hash wasn't added until Django 1.4
+ requires14 = False
+
+ def fuzz_verifier_django(self):
+ from passlib.tests.test_ext_django import DJANGO_VERSION
+ if DJANGO_VERSION < (1,0):
+ return None
+ if self.requires14 and DJANGO_VERSION < (1,4):
+ return None
+ from django.contrib.auth.models import check_password
+ def verify_django(secret, hash):
+ "django/check_password"
+ if DJANGO_VERSION >= (1,4) and not secret:
+ return "skip"
+ if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"):
+ hash = hash.replace("$$2y$", "$$2a$")
+ if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes):
+ # e.g. unsalted_md5 on 1.5 and higher try to combine
+ # salt + password before encoding to bytes, leading to ascii error.
+ # this works around that issue.
+ secret = secret.decode("utf-8")
+ return check_password(secret, hash)
+ return verify_django
+
+ def test_90_django_reference(self):
+ "run known correct hashes through Django's check_password()"
+ from passlib.tests.test_ext_django import has_django1, has_django14
+ if self.requires14 and not has_django14:
+ raise self.skipTest("Django >= 1.4 not installed")
+ if not has_django1:
+ raise self.skipTest("Django >= 1.0 not installed")
+ from django.contrib.auth.models import check_password
+ assert self.known_correct_hashes
+ for secret, hash in self.iter_known_hashes():
+ if has_django14 and not secret:
+ # django 1.4 rejects empty passwords
+ self.assertFalse(check_password(secret, hash),
+ "empty string should not have verified")
+ continue
+ self.assertTrue(check_password(secret, hash),
+ "secret=%r hash=%r failed to verify" %
+ (secret, hash))
+ self.assertFalse(check_password('x' + secret, hash),
+ "mangled secret=%r hash=%r incorrect verified" %
+ (secret, hash))
+
+ django_has_encoding_glitch = False
+
+ def test_91_django_generation(self):
+ "test against output of Django's make_password()"
+ from passlib.tests.test_ext_django import DJANGO_VERSION
+ if DJANGO_VERSION < (1,4):
+ raise self.skipTest("Django >= 1.4 not installed")
+ from passlib.utils import tick
+ from django.contrib.auth.hashers import make_password
+ name = self.handler.django_name # set for all the django_* handlers
+ end = tick() + self.max_fuzz_time/2
+ while tick() < end:
+ secret, other = self.get_fuzz_password_pair()
+ if not secret: # django 1.4 rejects empty passwords.
+ continue
+ if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes):
+ # e.g. unsalted_md5 on 1.5 and higher try to combine
+ # salt + password before encoding to bytes, leading to ascii error.
+ # this works around that issue.
+ secret = secret.decode("utf-8")
+ hash = make_password(secret, hasher=name)
+ self.assertTrue(self.do_identify(hash))
+ self.assertTrue(self.do_verify(secret, hash))
+ self.assertFalse(self.do_verify(other, hash))
+
+class django_disabled_test(HandlerCase):
+ "test django_disabled"
+ handler = hash.django_disabled
+ is_disabled_handler = True
+
+ known_correct_hashes = [
+ # *everything* should hash to "!", and nothing should verify
+ ("password", "!"),
+ ("", "!"),
+ (UPASS_TABLE, "!"),
+ ]
+
+ known_alternate_hashes = [
+ # django 1.6 appends random alpnum string
+ ("!9wa845vn7098ythaehasldkfj", "password", "!"),
+ ]
+
+class django_des_crypt_test(HandlerCase, _DjangoHelper):
+ "test django_des_crypt"
+ handler = hash.django_des_crypt
+ secret_size = 8
+
+ known_correct_hashes = [
+ # ensures only first two digits of salt count.
+ ("password", 'crypt$c2$c2M87q...WWcU'),
+ ("password", 'crypt$c2e86$c2M87q...WWcU'),
+ ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'),
+
+ # ensures utf-8 used for unicode
+ (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'),
+ (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'),
+ (u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"),
+
+ # prevent regression of issue 22
+ ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'),
+ ]
+
+ known_alternate_hashes = [
+ # ensure django 1.4 empty salt field is accepted;
+ # but that salt field is re-filled (for django 1.0 compatibility)
+ ('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'),
+ ]
+
+ known_unidentified_hashes = [
+ 'sha1$aa$bb',
+ ]
+
+ known_malformed_hashes = [
+ # checksum too short
+ 'crypt$c2$c2M87q',
+
+ # salt must be >2
+ 'crypt$f$c2M87q...WWcU',
+
+ # make sure first 2 chars of salt & chk field agree.
+ 'crypt$ffe86$c2M87q...WWcU',
+ ]
+
+class django_salted_md5_test(HandlerCase, _DjangoHelper):
+ "test django_salted_md5"
+ handler = hash.django_salted_md5
+
+ django_has_encoding_glitch = True
+
+ known_correct_hashes = [
+ # test extra large salt
+ ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'),
+
+ # test django 1.4 alphanumeric salt
+ ("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'),
+
+ # ensures utf-8 used for unicode
+ (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'),
+ (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'),
+ ]
+
+ known_unidentified_hashes = [
+ 'sha1$aa$bb',
+ ]
+
+ known_malformed_hashes = [
+ # checksum too short
+ 'md5$aa$bb',
+ ]
+
+ def fuzz_setting_salt_size(self):
+ # workaround for django14 regression --
+ # 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier.
+ # looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144
+ # for now, we avoid salt_size==0 under 1.4
+ handler = self.handler
+ from passlib.tests.test_ext_django import has_django14
+ default = handler.default_salt_size
+ assert handler.min_salt_size == 0
+ lower = 1 if has_django14 else 0
+ upper = handler.max_salt_size or default*4
+ return randintgauss(lower, upper, default, default*.5)
+
+class django_salted_sha1_test(HandlerCase, _DjangoHelper):
+ "test django_salted_sha1"
+ handler = hash.django_salted_sha1
+
+ django_has_encoding_glitch = True
+
+ known_correct_hashes = [
+ # test extra large salt
+ ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'),
+
+ # test django 1.4 alphanumeric salt
+ ("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'),
+
+ # ensures utf-8 used for unicode
+ (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'),
+ (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'),
+
+ # generic password
+ ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'),
+ ]
+
+ known_unidentified_hashes = [
+ 'md5$aa$bb',
+ ]
+
+ known_malformed_hashes = [
+ # checksum too short
+ 'sha1$c2e86$0f75',
+ ]
+
+ fuzz_setting_salt_size = get_method_function(django_salted_md5_test.fuzz_setting_salt_size)
+
+class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper):
+ "test django_pbkdf2_sha256"
+ handler = hash.django_pbkdf2_sha256
+ requires14 = True
+
+ known_correct_hashes = [
+ #
+ # custom - generated via django 1.4 hasher
+ #
+ ('not a password',
+ 'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='),
+ (UPASS_TABLE,
+ 'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='),
+ ]
+
+class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper):
+ "test django_pbkdf2_sha1"
+ handler = hash.django_pbkdf2_sha1
+ requires14 = True
+
+ known_correct_hashes = [
+ #
+ # custom - generated via django 1.4 hashers
+ #
+ ('not a password',
+ 'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='),
+ (UPASS_TABLE,
+ 'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='),
+ ]
+
+class django_bcrypt_test(HandlerCase, _DjangoHelper):
+ "test django_bcrypt"
+ handler = hash.django_bcrypt
+ secret_size = 72
+ requires14 = True
+
+ known_correct_hashes = [
+ #
+ # just copied and adapted a few test vectors from bcrypt (above),
+ # since django_bcrypt is just a wrapper for the real bcrypt class.
+ #
+ ('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
+ ('abcdefghijklmnopqrstuvwxyz',
+ 'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
+ (UPASS_TABLE,
+ 'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
+ ]
+
+ # NOTE: the following have been cloned from _bcrypt_test()
+
+ def populate_settings(self, kwds):
+ # speed up test w/ lower rounds
+ kwds.setdefault("rounds", 4)
+ super(django_bcrypt_test, self).populate_settings(kwds)
+
+ def fuzz_setting_rounds(self):
+ # decrease default rounds for fuzz testing to speed up volume.
+ return randintgauss(5, 8, 6, 1)
+
+ def fuzz_setting_ident(self):
+ # omit multi-ident tests, only $2a$ counts for this class
+ return None
+
+django_bcrypt_test = skipUnless(hash.bcrypt.has_backend(),
+ "no bcrypt backends available")(django_bcrypt_test)
+
+#=============================================================================
+# eof
+#=============================================================================
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index 29254c8..2965689 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -203,6 +203,22 @@ def quicksleep(delay):
#=============================================================================
# custom test harness
#=============================================================================
+
+def patchAttr(test, obj, attr, value):
+ """monkeypatch object value, restoring original on cleanup"""
+ try:
+ orig = getattr(obj, attr)
+ except AttributeError:
+ def cleanup():
+ try:
+ delattr(obj, attr)
+ except AttributeError:
+ pass
+ test.addCleanup(cleanup)
+ else:
+ test.addCleanup(setattr, obj, attr, orig)
+ setattr(obj, attr, value)
+
class TestCase(_TestCase):
"""passlib-specific test case class