2022-04-07 05:05:59 +00:00
|
|
|
import asyncio
|
2019-12-02 20:02:21 +00:00
|
|
|
import os
|
2022-04-14 08:43:34 +00:00
|
|
|
from unittest import mock
|
2019-10-24 08:43:05 +00:00
|
|
|
|
2022-12-20 10:10:48 +00:00
|
|
|
from asgiref.sync import async_to_sync, iscoroutinefunction
|
2019-04-12 13:15:18 +00:00
|
|
|
|
2020-02-07 01:59:20 +00:00
|
|
|
from django.core.cache import DEFAULT_CACHE_ALIAS, caches
|
2022-04-07 05:05:59 +00:00
|
|
|
from django.core.exceptions import ImproperlyConfigured, SynchronousOnlyOperation
|
2022-09-29 10:10:02 +00:00
|
|
|
from django.http import HttpResponse, HttpResponseNotAllowed
|
|
|
|
from django.test import RequestFactory, SimpleTestCase
|
2019-04-12 13:15:18 +00:00
|
|
|
from django.utils.asyncio import async_unsafe
|
2022-04-07 05:05:59 +00:00
|
|
|
from django.views.generic.base import View
|
2019-04-12 13:15:18 +00:00
|
|
|
|
|
|
|
from .models import SimpleModel
|
|
|
|
|
|
|
|
|
2020-02-07 01:59:20 +00:00
|
|
|
class CacheTest(SimpleTestCase):
|
|
|
|
def test_caches_local(self):
|
|
|
|
@async_to_sync
|
|
|
|
async def async_cache():
|
|
|
|
return caches[DEFAULT_CACHE_ALIAS]
|
|
|
|
|
|
|
|
cache_1 = async_cache()
|
|
|
|
cache_2 = async_cache()
|
|
|
|
self.assertIs(cache_1, cache_2)
|
|
|
|
|
|
|
|
|
2019-04-12 13:15:18 +00:00
|
|
|
class DatabaseConnectionTest(SimpleTestCase):
|
|
|
|
"""A database connection cannot be used in an async context."""
|
2022-02-03 19:24:19 +00:00
|
|
|
|
2019-04-12 13:15:18 +00:00
|
|
|
async def test_get_async_connection(self):
|
|
|
|
with self.assertRaises(SynchronousOnlyOperation):
|
|
|
|
list(SimpleModel.objects.all())
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncUnsafeTest(SimpleTestCase):
|
|
|
|
"""
|
|
|
|
async_unsafe decorator should work correctly and returns the correct
|
|
|
|
message.
|
|
|
|
"""
|
2022-02-03 19:24:19 +00:00
|
|
|
|
2019-04-12 13:15:18 +00:00
|
|
|
@async_unsafe
|
|
|
|
def dangerous_method(self):
|
|
|
|
return True
|
|
|
|
|
|
|
|
async def test_async_unsafe(self):
|
|
|
|
# async_unsafe decorator catches bad access and returns the right
|
|
|
|
# message.
|
|
|
|
msg = (
|
2022-02-03 19:24:19 +00:00
|
|
|
"You cannot call this from an async context - use a thread or "
|
|
|
|
"sync_to_async."
|
2019-04-12 13:15:18 +00:00
|
|
|
)
|
|
|
|
with self.assertRaisesMessage(SynchronousOnlyOperation, msg):
|
|
|
|
self.dangerous_method()
|
2019-12-02 20:02:21 +00:00
|
|
|
|
2022-02-03 19:24:19 +00:00
|
|
|
@mock.patch.dict(os.environ, {"DJANGO_ALLOW_ASYNC_UNSAFE": "true"})
|
2020-03-05 13:26:33 +00:00
|
|
|
@async_to_sync # mock.patch() is not async-aware.
|
2019-12-02 20:02:21 +00:00
|
|
|
async def test_async_unsafe_suppressed(self):
|
|
|
|
# Decorator doesn't trigger check when the environment variable to
|
|
|
|
# suppress it is set.
|
|
|
|
try:
|
|
|
|
self.dangerous_method()
|
|
|
|
except SynchronousOnlyOperation:
|
2022-02-03 19:24:19 +00:00
|
|
|
self.fail("SynchronousOnlyOperation should not be raised.")
|
2022-04-07 05:05:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SyncView(View):
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
return HttpResponse("Hello (sync) world!")
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncView(View):
|
|
|
|
async def get(self, request, *args, **kwargs):
|
|
|
|
return HttpResponse("Hello (async) world!")
|
|
|
|
|
|
|
|
|
|
|
|
class ViewTests(SimpleTestCase):
|
|
|
|
def test_views_are_correctly_marked(self):
|
|
|
|
tests = [
|
|
|
|
(SyncView, False),
|
|
|
|
(AsyncView, True),
|
|
|
|
]
|
|
|
|
for view_cls, is_async in tests:
|
|
|
|
with self.subTest(view_cls=view_cls, is_async=is_async):
|
|
|
|
self.assertIs(view_cls.view_is_async, is_async)
|
|
|
|
callback = view_cls.as_view()
|
2022-12-20 10:10:48 +00:00
|
|
|
self.assertIs(iscoroutinefunction(callback), is_async)
|
2022-04-07 05:05:59 +00:00
|
|
|
|
|
|
|
def test_mixed_views_raise_error(self):
|
|
|
|
class MixedView(View):
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
return HttpResponse("Hello (mixed) world!")
|
|
|
|
|
|
|
|
async def post(self, request, *args, **kwargs):
|
|
|
|
return HttpResponse("Hello (mixed) world!")
|
|
|
|
|
|
|
|
msg = (
|
|
|
|
f"{MixedView.__qualname__} HTTP handlers must either be all sync or all "
|
|
|
|
"async."
|
|
|
|
)
|
|
|
|
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
|
|
|
MixedView.as_view()
|
|
|
|
|
|
|
|
def test_options_handler_responds_correctly(self):
|
|
|
|
tests = [
|
|
|
|
(SyncView, False),
|
|
|
|
(AsyncView, True),
|
|
|
|
]
|
|
|
|
for view_cls, is_coroutine in tests:
|
|
|
|
with self.subTest(view_cls=view_cls, is_coroutine=is_coroutine):
|
|
|
|
instance = view_cls()
|
|
|
|
response = instance.options(None)
|
|
|
|
self.assertIs(
|
|
|
|
asyncio.iscoroutine(response),
|
|
|
|
is_coroutine,
|
|
|
|
)
|
|
|
|
if is_coroutine:
|
|
|
|
response = asyncio.run(response)
|
|
|
|
|
|
|
|
self.assertIsInstance(response, HttpResponse)
|
|
|
|
|
2022-09-29 10:10:02 +00:00
|
|
|
def test_http_method_not_allowed_responds_correctly(self):
|
|
|
|
request_factory = RequestFactory()
|
|
|
|
tests = [
|
|
|
|
(SyncView, False),
|
|
|
|
(AsyncView, True),
|
|
|
|
]
|
|
|
|
for view_cls, is_coroutine in tests:
|
|
|
|
with self.subTest(view_cls=view_cls, is_coroutine=is_coroutine):
|
|
|
|
instance = view_cls()
|
|
|
|
response = instance.http_method_not_allowed(request_factory.post("/"))
|
|
|
|
self.assertIs(
|
|
|
|
asyncio.iscoroutine(response),
|
|
|
|
is_coroutine,
|
|
|
|
)
|
|
|
|
if is_coroutine:
|
|
|
|
response = asyncio.run(response)
|
|
|
|
|
|
|
|
self.assertIsInstance(response, HttpResponseNotAllowed)
|
|
|
|
|
2022-04-07 05:05:59 +00:00
|
|
|
def test_base_view_class_is_sync(self):
|
|
|
|
"""
|
|
|
|
View and by extension any subclasses that don't define handlers are
|
|
|
|
sync.
|
|
|
|
"""
|
|
|
|
self.assertIs(View.view_is_async, False)
|