This repository has been archived by the owner on Jan 29, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 80
/
test_base.py
432 lines (360 loc) · 15.7 KB
/
test_base.py
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from datetime import datetime
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.utils import six
import requests
from mock import Mock, patch
from django_browserid import base
from django_browserid.compat import pybrowserid_found
from django_browserid.tests import TestCase
class SanityCheckTests(TestCase):
def setUp(self):
self.factory = RequestFactory()
@override_settings(DEBUG=True)
def test_debug_true(self):
"""
If DEBUG is True and BROWSERID_DISABLE_SANITY_CHECKS is not set,
run the checks.
"""
request = self.factory.get('/')
self.assertTrue(base.sanity_checks(request))
@override_settings(DEBUG=False)
def test_debug_false(self):
"""
If DEBUG is True and BROWSERID_DISABLE_SANITY_CHECKS is not set,
run the checks.
"""
request = self.factory.get('/')
self.assertTrue(not base.sanity_checks(request))
@override_settings(BROWSERID_DISABLE_SANITY_CHECKS=True)
def test_disable_sanity_checks(self):
"""
If BROWSERID_DISABLE_SANITY_CHECKS is True, do not run any
checks.
"""
request = self.factory.get('/')
self.assertTrue(not base.sanity_checks(request))
@override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False, SESSION_COOKIE_SECURE=True)
def test_sanity_session_cookie(self):
"""
If SESSION_COOKIE_SECURE == True and the current request isn't
https, log a debug message warning about it.
"""
request = self.factory.get('/')
request.is_secure = Mock(return_value=False)
with patch('django_browserid.base.logger.warning') as warning:
base.sanity_checks(request)
self.assertTrue(warning.called)
@override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False,
MIDDLEWARE_CLASSES=['csp.middleware.CSPMiddleware'])
@patch('django_browserid.base.logger.warning')
def test_sanity_csp(self, warning):
"""
If the django-csp middleware is present and Persona isn't
allowed by CSP, log a debug message warning about it.
"""
request = self.factory.get('/')
# Test if allowed properly.
with self.settings(CSP_DEFAULT_SRC=[],
CSP_SCRIPT_SRC=['https://login.persona.org'],
CSP_FRAME_SRC=['https://login.persona.org']):
base.sanity_checks(request)
self.assertTrue(not warning.called)
warning.reset_mock()
# Test fallback to default-src.
with self.settings(CSP_DEFAULT_SRC=['https://login.persona.org'],
CSP_SCRIPT_SRC=[],
CSP_FRAME_SRC=[]):
base.sanity_checks(request)
self.assertTrue(not warning.called)
warning.reset_mock()
# Test incorrect csp.
with self.settings(CSP_DEFAULT_SRC=[],
CSP_SCRIPT_SRC=[],
CSP_FRAME_SRC=[]):
base.sanity_checks(request)
self.assertTrue(warning.called)
warning.reset_mock()
# Test partial incorrectness.
with self.settings(CSP_DEFAULT_SRC=[],
CSP_SCRIPT_SRC=['https://login.persona.org'],
CSP_FRAME_SRC=[]):
base.sanity_checks(request)
self.assertTrue(warning.called)
@override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False,
MIDDLEWARE_CLASSES=['csp.middleware.CSPMiddleware'])
@patch('django_browserid.base.logger.warning')
def test_unset_csp(self, warning):
"""Check for errors when CSP settings aren't specified."""
request = self.factory.get('/')
correct = ['https://login.persona.org']
setting_kwargs = {
'CSP_DEFAULT_SRC': correct,
'CSP_SCRIPT_SRC': correct,
'CSP_FRAME_SRC': correct
}
# There's no easy way to use a variable for deleting the
# attribute on the settings object, so we can't easily turn this
# into a function, sadly.
with self.settings(**setting_kwargs):
del settings.CSP_DEFAULT_SRC
base.sanity_checks(request)
self.assertTrue(not warning.called)
warning.reset_mock()
with self.settings(**setting_kwargs):
del settings.CSP_FRAME_SRC
base.sanity_checks(request)
self.assertTrue(not warning.called)
warning.reset_mock()
with self.settings(**setting_kwargs):
del settings.CSP_SCRIPT_SRC
base.sanity_checks(request)
self.assertTrue(not warning.called)
warning.reset_mock()
class GetAudienceTests(TestCase):
def setUp(self):
self.factory = RequestFactory()
def test_setting_missing(self):
"""
If BROWSERID_AUDIENCES isn't defined, raise
ImproperlyConfigured.
"""
request = self.factory.get('/')
with patch('django_browserid.base.settings') as settings:
del settings.BROWSERID_AUDIENCES
settings.DEBUG = False
with self.assertRaises(ImproperlyConfigured):
base.get_audience(request)
def test_same_origin_found(self):
"""
If an audience is found in BROWSERID_AUDIENCES with the same
origin as the request URI, return it.
"""
request = self.factory.get('http://testserver')
audiences = ['https://example.com', 'http://testserver']
with self.settings(BROWSERID_AUDIENCES=audiences, DEBUG=False):
self.assertEqual(base.get_audience(request), 'http://testserver')
def test_no_audience(self):
"""
If no matching audiences is found in BROWSERID_AUDIENCES, raise
ImproperlyConfigured.
"""
request = self.factory.get('http://testserver')
with self.settings(BROWSERID_AUDIENCES=['https://example.com']):
with self.assertRaises(ImproperlyConfigured):
base.get_audience(request)
def test_missing_setting_but_in_debug(self):
"""
If no BROWSERID_AUDIENCES is set but in DEBUG just use the
current protocal and host.
"""
request = self.factory.get('/')
# Simulate that no BROWSERID_AUDIENCES has been set
with patch('django_browserid.base.settings') as settings:
del settings.BROWSERID_AUDIENCES
settings.DEBUG = True
self.assertEqual(base.get_audience(request), 'http://testserver')
def test_no_audience_but_in_debug(self):
"""
If no BROWSERID_AUDIENCES is set but in DEBUG just use the
current protocal and host.
"""
request = self.factory.get('/')
# Simulate that no BROWSERID_AUDIENCES has been set
with self.settings(BROWSERID_AUDIENCES=[], DEBUG=True):
self.assertEqual(base.get_audience(request), 'http://testserver')
class VerificationResultTests(TestCase):
def test_getattr_attribute_exists(self):
"""
If a value exists in the response dict, it should be an
attribute on the result.
"""
result = base.VerificationResult({'myattr': 'foo'})
self.assertEqual(result.myattr, 'foo')
def test_getattr_attribute_doesnt_exist(self):
"""
If a value doesn't exist in the response dict, accessing it as
an attribute should raise an AttributeError.
"""
result = base.VerificationResult({'myattr': 'foo'})
with self.assertRaises(AttributeError):
result.bar
def test_expires_no_attribute(self):
"""
If no expires attribute was in the response, raise an
AttributeError.
"""
result = base.VerificationResult({'myattr': 'foo'})
with self.assertRaises(AttributeError):
result.expires
def test_expires_invalid_timestamp(self):
"""
If the expires attribute cannot be parsed as a timestamp, return
the raw string instead.
"""
result = base.VerificationResult({'expires': 'foasdfhas'})
self.assertEqual(result.expires, 'foasdfhas')
def test_expires_valid_timestamp(self):
"""
If expires contains a valid millisecond timestamp, return a
corresponding datetime.
"""
result = base.VerificationResult({'expires': '1379307128000'})
self.assertEqual(datetime(2013, 9, 16, 4, 52, 8), result.expires)
def test_nonzero_failure(self):
"""
If the response status is not 'okay', the result should be
falsy.
"""
self.assertTrue(not base.VerificationResult({'status': 'failure'}))
def test_nonzero_okay(self):
"""
If the response status is 'okay', the result should be truthy.
"""
self.assertTrue(base.VerificationResult({'status': 'okay'}))
def test_str_success(self):
"""
If the result is successful, include 'Success' and the email in
the string.
"""
result = base.VerificationResult({'status': 'okay', 'email': 'a@example.com'})
self.assertEqual(six.text_type(result), '<VerificationResult Success email=a@example.com>')
# If the email is missing, don't include it.
result = base.VerificationResult({'status': 'okay'})
self.assertEqual(six.text_type(result), '<VerificationResult Success>')
def test_str_failure(self):
"""
If the result is a failure, include 'Failure' in the string.
"""
result = base.VerificationResult({'status': 'failure'})
self.assertEqual(six.text_type(result), '<VerificationResult Failure>')
def test_str_unicode(self):
"""Ensure that __str__ can handle unicode values."""
result = base.VerificationResult({'status': 'okay', 'email': six.u('\x80@example.com')})
self.assertEqual(six.text_type(result), six.u('<VerificationResult Success email=\x80@example.com>'))
class RemoteVerifierTests(TestCase):
def _response(self, **kwargs):
return Mock(spec=requests.Response, **kwargs)
def test_verify_requests_parameters(self):
"""
If a subclass overrides requests_parameters, the parameters
should be passed to requests.post.
"""
class MyVerifier(base.RemoteVerifier):
requests_parameters = {'foo': 'bar'}
verifier = MyVerifier()
with patch('django_browserid.base.requests.post') as post:
post.return_value = self._response(content='{"status":"failure"}')
verifier.verify('asdf', 'http://testserver')
# foo parameter passed with 'bar' value.
self.assertEqual(post.call_args[1]['foo'], 'bar')
def test_verify_kwargs(self):
"""
Any keyword arguments passed to verify should be passed on as
POST arguments.
"""
verifier = base.RemoteVerifier()
with patch('django_browserid.base.requests.post') as post:
post.return_value = self._response(content='{"status":"failure"}')
verifier.verify('asdf', 'http://testserver', foo='bar', baz=5)
# foo parameter passed with 'bar' value.
self.assertEqual(post.call_args[1]['data']['foo'], 'bar')
self.assertEqual(post.call_args[1]['data']['baz'], 5)
def test_verify_request_exception(self):
"""
If a RequestException is raised during the POST, raise a
BrowserIDException with the RequestException as the cause.
"""
verifier = base.RemoteVerifier()
request_exception = requests.exceptions.RequestException()
with patch('django_browserid.base.requests.post') as post:
post.side_effect = request_exception
with self.assertRaises(base.BrowserIDException) as cm:
verifier.verify('asdf', 'http://testserver')
self.assertEqual(cm.exception.exc, request_exception)
def test_verify_invalid_json(self):
"""
If the response contains invalid JSON, return a failure result.
"""
verifier = base.RemoteVerifier()
with patch('django_browserid.base.requests.post') as post:
response = self._response(content='{asg9=3{{{}}{')
response.json.side_effect = ValueError("Couldn't parse json")
post.return_value = response
result = verifier.verify('asdf', 'http://testserver')
self.assertTrue(not result)
self.assertTrue(result.reason.startswith('Could not parse verifier response'))
def test_verify_success(self):
"""
If the response contains valid JSON, return a result object for
that response.
"""
verifier = base.RemoteVerifier()
with patch('django_browserid.base.requests.post') as post:
response = self._response(
content='{"status": "okay", "email": "foo@example.com"}')
response.json.return_value = {"status": "okay", "email": "foo@example.com"}
post.return_value = response
result = verifier.verify('asdf', 'http://testserver')
self.assertTrue(result)
self.assertEqual(result.email, 'foo@example.com')
class MockVerifierTests(TestCase):
def test_verify_no_email(self):
"""
If the given email is None, verify should return a failure
result.
"""
verifier = base.MockVerifier(None)
result = verifier.verify('asdf', 'http://testserver')
self.assertTrue(not result)
self.assertEqual(result.reason, 'No email given to MockVerifier.')
def test_verify_email(self):
"""
If an email is given to the constructor, return a successful
result.
"""
verifier = base.MockVerifier('a@example.com')
result = verifier.verify('asdf', 'http://testserver')
self.assertTrue(result)
self.assertEqual(result.audience, 'http://testserver')
self.assertEqual(result.email, 'a@example.com')
def test_verify_result_attributes(self):
"""Extra kwargs to the constructor are added to the result."""
verifier = base.MockVerifier('a@example.com', foo='bar', baz=5)
result = verifier.verify('asdf', 'http://testserver')
self.assertEqual(result.foo, 'bar')
self.assertEqual(result.baz, 5)
class LocalVerifierTests(TestCase):
def setUp(self):
# Skip tests if PyBrowserID is not installed.
if not pybrowserid_found:
self.skipTest('PyBrowserID required for test but not installed.')
self.verifier = base.LocalVerifier()
def test_verify_error(self):
"""
If verify raises a PyBrowserIDError, return a failure
result.
"""
from browserid.errors import Error as PyBrowserIDError
pybid_verifier = Mock()
error = PyBrowserIDError()
self.verifier.pybid_verifier = pybid_verifier
pybid_verifier.verify.side_effect = error
result = self.verifier.verify('asdf', 'qwer')
pybid_verifier.verify.assert_called_with('asdf', 'qwer')
self.assertFalse(result)
self.assertEqual(result.reason, error)
def test_verify_success(self):
pybid_verifier = Mock()
self.verifier.pybid_verifier = pybid_verifier
response = {'status': 'okay'}
pybid_verifier.verify.return_value = response
result = self.verifier.verify('asdf', 'qwer')
pybid_verifier.verify.assert_called_with('asdf', 'qwer')
self.assertTrue(result)
self.assertEqual(result._response, response)