tests.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import asyncio
  2. import sys
  3. import threading
  4. from pathlib import Path
  5. from asgiref.testing import ApplicationCommunicator
  6. from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
  7. from django.core.asgi import get_asgi_application
  8. from django.core.handlers.asgi import ASGIHandler, ASGIRequest
  9. from django.core.signals import request_finished, request_started
  10. from django.db import close_old_connections
  11. from django.http import HttpResponse
  12. from django.test import (
  13. AsyncRequestFactory,
  14. SimpleTestCase,
  15. ignore_warnings,
  16. modify_settings,
  17. override_settings,
  18. )
  19. from django.urls import path
  20. from django.utils.http import http_date
  21. from .urls import sync_waiter, test_filename
  22. TEST_STATIC_ROOT = Path(__file__).parent / "project" / "static"
  23. @override_settings(ROOT_URLCONF="asgi.urls")
  24. class ASGITest(SimpleTestCase):
  25. async_request_factory = AsyncRequestFactory()
  26. def setUp(self):
  27. request_started.disconnect(close_old_connections)
  28. def tearDown(self):
  29. request_started.connect(close_old_connections)
  30. async def test_get_asgi_application(self):
  31. """
  32. get_asgi_application() returns a functioning ASGI callable.
  33. """
  34. application = get_asgi_application()
  35. # Construct HTTP request.
  36. scope = self.async_request_factory._base_scope(path="/")
  37. communicator = ApplicationCommunicator(application, scope)
  38. await communicator.send_input({"type": "http.request"})
  39. # Read the response.
  40. response_start = await communicator.receive_output()
  41. self.assertEqual(response_start["type"], "http.response.start")
  42. self.assertEqual(response_start["status"], 200)
  43. self.assertEqual(
  44. set(response_start["headers"]),
  45. {
  46. (b"Content-Length", b"12"),
  47. (b"Content-Type", b"text/html; charset=utf-8"),
  48. },
  49. )
  50. response_body = await communicator.receive_output()
  51. self.assertEqual(response_body["type"], "http.response.body")
  52. self.assertEqual(response_body["body"], b"Hello World!")
  53. # Allow response.close() to finish.
  54. await communicator.wait()
  55. # Python's file API is not async compatible. A third-party library such
  56. # as https://github.com/Tinche/aiofiles allows passing the file to
  57. # FileResponse as an async iterator. With a sync iterator
  58. # StreamingHTTPResponse triggers a warning when iterating the file.
  59. # assertWarnsMessage is not async compatible, so ignore_warnings for the
  60. # test.
  61. @ignore_warnings(module="django.http.response")
  62. async def test_file_response(self):
  63. """
  64. Makes sure that FileResponse works over ASGI.
  65. """
  66. application = get_asgi_application()
  67. # Construct HTTP request.
  68. scope = self.async_request_factory._base_scope(path="/file/")
  69. communicator = ApplicationCommunicator(application, scope)
  70. await communicator.send_input({"type": "http.request"})
  71. # Get the file content.
  72. with open(test_filename, "rb") as test_file:
  73. test_file_contents = test_file.read()
  74. # Read the response.
  75. response_start = await communicator.receive_output()
  76. self.assertEqual(response_start["type"], "http.response.start")
  77. self.assertEqual(response_start["status"], 200)
  78. headers = response_start["headers"]
  79. self.assertEqual(len(headers), 3)
  80. expected_headers = {
  81. b"Content-Length": str(len(test_file_contents)).encode("ascii"),
  82. b"Content-Type": b"text/x-python",
  83. b"Content-Disposition": b'inline; filename="urls.py"',
  84. }
  85. for key, value in headers:
  86. try:
  87. self.assertEqual(value, expected_headers[key])
  88. except AssertionError:
  89. # Windows registry may not be configured with correct
  90. # mimetypes.
  91. if sys.platform == "win32" and key == b"Content-Type":
  92. self.assertEqual(value, b"text/plain")
  93. else:
  94. raise
  95. # Warning ignored here.
  96. response_body = await communicator.receive_output()
  97. self.assertEqual(response_body["type"], "http.response.body")
  98. self.assertEqual(response_body["body"], test_file_contents)
  99. # Allow response.close() to finish.
  100. await communicator.wait()
  101. @modify_settings(INSTALLED_APPS={"append": "django.contrib.staticfiles"})
  102. @override_settings(
  103. STATIC_URL="static/",
  104. STATIC_ROOT=TEST_STATIC_ROOT,
  105. STATICFILES_DIRS=[TEST_STATIC_ROOT],
  106. STATICFILES_FINDERS=[
  107. "django.contrib.staticfiles.finders.FileSystemFinder",
  108. ],
  109. )
  110. async def test_static_file_response(self):
  111. application = ASGIStaticFilesHandler(get_asgi_application())
  112. # Construct HTTP request.
  113. scope = self.async_request_factory._base_scope(path="/static/file.txt")
  114. communicator = ApplicationCommunicator(application, scope)
  115. await communicator.send_input({"type": "http.request"})
  116. # Get the file content.
  117. file_path = TEST_STATIC_ROOT / "file.txt"
  118. with open(file_path, "rb") as test_file:
  119. test_file_contents = test_file.read()
  120. # Read the response.
  121. stat = file_path.stat()
  122. response_start = await communicator.receive_output()
  123. self.assertEqual(response_start["type"], "http.response.start")
  124. self.assertEqual(response_start["status"], 200)
  125. self.assertEqual(
  126. set(response_start["headers"]),
  127. {
  128. (b"Content-Length", str(len(test_file_contents)).encode("ascii")),
  129. (b"Content-Type", b"text/plain"),
  130. (b"Content-Disposition", b'inline; filename="file.txt"'),
  131. (b"Last-Modified", http_date(stat.st_mtime).encode("ascii")),
  132. },
  133. )
  134. response_body = await communicator.receive_output()
  135. self.assertEqual(response_body["type"], "http.response.body")
  136. self.assertEqual(response_body["body"], test_file_contents)
  137. # Allow response.close() to finish.
  138. await communicator.wait()
  139. async def test_headers(self):
  140. application = get_asgi_application()
  141. communicator = ApplicationCommunicator(
  142. application,
  143. self.async_request_factory._base_scope(
  144. path="/meta/",
  145. headers=[
  146. [b"content-type", b"text/plain; charset=utf-8"],
  147. [b"content-length", b"77"],
  148. [b"referer", b"Scotland"],
  149. [b"referer", b"Wales"],
  150. ],
  151. ),
  152. )
  153. await communicator.send_input({"type": "http.request"})
  154. response_start = await communicator.receive_output()
  155. self.assertEqual(response_start["type"], "http.response.start")
  156. self.assertEqual(response_start["status"], 200)
  157. self.assertEqual(
  158. set(response_start["headers"]),
  159. {
  160. (b"Content-Length", b"19"),
  161. (b"Content-Type", b"text/plain; charset=utf-8"),
  162. },
  163. )
  164. response_body = await communicator.receive_output()
  165. self.assertEqual(response_body["type"], "http.response.body")
  166. self.assertEqual(response_body["body"], b"From Scotland,Wales")
  167. # Allow response.close() to finish
  168. await communicator.wait()
  169. async def test_post_body(self):
  170. application = get_asgi_application()
  171. scope = self.async_request_factory._base_scope(
  172. method="POST",
  173. path="/post/",
  174. query_string="echo=1",
  175. )
  176. communicator = ApplicationCommunicator(application, scope)
  177. await communicator.send_input({"type": "http.request", "body": b"Echo!"})
  178. response_start = await communicator.receive_output()
  179. self.assertEqual(response_start["type"], "http.response.start")
  180. self.assertEqual(response_start["status"], 200)
  181. response_body = await communicator.receive_output()
  182. self.assertEqual(response_body["type"], "http.response.body")
  183. self.assertEqual(response_body["body"], b"Echo!")
  184. async def test_untouched_request_body_gets_closed(self):
  185. application = get_asgi_application()
  186. scope = self.async_request_factory._base_scope(method="POST", path="/post/")
  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"], 204)
  192. response_body = await communicator.receive_output()
  193. self.assertEqual(response_body["type"], "http.response.body")
  194. self.assertEqual(response_body["body"], b"")
  195. # Allow response.close() to finish
  196. await communicator.wait()
  197. async def test_get_query_string(self):
  198. application = get_asgi_application()
  199. for query_string in (b"name=Andrew", "name=Andrew"):
  200. with self.subTest(query_string=query_string):
  201. scope = self.async_request_factory._base_scope(
  202. path="/",
  203. query_string=query_string,
  204. )
  205. communicator = ApplicationCommunicator(application, scope)
  206. await communicator.send_input({"type": "http.request"})
  207. response_start = await communicator.receive_output()
  208. self.assertEqual(response_start["type"], "http.response.start")
  209. self.assertEqual(response_start["status"], 200)
  210. response_body = await communicator.receive_output()
  211. self.assertEqual(response_body["type"], "http.response.body")
  212. self.assertEqual(response_body["body"], b"Hello Andrew!")
  213. # Allow response.close() to finish
  214. await communicator.wait()
  215. async def test_disconnect(self):
  216. application = get_asgi_application()
  217. scope = self.async_request_factory._base_scope(path="/")
  218. communicator = ApplicationCommunicator(application, scope)
  219. await communicator.send_input({"type": "http.disconnect"})
  220. with self.assertRaises(asyncio.TimeoutError):
  221. await communicator.receive_output()
  222. async def test_disconnect_with_body(self):
  223. application = get_asgi_application()
  224. scope = self.async_request_factory._base_scope(path="/")
  225. communicator = ApplicationCommunicator(application, scope)
  226. await communicator.send_input({"type": "http.request", "body": b"some body"})
  227. await communicator.send_input({"type": "http.disconnect"})
  228. with self.assertRaises(asyncio.TimeoutError):
  229. await communicator.receive_output()
  230. async def test_assert_in_listen_for_disconnect(self):
  231. application = get_asgi_application()
  232. scope = self.async_request_factory._base_scope(path="/")
  233. communicator = ApplicationCommunicator(application, scope)
  234. await communicator.send_input({"type": "http.request"})
  235. await communicator.send_input({"type": "http.not_a_real_message"})
  236. msg = "Invalid ASGI message after request body: http.not_a_real_message"
  237. with self.assertRaisesMessage(AssertionError, msg):
  238. await communicator.receive_output()
  239. async def test_delayed_disconnect_with_body(self):
  240. application = get_asgi_application()
  241. scope = self.async_request_factory._base_scope(path="/delayed_hello/")
  242. communicator = ApplicationCommunicator(application, scope)
  243. await communicator.send_input({"type": "http.request", "body": b"some body"})
  244. await communicator.send_input({"type": "http.disconnect"})
  245. with self.assertRaises(asyncio.TimeoutError):
  246. await communicator.receive_output()
  247. async def test_wrong_connection_type(self):
  248. application = get_asgi_application()
  249. scope = self.async_request_factory._base_scope(path="/", type="other")
  250. communicator = ApplicationCommunicator(application, scope)
  251. await communicator.send_input({"type": "http.request"})
  252. msg = "Django can only handle ASGI/HTTP connections, not other."
  253. with self.assertRaisesMessage(ValueError, msg):
  254. await communicator.receive_output()
  255. async def test_non_unicode_query_string(self):
  256. application = get_asgi_application()
  257. scope = self.async_request_factory._base_scope(path="/", query_string=b"\xff")
  258. communicator = ApplicationCommunicator(application, scope)
  259. await communicator.send_input({"type": "http.request"})
  260. response_start = await communicator.receive_output()
  261. self.assertEqual(response_start["type"], "http.response.start")
  262. self.assertEqual(response_start["status"], 400)
  263. response_body = await communicator.receive_output()
  264. self.assertEqual(response_body["type"], "http.response.body")
  265. self.assertEqual(response_body["body"], b"")
  266. async def test_request_lifecycle_signals_dispatched_with_thread_sensitive(self):
  267. class SignalHandler:
  268. """Track threads handler is dispatched on."""
  269. threads = []
  270. def __call__(self, **kwargs):
  271. self.threads.append(threading.current_thread())
  272. signal_handler = SignalHandler()
  273. request_started.connect(signal_handler)
  274. request_finished.connect(signal_handler)
  275. # Perform a basic request.
  276. application = get_asgi_application()
  277. scope = self.async_request_factory._base_scope(path="/")
  278. communicator = ApplicationCommunicator(application, scope)
  279. await communicator.send_input({"type": "http.request"})
  280. response_start = await communicator.receive_output()
  281. self.assertEqual(response_start["type"], "http.response.start")
  282. self.assertEqual(response_start["status"], 200)
  283. response_body = await communicator.receive_output()
  284. self.assertEqual(response_body["type"], "http.response.body")
  285. self.assertEqual(response_body["body"], b"Hello World!")
  286. # Give response.close() time to finish.
  287. await communicator.wait()
  288. # AsyncToSync should have executed the signals in the same thread.
  289. request_started_thread, request_finished_thread = signal_handler.threads
  290. self.assertEqual(request_started_thread, request_finished_thread)
  291. request_started.disconnect(signal_handler)
  292. request_finished.disconnect(signal_handler)
  293. async def test_concurrent_async_uses_multiple_thread_pools(self):
  294. sync_waiter.active_threads.clear()
  295. # Send 2 requests concurrently
  296. application = get_asgi_application()
  297. scope = self.async_request_factory._base_scope(path="/wait/")
  298. communicators = []
  299. for _ in range(2):
  300. communicators.append(ApplicationCommunicator(application, scope))
  301. await communicators[-1].send_input({"type": "http.request"})
  302. # Each request must complete with a status code of 200
  303. # If requests aren't scheduled concurrently, the barrier in the
  304. # sync_wait view will time out, resulting in a 500 status code.
  305. for communicator in communicators:
  306. response_start = await communicator.receive_output()
  307. self.assertEqual(response_start["type"], "http.response.start")
  308. self.assertEqual(response_start["status"], 200)
  309. response_body = await communicator.receive_output()
  310. self.assertEqual(response_body["type"], "http.response.body")
  311. self.assertEqual(response_body["body"], b"Hello World!")
  312. # Give response.close() time to finish.
  313. await communicator.wait()
  314. # The requests should have scheduled on different threads. Note
  315. # active_threads is a set (a thread can only appear once), therefore
  316. # length is a sufficient check.
  317. self.assertEqual(len(sync_waiter.active_threads), 2)
  318. sync_waiter.active_threads.clear()
  319. async def test_asyncio_cancel_error(self):
  320. # Flag to check if the view was cancelled.
  321. view_did_cancel = False
  322. # A view that will listen for the cancelled error.
  323. async def view(request):
  324. nonlocal view_did_cancel
  325. try:
  326. await asyncio.sleep(0.2)
  327. return HttpResponse("Hello World!")
  328. except asyncio.CancelledError:
  329. # Set the flag.
  330. view_did_cancel = True
  331. raise
  332. # Request class to use the view.
  333. class TestASGIRequest(ASGIRequest):
  334. urlconf = (path("cancel/", view),)
  335. # Handler to use request class.
  336. class TestASGIHandler(ASGIHandler):
  337. request_class = TestASGIRequest
  338. # Request cycle should complete since no disconnect was sent.
  339. application = TestASGIHandler()
  340. scope = self.async_request_factory._base_scope(path="/cancel/")
  341. communicator = ApplicationCommunicator(application, scope)
  342. await communicator.send_input({"type": "http.request"})
  343. response_start = await communicator.receive_output()
  344. self.assertEqual(response_start["type"], "http.response.start")
  345. self.assertEqual(response_start["status"], 200)
  346. response_body = await communicator.receive_output()
  347. self.assertEqual(response_body["type"], "http.response.body")
  348. self.assertEqual(response_body["body"], b"Hello World!")
  349. # Give response.close() time to finish.
  350. await communicator.wait()
  351. self.assertIs(view_did_cancel, False)
  352. # Request cycle with a disconnect before the view can respond.
  353. application = TestASGIHandler()
  354. scope = self.async_request_factory._base_scope(path="/cancel/")
  355. communicator = ApplicationCommunicator(application, scope)
  356. await communicator.send_input({"type": "http.request"})
  357. # Let the view actually start.
  358. await asyncio.sleep(0.1)
  359. # Disconnect the client.
  360. await communicator.send_input({"type": "http.disconnect"})
  361. # The handler should not send a response.
  362. with self.assertRaises(asyncio.TimeoutError):
  363. await communicator.receive_output()
  364. await communicator.wait()
  365. self.assertIs(view_did_cancel, True)