"""tests for passlib.util""" #========================================================= #imports #========================================================= from __future__ import with_statement #core from binascii import hexlify, unhexlify import sys import random import warnings #site #pkg #module from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \ unicode, join_bytes, SUPPORTS_DIR_METHOD from passlib.tests.utils import TestCase, catch_warnings def hb(source): return unhexlify(b(source)) #========================================================= #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) if SUPPORTS_DIR_METHOD: self.assertTrue('UnicodeIO' in dir(compat)) self.assertTrue('irange' in dir(compat)) 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 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 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 def f(*a,**k): return getrandbytes(rng, *a, **k) self.assertEqual(len(f(0)), 0) a = f(10) b = f(10) self.assertIsInstance(a, bytes) self.assertEqual(len(a), 10) self.assertEqual(len(b), 10) self.assertNotEqual(a, b) def test_getrandstr(self): "test getrandstr()" from passlib.utils import getrandstr, rng def f(*a,**k): return getrandstr(rng, *a, **k) #count 0 self.assertEqual(f('abc',0), '') #count <0 self.assertRaises(ValueError, f, 'abc', -1) #letters 0 self.assertRaises(ValueError, f, '', 0) #letters 1 self.assertEqual(f('a',5), 'aaaaa') #letters x = f(u('abc'), 16) y = f(u('abc'), 16) self.assertIsInstance(x, unicode) self.assertNotEqual(x,y) self.assertEqual(sorted(set(x)), [u('a'),u('b'),u('c')]) #bytes x = f(b('abc'), 16) y = f(b('abc'), 16) 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')]) #generate_password from passlib.utils import generate_password 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, 100000) rng = random.Random(genseed()) b = rng.randint(0, 100000) 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 # 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('')) # check equal inputs compare correctly for value in [ u("a"), u("abc"), u("\xff\xa2\x12\x00")*10, ]: self.assertTrue(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")), ]: self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) self.assertFalse(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 = random.randint(1 if saw_zero else 0, 12) if not size: saw_zero = True enc_size = (4*size+2)//3 raw = getrandbytes(random, 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 += random.choice([-1,1,2]) raw_size = 3*size//4 encoded = getrandstr(random, 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 rng, getrandstr 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" 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<