tests.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. from django.core.exceptions import ImproperlyConfigured
  2. from django.core.handlers.wsgi import WSGIHandler, WSGIRequest, get_script_name
  3. from django.core.signals import request_finished, request_started
  4. from django.db import close_old_connections, connection
  5. from django.test import (
  6. RequestFactory, SimpleTestCase, TransactionTestCase, override_settings,
  7. )
  8. class HandlerTests(SimpleTestCase):
  9. request_factory = RequestFactory()
  10. def setUp(self):
  11. request_started.disconnect(close_old_connections)
  12. def tearDown(self):
  13. request_started.connect(close_old_connections)
  14. def test_middleware_initialized(self):
  15. handler = WSGIHandler()
  16. self.assertIsNotNone(handler._middleware_chain)
  17. def test_bad_path_info(self):
  18. """
  19. A non-UTF-8 path populates PATH_INFO with an URL-encoded path and
  20. produces a 404.
  21. """
  22. environ = self.request_factory.get('/').environ
  23. environ['PATH_INFO'] = '\xed'
  24. handler = WSGIHandler()
  25. response = handler(environ, lambda *a, **k: None)
  26. # The path of the request will be encoded to '/%ED'.
  27. self.assertEqual(response.status_code, 404)
  28. def test_non_ascii_query_string(self):
  29. """
  30. Non-ASCII query strings are properly decoded (#20530, #22996).
  31. """
  32. environ = self.request_factory.get('/').environ
  33. raw_query_strings = [
  34. b'want=caf%C3%A9', # This is the proper way to encode 'café'
  35. b'want=caf\xc3\xa9', # UA forgot to quote bytes
  36. b'want=caf%E9', # UA quoted, but not in UTF-8
  37. b'want=caf\xe9', # UA forgot to convert Latin-1 to UTF-8 and to quote (typical of MSIE)
  38. ]
  39. got = []
  40. for raw_query_string in raw_query_strings:
  41. # Simulate http.server.BaseHTTPRequestHandler.parse_request handling of raw request
  42. environ['QUERY_STRING'] = str(raw_query_string, 'iso-8859-1')
  43. request = WSGIRequest(environ)
  44. got.append(request.GET['want'])
  45. # %E9 is converted to the Unicode replacement character by parse_qsl
  46. self.assertEqual(got, ['café', 'café', 'caf\ufffd', 'café'])
  47. def test_non_ascii_cookie(self):
  48. """Non-ASCII cookies set in JavaScript are properly decoded (#20557)."""
  49. environ = self.request_factory.get('/').environ
  50. raw_cookie = 'want="café"'.encode('utf-8').decode('iso-8859-1')
  51. environ['HTTP_COOKIE'] = raw_cookie
  52. request = WSGIRequest(environ)
  53. self.assertEqual(request.COOKIES['want'], "café")
  54. def test_invalid_unicode_cookie(self):
  55. """
  56. Invalid cookie content should result in an absent cookie, but not in a
  57. crash while trying to decode it (#23638).
  58. """
  59. environ = self.request_factory.get('/').environ
  60. environ['HTTP_COOKIE'] = 'x=W\x03c(h]\x8e'
  61. request = WSGIRequest(environ)
  62. # We don't test COOKIES content, as the result might differ between
  63. # Python version because parsing invalid content became stricter in
  64. # latest versions.
  65. self.assertIsInstance(request.COOKIES, dict)
  66. @override_settings(ROOT_URLCONF='handlers.urls')
  67. def test_invalid_multipart_boundary(self):
  68. """
  69. Invalid boundary string should produce a "Bad Request" response, not a
  70. server error (#23887).
  71. """
  72. environ = self.request_factory.post('/malformed_post/').environ
  73. environ['CONTENT_TYPE'] = 'multipart/form-data; boundary=WRONG\x07'
  74. handler = WSGIHandler()
  75. response = handler(environ, lambda *a, **k: None)
  76. # Expect "bad request" response
  77. self.assertEqual(response.status_code, 400)
  78. @override_settings(ROOT_URLCONF='handlers.urls', MIDDLEWARE=[])
  79. class TransactionsPerRequestTests(TransactionTestCase):
  80. available_apps = []
  81. def test_no_transaction(self):
  82. response = self.client.get('/in_transaction/')
  83. self.assertContains(response, 'False')
  84. def test_auto_transaction(self):
  85. old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
  86. try:
  87. connection.settings_dict['ATOMIC_REQUESTS'] = True
  88. response = self.client.get('/in_transaction/')
  89. finally:
  90. connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
  91. self.assertContains(response, 'True')
  92. async def test_auto_transaction_async_view(self):
  93. old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
  94. try:
  95. connection.settings_dict['ATOMIC_REQUESTS'] = True
  96. msg = 'You cannot use ATOMIC_REQUESTS with async views.'
  97. with self.assertRaisesMessage(RuntimeError, msg):
  98. await self.async_client.get('/async_regular/')
  99. finally:
  100. connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
  101. def test_no_auto_transaction(self):
  102. old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
  103. try:
  104. connection.settings_dict['ATOMIC_REQUESTS'] = True
  105. response = self.client.get('/not_in_transaction/')
  106. finally:
  107. connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
  108. self.assertContains(response, 'False')
  109. @override_settings(ROOT_URLCONF='handlers.urls')
  110. class SignalsTests(SimpleTestCase):
  111. def setUp(self):
  112. self.signals = []
  113. self.signaled_environ = None
  114. request_started.connect(self.register_started)
  115. request_finished.connect(self.register_finished)
  116. def tearDown(self):
  117. request_started.disconnect(self.register_started)
  118. request_finished.disconnect(self.register_finished)
  119. def register_started(self, **kwargs):
  120. self.signals.append('started')
  121. self.signaled_environ = kwargs.get('environ')
  122. def register_finished(self, **kwargs):
  123. self.signals.append('finished')
  124. def test_request_signals(self):
  125. response = self.client.get('/regular/')
  126. self.assertEqual(self.signals, ['started', 'finished'])
  127. self.assertEqual(response.content, b"regular content")
  128. self.assertEqual(self.signaled_environ, response.wsgi_request.environ)
  129. def test_request_signals_streaming_response(self):
  130. response = self.client.get('/streaming/')
  131. self.assertEqual(self.signals, ['started'])
  132. self.assertEqual(b''.join(response.streaming_content), b"streaming content")
  133. self.assertEqual(self.signals, ['started', 'finished'])
  134. def empty_middleware(get_response):
  135. pass
  136. @override_settings(ROOT_URLCONF='handlers.urls')
  137. class HandlerRequestTests(SimpleTestCase):
  138. request_factory = RequestFactory()
  139. def test_async_view(self):
  140. """Calling an async view down the normal synchronous path."""
  141. response = self.client.get('/async_regular/')
  142. self.assertEqual(response.status_code, 200)
  143. def test_suspiciousop_in_view_returns_400(self):
  144. response = self.client.get('/suspicious/')
  145. self.assertEqual(response.status_code, 400)
  146. def test_bad_request_in_view_returns_400(self):
  147. response = self.client.get('/bad_request/')
  148. self.assertEqual(response.status_code, 400)
  149. def test_invalid_urls(self):
  150. response = self.client.get('~%A9helloworld')
  151. self.assertEqual(response.status_code, 404)
  152. self.assertEqual(response.context['request_path'], '/~%25A9helloworld')
  153. response = self.client.get('d%aao%aaw%aan%aal%aao%aaa%aad%aa/')
  154. self.assertEqual(response.context['request_path'], '/d%25AAo%25AAw%25AAn%25AAl%25AAo%25AAa%25AAd%25AA')
  155. response = self.client.get('/%E2%99%E2%99%A5/')
  156. self.assertEqual(response.context['request_path'], '/%25E2%2599%E2%99%A5/')
  157. response = self.client.get('/%E2%98%8E%E2%A9%E2%99%A5/')
  158. self.assertEqual(response.context['request_path'], '/%E2%98%8E%25E2%25A9%E2%99%A5/')
  159. def test_environ_path_info_type(self):
  160. environ = self.request_factory.get('/%E2%A8%87%87%A5%E2%A8%A0').environ
  161. self.assertIsInstance(environ['PATH_INFO'], str)
  162. def test_handle_accepts_httpstatus_enum_value(self):
  163. def start_response(status, headers):
  164. start_response.status = status
  165. environ = self.request_factory.get('/httpstatus_enum/').environ
  166. WSGIHandler()(environ, start_response)
  167. self.assertEqual(start_response.status, '200 OK')
  168. @override_settings(MIDDLEWARE=['handlers.tests.empty_middleware'])
  169. def test_middleware_returns_none(self):
  170. msg = 'Middleware factory handlers.tests.empty_middleware returned None.'
  171. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  172. self.client.get('/')
  173. def test_no_response(self):
  174. msg = "The view %s didn't return an HttpResponse object. It returned None instead."
  175. tests = (
  176. ('/no_response_fbv/', 'handlers.views.no_response'),
  177. ('/no_response_cbv/', 'handlers.views.NoResponse.__call__'),
  178. )
  179. for url, view in tests:
  180. with self.subTest(url=url), self.assertRaisesMessage(ValueError, msg % view):
  181. self.client.get(url)
  182. class ScriptNameTests(SimpleTestCase):
  183. def test_get_script_name(self):
  184. # Regression test for #23173
  185. # Test first without PATH_INFO
  186. script_name = get_script_name({'SCRIPT_URL': '/foobar/'})
  187. self.assertEqual(script_name, '/foobar/')
  188. script_name = get_script_name({'SCRIPT_URL': '/foobar/', 'PATH_INFO': '/'})
  189. self.assertEqual(script_name, '/foobar')
  190. def test_get_script_name_double_slashes(self):
  191. """
  192. WSGI squashes multiple successive slashes in PATH_INFO, get_script_name
  193. should take that into account when forming SCRIPT_NAME (#17133).
  194. """
  195. script_name = get_script_name({
  196. 'SCRIPT_URL': '/mst/milestones//accounts/login//help',
  197. 'PATH_INFO': '/milestones/accounts/login/help',
  198. })
  199. self.assertEqual(script_name, '/mst')
  200. @override_settings(ROOT_URLCONF='handlers.urls')
  201. class AsyncHandlerRequestTests(SimpleTestCase):
  202. """Async variants of the normal handler request tests."""
  203. async def test_sync_view(self):
  204. """Calling a sync view down the asynchronous path."""
  205. response = await self.async_client.get('/regular/')
  206. self.assertEqual(response.status_code, 200)
  207. async def test_async_view(self):
  208. """Calling an async view down the asynchronous path."""
  209. response = await self.async_client.get('/async_regular/')
  210. self.assertEqual(response.status_code, 200)
  211. async def test_suspiciousop_in_view_returns_400(self):
  212. response = await self.async_client.get('/suspicious/')
  213. self.assertEqual(response.status_code, 400)
  214. async def test_bad_request_in_view_returns_400(self):
  215. response = await self.async_client.get('/bad_request/')
  216. self.assertEqual(response.status_code, 400)
  217. async def test_no_response(self):
  218. msg = (
  219. "The view handlers.views.no_response didn't return an "
  220. "HttpResponse object. It returned None instead."
  221. )
  222. with self.assertRaisesMessage(ValueError, msg):
  223. await self.async_client.get('/no_response_fbv/')
  224. async def test_unawaited_response(self):
  225. msg = (
  226. "The view handlers.views.CoroutineClearingView.__call__ didn't"
  227. " return an HttpResponse object. It returned an unawaited"
  228. " coroutine instead. You may need to add an 'await'"
  229. " into your view."
  230. )
  231. with self.assertRaisesMessage(ValueError, msg):
  232. await self.async_client.get('/unawaited/')