diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2012-05-01 12:40:12 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2012-05-01 12:40:12 -0400 |
commit | 3fa87b2933ba2f596297297f97b8fc3e335ba581 (patch) | |
tree | 52ad1cbce3718d49e775afc4322b4cb8475ab9ef | |
parent | 0f99c2a8d6106b980bb9b3c687669ae9fca5eb94 (diff) | |
download | passlib-3fa87b2933ba2f596297297f97b8fc3e335ba581.tar.gz |
bunch bugfixes to the unittests
* timer issues under windows
* id() issues under jython
* mtime issues under jython & darwin
* corrected expectations of test_ext_django's patch checker
* added darwin flags to platform_os_crypt info
* fixed regression in os_crypt backend test mocking
-rw-r--r-- | passlib/tests/test_apache.py | 4 | ||||
-rw-r--r-- | passlib/tests/test_context.py | 13 | ||||
-rw-r--r-- | passlib/tests/test_ext_django.py | 57 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 33 | ||||
-rw-r--r-- | passlib/tests/test_utils_crypto.py | 4 | ||||
-rw-r--r-- | passlib/tests/utils.py | 41 | ||||
-rw-r--r-- | passlib/utils/__init__.py | 25 | ||||
-rw-r--r-- | passlib/utils/compat.py | 23 |
8 files changed, 116 insertions, 84 deletions
diff --git a/passlib/tests/test_apache.py b/passlib/tests/test_apache.py index f88fb33..5023903 100644 --- a/passlib/tests/test_apache.py +++ b/passlib/tests/test_apache.py @@ -12,7 +12,7 @@ import time #pkg from passlib import apache from passlib.utils.compat import irange, unicode -from passlib.tests.utils import TestCase, get_file, set_file, catch_warnings +from passlib.tests.utils import TestCase, get_file, set_file, catch_warnings, HAS_INTEGER_MTIME from passlib.utils.compat import b, bytes, u #module log = getLogger(__name__) @@ -474,7 +474,7 @@ class HtdigestFileTest(TestCase): self.assertEqual(hc.to_string(), self.sample_01) #change file, test deprecated force=True kwd - time.sleep(.1) # pause so mtime changes + time.sleep(1 if HAS_INTEGER_MTIME else .1) # pause so mtime changes set_file(path, "") with self.assertWarningList(r"load\(force=False\) is deprecated"): ha.load(force=False) diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py index 85eccbc..90709d1 100644 --- a/passlib/tests/test_context.py +++ b/passlib/tests/test_context.py @@ -24,7 +24,7 @@ from passlib.exc import PasslibConfigWarning from passlib.utils import tick, to_bytes, to_unicode from passlib.utils.compat import irange, u, unicode, str_to_uascii import passlib.utils.handlers as uh -from passlib.tests.utils import TestCase, catch_warnings, set_file, get_timer_resolution +from passlib.tests.utils import TestCase, catch_warnings, set_file, TICK_RESOLUTION, quicksleep from passlib.registry import (register_crypt_handler_path, _has_crypt_handler as has_crypt_handler, _unload_handler_name as unload_handler_name, @@ -289,7 +289,7 @@ sha512_crypt__min_rounds = 45000 def test_09_repr(self): "test repr()" cc1 = CryptContext(**self.sample_1_dict) - self.assertRegex(repr(cc1), "^<CryptContext at 0x[0-9a-f]{6,}>$") + self.assertRegex(repr(cc1), "^<CryptContext at 0x[0-9a-f]+>$") #========================================================= # modifiers @@ -1353,8 +1353,8 @@ sha512_crypt__min_rounds = 45000 #========================================================= def test_60_min_verify_time(self): "test verify() honors min_verify_time" - delta = .01 - if get_timer_resolution(tick) >= delta: + delta = .05 + if TICK_RESOLUTION >= delta/10: raise self.skipTest("timer not accurate enough") min_delay = 2*delta min_verify_time = 5*delta @@ -1370,7 +1370,7 @@ sha512_crypt__min_rounds = 45000 return True def _calc_checksum(self, secret): - time.sleep(self.delay) + quicksleep(self.delay) return to_unicode(secret + 'x') # check mvt issues a warning, and then filter for remainder of test @@ -1382,8 +1382,7 @@ sha512_crypt__min_rounds = 45000 def timecall(func, *args, **kwds): start = tick() result = func(*args, **kwds) - end = tick() - return end-start, result + return tick()-start, result # verify genhash delay works TimedHash.delay = min_delay diff --git a/passlib/tests/test_ext_django.py b/passlib/tests/test_ext_django.py index c0ea4b6..c7cdecf 100644 --- a/passlib/tests/test_ext_django.py +++ b/passlib/tests/test_ext_django.py @@ -110,7 +110,7 @@ def create_mock_setter(): if has_django14: # have to modify this a little - # all but pbkdf2_sha256 will be deprecated here, - # whereas stock passlib policy is more permissive + # whereas preconfigured passlib policy is more permissive stock_config = django14_context.to_dict() stock_config['deprecated'] = ["django_pbkdf2_sha1", "django_bcrypt"] + stock_config['deprecated'] elif has_django1: @@ -128,27 +128,40 @@ class _ExtensionSupport(object): #========================================================= # support funcs #========================================================= - # attrs we're patching in various modules. - _patched_attrs = ["set_password", "check_password", - "make_password", "get_hasher", "identify_hasher"] - @classmethod def _iter_patch_candidates(cls): - "helper to scan for monkeypatches" + """helper to scan for monkeypatches. + + returns tuple containing: + * object (module or class) + * attribute of object + * value of attribute + * whether it should or should not be patched + """ + # XXX: this and assert_unpatched() could probably be refactored to use + # the PatchManager class to do the heavy lifting. from django.contrib.auth import models - objs = [models, models.User] + user_attrs = ["check_password", "set_password"] + model_attrs = ["check_password"] + objs = [(models, model_attrs), (models.User, user_attrs)] if has_django14: from django.contrib.auth import hashers - objs.append(hashers) - for obj in objs: + model_attrs.append("make_password") + objs.append((hashers, ["check_password", "make_password", + "get_hasher", "identify_hasher"])) + if has_django0: + user_attrs.extend(["has_usable_password", "set_unusable_password"]) + for obj, patched in objs: for attr in dir(obj): if attr.startswith("_"): continue - value = getattr(obj, attr) + value = obj.__dict__.get(attr, UNSET) # can't use getattr() due to GAE + if value is UNSET and attr not in patched: + continue value = get_method_function(value) source = getattr(value, "__module__", None) if source: - yield obj, attr, source + yield obj, attr, source, (attr in patched) #========================================================= # verify current patch state @@ -160,8 +173,8 @@ class _ExtensionSupport(object): self.assertFalse(mod and mod._patched, "patch should not be enabled") # make sure no objects have been replaced, by checking __module__ - for obj, attr, source in self._iter_patch_candidates(): - if attr in self._patched_attrs: + for obj, attr, source, patched in self._iter_patch_candidates(): + if patched: self.assertTrue(source.startswith("django.contrib.auth."), "obj=%r attr=%r was not reverted: %r" % (obj, attr, source)) @@ -177,8 +190,8 @@ class _ExtensionSupport(object): self.assertTrue(mod and mod._patched, "patch should have been enabled") # make sure only the expected objects have been patched - for obj, attr, source in self._iter_patch_candidates(): - if attr in self._patched_attrs: + for obj, attr, source, patched in self._iter_patch_candidates(): + if patched: self.assertTrue(source == "passlib.ext.django.models", "obj=%r attr=%r should have been patched: %r" % (obj, attr, source)) @@ -261,7 +274,8 @@ class DjangoBehaviorTest(_ExtensionTest): def assert_unusable_password(self, user): self.assertEqual(user.password, "!") - self.assertFalse(user.has_usable_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): @@ -270,7 +284,8 @@ class DjangoBehaviorTest(_ExtensionTest): self.assertNotEqual(user.password, None) else: self.assertEqual(user.password, hash) - self.assertTrue(user.has_usable_password()) + if has_django1 or self.patched: + self.assertTrue(user.has_usable_password()) self.assertEqual(user.pop_saved_passwords(), [] if saved is None else [saved]) @@ -431,7 +446,8 @@ class DjangoBehaviorTest(_ExtensionTest): self.assertFalse(user.check_password(PASS1)) else: self.assertRaises(TypeError, user.check_password, PASS1) - self.assertFalse(user.has_usable_password()) + if has_django1 or patched: + self.assertFalse(user.has_usable_password()) # make_password() - n/a @@ -507,7 +523,10 @@ class DjangoBehaviorTest(_ExtensionTest): user.password = hash # check against invalid password - self.assertFalse(user.check_password(None)) + if has_django1 or patched: + self.assertFalse(user.check_password(None)) + else: + self.assertRaises(TypeError, user.check_password, None) ##self.assertFalse(user.check_password('')) self.assertFalse(user.check_password(other)) self.assert_valid_password(user, hash) diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index d6d6ede..dac8849 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -183,6 +183,7 @@ class _bcrypt_test(HandlerCase): freebsd=True, openbsd=True, netbsd=True, + darwin=False, # linux - some systems # solaris - depends on policy ) @@ -461,7 +462,7 @@ class _bsdi_crypt_test(HandlerCase): netbsd=True, linux=False, solaris=False, - # darwin ? + darwin=True, ) def setUp(self): @@ -690,7 +691,7 @@ class _des_crypt_test(HandlerCase): netbsd=True, linux=True, solaris=True, - # darwin? + darwin=True, ) des_crypt_os_crypt_test, des_crypt_builtin_test = \ @@ -1216,10 +1217,9 @@ class ldap_plaintext_test(HandlerCase): pwd = super(ldap_plaintext_test, self).get_fuzz_password() return pwd -#NOTE: since the ldap_{crypt} handlers are all wrappers, -# don't need separate test. have just one for end-to-end testing purposes. - class _ldap_md5_crypt_test(HandlerCase): + #NOTE: since the ldap_{crypt} handlers are all wrappers, + # don't need separate test. this is just to test the codebase end-to-end handler = hash.ldap_md5_crypt known_correct_hashes = [ @@ -1245,6 +1245,25 @@ class _ldap_md5_crypt_test(HandlerCase): ldap_md5_crypt_os_crypt_test, ldap_md5_crypt_builtin_test = \ _ldap_md5_crypt_test.create_backend_cases(["os_crypt","builtin"]) +class _ldap_sha1_crypt_test(HandlerCase): + # NOTE: this isn't for testing the hash (see ldap_md5_crypt note) + # but as a self-test of the os_crypt patching code in HandlerCase. + handler = hash.ldap_sha1_crypt + + known_correct_hashes = [ + ('password', '{CRYPT}$sha1$10$c.mcTzCw$gF8UeYst9yXX7WNZKc5Fjkq0.au7'), + (UPASS_TABLE, '{CRYPT}$sha1$10$rnqXlOsF$aGJf.cdRPewJAXo1Rn1BkbaYh0fP'), + ] + + def populate_settings(self, kwds): + kwds.setdefault("rounds", 10) + super(_ldap_sha1_crypt_test, self).populate_settings(kwds) + + def test_77_fuzz_input(self): + raise self.skipTest("unneeded") + +ldap_sha1_crypt_os_crypt_test, = _ldap_sha1_crypt_test.create_backend_cases(["os_crypt"]) + #========================================================= #ldap_pbkdf2_{digest} #========================================================= @@ -1381,7 +1400,7 @@ class _md5_crypt_test(HandlerCase): netbsd=True, linux=True, solaris=True, - # darwin? + darwin=False, ) md5_crypt_os_crypt_test, md5_crypt_builtin_test = \ @@ -2486,7 +2505,7 @@ class _sha256_crypt_test(HandlerCase): netbsd=False, linux=True, # solaris - depends on policy - # darwin ?, + darwin=False, ) sha256_crypt_os_crypt_test, sha256_crypt_builtin_test = \ diff --git a/passlib/tests/test_utils_crypto.py b/passlib/tests/test_utils_crypto.py index 76d22b8..bbf4146 100644 --- a/passlib/tests/test_utils_crypto.py +++ b/passlib/tests/test_utils_crypto.py @@ -18,7 +18,7 @@ except ImportError: #pkg #module from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \ - unicode, join_bytes, PYPY + unicode, join_bytes, PYPY, JYTHON from passlib.tests.utils import TestCase, TEST_MODE, catch_warnings, skipUnless, skipIf #========================================================= @@ -363,7 +363,7 @@ class Pbkdf1_Test(TestCase): (b('password'), b('salt'), 1000, None, 'md5', hb('8475c6a8531a5d27e386cd496457812c')), (b('password'), b('salt'), 1000, None, 'sha1', hb('4a8fd48e426ed081b535be5769892fa396293efb')), ] - if not PYPY: + if not (PYPY or JYTHON): pbkdf1_tests.append( (b('password'), b('salt'), 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453')) ) diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index be3eb61..4c30b20 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -4,14 +4,13 @@ #========================================================= from __future__ import with_statement #core -import atexit import logging; log = logging.getLogger(__name__) import re import os import sys import tempfile from passlib.exc import PasslibHashWarning -from passlib.utils.compat import PY27, PY_MIN_32, PY3 +from passlib.utils.compat import PY27, PY_MIN_32, PY3, JYTHON import warnings from warnings import warn #site @@ -21,7 +20,7 @@ import passlib.registry as registry from passlib.tests.backports import TestCase as _TestCase, catch_warnings, skip, skipIf, skipUnless from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \ classproperty, rng, getrandstr, is_ascii_safe, to_native_str, \ - repeat_string + repeat_string, tick from passlib.utils.compat import b, bytes, iteritems, irange, callable, \ base_string_types, exc_err, u, unicode, PY2 import passlib.utils.handlers as uh @@ -49,6 +48,17 @@ except ImportError: else: GAE = True +HAS_INTEGER_MTIME = JYTHON or sys.platform.startswith("darwin") + +def _get_timer_resolution(timer): + def sample(): + start = cur = timer() + while start == cur: + cur = timer() + return cur-start + return min(sample() for _ in range(3)) +TICK_RESOLUTION = _get_timer_resolution(tick) + #========================================================= # test mode #========================================================= @@ -176,13 +186,11 @@ def randintgauss(lower, upper, mu, sigma): "hack used by fuzz testing" return int(limit(rng.normalvariate(mu, sigma), lower, upper)) -def get_timer_resolution(timer): - def sample(): - start = cur = timer() - while start == cur: - cur = timer() - return cur-start - return min(sample() for _ in range(3)) +def quicksleep(delay): + "because time.sleep() doesn't even have 10ms accuracy on some OSes" + start = tick() + while tick()-start < delay: + pass #========================================================= # custom test harness @@ -702,13 +710,13 @@ class HandlerCase(TestCase): if handler.name == "bcrypt" and backend == "builtin" and TEST_MODE("full"): # this will be auto-enabled under TEST_MODE 'full'. return None - if backend == "os_crypt": - if cls.find_crypt_replacement() and TEST_MODE("full"): + from passlib.utils import has_crypt + if backend == "os_crypt" and has_crypt: + if TEST_MODE("full") and cls.find_crypt_replacement(): #in this case, HandlerCase will monkeypatch os_crypt #to use another backend, just so we can test os_crypt fully. return None - from passlib.utils import has_crypt - if has_crypt: + else: return "hash not supported by os crypt()" return "backend not available" @@ -1905,6 +1913,9 @@ class OsCryptMixin(HandlerCase): as possible. """ handler = self.handler + # resolve wrappers, since we want to return crypt compatible hash. + while hasattr(handler, "wrapped"): + handler = handler.wrapped alt_backend = self.find_crypt_replacement() if not alt_backend: raise AssertionError("handler has no available backends!") @@ -1971,6 +1982,8 @@ class OsCryptMixin(HandlerCase): def test_82_crypt_support(self): "test platform-specific crypt() support detection" + if hasattr(self.handler, "orig_prefix"): + raise self.skipTest("not applicable to wrappers") platform = sys.platform for name, flag in self.platform_crypt_support.items(): if not platform.startswith(name): diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py index 1e086d3..a40b744 100644 --- a/passlib/utils/__init__.py +++ b/passlib/utils/__init__.py @@ -1396,31 +1396,6 @@ else: # On most other platforms the best timer is time.time() from time import time as tick -# works but not used -##def _get_timer_resolution(timer=timer, repeat=3): -## best = None -## i = 0 -## while i < repeat: -## start = end = timer() -## while start == end: -## end = timer() -## delta = end-start -## if delta < 0: -## # probably NTP adjust or some such. -## log.error("timer jumped backwards! (%r => %r)", start, end) -## continue -## if delta > 1: -## # should have at least this resolution, -## # so probably NTP adjust or some such. -## log.error("timer jumped too far! (%r => %r)", start, end) -## continue -## if best is None or delta < best: -## best = delta -## i += 1 -## return best -## -##timer_resolution = _get_timer_resolution() - #================================================================================= # randomness #================================================================================= diff --git a/passlib/utils/compat.py b/passlib/utils/compat.py index 02b808b..428e765 100644 --- a/passlib/utils/compat.py +++ b/passlib/utils/compat.py @@ -1,7 +1,11 @@ """passlib.utils.compat - python 2/3 compatibility helpers""" #============================================================================= -# figure out what version we're running +# figure out what we're running #============================================================================= + +#----------------------------------------------------- +# python version +#----------------------------------------------------- import sys PY2 = sys.version_info < (3,0) PY3 = sys.version_info >= (3,0) @@ -9,16 +13,19 @@ PY_MAX_25 = sys.version_info < (2,6) # py 2.5 or earlier PY27 = sys.version_info[:2] == (2,7) # supports last 2.x release PY_MIN_32 = sys.version_info >= (3,2) # py 3.2 or later -# __dir__() added in py2.6 -# NOTE: testing shows pypy1.5 doesn't either; but added somewhere <= 1.8 -SUPPORTS_DIR_METHOD = not PY_MAX_25 - -#============================================================================= -# figure out what VM we're running -#============================================================================= +#----------------------------------------------------- +# python implementation +#----------------------------------------------------- PYPY = hasattr(sys, "pypy_version_info") JYTHON = sys.platform.startswith('java') +#----------------------------------------------------- +# capabilities +#----------------------------------------------------- + +# __dir__() added in py2.6 +SUPPORTS_DIR_METHOD = not PY_MAX_25 and not (PYPY and sys.pypy_version_info < (1,6)) + #============================================================================= # common imports #============================================================================= |