summaryrefslogtreecommitdiff
path: root/passlib/tests/test_ext_django_source.py
blob: a6da6b6345641f564290f8db7a1ab4dc8fd440e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
"""
test passlib.ext.django against django source tests
"""
#=============================================================================
# imports
#=============================================================================
# core
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib.ext.django.utils import DJANGO_VERSION, DjangoTranslator, _PasslibHasherWrapper
# tests
from passlib.tests.utils import TestCase, TEST_MODE
from .test_ext_django import (
    has_min_django, stock_config, _ExtensionSupport,
)
if has_min_django:
    from .test_ext_django import settings
# local
__all__ = [
    "HashersTest",
]
#=============================================================================
# HashersTest --
# hack up the some of the real django tests to run w/ extension loaded,
# to ensure we mimic their behavior.
# however, the django tests were moved out of the package, and into a source-only location
# as of django 1.7. so we disable tests from that point on unless test-runner specifies
#=============================================================================

#: ref to django unittest root module (if found)
test_hashers_mod = None

#: message about why test module isn't present (if not found)
hashers_skip_msg = None

#----------------------------------------------------------------------
# try to load django's tests/auth_tests/test_hasher.py module,
# or note why we failed.
#----------------------------------------------------------------------
if TEST_MODE(max="quick"):
    hashers_skip_msg = "requires >= 'default' test mode"

elif has_min_django:
    import os
    import sys
    source_path = os.environ.get("PASSLIB_TESTS_DJANGO_SOURCE_PATH")

    if source_path:
        if not os.path.exists(source_path):
            raise EnvironmentError("django source path not found: %r" % source_path)
        if not all(os.path.exists(os.path.join(source_path, name))
                   for name in ["django", "tests"]):
            raise EnvironmentError("invalid django source path: %r" % source_path)
        log.info("using django tests from source path: %r", source_path)
        tests_path = os.path.join(source_path, "tests")
        sys.path.insert(0, tests_path)
        try:
            from auth_tests import test_hashers as test_hashers_mod
        except ImportError as err:
            raise  EnvironmentError("error trying to import django tests "
                                    "from source path (%r): %r" % (source_path, err)) from None
        finally:
            sys.path.remove(tests_path)

    else:
        hashers_skip_msg = "requires PASSLIB_TESTS_DJANGO_SOURCE_PATH to be set"

        if TEST_MODE("full"):
            # print warning so user knows what's happening
            sys.stderr.write("\nWARNING: $PASSLIB_TESTS_DJANGO_SOURCE_PATH is not set; "
                             "can't run Django's own unittests against passlib.ext.django\n")

elif DJANGO_VERSION:
    hashers_skip_msg = "django version too old"

else:
    hashers_skip_msg = "django not installed"

#----------------------------------------------------------------------
# if found module, create wrapper to run django's own tests,
# but with passlib monkeypatched in.
#----------------------------------------------------------------------
if test_hashers_mod:
    from django.core.signals import setting_changed
    from django.dispatch import receiver
    from django.utils.module_loading import import_string

    class HashersTest(test_hashers_mod.TestUtilsHashPass, _ExtensionSupport):
        """
        Run django's hasher unittests against passlib's extension
        and workalike implementations
        """

        #==================================================================
        # helpers
        #==================================================================

        # port patchAttr() helper method from passlib.tests.utils.TestCase
        patchAttr = TestCase.patchAttr

        #==================================================================
        # custom setup
        #==================================================================
        def setUp(self):
            #---------------------------------------------------------
            # install passlib.ext.django adapter, and get context
            #---------------------------------------------------------
            self.load_extension(PASSLIB_CONTEXT=stock_config, check=False)
            from passlib.ext.django.models import adapter
            context = adapter.context

            #---------------------------------------------------------
            # patch tests module to use our versions of patched funcs
            # (which should be installed in hashers module)
            #---------------------------------------------------------
            from django.contrib.auth import hashers
            for attr in ["make_password",
                         "check_password",
                         "identify_hasher",
                         "is_password_usable",
                         "get_hasher"]:
                self.patchAttr(test_hashers_mod, attr, getattr(hashers, attr))

            #---------------------------------------------------------
            # django tests expect empty django_des_crypt salt field
            #---------------------------------------------------------
            from passlib.hash import django_des_crypt
            self.patchAttr(django_des_crypt, "use_duplicate_salt", False)

            #---------------------------------------------------------
            # install receiver to update scheme list if test changes settings
            #---------------------------------------------------------
            django_to_passlib_name = DjangoTranslator().django_to_passlib_name

            @receiver(setting_changed, weak=False)
            def update_schemes(**kwds):
                if kwds and kwds['setting'] != 'PASSWORD_HASHERS':
                    return
                assert context is adapter.context
                schemes = [
                    django_to_passlib_name(import_string(hash_path)())
                    for hash_path in settings.PASSWORD_HASHERS
                ]
                # workaround for a few tests that only specify hex_md5,
                # but test for django_salted_md5 format.
                if "hex_md5" in schemes and "django_salted_md5" not in schemes:
                    schemes.append("django_salted_md5")
                schemes.append("django_disabled")
                context.update(schemes=schemes, deprecated="auto")
                adapter.reset_hashers()

            self.addCleanup(setting_changed.disconnect, update_schemes)

            update_schemes()

            #---------------------------------------------------------
            # need password_context to keep up to date with django_hasher.iterations,
            # which is frequently patched by django tests.
            #
            # HACK: to fix this, inserting wrapper around a bunch of context
            #       methods so that any time adapter calls them,
            #       attrs are resynced first.
            #---------------------------------------------------------

            def update_rounds():
                """
                sync django hasher config -> passlib hashers
                """
                for handler in context.schemes(resolve=True):
                    if 'rounds' not in handler.setting_kwds:
                        continue
                    hasher = adapter.passlib_to_django(handler)
                    if isinstance(hasher, _PasslibHasherWrapper):
                        continue
                    rounds = getattr(hasher, "rounds", None) or \
                             getattr(hasher, "iterations", None)
                    if rounds is None:
                        continue
                    # XXX: this doesn't modify the context, which would
                    #      cause other weirdness (since it would replace handler factories completely,
                    #      instead of just updating their state)
                    handler.min_desired_rounds = handler.max_desired_rounds = handler.default_rounds = rounds

            _in_update = [False]

            def update_wrapper(wrapped, *args, **kwds):
                """
                wrapper around arbitrary func, that first triggers sync
                """
                if not _in_update[0]:
                    _in_update[0] = True
                    try:
                        update_rounds()
                    finally:
                        _in_update[0] = False
                return wrapped(*args, **kwds)

            # sync before any context call
            for attr in ["schemes", "handler", "default_scheme", "hash",
                         "verify", "needs_update", "verify_and_update"]:
                self.patchAttr(context, attr, update_wrapper, wrap=True)

            # sync whenever adapter tries to resolve passlib hasher
            self.patchAttr(adapter, "django_to_passlib", update_wrapper, wrap=True)

        def tearDown(self):
            # NOTE: could rely on addCleanup() instead, but need py26 compat
            self.unload_extension()
            super().tearDown()

        #==================================================================
        # skip a few methods that can't be replicated properly
        # *want to minimize these as much as possible*
        #==================================================================

        _OMIT = lambda self: self.skipTest("omitted by passlib")

        # XXX: this test registers two classes w/ same algorithm id,
        #      something we don't support -- how does django sanely handle
        #      that anyways? get_hashers_by_algorithm() should throw KeyError, right?
        test_pbkdf2_upgrade_new_hasher = _OMIT

        # TODO: support wrapping django's harden-runtime feature?
        #       would help pass their tests.
        test_check_password_calls_harden_runtime = _OMIT
        test_bcrypt_harden_runtime = _OMIT
        test_pbkdf2_harden_runtime = _OMIT

        #==================================================================
        # eoc
        #==================================================================

else:
    # otherwise leave a stub so test log tells why test was skipped.

    class HashersTest(TestCase):

        def test_external_django_hasher_tests(self):
            """external django hasher tests"""
            raise self.skipTest(hashers_skip_msg)

#=============================================================================
# eof
#=============================================================================