summaryrefslogtreecommitdiff
path: root/passlib/tests
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-02-08 22:38:25 -0500
committerEli Collins <elic@assurancetechnologies.com>2012-02-08 22:38:25 -0500
commit86a2dc3ed68fcdf7853f5e219541a19b5fcacfff (patch)
tree8319e8704f04a23faab1eedfad383d50ff6d3671 /passlib/tests
parent4c4615329b64287dabd729e3078ab03cb2bb7442 (diff)
downloadpasslib-86a2dc3ed68fcdf7853f5e219541a19b5fcacfff.tar.gz
large refactor of GenericHandler internals
strict keyword -------------- * GenericHandler's "strict" keyword had poorly defined semantics; replaced this with "use_defaults" and "relaxed" keywords. Most handlers' from_string() method specified strict=True. This is now the default behavior, use_defaults=True is enabled only for encrypt() and genconfig(). relaxed=True is enabled only for specific handlers (and unittests) whose code requires it. This *does* break backward compat with passlib 1.5 handlers, but this is mostly and internal class. * missing required settings now throws a TypeError instead of a ValueError, to be more in line with std python behavior. * The norm_xxx functions provided by the GenericHandler mixins (e.g. norm_salt) have been renamed to _norm_xxx() to reflect their private nature; and converted from class methods to instance methods, to simplify their call signature for subclassing. misc ---- * rewrote GenericHandler unittests to use constructor only, instead of poking into norm_salt/norm_rounds internals. * checksum/salt charset checks speed up using set comparison * some small cleanups to FHSP implementation
Diffstat (limited to 'passlib/tests')
-rw-r--r--passlib/tests/test_context.py3
-rw-r--r--passlib/tests/test_handlers.py45
-rw-r--r--passlib/tests/test_utils_handlers.py267
-rw-r--r--passlib/tests/utils.py14
4 files changed, 211 insertions, 118 deletions
diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py
index 72ee39b..2ee966a 100644
--- a/passlib/tests/test_context.py
+++ b/passlib/tests/test_context.py
@@ -781,11 +781,12 @@ class CryptContextTest(TestCase):
c2 = cc.replace(all__vary_rounds="100%")
self.assert_rounds_range(c2, "bcrypt", 15, 21)
- def assert_rounds_range(self, context, scheme, lower, upper, salt="."*22):
+ def assert_rounds_range(self, context, scheme, lower, upper):
"helper to check vary_rounds covers specified range"
# NOTE: this runs enough times the min and max *should* be hit,
# though there's a faint chance it will randomly fail.
handler = context.policy.get_handler(scheme)
+ salt = handler.default_salt_chars[0:1] * handler.max_salt_size
seen = set()
for i in irange(300):
h = context.genconfig(scheme, salt=salt)
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index 9da90d0..98a741f 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -110,16 +110,14 @@ class _BCryptTest(HandlerCase):
kwds = dict(checksum='8CIhhFCj15KqqFvo/n.Jatx8dJ92f82',
salt='VlsfIX9.apXuQBr6tego0.',
- rounds=12, ident="2a", strict=True)
+ rounds=12, ident="2a")
handler(**kwds)
- kwds['ident'] = None
- self.assertRaises(ValueError, handler, **kwds)
-
- del kwds['strict']
+ kwds.update(ident=None)
+ self.assertRaises(TypeError, handler, **kwds)
- kwds['ident'] = 'Q'
+ kwds.update(use_defaults=True, ident='Q')
self.assertRaises(ValueError, handler, **kwds)
#===============================================================
@@ -149,7 +147,7 @@ class _BCryptTest(HandlerCase):
self.assertWarningMatches(wlog.pop(0),
message_re="^encountered a bcrypt hash with incorrectly set padding bits.*",
)
- self.assertFalse(wlog)
+ self.assertNoWarnings(wlog)
def check_padding(hash):
"check bcrypt hash doesn't have salt padding bits set"
@@ -174,12 +172,14 @@ class _BCryptTest(HandlerCase):
with catch_warnings(record=True) as wlog:
warnings.simplefilter("always")
- hash = bcrypt.genconfig(salt="."*21 + "A.")
+ hash = bcrypt.genconfig(salt="."*21 + "A.", relaxed=True)
+ self.assertWarningMatches(wlog.pop(0), message_re="salt too large")
check_warning(wlog)
self.assertEqual(hash, "$2a$12$" + "." * 22)
- hash = bcrypt.genconfig(salt="."*23)
- self.assertFalse(wlog)
+ hash = bcrypt.genconfig(salt="."*23, relaxed=True)
+ self.assertWarningMatches(wlog.pop(0), message_re="salt too large")
+ self.assertNoWarnings(wlog)
self.assertEqual(hash, "$2a$12$" + "." * 22)
#===============================================================
@@ -710,14 +710,13 @@ class NTHashTest(HandlerCase):
def test_idents(self):
handler = self.handler
- kwds = dict(checksum='7f8fe03093cc84b267b109625f6bbf4b', ident="3", strict=True)
+ kwds = dict(checksum='7f8fe03093cc84b267b109625f6bbf4b', ident="3")
handler(**kwds)
- kwds['ident'] = None
- self.assertRaises(ValueError, handler, **kwds)
+ kwds.update(ident=None)
+ self.assertRaises(TypeError, handler, **kwds)
- del kwds['strict']
- kwds['ident'] = 'Q'
+ kwds.update(use_defaults=True, ident='Q')
self.assertRaises(ValueError, handler, **kwds)
#=========================================================
@@ -917,14 +916,14 @@ class PHPassTest(HandlerCase):
def test_idents(self):
handler = self.handler
- kwds = dict(checksum='eRo7ud9Fh4E2PdI0S3r.L0', salt='IQRaTwmf', rounds=9, ident="P", strict=True)
+ kwds = dict(checksum='eRo7ud9Fh4E2PdI0S3r.L0', salt='IQRaTwmf',
+ rounds=9, ident="P")
handler(**kwds)
- kwds['ident'] = None
- self.assertRaises(ValueError, handler, **kwds)
+ kwds.update(ident=None)
+ self.assertRaises(TypeError, handler, **kwds)
- del kwds['strict']
- kwds['ident'] = 'Q'
+ kwds.update(use_defaults=True, ident='Q')
self.assertRaises(ValueError, handler, **kwds)
#=========================================================
@@ -1067,8 +1066,8 @@ class ScramTest(HandlerCase):
def test_100_algs(self):
"test parsing of 'algs' setting"
- def parse(source):
- return self.handler(algs=source).algs
+ def parse(algs, **kwds):
+ return self.handler(algs=algs, use_defaults=True, **kwds).algs
# None -> default list
self.assertEqual(parse(None), ["sha-1","sha-256","sha-512"])
@@ -1087,7 +1086,7 @@ class ScramTest(HandlerCase):
self.assertRaises(ValueError, parse, ["sha-1","shaxxx-890"])
# alg & checksum mutually exclusive.
- self.assertRaises(RuntimeError, self.handler, algs=['sha-1'],
+ self.assertRaises(RuntimeError, parse, ['sha-1'],
checksum={"sha-1": b("\x00"*20)})
def test_101_extract_digest_info(self):
diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py
index fe2667a..def6d3e 100644
--- a/passlib/tests/test_utils_handlers.py
+++ b/passlib/tests/test_utils_handlers.py
@@ -13,10 +13,10 @@ import warnings
from passlib.hash import ldap_md5, sha256_crypt
from passlib.registry import _unload_handler_name as unload_handler_name, \
register_crypt_handler, get_crypt_handler
-from passlib.exc import MissingBackendError
+from passlib.exc import MissingBackendError, PasslibHandlerWarning
from passlib.utils import getrandstr, JYTHON, rng, to_unicode
from passlib.utils.compat import b, bytes, bascii_to_str, str_to_uascii, \
- uascii_to_str, unicode
+ uascii_to_str, unicode, PY_MAX_25
import passlib.utils.handlers as uh
from passlib.tests.utils import HandlerCase, TestCase, catch_warnings, \
dummy_handler_in_registry
@@ -25,6 +25,21 @@ from passlib.utils.compat import u
log = getLogger(__name__)
#=========================================================
+# utils
+#=========================================================
+def _makelang(alphabet, size):
+ "generate all strings of given size using alphabet"
+ def helper(size):
+ if size < 2:
+ for char in alphabet:
+ yield char
+ else:
+ for char in alphabet:
+ for tail in helper(size-1):
+ yield char+tail
+ return set(helper(size))
+
+#=========================================================
#test support classes - StaticHandler, GenericHandler, etc
#=========================================================
class SkeletonTest(TestCase):
@@ -95,13 +110,13 @@ class SkeletonTest(TestCase):
else:
raise ValueError
- #check fallback
+ # check fallback
self.assertFalse(d1.identify(None))
self.assertFalse(d1.identify(''))
self.assertTrue(d1.identify('a'))
self.assertFalse(d1.identify('b'))
- #check ident-based
+ # check ident-based
d1.ident = u('!')
self.assertFalse(d1.identify(None))
self.assertFalse(d1.identify(''))
@@ -109,70 +124,108 @@ class SkeletonTest(TestCase):
self.assertFalse(d1.identify('a'))
def test_11_norm_checksum(self):
- "test GenericHandler.norm_checksum()"
+ "test GenericHandler checksum handling"
+ # setup helpers
class d1(uh.GenericHandler):
name = 'd1'
checksum_size = 4
checksum_chars = 'x'
- self.assertRaises(ValueError, d1.norm_checksum, 'xxx')
- self.assertEqual(d1.norm_checksum('xxxx'), 'xxxx')
- self.assertRaises(ValueError, d1.norm_checksum, 'xxxxx')
- self.assertRaises(ValueError, d1.norm_checksum, 'xxyx')
+
+ def norm_checksum(*a, **k):
+ return d1(*a, **k).checksum
+
+ # too small
+ self.assertRaises(ValueError, norm_checksum, 'xxx')
+
+ # right size
+ self.assertEqual(norm_checksum('xxxx'), 'xxxx')
+
+ # too large
+ self.assertRaises(ValueError, norm_checksum, 'xxxxx')
+
+ # wrong chars
+ self.assertRaises(ValueError, norm_checksum, 'xxyx')
def test_20_norm_salt(self):
- "test GenericHandler+HasSalt: .norm_salt(), .generate_salt()"
- class d1(uh.HasSalt, uh.GenericHandler):
- name = 'd1'
- setting_kwds = ('salt',)
- min_salt_size = 1
- max_salt_size = 3
- default_salt_size = 2
- salt_chars = 'a'
-
- #check salt=None
- self.assertEqual(d1.norm_salt(None), 'aa')
- self.assertRaises(ValueError, d1.norm_salt, None, strict=True)
-
- #check small & large salts
- with catch_warnings():
- warnings.filterwarnings("ignore", ".* salt string must be at (least|most) .*", UserWarning)
- self.assertEqual(d1.norm_salt('aaaa'), 'aaa')
- self.assertRaises(ValueError, d1.norm_salt, '')
- self.assertRaises(ValueError, d1.norm_salt, 'aaaa', strict=True)
-
- #check generate salt (indirectly)
- self.assertEqual(len(d1.norm_salt(None)), 2)
- self.assertEqual(len(d1.norm_salt(None,salt_size=1)), 1)
- self.assertEqual(len(d1.norm_salt(None,salt_size=3)), 3)
- self.assertEqual(len(d1.norm_salt(None,salt_size=5)), 3)
- self.assertRaises(ValueError, d1.norm_salt, None, salt_size=5, strict=True)
-
- def test_21_norm_salt(self):
- "test GenericHandler+HasSalt: .norm_salt(), .generate_salt() - with no max_salt_size"
+ "test GenericHandler + HasSalt mixin"
+ # setup helpers
class d1(uh.HasSalt, uh.GenericHandler):
name = 'd1'
setting_kwds = ('salt',)
- min_salt_size = 1
- max_salt_size = None
- default_salt_size = 2
- salt_chars = 'a'
-
- #check salt=None
- self.assertEqual(d1.norm_salt(None), 'aa')
- self.assertRaises(ValueError, d1.norm_salt, None, strict=True)
-
- #check small & large salts
- self.assertRaises(ValueError, d1.norm_salt, '')
- self.assertEqual(d1.norm_salt('aaaa', strict=True), 'aaaa')
-
- #check generate salt (indirectly)
- self.assertEqual(len(d1.norm_salt(None)), 2)
- self.assertEqual(len(d1.norm_salt(None,salt_size=1)), 1)
- self.assertEqual(len(d1.norm_salt(None,salt_size=3)), 3)
- self.assertEqual(len(d1.norm_salt(None,salt_size=5)), 5)
+ min_salt_size = 2
+ max_salt_size = 4
+ default_salt_size = 3
+ salt_chars = 'ab'
+
+ def norm_salt(**k):
+ return d1(**k).salt
+
+ def gen_salt(sz, **k):
+ return d1(use_defaults=True, salt_size=sz, **k).salt
+
+ salts2 = _makelang('ab', 2)
+ salts3 = _makelang('ab', 3)
+ salts4 = _makelang('ab', 4)
+
+ # check salt=None
+ self.assertRaises(TypeError, norm_salt)
+ self.assertRaises(TypeError, norm_salt, salt=None)
+ self.assertIn(norm_salt(use_defaults=True), salts3)
+
+ # check explicit salts
+ with catch_warnings(record=True) as wlog:
+
+ # check too-small salts
+ self.assertRaises(ValueError, norm_salt, salt='')
+ self.assertRaises(ValueError, norm_salt, salt='a')
+ self.assertNoWarnings(wlog)
+
+ # check correct salts
+ self.assertEqual(norm_salt(salt='ab'), 'ab')
+ self.assertEqual(norm_salt(salt='aba'), 'aba')
+ self.assertEqual(norm_salt(salt='abba'), 'abba')
+ self.assertNoWarnings(wlog)
+
+ # check too-large salts
+ self.assertRaises(ValueError, norm_salt, salt='aaaabb')
+ self.assertNoWarnings(wlog)
+
+ self.assertEqual(norm_salt(salt='aaaabb', relaxed=True), 'aaaa')
+ self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning)
+ self.assertNoWarnings(wlog)
+
+ #check generated salts
+ with catch_warnings(record=True) as wlog:
+
+ # check too-small salt size
+ self.assertRaises(ValueError, gen_salt, 0)
+ self.assertRaises(ValueError, gen_salt, 1)
+ self.assertNoWarnings(wlog)
+
+ # check correct salt size
+ self.assertIn(gen_salt(2), salts2)
+ self.assertIn(gen_salt(3), salts3)
+ self.assertIn(gen_salt(4), salts4)
+ self.assertNoWarnings(wlog)
+
+ # check too-large salt size
+ self.assertRaises(ValueError, gen_salt, 5)
+ self.assertNoWarnings(wlog)
+
+ self.assertIn(gen_salt(5, relaxed=True), salts4)
+ self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning)
+ self.assertNoWarnings(wlog)
+
+ # test with max_salt_size=None
+ del d1.max_salt_size
+ with catch_warnings(record=True) as wlog:
+ self.assertEqual(len(gen_salt(None)), 3)
+ self.assertEqual(len(gen_salt(5)), 5)
+ self.assertNoWarnings(wlog)
def test_30_norm_rounds(self):
- "test GenericHandler+HasRounds: .norm_rounds()"
+ "test GenericHandler + HasRounds mixin"
+ # setup helpers
class d1(uh.HasRounds, uh.GenericHandler):
name = 'd1'
setting_kwds = ('rounds',)
@@ -180,24 +233,44 @@ class SkeletonTest(TestCase):
max_rounds = 3
default_rounds = 2
- #check rounds=None
- self.assertEqual(d1.norm_rounds(None), 2)
- self.assertRaises(ValueError, d1.norm_rounds, None, strict=True)
+ def norm_rounds(**k):
+ return d1(**k).rounds
+
+ # check rounds=None
+ self.assertRaises(TypeError, norm_rounds)
+ self.assertRaises(TypeError, norm_rounds, rounds=None)
+ self.assertEqual(norm_rounds(use_defaults=True), 2)
+
+ # check explicit rounds
+ with catch_warnings(record=True) as wlog:
+ # too small
+ self.assertRaises(ValueError, norm_rounds, rounds=0)
+ self.assertNoWarnings(wlog)
+
+ self.assertEqual(norm_rounds(rounds=0, relaxed=True), 1)
+ self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning)
+ self.assertNoWarnings(wlog)
+
+ # just right
+ self.assertEqual(norm_rounds(rounds=1), 1)
+ self.assertEqual(norm_rounds(rounds=2), 2)
+ self.assertEqual(norm_rounds(rounds=3), 3)
+ self.assertNoWarnings(wlog)
- #check small & large rounds
- with catch_warnings():
- warnings.filterwarnings("ignore", ".* does not allow (less|more) than \d rounds: .*", UserWarning)
- self.assertEqual(d1.norm_rounds(0), 1)
- self.assertEqual(d1.norm_rounds(4), 3)
- self.assertRaises(ValueError, d1.norm_rounds, 0, strict=True)
- self.assertRaises(ValueError, d1.norm_rounds, 4, strict=True)
+ # too large
+ self.assertRaises(ValueError, norm_rounds, rounds=4)
+ self.assertNoWarnings(wlog)
- #check no default rounds
+ self.assertEqual(norm_rounds(rounds=4, relaxed=True), 3)
+ self.assertWarningMatches(wlog.pop(0), category=PasslibHandlerWarning)
+ self.assertNoWarnings(wlog)
+
+ # check no default rounds
d1.default_rounds = None
- self.assertRaises(ValueError, d1.norm_rounds, None)
+ self.assertRaises(TypeError, norm_rounds, use_defaults=True)
def test_40_backends(self):
- "test GenericHandler+HasManyBackends"
+ "test GenericHandler + HasManyBackends mixin"
class d1(uh.HasManyBackends, uh.GenericHandler):
name = 'd1'
setting_kwds = ()
@@ -249,33 +322,36 @@ class SkeletonTest(TestCase):
self.assertRaises(ValueError, d1.set_backend, 'c')
self.assertRaises(ValueError, d1.has_backend, 'c')
- def test_50_bh_norm_ident(self):
- "test GenericHandler+HasManyIdents: .norm_ident() & .identify()"
+ def test_50_norm_ident(self):
+ "test GenericHandler + HasManyIdents"
+ # setup helpers
class d1(uh.HasManyIdents, uh.GenericHandler):
name = 'd1'
setting_kwds = ('ident',)
+ default_ident = u("!A")
ident_values = [ u("!A"), u("!B") ]
ident_aliases = { u("A"): u("!A")}
- #check ident=None w/ no default
- self.assertIs(d1.norm_ident(None), None)
- self.assertRaises(ValueError, d1.norm_ident, None, strict=True)
+ def norm_ident(**k):
+ return d1(**k).ident
+
+ # check ident=None
+ self.assertRaises(TypeError, norm_ident)
+ self.assertRaises(TypeError, norm_ident, ident=None)
+ self.assertEqual(norm_ident(use_defaults=True), u('!A'))
- #check ident=None w/ default
- d1.default_ident = u("!A")
- self.assertEqual(d1.norm_ident(None), u('!A'))
- self.assertRaises(ValueError, d1.norm_ident, None, strict=True)
+ # check valid idents
+ self.assertEqual(norm_ident(ident=u('!A')), u('!A'))
+ self.assertEqual(norm_ident(ident=u('!B')), u('!B'))
+ self.assertRaises(ValueError, norm_ident, ident=u('!C'))
- #check explicit
- self.assertEqual(d1.norm_ident(u('!A')), u('!A'))
- self.assertEqual(d1.norm_ident(u('!B')), u('!B'))
- self.assertRaises(ValueError, d1.norm_ident, u('!C'))
+ # check aliases
+ self.assertEqual(norm_ident(ident=u('A')), u('!A'))
- #check aliases
- self.assertEqual(d1.norm_ident(u('A')), u('!A'))
- self.assertRaises(ValueError, d1.norm_ident, u('B'))
+ # check invalid idents
+ self.assertRaises(ValueError, norm_ident, ident=u('B'))
- #check identify
+ # check identify is honoring ident system
self.assertTrue(d1.identify(u("!Axxx")))
self.assertTrue(d1.identify(u("!Bxxx")))
self.assertFalse(d1.identify(u("!Cxxx")))
@@ -283,6 +359,10 @@ class SkeletonTest(TestCase):
self.assertFalse(d1.identify(u("")))
self.assertFalse(d1.identify(None))
+ # check default_ident missing is detected.
+ d1.default_ident = None
+ self.assertRaises(AssertionError, norm_ident, use_defaults=True)
+
#=========================================================
#eoc
#=========================================================
@@ -344,7 +424,7 @@ class PrefixWrapperTest(TestCase):
d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}")
self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds)
- if PY_25_MAX: # lacks __dir__() support
+ if PY_MAX_25: # __dir__() support not added until py 2.6
self.assertFalse('max_rounds' in dir(d2))
else:
self.assertTrue('max_rounds' in dir(d2))
@@ -444,7 +524,7 @@ class SaltedHash(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler):
raise ValueError("not a salted-example hash")
if isinstance(hash, bytes):
hash = hash.decode("ascii")
- return cls(salt=hash[5:-40], checksum=hash[-40:], strict=True)
+ return cls(salt=hash[5:-40], checksum=hash[-40:])
_stub_checksum = u('0') * 40
@@ -481,9 +561,6 @@ class UnsaltedHashTest(HandlerCase):
# behavior, but that's a lot of effort for a non-critical
# border case. so just skipping this test instead...
self.assertRaises(TypeError, UnsaltedHash, salt='x')
- self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt=None, strict=True)
- self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt='xxx', strict=True)
-
self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1)
class SaltedHashTest(HandlerCase):
@@ -495,6 +572,12 @@ class SaltedHashTest(HandlerCase):
'@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'),
]
+ def test_bad_kwds(self):
+ self.assertRaises(TypeError, SaltedHash,
+ checksum=SaltedHash._stub_checksum, salt=None)
+ self.assertRaises(ValueError, SaltedHash,
+ checksum=SaltedHash._stub_checksum, salt='xxx')
+
#=========================================================
#EOF
#=========================================================
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index 628c4ee..a89e707 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -10,6 +10,7 @@ import re
import os
import sys
import tempfile
+from passlib.exc import PasslibHandlerWarning
from passlib.utils.compat import PY2, PY27, PY_MIN_32, PY3
try:
@@ -870,8 +871,11 @@ class HandlerCase(TestCase):
#make sure salt is truncated exactly where it should be.
salt = cc * mx
c1 = self.do_genconfig(salt=salt)
- c2 = self.do_genconfig(salt=salt + cc)
- self.assertEqual(c1,c2)
+ self.assertRaises(ValueError, self.do_genconfig, salt=salt + cc)
+ if _has_relaxed_setting(handler):
+ with catch_warnings(record=True): # issues passlibhandlerwarning
+ c2 = self.do_genconfig(salt=salt + cc, relaxed=True)
+ self.assertEqual(c1,c2)
#if min_salt supports it, check smaller than mx is NOT truncated
if handler.min_salt_size < mx:
@@ -1112,6 +1116,12 @@ def _has_possible_crypt_support(handler):
'os_crypt' in handler.backends and \
not hasattr(handler, "orig_prefix") # ignore wrapper classes
+def _has_relaxed_setting(handler):
+ # FIXME: I've been lazy, should probably just add 'relaxed' kwd
+ # to all handlers that derive from GenericHandler
+ return 'relaxed' in handler.setting_kwds or issubclass(handler,
+ uh.GenericHandler)
+
def create_backend_case(base, name, module="passlib.tests.test_handlers"):
"create a test case for specific backend of a multi-backend handler"
#get handler, figure out if backend should be tested