test_base.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. # This Source Code Form is subject to the terms of the Mozilla Public
  2. # License, v. 2.0. If a copy of the MPL was not distributed with this
  3. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. from datetime import datetime
  5. from django.conf import settings
  6. from django.core.exceptions import ImproperlyConfigured
  7. from django.test.client import RequestFactory
  8. from django.test.utils import override_settings
  9. from django.utils import six
  10. import requests
  11. from mock import Mock, patch
  12. from django_browserid import base
  13. from django_browserid.compat import pybrowserid_found
  14. from django_browserid.tests import TestCase
  15. class SanityCheckTests(TestCase):
  16. def setUp(self):
  17. self.factory = RequestFactory()
  18. @override_settings(DEBUG=True)
  19. def test_debug_true(self):
  20. """
  21. If DEBUG is True and BROWSERID_DISABLE_SANITY_CHECKS is not set,
  22. run the checks.
  23. """
  24. request = self.factory.get('/')
  25. self.assertTrue(base.sanity_checks(request))
  26. @override_settings(DEBUG=False)
  27. def test_debug_false(self):
  28. """
  29. If DEBUG is True and BROWSERID_DISABLE_SANITY_CHECKS is not set,
  30. run the checks.
  31. """
  32. request = self.factory.get('/')
  33. self.assertTrue(not base.sanity_checks(request))
  34. @override_settings(BROWSERID_DISABLE_SANITY_CHECKS=True)
  35. def test_disable_sanity_checks(self):
  36. """
  37. If BROWSERID_DISABLE_SANITY_CHECKS is True, do not run any
  38. checks.
  39. """
  40. request = self.factory.get('/')
  41. self.assertTrue(not base.sanity_checks(request))
  42. @override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False, SESSION_COOKIE_SECURE=True)
  43. def test_sanity_session_cookie(self):
  44. """
  45. If SESSION_COOKIE_SECURE == True and the current request isn't
  46. https, log a debug message warning about it.
  47. """
  48. request = self.factory.get('/')
  49. request.is_secure = Mock(return_value=False)
  50. with patch('django_browserid.base.logger.warning') as warning:
  51. base.sanity_checks(request)
  52. self.assertTrue(warning.called)
  53. @override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False,
  54. MIDDLEWARE_CLASSES=['csp.middleware.CSPMiddleware'])
  55. @patch('django_browserid.base.logger.warning')
  56. def test_sanity_csp(self, warning):
  57. """
  58. If the django-csp middleware is present and Persona isn't
  59. allowed by CSP, log a debug message warning about it.
  60. """
  61. request = self.factory.get('/')
  62. # Test if allowed properly.
  63. with self.settings(CSP_DEFAULT_SRC=[],
  64. CSP_SCRIPT_SRC=['https://login.persona.org'],
  65. CSP_FRAME_SRC=['https://login.persona.org']):
  66. base.sanity_checks(request)
  67. self.assertTrue(not warning.called)
  68. warning.reset_mock()
  69. # Test fallback to default-src.
  70. with self.settings(CSP_DEFAULT_SRC=['https://login.persona.org'],
  71. CSP_SCRIPT_SRC=[],
  72. CSP_FRAME_SRC=[]):
  73. base.sanity_checks(request)
  74. self.assertTrue(not warning.called)
  75. warning.reset_mock()
  76. # Test incorrect csp.
  77. with self.settings(CSP_DEFAULT_SRC=[],
  78. CSP_SCRIPT_SRC=[],
  79. CSP_FRAME_SRC=[]):
  80. base.sanity_checks(request)
  81. self.assertTrue(warning.called)
  82. warning.reset_mock()
  83. # Test partial incorrectness.
  84. with self.settings(CSP_DEFAULT_SRC=[],
  85. CSP_SCRIPT_SRC=['https://login.persona.org'],
  86. CSP_FRAME_SRC=[]):
  87. base.sanity_checks(request)
  88. self.assertTrue(warning.called)
  89. @override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False,
  90. MIDDLEWARE_CLASSES=['csp.middleware.CSPMiddleware'])
  91. @patch('django_browserid.base.logger.warning')
  92. def test_unset_csp(self, warning):
  93. """Check for errors when CSP settings aren't specified."""
  94. request = self.factory.get('/')
  95. correct = ['https://login.persona.org']
  96. setting_kwargs = {
  97. 'CSP_DEFAULT_SRC': correct,
  98. 'CSP_SCRIPT_SRC': correct,
  99. 'CSP_FRAME_SRC': correct
  100. }
  101. # There's no easy way to use a variable for deleting the
  102. # attribute on the settings object, so we can't easily turn this
  103. # into a function, sadly.
  104. with self.settings(**setting_kwargs):
  105. del settings.CSP_DEFAULT_SRC
  106. base.sanity_checks(request)
  107. self.assertTrue(not warning.called)
  108. warning.reset_mock()
  109. with self.settings(**setting_kwargs):
  110. del settings.CSP_FRAME_SRC
  111. base.sanity_checks(request)
  112. self.assertTrue(not warning.called)
  113. warning.reset_mock()
  114. with self.settings(**setting_kwargs):
  115. del settings.CSP_SCRIPT_SRC
  116. base.sanity_checks(request)
  117. self.assertTrue(not warning.called)
  118. warning.reset_mock()
  119. class GetAudienceTests(TestCase):
  120. def setUp(self):
  121. self.factory = RequestFactory()
  122. def test_setting_missing(self):
  123. """
  124. If BROWSERID_AUDIENCES isn't defined, raise
  125. ImproperlyConfigured.
  126. """
  127. request = self.factory.get('/')
  128. with patch('django_browserid.base.settings') as settings:
  129. del settings.BROWSERID_AUDIENCES
  130. settings.DEBUG = False
  131. with self.assertRaises(ImproperlyConfigured):
  132. base.get_audience(request)
  133. def test_same_origin_found(self):
  134. """
  135. If an audience is found in BROWSERID_AUDIENCES with the same
  136. origin as the request URI, return it.
  137. """
  138. request = self.factory.get('http://testserver')
  139. audiences = ['https://example.com', 'http://testserver']
  140. with self.settings(BROWSERID_AUDIENCES=audiences, DEBUG=False):
  141. self.assertEqual(base.get_audience(request), 'http://testserver')
  142. def test_no_audience(self):
  143. """
  144. If no matching audiences is found in BROWSERID_AUDIENCES, raise
  145. ImproperlyConfigured.
  146. """
  147. request = self.factory.get('http://testserver')
  148. with self.settings(BROWSERID_AUDIENCES=['https://example.com']):
  149. with self.assertRaises(ImproperlyConfigured):
  150. base.get_audience(request)
  151. def test_missing_setting_but_in_debug(self):
  152. """
  153. If no BROWSERID_AUDIENCES is set but in DEBUG just use the
  154. current protocal and host.
  155. """
  156. request = self.factory.get('/')
  157. # Simulate that no BROWSERID_AUDIENCES has been set
  158. with patch('django_browserid.base.settings') as settings:
  159. del settings.BROWSERID_AUDIENCES
  160. settings.DEBUG = True
  161. self.assertEqual(base.get_audience(request), 'http://testserver')
  162. def test_no_audience_but_in_debug(self):
  163. """
  164. If no BROWSERID_AUDIENCES is set but in DEBUG just use the
  165. current protocal and host.
  166. """
  167. request = self.factory.get('/')
  168. # Simulate that no BROWSERID_AUDIENCES has been set
  169. with self.settings(BROWSERID_AUDIENCES=[], DEBUG=True):
  170. self.assertEqual(base.get_audience(request), 'http://testserver')
  171. class VerificationResultTests(TestCase):
  172. def test_getattr_attribute_exists(self):
  173. """
  174. If a value exists in the response dict, it should be an
  175. attribute on the result.
  176. """
  177. result = base.VerificationResult({'myattr': 'foo'})
  178. self.assertEqual(result.myattr, 'foo')
  179. def test_getattr_attribute_doesnt_exist(self):
  180. """
  181. If a value doesn't exist in the response dict, accessing it as
  182. an attribute should raise an AttributeError.
  183. """
  184. result = base.VerificationResult({'myattr': 'foo'})
  185. with self.assertRaises(AttributeError):
  186. result.bar
  187. def test_expires_no_attribute(self):
  188. """
  189. If no expires attribute was in the response, raise an
  190. AttributeError.
  191. """
  192. result = base.VerificationResult({'myattr': 'foo'})
  193. with self.assertRaises(AttributeError):
  194. result.expires
  195. def test_expires_invalid_timestamp(self):
  196. """
  197. If the expires attribute cannot be parsed as a timestamp, return
  198. the raw string instead.
  199. """
  200. result = base.VerificationResult({'expires': 'foasdfhas'})
  201. self.assertEqual(result.expires, 'foasdfhas')
  202. def test_expires_valid_timestamp(self):
  203. """
  204. If expires contains a valid millisecond timestamp, return a
  205. corresponding datetime.
  206. """
  207. result = base.VerificationResult({'expires': '1379307128000'})
  208. self.assertEqual(datetime(2013, 9, 16, 4, 52, 8), result.expires)
  209. def test_nonzero_failure(self):
  210. """
  211. If the response status is not 'okay', the result should be
  212. falsy.
  213. """
  214. self.assertTrue(not base.VerificationResult({'status': 'failure'}))
  215. def test_nonzero_okay(self):
  216. """
  217. If the response status is 'okay', the result should be truthy.
  218. """
  219. self.assertTrue(base.VerificationResult({'status': 'okay'}))
  220. def test_str_success(self):
  221. """
  222. If the result is successful, include 'Success' and the email in
  223. the string.
  224. """
  225. result = base.VerificationResult({'status': 'okay', 'email': 'a@example.com'})
  226. self.assertEqual(six.text_type(result), '<VerificationResult Success email=a@example.com>')
  227. # If the email is missing, don't include it.
  228. result = base.VerificationResult({'status': 'okay'})
  229. self.assertEqual(six.text_type(result), '<VerificationResult Success>')
  230. def test_str_failure(self):
  231. """
  232. If the result is a failure, include 'Failure' in the string.
  233. """
  234. result = base.VerificationResult({'status': 'failure'})
  235. self.assertEqual(six.text_type(result), '<VerificationResult Failure>')
  236. def test_str_unicode(self):
  237. """Ensure that __str__ can handle unicode values."""
  238. result = base.VerificationResult({'status': 'okay', 'email': six.u('\x80@example.com')})
  239. self.assertEqual(six.text_type(result), six.u('<VerificationResult Success email=\x80@example.com>'))
  240. class RemoteVerifierTests(TestCase):
  241. def _response(self, **kwargs):
  242. return Mock(spec=requests.Response, **kwargs)
  243. def test_verify_requests_parameters(self):
  244. """
  245. If a subclass overrides requests_parameters, the parameters
  246. should be passed to requests.post.
  247. """
  248. class MyVerifier(base.RemoteVerifier):
  249. requests_parameters = {'foo': 'bar'}
  250. verifier = MyVerifier()
  251. with patch('django_browserid.base.requests.post') as post:
  252. post.return_value = self._response(content='{"status":"failure"}')
  253. verifier.verify('asdf', 'http://testserver')
  254. # foo parameter passed with 'bar' value.
  255. self.assertEqual(post.call_args[1]['foo'], 'bar')
  256. def test_verify_kwargs(self):
  257. """
  258. Any keyword arguments passed to verify should be passed on as
  259. POST arguments.
  260. """
  261. verifier = base.RemoteVerifier()
  262. with patch('django_browserid.base.requests.post') as post:
  263. post.return_value = self._response(content='{"status":"failure"}')
  264. verifier.verify('asdf', 'http://testserver', foo='bar', baz=5)
  265. # foo parameter passed with 'bar' value.
  266. self.assertEqual(post.call_args[1]['data']['foo'], 'bar')
  267. self.assertEqual(post.call_args[1]['data']['baz'], 5)
  268. def test_verify_request_exception(self):
  269. """
  270. If a RequestException is raised during the POST, raise a
  271. BrowserIDException with the RequestException as the cause.
  272. """
  273. verifier = base.RemoteVerifier()
  274. request_exception = requests.exceptions.RequestException()
  275. with patch('django_browserid.base.requests.post') as post:
  276. post.side_effect = request_exception
  277. with self.assertRaises(base.BrowserIDException) as cm:
  278. verifier.verify('asdf', 'http://testserver')
  279. self.assertEqual(cm.exception.exc, request_exception)
  280. def test_verify_invalid_json(self):
  281. """
  282. If the response contains invalid JSON, return a failure result.
  283. """
  284. verifier = base.RemoteVerifier()
  285. with patch('django_browserid.base.requests.post') as post:
  286. response = self._response(content='{asg9=3{{{}}{')
  287. response.json.side_effect = ValueError("Couldn't parse json")
  288. post.return_value = response
  289. result = verifier.verify('asdf', 'http://testserver')
  290. self.assertTrue(not result)
  291. self.assertTrue(result.reason.startswith('Could not parse verifier response'))
  292. def test_verify_success(self):
  293. """
  294. If the response contains valid JSON, return a result object for
  295. that response.
  296. """
  297. verifier = base.RemoteVerifier()
  298. with patch('django_browserid.base.requests.post') as post:
  299. response = self._response(
  300. content='{"status": "okay", "email": "foo@example.com"}')
  301. response.json.return_value = {"status": "okay", "email": "foo@example.com"}
  302. post.return_value = response
  303. result = verifier.verify('asdf', 'http://testserver')
  304. self.assertTrue(result)
  305. self.assertEqual(result.email, 'foo@example.com')
  306. class MockVerifierTests(TestCase):
  307. def test_verify_no_email(self):
  308. """
  309. If the given email is None, verify should return a failure
  310. result.
  311. """
  312. verifier = base.MockVerifier(None)
  313. result = verifier.verify('asdf', 'http://testserver')
  314. self.assertTrue(not result)
  315. self.assertEqual(result.reason, 'No email given to MockVerifier.')
  316. def test_verify_email(self):
  317. """
  318. If an email is given to the constructor, return a successful
  319. result.
  320. """
  321. verifier = base.MockVerifier('a@example.com')
  322. result = verifier.verify('asdf', 'http://testserver')
  323. self.assertTrue(result)
  324. self.assertEqual(result.audience, 'http://testserver')
  325. self.assertEqual(result.email, 'a@example.com')
  326. def test_verify_result_attributes(self):
  327. """Extra kwargs to the constructor are added to the result."""
  328. verifier = base.MockVerifier('a@example.com', foo='bar', baz=5)
  329. result = verifier.verify('asdf', 'http://testserver')
  330. self.assertEqual(result.foo, 'bar')
  331. self.assertEqual(result.baz, 5)
  332. class LocalVerifierTests(TestCase):
  333. def setUp(self):
  334. # Skip tests if PyBrowserID is not installed.
  335. if not pybrowserid_found:
  336. self.skipTest('PyBrowserID required for test but not installed.')
  337. self.verifier = base.LocalVerifier()
  338. def test_verify_error(self):
  339. """
  340. If verify raises a PyBrowserIDError, return a failure
  341. result.
  342. """
  343. from browserid.errors import Error as PyBrowserIDError
  344. pybid_verifier = Mock()
  345. error = PyBrowserIDError()
  346. self.verifier.pybid_verifier = pybid_verifier
  347. pybid_verifier.verify.side_effect = error
  348. result = self.verifier.verify('asdf', 'qwer')
  349. pybid_verifier.verify.assert_called_with('asdf', 'qwer')
  350. self.assertFalse(result)
  351. self.assertEqual(result.reason, error)
  352. def test_verify_success(self):
  353. pybid_verifier = Mock()
  354. self.verifier.pybid_verifier = pybid_verifier
  355. response = {'status': 'okay'}
  356. pybid_verifier.verify.return_value = response
  357. result = self.verifier.verify('asdf', 'qwer')
  358. pybid_verifier.verify.assert_called_with('asdf', 'qwer')
  359. self.assertTrue(result)
  360. self.assertEqual(result._response, response)