tests.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import asyncio
  2. import sys
  3. import threading
  4. from pathlib import Path
  5. from unittest import skipIf
  6. from asgiref.sync import SyncToAsync
  7. from asgiref.testing import ApplicationCommunicator
  8. from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
  9. from django.core.asgi import get_asgi_application
  10. from django.core.signals import request_finished, request_started
  11. from django.db import close_old_connections
  12. from django.test import (
  13. AsyncRequestFactory, SimpleTestCase, modify_settings, override_settings,
  14. )
  15. from django.utils.http import http_date
  16. from .urls import test_filename
  17. TEST_STATIC_ROOT = Path(__file__).parent / 'project' / 'static'
  18. @skipIf(sys.platform == 'win32' and (3, 8, 0) < sys.version_info < (3, 8, 1), 'https://bugs.python.org/issue38563')
  19. @override_settings(ROOT_URLCONF='asgi.urls')
  20. class ASGITest(SimpleTestCase):
  21. async_request_factory = AsyncRequestFactory()
  22. def setUp(self):
  23. request_started.disconnect(close_old_connections)
  24. def tearDown(self):
  25. request_started.connect(close_old_connections)
  26. async def test_get_asgi_application(self):
  27. """
  28. get_asgi_application() returns a functioning ASGI callable.
  29. """
  30. application = get_asgi_application()
  31. # Construct HTTP request.
  32. scope = self.async_request_factory._base_scope(path='/')
  33. communicator = ApplicationCommunicator(application, scope)
  34. await communicator.send_input({'type': 'http.request'})
  35. # Read the response.
  36. response_start = await communicator.receive_output()
  37. self.assertEqual(response_start['type'], 'http.response.start')
  38. self.assertEqual(response_start['status'], 200)
  39. self.assertEqual(
  40. set(response_start['headers']),
  41. {
  42. (b'Content-Length', b'12'),
  43. (b'Content-Type', b'text/html; charset=utf-8'),
  44. },
  45. )
  46. response_body = await communicator.receive_output()
  47. self.assertEqual(response_body['type'], 'http.response.body')
  48. self.assertEqual(response_body['body'], b'Hello World!')
  49. async def test_file_response(self):
  50. """
  51. Makes sure that FileResponse works over ASGI.
  52. """
  53. application = get_asgi_application()
  54. # Construct HTTP request.
  55. scope = self.async_request_factory._base_scope(path='/file/')
  56. communicator = ApplicationCommunicator(application, scope)
  57. await communicator.send_input({'type': 'http.request'})
  58. # Get the file content.
  59. with open(test_filename, 'rb') as test_file:
  60. test_file_contents = test_file.read()
  61. # Read the response.
  62. response_start = await communicator.receive_output()
  63. self.assertEqual(response_start['type'], 'http.response.start')
  64. self.assertEqual(response_start['status'], 200)
  65. headers = response_start['headers']
  66. self.assertEqual(len(headers), 3)
  67. expected_headers = {
  68. b'Content-Length': str(len(test_file_contents)).encode('ascii'),
  69. b'Content-Type': b'text/x-python',
  70. b'Content-Disposition': b'inline; filename="urls.py"',
  71. }
  72. for key, value in headers:
  73. try:
  74. self.assertEqual(value, expected_headers[key])
  75. except AssertionError:
  76. # Windows registry may not be configured with correct
  77. # mimetypes.
  78. if sys.platform == 'win32' and key == b'Content-Type':
  79. self.assertEqual(value, b'text/plain')
  80. else:
  81. raise
  82. response_body = await communicator.receive_output()
  83. self.assertEqual(response_body['type'], 'http.response.body')
  84. self.assertEqual(response_body['body'], test_file_contents)
  85. # Allow response.close() to finish.
  86. await communicator.wait()
  87. @modify_settings(INSTALLED_APPS={'append': 'django.contrib.staticfiles'})
  88. @override_settings(
  89. STATIC_URL='static/',
  90. STATIC_ROOT=TEST_STATIC_ROOT,
  91. STATICFILES_DIRS=[TEST_STATIC_ROOT],
  92. STATICFILES_FINDERS=[
  93. 'django.contrib.staticfiles.finders.FileSystemFinder',
  94. ],
  95. )
  96. async def test_static_file_response(self):
  97. application = ASGIStaticFilesHandler(get_asgi_application())
  98. # Construct HTTP request.
  99. scope = self.async_request_factory._base_scope(path='/static/file.txt')
  100. communicator = ApplicationCommunicator(application, scope)
  101. await communicator.send_input({'type': 'http.request'})
  102. # Get the file content.
  103. file_path = TEST_STATIC_ROOT / 'file.txt'
  104. with open(file_path, 'rb') as test_file:
  105. test_file_contents = test_file.read()
  106. # Read the response.
  107. stat = file_path.stat()
  108. response_start = await communicator.receive_output()
  109. self.assertEqual(response_start['type'], 'http.response.start')
  110. self.assertEqual(response_start['status'], 200)
  111. self.assertEqual(
  112. set(response_start['headers']),
  113. {
  114. (b'Content-Length', str(len(test_file_contents)).encode('ascii')),
  115. (b'Content-Type', b'text/plain'),
  116. (b'Content-Disposition', b'inline; filename="file.txt"'),
  117. (b'Last-Modified', http_date(stat.st_mtime).encode('ascii')),
  118. },
  119. )
  120. response_body = await communicator.receive_output()
  121. self.assertEqual(response_body['type'], 'http.response.body')
  122. self.assertEqual(response_body['body'], test_file_contents)
  123. # Allow response.close() to finish.
  124. await communicator.wait()
  125. async def test_headers(self):
  126. application = get_asgi_application()
  127. communicator = ApplicationCommunicator(
  128. application,
  129. self.async_request_factory._base_scope(
  130. path='/meta/',
  131. headers=[
  132. [b'content-type', b'text/plain; charset=utf-8'],
  133. [b'content-length', b'77'],
  134. [b'referer', b'Scotland'],
  135. [b'referer', b'Wales'],
  136. ],
  137. ),
  138. )
  139. await communicator.send_input({'type': 'http.request'})
  140. response_start = await communicator.receive_output()
  141. self.assertEqual(response_start['type'], 'http.response.start')
  142. self.assertEqual(response_start['status'], 200)
  143. self.assertEqual(
  144. set(response_start['headers']),
  145. {
  146. (b'Content-Length', b'19'),
  147. (b'Content-Type', b'text/plain; charset=utf-8'),
  148. },
  149. )
  150. response_body = await communicator.receive_output()
  151. self.assertEqual(response_body['type'], 'http.response.body')
  152. self.assertEqual(response_body['body'], b'From Scotland,Wales')
  153. async def test_get_query_string(self):
  154. application = get_asgi_application()
  155. for query_string in (b'name=Andrew', 'name=Andrew'):
  156. with self.subTest(query_string=query_string):
  157. scope = self.async_request_factory._base_scope(
  158. path='/',
  159. query_string=query_string,
  160. )
  161. communicator = ApplicationCommunicator(application, scope)
  162. await communicator.send_input({'type': 'http.request'})
  163. response_start = await communicator.receive_output()
  164. self.assertEqual(response_start['type'], 'http.response.start')
  165. self.assertEqual(response_start['status'], 200)
  166. response_body = await communicator.receive_output()
  167. self.assertEqual(response_body['type'], 'http.response.body')
  168. self.assertEqual(response_body['body'], b'Hello Andrew!')
  169. async def test_disconnect(self):
  170. application = get_asgi_application()
  171. scope = self.async_request_factory._base_scope(path='/')
  172. communicator = ApplicationCommunicator(application, scope)
  173. await communicator.send_input({'type': 'http.disconnect'})
  174. with self.assertRaises(asyncio.TimeoutError):
  175. await communicator.receive_output()
  176. async def test_wrong_connection_type(self):
  177. application = get_asgi_application()
  178. scope = self.async_request_factory._base_scope(path='/', type='other')
  179. communicator = ApplicationCommunicator(application, scope)
  180. await communicator.send_input({'type': 'http.request'})
  181. msg = 'Django can only handle ASGI/HTTP connections, not other.'
  182. with self.assertRaisesMessage(ValueError, msg):
  183. await communicator.receive_output()
  184. async def test_non_unicode_query_string(self):
  185. application = get_asgi_application()
  186. scope = self.async_request_factory._base_scope(path='/', query_string=b'\xff')
  187. communicator = ApplicationCommunicator(application, scope)
  188. await communicator.send_input({'type': 'http.request'})
  189. response_start = await communicator.receive_output()
  190. self.assertEqual(response_start['type'], 'http.response.start')
  191. self.assertEqual(response_start['status'], 400)
  192. response_body = await communicator.receive_output()
  193. self.assertEqual(response_body['type'], 'http.response.body')
  194. self.assertEqual(response_body['body'], b'')
  195. async def test_request_lifecycle_signals_dispatched_with_thread_sensitive(self):
  196. class SignalHandler:
  197. """Track threads handler is dispatched on."""
  198. threads = []
  199. def __call__(self, **kwargs):
  200. self.threads.append(threading.current_thread())
  201. signal_handler = SignalHandler()
  202. request_started.connect(signal_handler)
  203. request_finished.connect(signal_handler)
  204. # Perform a basic request.
  205. application = get_asgi_application()
  206. scope = self.async_request_factory._base_scope(path='/')
  207. communicator = ApplicationCommunicator(application, scope)
  208. await communicator.send_input({'type': 'http.request'})
  209. response_start = await communicator.receive_output()
  210. self.assertEqual(response_start['type'], 'http.response.start')
  211. self.assertEqual(response_start['status'], 200)
  212. response_body = await communicator.receive_output()
  213. self.assertEqual(response_body['type'], 'http.response.body')
  214. self.assertEqual(response_body['body'], b'Hello World!')
  215. # Give response.close() time to finish.
  216. await communicator.wait()
  217. # At this point, AsyncToSync does not have a current executor. Thus
  218. # SyncToAsync falls-back to .single_thread_executor.
  219. target_thread = next(iter(SyncToAsync.single_thread_executor._threads))
  220. request_started_thread, request_finished_thread = signal_handler.threads
  221. self.assertEqual(request_started_thread, target_thread)
  222. self.assertEqual(request_finished_thread, target_thread)
  223. request_started.disconnect(signal_handler)
  224. request_finished.disconnect(signal_handler)