summaryrefslogtreecommitdiff
path: root/tests/asgi
diff options
context:
space:
mode:
authorth3nn3ss <chuksmcdennis@yahoo.com>2022-12-21 14:25:24 -0500
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-04-03 14:01:48 +0200
commit1d1ddffc27cd55c011298cd09bfa4de3fa73cf7a (patch)
treec77c385a04bb16b84bab217258356cc480a85abc /tests/asgi
parent4e4eda6d6c8a5867dafd2ba9167ad8c064bb644a (diff)
downloaddjango-1d1ddffc27cd55c011298cd09bfa4de3fa73cf7a.tar.gz
Fixed #33738 -- Allowed handling ASGI http.disconnect in long-lived requests.
Diffstat (limited to 'tests/asgi')
-rw-r--r--tests/asgi/tests.py84
-rw-r--r--tests/asgi/urls.py8
2 files changed, 92 insertions, 0 deletions
diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py
index fc22a992a7..0222b5356e 100644
--- a/tests/asgi/tests.py
+++ b/tests/asgi/tests.py
@@ -7,8 +7,10 @@ from asgiref.testing import ApplicationCommunicator
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
from django.core.asgi import get_asgi_application
+from django.core.handlers.asgi import ASGIHandler, ASGIRequest
from django.core.signals import request_finished, request_started
from django.db import close_old_connections
+from django.http import HttpResponse
from django.test import (
AsyncRequestFactory,
SimpleTestCase,
@@ -16,6 +18,7 @@ from django.test import (
modify_settings,
override_settings,
)
+from django.urls import path
from django.utils.http import http_date
from .urls import sync_waiter, test_filename
@@ -234,6 +237,34 @@ class ASGITest(SimpleTestCase):
with self.assertRaises(asyncio.TimeoutError):
await communicator.receive_output()
+ async def test_disconnect_with_body(self):
+ application = get_asgi_application()
+ scope = self.async_request_factory._base_scope(path="/")
+ communicator = ApplicationCommunicator(application, scope)
+ await communicator.send_input({"type": "http.request", "body": b"some body"})
+ await communicator.send_input({"type": "http.disconnect"})
+ with self.assertRaises(asyncio.TimeoutError):
+ await communicator.receive_output()
+
+ async def test_assert_in_listen_for_disconnect(self):
+ application = get_asgi_application()
+ scope = self.async_request_factory._base_scope(path="/")
+ communicator = ApplicationCommunicator(application, scope)
+ await communicator.send_input({"type": "http.request"})
+ await communicator.send_input({"type": "http.not_a_real_message"})
+ msg = "Invalid ASGI message after request body: http.not_a_real_message"
+ with self.assertRaisesMessage(AssertionError, msg):
+ await communicator.receive_output()
+
+ async def test_delayed_disconnect_with_body(self):
+ application = get_asgi_application()
+ scope = self.async_request_factory._base_scope(path="/delayed_hello/")
+ communicator = ApplicationCommunicator(application, scope)
+ await communicator.send_input({"type": "http.request", "body": b"some body"})
+ await communicator.send_input({"type": "http.disconnect"})
+ with self.assertRaises(asyncio.TimeoutError):
+ await communicator.receive_output()
+
async def test_wrong_connection_type(self):
application = get_asgi_application()
scope = self.async_request_factory._base_scope(path="/", type="other")
@@ -318,3 +349,56 @@ class ASGITest(SimpleTestCase):
self.assertEqual(len(sync_waiter.active_threads), 2)
sync_waiter.active_threads.clear()
+
+ async def test_asyncio_cancel_error(self):
+ # Flag to check if the view was cancelled.
+ view_did_cancel = False
+
+ # A view that will listen for the cancelled error.
+ async def view(request):
+ nonlocal view_did_cancel
+ try:
+ await asyncio.sleep(0.2)
+ return HttpResponse("Hello World!")
+ except asyncio.CancelledError:
+ # Set the flag.
+ view_did_cancel = True
+ raise
+
+ # Request class to use the view.
+ class TestASGIRequest(ASGIRequest):
+ urlconf = (path("cancel/", view),)
+
+ # Handler to use request class.
+ class TestASGIHandler(ASGIHandler):
+ request_class = TestASGIRequest
+
+ # Request cycle should complete since no disconnect was sent.
+ application = TestASGIHandler()
+ scope = self.async_request_factory._base_scope(path="/cancel/")
+ communicator = ApplicationCommunicator(application, scope)
+ await communicator.send_input({"type": "http.request"})
+ response_start = await communicator.receive_output()
+ self.assertEqual(response_start["type"], "http.response.start")
+ self.assertEqual(response_start["status"], 200)
+ response_body = await communicator.receive_output()
+ self.assertEqual(response_body["type"], "http.response.body")
+ self.assertEqual(response_body["body"], b"Hello World!")
+ # Give response.close() time to finish.
+ await communicator.wait()
+ self.assertIs(view_did_cancel, False)
+
+ # Request cycle with a disconnect before the view can respond.
+ application = TestASGIHandler()
+ scope = self.async_request_factory._base_scope(path="/cancel/")
+ communicator = ApplicationCommunicator(application, scope)
+ await communicator.send_input({"type": "http.request"})
+ # Let the view actually start.
+ await asyncio.sleep(0.1)
+ # Disconnect the client.
+ await communicator.send_input({"type": "http.disconnect"})
+ # The handler should not send a response.
+ with self.assertRaises(asyncio.TimeoutError):
+ await communicator.receive_output()
+ await communicator.wait()
+ self.assertIs(view_did_cancel, True)
diff --git a/tests/asgi/urls.py b/tests/asgi/urls.py
index 34595c1b6c..0f74fc9b97 100644
--- a/tests/asgi/urls.py
+++ b/tests/asgi/urls.py
@@ -1,4 +1,5 @@
import threading
+import time
from django.http import FileResponse, HttpResponse
from django.urls import path
@@ -10,6 +11,12 @@ def hello(request):
return HttpResponse("Hello %s!" % name)
+def hello_with_delay(request):
+ name = request.GET.get("name") or "World"
+ time.sleep(1)
+ return HttpResponse(f"Hello {name}!")
+
+
def hello_meta(request):
return HttpResponse(
"From %s" % request.META.get("HTTP_REFERER") or "",
@@ -46,4 +53,5 @@ urlpatterns = [
path("meta/", hello_meta),
path("post/", post_echo),
path("wait/", sync_waiter),
+ path("delayed_hello/", hello_with_delay),
]