From fc0fa72ff4cdbf5861a366e31cb8bbacd44da22d Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Wed, 12 Feb 2020 15:15:00 -0700 Subject: Fixed #31224 -- Added support for asynchronous views and middleware. This implements support for asynchronous views, asynchronous tests, asynchronous middleware, and an asynchronous test client. --- tests/middleware_exceptions/middleware.py | 64 ++++++++++++ tests/middleware_exceptions/tests.py | 159 ++++++++++++++++++++++++++++++ tests/middleware_exceptions/urls.py | 5 + tests/middleware_exceptions/views.py | 8 ++ 4 files changed, 236 insertions(+) (limited to 'tests/middleware_exceptions') diff --git a/tests/middleware_exceptions/middleware.py b/tests/middleware_exceptions/middleware.py index 63502c6902..69c6db57e7 100644 --- a/tests/middleware_exceptions/middleware.py +++ b/tests/middleware_exceptions/middleware.py @@ -1,6 +1,9 @@ from django.http import Http404, HttpResponse from django.template import engines from django.template.response import TemplateResponse +from django.utils.decorators import ( + async_only_middleware, sync_and_async_middleware, sync_only_middleware, +) log = [] @@ -18,6 +21,12 @@ class ProcessExceptionMiddleware(BaseMiddleware): return HttpResponse('Exception caught') +@async_only_middleware +class AsyncProcessExceptionMiddleware(BaseMiddleware): + async def process_exception(self, request, exception): + return HttpResponse('Exception caught') + + class ProcessExceptionLogMiddleware(BaseMiddleware): def process_exception(self, request, exception): log.append('process-exception') @@ -33,6 +42,12 @@ class ProcessViewMiddleware(BaseMiddleware): return HttpResponse('Processed view %s' % view_func.__name__) +@async_only_middleware +class AsyncProcessViewMiddleware(BaseMiddleware): + async def process_view(self, request, view_func, view_args, view_kwargs): + return HttpResponse('Processed view %s' % view_func.__name__) + + class ProcessViewNoneMiddleware(BaseMiddleware): def process_view(self, request, view_func, view_args, view_kwargs): log.append('processed view %s' % view_func.__name__) @@ -51,6 +66,13 @@ class TemplateResponseMiddleware(BaseMiddleware): return response +@async_only_middleware +class AsyncTemplateResponseMiddleware(BaseMiddleware): + async def process_template_response(self, request, response): + response.context_data['mw'].append(self.__class__.__name__) + return response + + class LogMiddleware(BaseMiddleware): def __call__(self, request): response = self.get_response(request) @@ -63,6 +85,48 @@ class NoTemplateResponseMiddleware(BaseMiddleware): return None +@async_only_middleware +class AsyncNoTemplateResponseMiddleware(BaseMiddleware): + async def process_template_response(self, request, response): + return None + + class NotFoundMiddleware(BaseMiddleware): def __call__(self, request): raise Http404('not found') + + +class TeapotMiddleware(BaseMiddleware): + def __call__(self, request): + response = self.get_response(request) + response.status_code = 418 + return response + + +@async_only_middleware +def async_teapot_middleware(get_response): + async def middleware(request): + response = await get_response(request) + response.status_code = 418 + return response + + return middleware + + +@sync_and_async_middleware +class SyncAndAsyncMiddleware(BaseMiddleware): + pass + + +@sync_only_middleware +class DecoratedTeapotMiddleware(TeapotMiddleware): + pass + + +class NotSyncOrAsyncMiddleware(BaseMiddleware): + """Middleware that is deliberately neither sync or async.""" + sync_capable = False + async_capable = False + + def __call__(self, request): + return self.get_response(request) diff --git a/tests/middleware_exceptions/tests.py b/tests/middleware_exceptions/tests.py index 3e614ae0de..697841a35d 100644 --- a/tests/middleware_exceptions/tests.py +++ b/tests/middleware_exceptions/tests.py @@ -180,3 +180,162 @@ class MiddlewareNotUsedTests(SimpleTestCase): with self.assertRaisesMessage(AssertionError, 'no logs'): with self.assertLogs('django.request', 'DEBUG'): self.client.get('/middleware_exceptions/view/') + + +@override_settings( + DEBUG=True, + ROOT_URLCONF='middleware_exceptions.urls', +) +class MiddlewareSyncAsyncTests(SimpleTestCase): + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.TeapotMiddleware', + ]) + def test_sync_teapot_middleware(self): + response = self.client.get('/middleware_exceptions/view/') + self.assertEqual(response.status_code, 418) + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.DecoratedTeapotMiddleware', + ]) + def test_sync_decorated_teapot_middleware(self): + response = self.client.get('/middleware_exceptions/view/') + self.assertEqual(response.status_code, 418) + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.async_teapot_middleware', + ]) + def test_async_teapot_middleware(self): + with self.assertLogs('django.request', 'DEBUG') as cm: + response = self.client.get('/middleware_exceptions/view/') + self.assertEqual(response.status_code, 418) + self.assertEqual( + cm.records[0].getMessage(), + "Synchronous middleware " + "middleware_exceptions.middleware.async_teapot_middleware " + "adapted.", + ) + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.NotSyncOrAsyncMiddleware', + ]) + def test_not_sync_or_async_middleware(self): + msg = ( + 'Middleware ' + 'middleware_exceptions.middleware.NotSyncOrAsyncMiddleware must ' + 'have at least one of sync_capable/async_capable set to True.' + ) + with self.assertRaisesMessage(RuntimeError, msg): + self.client.get('/middleware_exceptions/view/') + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.TeapotMiddleware', + ]) + async def test_sync_teapot_middleware_async(self): + with self.assertLogs('django.request', 'DEBUG') as cm: + response = await self.async_client.get('/middleware_exceptions/view/') + self.assertEqual(response.status_code, 418) + self.assertEqual( + cm.records[0].getMessage(), + "Asynchronous middleware " + "middleware_exceptions.middleware.TeapotMiddleware adapted.", + ) + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.async_teapot_middleware', + ]) + async def test_async_teapot_middleware_async(self): + with self.assertLogs('django.request', 'WARNING') as cm: + response = await self.async_client.get('/middleware_exceptions/view/') + self.assertEqual(response.status_code, 418) + self.assertEqual( + cm.records[0].getMessage(), + 'Unknown Status Code: /middleware_exceptions/view/', + ) + + @override_settings( + DEBUG=False, + MIDDLEWARE=[ + 'middleware_exceptions.middleware.AsyncNoTemplateResponseMiddleware', + ], + ) + def test_async_process_template_response_returns_none_with_sync_client(self): + msg = ( + "AsyncNoTemplateResponseMiddleware.process_template_response " + "didn't return an HttpResponse object." + ) + with self.assertRaisesMessage(ValueError, msg): + self.client.get('/middleware_exceptions/template_response/') + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.SyncAndAsyncMiddleware', + ]) + async def test_async_and_sync_middleware_async_call(self): + response = await self.async_client.get('/middleware_exceptions/view/') + self.assertEqual(response.content, b'OK') + self.assertEqual(response.status_code, 200) + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.SyncAndAsyncMiddleware', + ]) + def test_async_and_sync_middleware_sync_call(self): + response = self.client.get('/middleware_exceptions/view/') + self.assertEqual(response.content, b'OK') + self.assertEqual(response.status_code, 200) + + +@override_settings(ROOT_URLCONF='middleware_exceptions.urls') +class AsyncMiddlewareTests(SimpleTestCase): + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.AsyncTemplateResponseMiddleware', + ]) + async def test_process_template_response(self): + response = await self.async_client.get( + '/middleware_exceptions/template_response/' + ) + self.assertEqual( + response.content, + b'template_response OK\nAsyncTemplateResponseMiddleware', + ) + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.AsyncNoTemplateResponseMiddleware', + ]) + async def test_process_template_response_returns_none(self): + msg = ( + "AsyncNoTemplateResponseMiddleware.process_template_response " + "didn't return an HttpResponse object. It returned None instead." + ) + with self.assertRaisesMessage(ValueError, msg): + await self.async_client.get('/middleware_exceptions/template_response/') + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.AsyncProcessExceptionMiddleware', + ]) + async def test_exception_in_render_passed_to_process_exception(self): + response = await self.async_client.get( + '/middleware_exceptions/exception_in_render/' + ) + self.assertEqual(response.content, b'Exception caught') + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.AsyncProcessExceptionMiddleware', + ]) + async def test_exception_in_async_render_passed_to_process_exception(self): + response = await self.async_client.get( + '/middleware_exceptions/async_exception_in_render/' + ) + self.assertEqual(response.content, b'Exception caught') + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.AsyncProcessExceptionMiddleware', + ]) + async def test_view_exception_handled_by_process_exception(self): + response = await self.async_client.get('/middleware_exceptions/error/') + self.assertEqual(response.content, b'Exception caught') + + @override_settings(MIDDLEWARE=[ + 'middleware_exceptions.middleware.AsyncProcessViewMiddleware', + ]) + async def test_process_view_return_response(self): + response = await self.async_client.get('/middleware_exceptions/view/') + self.assertEqual(response.content, b'Processed view normal_view') diff --git a/tests/middleware_exceptions/urls.py b/tests/middleware_exceptions/urls.py index 46332916b6..d676ef470c 100644 --- a/tests/middleware_exceptions/urls.py +++ b/tests/middleware_exceptions/urls.py @@ -8,4 +8,9 @@ urlpatterns = [ path('middleware_exceptions/permission_denied/', views.permission_denied), path('middleware_exceptions/exception_in_render/', views.exception_in_render), path('middleware_exceptions/template_response/', views.template_response), + # Async views. + path( + 'middleware_exceptions/async_exception_in_render/', + views.async_exception_in_render, + ), ] diff --git a/tests/middleware_exceptions/views.py b/tests/middleware_exceptions/views.py index 3ae54081ab..7a1d244863 100644 --- a/tests/middleware_exceptions/views.py +++ b/tests/middleware_exceptions/views.py @@ -27,3 +27,11 @@ def exception_in_render(request): raise Exception('Exception in HttpResponse.render()') return CustomHttpResponse('Error') + + +async def async_exception_in_render(request): + class CustomHttpResponse(HttpResponse): + async def render(self): + raise Exception('Exception in HttpResponse.render()') + + return CustomHttpResponse('Error') -- cgit v1.3