summaryrefslogtreecommitdiff
path: root/passlib/tests
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-12 21:52:26 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-12 21:52:26 -0400
commitc0f420bf7d7659ee110432f7cbb0233554dfd32a (patch)
treed8416c7cd9b5f5d54e5fcb58fafa02f64da07352 /passlib/tests
parente71ddce83853566311effebf68b9bbbdebf4c2ab (diff)
downloadpasslib-c0f420bf7d7659ee110432f7cbb0233554dfd32a.tar.gz
assorted bugfixes, tweaks, and tests added; based on coverage examination
* test os_crypt backend has functional fallback * test handler methods accept all unicode/bytes combinations for secret & hash * fixed some incorrect error messages & types being caught & raised * other minor cleanups
Diffstat (limited to 'passlib/tests')
-rw-r--r--passlib/tests/test_ext_django.py129
-rw-r--r--passlib/tests/test_handlers.py92
-rw-r--r--passlib/tests/test_registry.py2
-rw-r--r--passlib/tests/test_utils.py82
-rw-r--r--passlib/tests/utils.py123
5 files changed, 351 insertions, 77 deletions
diff --git a/passlib/tests/test_ext_django.py b/passlib/tests/test_ext_django.py
index 890c87b..f4ea932 100644
--- a/passlib/tests/test_ext_django.py
+++ b/passlib/tests/test_ext_django.py
@@ -430,85 +430,70 @@ class PluginTest(TestCase):
descriptionPrefix = "passlib.ext.django plugin"
def setUp(self):
- #remove django patch
+ super(PluginTest, self).setUp()
+
+ # remove django patch now, and at end
utils.set_django_password_context(None)
+ self.addCleanup(utils.set_django_password_context, None)
- #ensure django settings are empty
+ # ensure django settings are empty
update_settings(
PASSLIB_CONTEXT=_NOTSET,
PASSLIB_GET_CATEGORY=_NOTSET,
)
- #unload module so it's re-run
+ # unload module so it's re-run when imported
sys.modules.pop("passlib.ext.django.models", None)
- def tearDown(self):
- #remove django patch
- utils.set_django_password_context(None)
+ def check_hashes(self, tests, default_scheme, deprecated=[], load=True):
+ """run through django api to verify patch is configured & functioning"""
+ # load extension if it hasn't been already.
+ if load:
+ import passlib.ext.django.models
- def check_hashes(self, tests, new_hash=None, deprecated=None):
- u = FakeUser()
- deprecated = None
+ # create fake user object
+ user = FakeUser()
- # check new hash construction
- if new_hash:
- u.set_password("placeholder")
- handler = get_crypt_handler(new_hash)
- self.assertTrue(handler.identify(u.password))
+ # check new hashes constructed using default scheme
+ user.set_password("stub")
+ handler = get_crypt_handler(default_scheme)
+ self.assertTrue(handler.identify(user.password),
+ "handler failed to identify hash: %r %r" %
+ (default_scheme, user.password))
# run against hashes from tests...
for test in tests:
for secret, hash in test.iter_known_hashes():
# check against valid password
- u.password = hash
+ user.password = hash
if has_django0 and isinstance(secret, unicode):
secret = secret.encode("utf-8")
- self.assertTrue(u.check_password(secret))
- if new_hash and deprecated and test.handler.name in deprecated:
+ self.assertTrue(user.check_password(secret))
+ if deprecated and test.handler.name in deprecated:
self.assertFalse(handler.identify(hash))
- self.assertTrue(handler.identify(u.password))
+ self.assertTrue(handler.identify(user.password))
# check against invalid password
- u.password = hash
- self.assertFalse(u.check_password('x'+secret))
- if new_hash and deprecated and test.handler.name in deprecated:
+ user.password = hash
+ self.assertFalse(user.check_password('x'+secret))
+ if deprecated and test.handler.name in deprecated:
self.assertFalse(handler.identify(hash))
- self.assertEqual(u.password, hash)
+ self.assertEqual(user.password, hash)
# check disabled handling
if has_django1:
- u.set_password(None)
+ user.set_password(None)
handler = get_crypt_handler("django_disabled")
- self.assertTrue(handler.identify(u.password))
- self.assertFalse(u.check_password('placeholder'))
+ self.assertTrue(handler.identify(user.password))
+ self.assertFalse(user.check_password('placeholder'))
- def test_00_actual_django(self):
- "test actual Django behavior has not changed"
- #NOTE: if this test fails,
- # probably means newer version of Django,
- # and passlib's policies should be updated.
+ def check_django_stock(self, load=True):
self.check_hashes(django_hash_tests,
"django_salted_sha1",
- ["hex_md5"])
-
- def test_01_explicit_unset(self, value=None):
- "test PASSLIB_CONTEXT = None"
- update_settings(
- PASSLIB_CONTEXT=value,
- )
- import passlib.ext.django.models
- self.check_hashes(django_hash_tests,
- "django_salted_sha1",
- ["hex_md5"])
-
- def test_02_stock_ctx(self):
- "test PASSLIB_CONTEXT = utils.STOCK_CTX"
- self.test_01_explicit_unset(value=utils.STOCK_CTX)
+ ["hex_md5"], load=load)
- def test_03_implicit_default_ctx(self):
- "test PASSLIB_CONTEXT unset"
- import passlib.ext.django.models
+ def check_passlib_stock(self):
self.check_hashes(default_hash_tests,
"sha512_crypt",
["hex_md5", "django_salted_sha1",
@@ -516,21 +501,43 @@ class PluginTest(TestCase):
"django_des_crypt",
])
- def test_04_explicit_default_ctx(self):
+ def test_10_django(self):
+ "test actual Django behavior has not changed"
+ #NOTE: if this test fails,
+ # probably means newer version of Django,
+ # and passlib's policies should be updated.
+ self.check_django_stock(load=False)
+
+ def test_11_none(self):
+ "test PASSLIB_CONTEXT=None"
+ update_settings(PASSLIB_CONTEXT=None)
+ self.check_django_stock(load=False)
+
+ def test_12_string(self):
+ "test PASSLIB_CONTEXT=string"
+ update_settings(PASSLIB_CONTEXT=utils.STOCK_CTX)
+ self.check_django_stock(load=False)
+
+ def test_13_unset(self):
+ "test unset PASSLIB_CONTEXT uses default"
+ self.check_passlib_stock()
+
+ def test_14_default(self):
"test PASSLIB_CONTEXT = utils.DEFAULT_CTX"
- update_settings(
- PASSLIB_CONTEXT=utils.DEFAULT_CTX,
- )
- self.test_03_implicit_default_ctx()
+ update_settings(PASSLIB_CONTEXT=utils.DEFAULT_CTX)
+ self.check_passlib_stock()
- def test_05_default_ctx_alias(self):
+ def test_15_default_alias(self):
"test PASSLIB_CONTEXT = 'passlib-default'"
- update_settings(
- PASSLIB_CONTEXT="passlib-default",
- )
- self.test_03_implicit_default_ctx()
+ update_settings(PASSLIB_CONTEXT="passlib-default")
+ self.check_passlib_stock()
+
+ def test_16_invalid(self):
+ "test PASSLIB_CONTEXT = invalid type"
+ update_settings(PASSLIB_CONTEXT=123)
+ self.assertRaises(TypeError, __import__, 'passlib.ext.django.models')
- def test_06_categories(self):
+ def test_20_categories(self):
"test PASSLIB_GET_CATEGORY unset"
update_settings(
PASSLIB_CONTEXT=category_context.policy.to_string(),
@@ -541,7 +548,7 @@ class PluginTest(TestCase):
self.assertEqual(get_cc_rounds(is_staff=True), 2000)
self.assertEqual(get_cc_rounds(is_superuser=True), 3000)
- def test_07_categories_explicit(self):
+ def test_21_categories_explicit(self):
"test PASSLIB_GET_CATEGORY = function"
def get_category(user):
return user.first_name or None
@@ -556,7 +563,7 @@ class PluginTest(TestCase):
self.assertEqual(get_cc_rounds(first_name='staff'), 2000)
self.assertEqual(get_cc_rounds(first_name='superuser'), 3000)
- def test_08_categories_disabled(self):
+ def test_22_categories_disabled(self):
"test PASSLIB_GET_CATEGORY = None"
update_settings(
PASSLIB_CONTEXT = category_context.policy.to_string(),
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index 63e457c..d1c5cbf 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -497,6 +497,7 @@ class cisco_pix_test(UserHandlerMixin, HandlerCase):
class cisco_type7_test(HandlerCase):
handler = hash.cisco_type7
salt_bits = 4
+ salt_type = int
known_correct_hashes = [
#
@@ -539,6 +540,41 @@ class cisco_type7_test(HandlerCase):
(UPASS_TABLE, '0958EDC8A9F495F6F8A5FD'),
]
+ known_unidentified_hashes = [
+ # salt with hex value
+ "0A480E051A33490E",
+
+ # salt value > 52. this may in fact be valid, but we reject it for now
+ # (see docs for more).
+ '99400E4812',
+ ]
+
+ def test_90_decode(self):
+ "test cisco_type7.decode()"
+ from passlib.utils import to_unicode, to_bytes
+
+ handler = self.handler
+ for secret, hash in self.known_correct_hashes:
+ usecret = to_unicode(secret)
+ bsecret = to_bytes(secret)
+ self.assertEqual(handler.decode(hash), usecret)
+ self.assertEqual(handler.decode(hash, None), bsecret)
+
+ self.assertRaises(UnicodeDecodeError, handler.decode,
+ '0958EDC8A9F495F6F8A5FD', 'ascii')
+
+ def test_91_salt(self):
+ "test salt value border cases"
+ handler = self.handler
+ self.assertRaises(TypeError, handler, salt=None)
+ handler(salt=None, use_defaults=True)
+ self.assertRaises(TypeError, handler, salt='abc')
+ self.assertRaises(ValueError, handler, salt=-10)
+ with catch_warnings(record=True) as wlog:
+ h = handler(salt=100, relaxed=True)
+ self.consumeWarningList(wlog, ["salt/offset must be.*"])
+ self.assertEqual(h.salt, 52)
+
#=========================================================
# crypt16
#=========================================================
@@ -794,6 +830,9 @@ class fshp_test(HandlerCase):
]
known_malformed_hashes = [
+ # bad base64 padding
+ '{FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M',
+
# wrong salt size
'{FSHP0|1|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=',
@@ -801,6 +840,32 @@ class fshp_test(HandlerCase):
'{FSHP0|0|A}qUqP5cyxm6YcTAhz05Hph5gvu9M=',
]
+ def test_90_variant(self):
+ "test variant keyword"
+ handler = self.handler
+ kwds = dict(salt='a', rounds=1)
+
+ # accepts ints
+ handler(variant=1, **kwds)
+
+ # accepts bytes or unicode
+ handler(variant=u('1'), **kwds)
+ handler(variant=b('1'), **kwds)
+
+ # aliases
+ handler(variant=u('sha256'), **kwds)
+ handler(variant=b('sha256'), **kwds)
+
+ # rejects None
+ self.assertRaises(TypeError, handler, variant=None, **kwds)
+
+ # rejects other types
+ self.assertRaises(TypeError, handler, variant=complex(1,1), **kwds)
+
+ # invalid variant
+ self.assertRaises(ValueError, handler, variant='9', **kwds)
+ self.assertRaises(ValueError, handler, variant=9, **kwds)
+
#=========================================================
#hex digests
#=========================================================
@@ -1080,6 +1145,9 @@ class _md5_crypt_test(HandlerCase):
known_malformed_hashes = [
# bad char in otherwise correct hash \/
'$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!',
+
+ # too many fields
+ '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.$',
]
platform_crypt_support = dict(
@@ -2005,6 +2073,12 @@ class _sha1_crypt_test(HandlerCase):
# zero padded rounds
'$sha1$01773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH',
+
+ # too many fields
+ '$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH$',
+
+ # empty rounds field
+ '$sha1$$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH$',
]
platform_crypt_support = dict(
@@ -2316,6 +2390,14 @@ class sun_md5_crypt_test(HandlerCase):
]
known_malformed_hashes = [
+ # unexpected end of hash
+ "$md5,rounds=5000",
+
+ # bad rounds
+ "$md5,rounds=500A$xxxx",
+ "$md5,rounds=0500$xxxx",
+ "$md5,rounds=0$xxxx",
+
# bad char in otherwise correct hash
"$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/",
@@ -2369,6 +2451,16 @@ class unix_disabled_test(HandlerCase):
# TODO: test custom marker support
# TODO: test default marker selection
+ def test_90_preserves_existing(self):
+ "test preserves existing disabled hash"
+ handler = self.handler
+
+ # use marker if no hash
+ self.assertEqual(handler.genhash("stub", None), handler.marker)
+
+ # use hash if provided and valid
+ self.assertEqual(handler.genhash("stub", "!asd"), "!asd")
+
class unix_fallback_test(HandlerCase):
handler = hash.unix_fallback
accepts_all_hashes = True
diff --git a/passlib/tests/test_registry.py b/passlib/tests/test_registry.py
index 1990919..3f18271 100644
--- a/passlib/tests/test_registry.py
+++ b/passlib/tests/test_registry.py
@@ -126,6 +126,8 @@ class RegistryTest(TestCase):
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name=None)))
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="AB_CD")))
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab-cd")))
+ self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab__cd")))
+ self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="default")))
class dummy_1(uh.StaticHandler):
name = "dummy_1"
diff --git a/passlib/tests/test_utils.py b/passlib/tests/test_utils.py
index 4c3a6d2..4f6b62d 100644
--- a/passlib/tests/test_utils.py
+++ b/passlib/tests/test_utils.py
@@ -26,6 +26,60 @@ class MiscTest(TestCase):
#NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test
+ def test_classproperty(self):
+ from passlib.utils import classproperty
+
+ class test(object):
+ xvar = 1
+ @classproperty
+ def xprop(cls):
+ return cls.xvar
+
+ self.assertEqual(test.xprop, 1)
+ prop = test.__dict__['xprop']
+ self.assertIs(prop.im_func, prop.__func__)
+
+ def test_deprecated_function(self):
+ from passlib.utils import deprecated_function
+ # NOTE: not comprehensive, just tests the basic behavior
+
+ @deprecated_function(deprecated="1.6", removed="1.8")
+ def test_func(*args):
+ "test docstring"
+ return args
+
+ self.assertTrue(".. deprecated::" in test_func.__doc__)
+
+ with catch_warnings(record=True) as wlog:
+ self.assertEqual(test_func(1,2), (1,2))
+ self.consumeWarningList(wlog,[
+ dict(category=DeprecationWarning,
+ message="the function passlib.tests.test_utils.test_func() "
+ "is deprecated as of Passlib 1.6, and will be "
+ "removed in Passlib 1.8."
+ ),
+ ])
+
+ def test_memoized_property(self):
+ from passlib.utils import memoized_property
+
+ class dummy(object):
+ counter = 0
+
+ @memoized_property
+ def value(self):
+ value = self.counter
+ self.counter = value+1
+ return value
+
+ d = dummy()
+ self.assertEqual(d.value, 0)
+ self.assertEqual(d.value, 0)
+ self.assertEqual(d.counter, 1)
+
+ prop = dummy.value
+ self.assertIs(prop.im_func, prop.__func__)
+
def test_getrandbytes(self):
"test getrandbytes()"
from passlib.utils import getrandbytes, rng
@@ -284,6 +338,8 @@ class MiscTest(TestCase):
# unassigned code points (as of unicode 3.2)
self.assertRaises(ValueError, sp, u("\u0900"))
self.assertRaises(ValueError, sp, u("\uFFF8"))
+ # tagging characters
+ self.assertRaises(ValueError, sp, u("\U000e0001"))
# verify bidi behavior
# if starts with R/AL -- must end with R/AL
@@ -524,6 +580,30 @@ b(""" 0000000000000000 0000000000000000 8CA64DE9C1B123A7
#=========================================================
# base64engine
#=========================================================
+class Base64EngineTest(TestCase):
+ "test standalone parts of Base64Engine"
+ # NOTE: most Base64Engine testing done via _Base64Test subclasses below.
+
+ def test_constructor(self):
+ from passlib.utils import Base64Engine, AB64_CHARS
+
+ # bad charmap type
+ self.assertRaises(TypeError, Base64Engine, 1)
+
+ # bad charmap size
+ self.assertRaises(ValueError, Base64Engine, AB64_CHARS[:-1])
+
+ # dup charmap letter
+ self.assertRaises(ValueError, Base64Engine, AB64_CHARS[:-1] + "A")
+
+ def test_ab64(self):
+ from passlib.utils import ab64_decode
+ # TODO: make ab64_decode (and a b64 variant) *much* stricter about
+ # padding chars, etc.
+
+ # 1 mod 4 not valid
+ self.assertRaises(ValueError, ab64_decode, "abcde")
+
class _Base64Test(TestCase):
"common tests for all Base64Engine instances"
#=========================================================
@@ -724,6 +804,8 @@ class _Base64Test(TestCase):
out = engine.decode_bytes(tmp)
self.assertEqual(out, result)
+ self.assertRaises(TypeError, engine.encode_transposed_bytes, u("a"), [])
+
def test_decode_transposed_bytes(self):
"test decode_transposed_bytes()"
engine = self.engine
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index 713824e..a75f428 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -42,7 +42,7 @@ from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \
classproperty, rng, getrandstr, is_ascii_safe, to_native_str, \
repeat_string
from passlib.utils.compat import b, bytes, iteritems, irange, callable, \
- base_string_types, exc_err, u, unicode
+ base_string_types, exc_err, u, unicode, PY2
import passlib.utils.handlers as uh
#local
__all__ = [
@@ -134,6 +134,18 @@ def get_file(path):
with open(path, "rb") as fh:
return fh.read()
+def tonn(source):
+ "convert native string to non-native string"
+ if not isinstance(source, str):
+ return source
+ elif PY3:
+ return source.encode("utf-8")
+ else:
+ try:
+ return source.decode("utf-8")
+ except UnicodeDecodeError:
+ return source.decode("latin-1")
+
#=========================================================
#custom test base
#=========================================================
@@ -741,7 +753,7 @@ class HandlerCase(TestCase):
# XXX: any more checks needed?
- def test_02_config(self):
+ def test_02_config_workflow(self):
"""test basic config-string workflow
this tests that genconfig() returns the expected types,
@@ -785,7 +797,7 @@ class HandlerCase(TestCase):
else:
self.assertRaises(TypeError, self.do_identify, config)
- def test_03_hash(self):
+ def test_03_hash_workflow(self):
"""test basic hash-string workflow.
this tests that encrypt()'s hashes are accepted
@@ -835,8 +847,36 @@ class HandlerCase(TestCase):
#
self.assertTrue(self.do_identify(result))
+ def test_04_hash_types(self):
+ "test hashes can be unicode or bytes"
+ # this runs through workflow similar to 03, but wraps
+ # everything using tonn() so we test unicode under py2,
+ # and bytes under py3.
+
+ # encrypt using non-native secret
+ result = self.do_encrypt(tonn('stub'))
+ self.check_returned_native_str(result, "encrypt")
+
+ # verify using non-native hash
+ self.check_verify('stub', tonn(result))
+
+ # verify using non-native hash AND secret
+ self.check_verify(tonn('stub'), tonn(result))
+
+ # genhash using non-native hash
+ other = self.do_genhash('stub', tonn(result))
+ self.check_returned_native_str(other, "genhash")
+ self.assertEqual(other, result)
- def test_04_backends(self):
+ # genhash using non-native hash AND secret
+ other = self.do_genhash(tonn('stub'), tonn(result))
+ self.check_returned_native_str(other, "genhash")
+ self.assertEqual(other, result)
+
+ # identify using non-native hash
+ self.assertTrue(self.do_identify(tonn(result)))
+
+ def test_05_backends(self):
"test multi-backend support"
handler = self.handler
if not hasattr(handler, "set_backend"):
@@ -1065,6 +1105,34 @@ class HandlerCase(TestCase):
self.assertRaises(ValueError, self.do_genconfig, salt=c*chunk,
__msg__="invalid salt char %r:" % (c,))
+ @property
+ def salt_type(self):
+ "hack to determine salt keyword's datatype"
+ # NOTE: cisco_type7 uses 'int'
+ if getattr(self.handler, "_salt_is_bytes", False):
+ return bytes
+ else:
+ return unicode
+
+ def test_15_salt_type(self):
+ "test non-string salt values"
+ self.require_salt()
+ salt_type = self.salt_type
+
+ # should always throw error for random class.
+ class fake(object):
+ pass
+ self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=fake())
+
+ # unicode should be accepted only if salt_type is unicode.
+ if salt_type is not unicode:
+ self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=u('x'))
+
+ # bytes should be accepted only if salt_type is bytes,
+ # OR if salt type is unicode and running PY2 - to allow native strings.
+ if not (salt_type is bytes or (PY2 and salt_type is unicode)):
+ self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=b('x'))
+
#==============================================================
# rounds
#==============================================================
@@ -1672,11 +1740,8 @@ class HandlerCase(TestCase):
return rng.choice(handler.ident_values)
#=========================================================
- # test 8x - mixin tests
- # test 9x - handler-specific tests
- #=========================================================
-
- #=========================================================
+ # test 8x - mixin tests
+ # test 9x - handler-specific tests
# eoc
#=========================================================
@@ -1746,20 +1811,27 @@ class OsCryptMixin(HandlerCase):
#=========================================================
# custom tests
#=========================================================
- def test_80_faulty_crypt(self):
- "test with faulty crypt()"
- # patch safe_crypt to return mock value.
+ def _use_mock_crypt(self):
+ "patch safe_crypt() so it returns mock value"
import passlib.utils as mod
- self.addCleanup(setattr, mod, "_crypt", mod._crypt)
+ if not self.using_patched_crypt:
+ self.addCleanup(setattr, mod, "_crypt", mod._crypt)
crypt_value = [None]
mod._crypt = lambda secret, config: crypt_value[0]
+ def setter(value):
+ crypt_value[0] = value
+ return setter
- # prepare framework
+ def test_80_faulty_crypt(self):
+ "test with faulty crypt()"
hash = self.get_sample_hash()[1]
exc_types = (AssertionError,)
+ setter = self._use_mock_crypt()
def test(value):
- crypt_value[0] = value
+ # set safe_crypt() to return specified value, and
+ # make sure assertion error is raised by handler.
+ setter(value)
self.assertRaises(exc_types, self.do_genhash, "stub", hash)
self.assertRaises(exc_types, self.do_encrypt, "stub")
self.assertRaises(exc_types, self.do_verify, "stub", hash)
@@ -1768,7 +1840,26 @@ class OsCryptMixin(HandlerCase):
test(hash[:-1]) # detect too short
test(hash + 'x') # detect too long
- def test_81_crypt_support(self):
+ def test_81_crypt_fallback(self):
+ "test per-call crypt() fallback"
+ # set safe_crypt to return None
+ setter = self._use_mock_crypt()
+ setter(None)
+ if _find_alternate_backend(self.handler, "os_crypt"):
+ # handler should have a fallback to use
+ h1 = self.do_encrypt("stub")
+ h2 = self.do_genhash("stub", h1)
+ self.assertEqual(h2, h1)
+ self.assertTrue(self.do_verify("stub", h1))
+ else:
+ # handler should give up
+ from passlib.exc import MissingBackendError
+ hash = self.get_sample_hash()[1]
+ self.assertRaises(MissingBackendError, self.do_encrypt, 'stub')
+ self.assertRaises(MissingBackendError, self.do_genhash, 'stub', hash)
+ self.assertRaises(MissingBackendError, self.do_verify, 'stub', hash)
+
+ def test_82_crypt_support(self):
"test crypt support detection"
platform = sys.platform
for name, flag in self.platform_crypt_support.items():