"""tests for passlib.util""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core from functools import partial import warnings # site # pkg # module from passlib.utils import is_ascii_safe from passlib.utils.compat import irange, PY2, PY3, u, unicode, join_bytes, PYPY from passlib.tests.utils import TestCase, hb, run_with_fixed_seeds #============================================================================= # byte funcs #============================================================================= class MiscTest(TestCase): """tests various parts of utils module""" # NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test def test_compat(self): """test compat's lazymodule""" from passlib.utils import compat # "" self.assertRegex(repr(compat), r"^$") # test synthentic dir() dir(compat) self.assertTrue('UnicodeIO' in dir(compat)) self.assertTrue('irange' in dir(compat)) def test_classproperty(self): from passlib.utils.decor 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.decor 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 self.assertWarningList(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." )): self.assertEqual(test_func(1,2), (1,2)) def test_memoized_property(self): from passlib.utils.decor 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 if not PY3: self.assertIs(prop.im_func, prop.__func__) def test_getrandbytes(self): """getrandbytes()""" from passlib.utils import getrandbytes wrapper = partial(getrandbytes, self.getRandom()) self.assertEqual(len(wrapper(0)), 0) a = wrapper(10) b = wrapper(10) self.assertIsInstance(a, bytes) self.assertEqual(len(a), 10) self.assertEqual(len(b), 10) self.assertNotEqual(a, b) @run_with_fixed_seeds(count=1024) def test_getrandstr(self, seed): """getrandstr()""" from passlib.utils import getrandstr wrapper = partial(getrandstr, self.getRandom(seed=seed)) # count 0 self.assertEqual(wrapper('abc',0), '') # count <0 self.assertRaises(ValueError, wrapper, 'abc', -1) # letters 0 self.assertRaises(ValueError, wrapper, '', 0) # letters 1 self.assertEqual(wrapper('a', 5), 'aaaaa') # NOTE: the following parts are non-deterministic, # with a small chance of failure (outside chance it may pick # a string w/o one char, even more remote chance of picking # same string). to combat this, we run it against multiple # fixed seeds (using run_with_fixed_seeds decorator), # and hope that they're sufficient to test the range of behavior. # letters x = wrapper(u('abc'), 32) y = wrapper(u('abc'), 32) self.assertIsInstance(x, unicode) self.assertNotEqual(x,y) self.assertEqual(sorted(set(x)), [u('a'),u('b'),u('c')]) # bytes x = wrapper(b'abc', 32) y = wrapper(b'abc', 32) self.assertIsInstance(x, bytes) self.assertNotEqual(x,y) # NOTE: decoding this due to py3 bytes self.assertEqual(sorted(set(x.decode("ascii"))), [u('a'),u('b'),u('c')]) def test_generate_password(self): """generate_password()""" from passlib.utils import generate_password warnings.filterwarnings("ignore", "The function.*generate_password\(\) is deprecated") self.assertEqual(len(generate_password(15)), 15) def test_is_crypt_context(self): """test is_crypt_context()""" from passlib.utils import is_crypt_context from passlib.context import CryptContext cc = CryptContext(["des_crypt"]) self.assertTrue(is_crypt_context(cc)) self.assertFalse(not is_crypt_context(cc)) def test_genseed(self): """test genseed()""" import random from passlib.utils import genseed rng = random.Random(genseed()) a = rng.randint(0, 10**10) rng = random.Random(genseed()) b = rng.randint(0, 10**10) self.assertNotEqual(a,b) rng.seed(genseed(rng)) def test_crypt(self): """test crypt.crypt() wrappers""" from passlib.utils import has_crypt, safe_crypt, test_crypt # test everything is disabled if not has_crypt: self.assertEqual(safe_crypt("test", "aa"), None) self.assertFalse(test_crypt("test", "aaqPiZY5xR5l.")) raise self.skipTest("crypt.crypt() not available") # XXX: this assumes *every* crypt() implementation supports des_crypt. # if this fails for some platform, this test will need modifying. # test return type self.assertIsInstance(safe_crypt(u("test"), u("aa")), unicode) # test ascii password h1 = u('aaqPiZY5xR5l.') self.assertEqual(safe_crypt(u('test'), u('aa')), h1) self.assertEqual(safe_crypt(b'test', b'aa'), h1) # test utf-8 / unicode password h2 = u('aahWwbrUsKZk.') self.assertEqual(safe_crypt(u('test\u1234'), 'aa'), h2) self.assertEqual(safe_crypt(b'test\xe1\x88\xb4', 'aa'), h2) # test latin-1 password hash = safe_crypt(b'test\xff', 'aa') if PY3: # py3 supports utf-8 bytes only. self.assertEqual(hash, None) else: # but py2 is fine. self.assertEqual(hash, u('aaOx.5nbTU/.M')) # test rejects null chars in password self.assertRaises(ValueError, safe_crypt, '\x00', 'aa') # check test_crypt() h1x = h1[:-1] + 'x' self.assertTrue(test_crypt("test", h1)) self.assertFalse(test_crypt("test", h1x)) # check crypt returning variant error indicators # some platforms return None on errors, others empty string, # The BSDs in some cases return ":" import passlib.utils as mod orig = mod._crypt try: fake = None mod._crypt = lambda secret, hash: fake for fake in [None, "", ":", ":0", "*0"]: self.assertEqual(safe_crypt("test", "aa"), None) self.assertFalse(test_crypt("test", h1)) fake = 'xxx' self.assertEqual(safe_crypt("test", "aa"), "xxx") finally: mod._crypt = orig def test_consteq(self): """test consteq()""" # NOTE: this test is kind of over the top, but that's only because # this is used for the critical task of comparing hashes for equality. from passlib.utils import consteq, str_consteq # ensure error raises for wrong types self.assertRaises(TypeError, consteq, u(''), b'') self.assertRaises(TypeError, consteq, u(''), 1) self.assertRaises(TypeError, consteq, u(''), None) self.assertRaises(TypeError, consteq, b'', u('')) self.assertRaises(TypeError, consteq, b'', 1) self.assertRaises(TypeError, consteq, b'', None) self.assertRaises(TypeError, consteq, None, u('')) self.assertRaises(TypeError, consteq, None, b'') self.assertRaises(TypeError, consteq, 1, u('')) self.assertRaises(TypeError, consteq, 1, b'') def consteq_supports_string(value): # under PY2, it supports all unicode strings (when present at all), # under PY3, compare_digest() only supports ascii unicode strings. # confirmed for: cpython 2.7.9, cpython 3.4, pypy, pypy3, pyston return (consteq is str_consteq or PY2 or is_ascii_safe(value)) # check equal inputs compare correctly for value in [ u("a"), u("abc"), u("\xff\xa2\x12\x00")*10, ]: if consteq_supports_string(value): self.assertTrue(consteq(value, value), "value %r:" % (value,)) else: self.assertRaises(TypeError, consteq, value, value) self.assertTrue(str_consteq(value, value), "value %r:" % (value,)) value = value.encode("latin-1") self.assertTrue(consteq(value, value), "value %r:" % (value,)) # check non-equal inputs compare correctly for l,r in [ # check same-size comparisons with differing contents fail. (u("a"), u("c")), (u("abcabc"), u("zbaabc")), (u("abcabc"), u("abzabc")), (u("abcabc"), u("abcabz")), ((u("\xff\xa2\x12\x00")*10)[:-1] + u("\x01"), u("\xff\xa2\x12\x00")*10), # check different-size comparisons fail. (u(""), u("a")), (u("abc"), u("abcdef")), (u("abc"), u("defabc")), (u("qwertyuiopasdfghjklzxcvbnm"), u("abc")), ]: if consteq_supports_string(l) and consteq_supports_string(r): self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) else: self.assertRaises(TypeError, consteq, l, r) self.assertRaises(TypeError, consteq, r, l) self.assertFalse(str_consteq(l, r), "values %r %r:" % (l,r)) self.assertFalse(str_consteq(r, l), "values %r %r:" % (r,l)) l = l.encode("latin-1") r = r.encode("latin-1") self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) # TODO: add some tests to ensure we take THETA(strlen) time. # this might be hard to do reproducably. # NOTE: below code was used to generate stats for analysis ##from math import log as logb ##import timeit ##multipliers = [ 1< encode() -> decode() -> raw # # generate some random bytes size = rng.randint(1 if saw_zero else 0, 12) if not size: saw_zero = True enc_size = (4*size+2)//3 raw = getrandbytes(rng, size) # encode them, check invariants encoded = engine.encode_bytes(raw) self.assertEqual(len(encoded), enc_size) # make sure decode returns original result = engine.decode_bytes(encoded) self.assertEqual(result, raw) # # test encoded -> decode() -> encode() -> encoded # # generate some random encoded data if size % 4 == 1: size += rng.choice([-1,1,2]) raw_size = 3*size//4 encoded = getrandstr(rng, engine.bytemap, size) # decode them, check invariants raw = engine.decode_bytes(encoded) self.assertEqual(len(raw), raw_size, "encoded %d:" % size) # make sure encode returns original (barring padding bits) result = engine.encode_bytes(raw) if size % 4: self.assertEqual(result[:-1], encoded[:-1]) else: self.assertEqual(result, encoded) def test_repair_unused(self): """test repair_unused()""" # NOTE: this test relies on encode_bytes() always returning clear # padding bits - which should be ensured by test vectors. from passlib.utils import getrandstr rng = self.getRandom() engine = self.engine check_repair_unused = self.engine.check_repair_unused i = 0 while i < 300: size = rng.randint(0,23) cdata = getrandstr(rng, engine.charmap, size).encode("ascii") if size & 3 == 1: # should throw error self.assertRaises(ValueError, check_repair_unused, cdata) continue rdata = engine.encode_bytes(engine.decode_bytes(cdata)) if rng.random() < .5: cdata = cdata.decode("ascii") rdata = rdata.decode("ascii") if cdata == rdata: # should leave unchanged ok, result = check_repair_unused(cdata) self.assertFalse(ok) self.assertEqual(result, rdata) else: # should repair bits self.assertNotEqual(size % 4, 0) ok, result = check_repair_unused(cdata) self.assertTrue(ok) self.assertEqual(result, rdata) i += 1 #=================================================================== # test transposed encode/decode - encoding independant #=================================================================== # NOTE: these tests assume normal encode/decode has been tested elsewhere. transposed = [ # orig, result, transpose map (b"\x33\x22\x11", b"\x11\x22\x33",[2,1,0]), (b"\x22\x33\x11", b"\x11\x22\x33",[1,2,0]), ] transposed_dups = [ # orig, result, transpose projection (b"\x11\x11\x22", b"\x11\x22\x33",[0,0,1]), ] def test_encode_transposed_bytes(self): """test encode_transposed_bytes()""" engine = self.engine for result, input, offsets in self.transposed + self.transposed_dups: tmp = engine.encode_transposed_bytes(input, offsets) 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 for input, result, offsets in self.transposed: tmp = engine.encode_bytes(input) out = engine.decode_transposed_bytes(tmp, offsets) self.assertEqual(out, result) def test_decode_transposed_bytes_bad(self): """test decode_transposed_bytes() fails if map is a one-way""" engine = self.engine for input, _, offsets in self.transposed_dups: tmp = engine.encode_bytes(input) self.assertRaises(TypeError, engine.decode_transposed_bytes, tmp, offsets) #=================================================================== # test 6bit handling #=================================================================== def check_int_pair(self, bits, encoded_pairs): """helper to check encode_intXX & decode_intXX functions""" rng = self.getRandom() engine = self.engine encode = getattr(engine, "encode_int%s" % bits) decode = getattr(engine, "decode_int%s" % bits) pad = -bits % 6 chars = (bits+pad)//6 upper = 1<