mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #34391 -- Added async-compatible interface to auth functions and related methods test clients.
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							2360ba2274
						
					
				
				
					commit
					5e98959d92
				
			| @@ -1,6 +1,8 @@ | ||||
| import inspect | ||||
| import re | ||||
|  | ||||
| from asgiref.sync import sync_to_async | ||||
|  | ||||
| from django.apps import apps as django_apps | ||||
| from django.conf import settings | ||||
| from django.core.exceptions import ImproperlyConfigured, PermissionDenied | ||||
| @@ -91,6 +93,12 @@ def authenticate(request=None, **credentials): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @sensitive_variables("credentials") | ||||
| async def aauthenticate(request=None, **credentials): | ||||
|     """See authenticate().""" | ||||
|     return await sync_to_async(authenticate)(request, **credentials) | ||||
|  | ||||
|  | ||||
| def login(request, user, backend=None): | ||||
|     """ | ||||
|     Persist a user id and a backend in the request. This way a user doesn't | ||||
| @@ -144,6 +152,11 @@ def login(request, user, backend=None): | ||||
|     user_logged_in.send(sender=user.__class__, request=request, user=user) | ||||
|  | ||||
|  | ||||
| async def alogin(request, user, backend=None): | ||||
|     """See login().""" | ||||
|     return await sync_to_async(login)(request, user, backend) | ||||
|  | ||||
|  | ||||
| def logout(request): | ||||
|     """ | ||||
|     Remove the authenticated user's ID from the request and flush their session | ||||
| @@ -162,6 +175,11 @@ def logout(request): | ||||
|         request.user = AnonymousUser() | ||||
|  | ||||
|  | ||||
| async def alogout(request): | ||||
|     """See logout().""" | ||||
|     return await sync_to_async(logout)(request) | ||||
|  | ||||
|  | ||||
| def get_user_model(): | ||||
|     """ | ||||
|     Return the User model that is active in this project. | ||||
| @@ -223,6 +241,11 @@ def get_user(request): | ||||
|     return user or AnonymousUser() | ||||
|  | ||||
|  | ||||
| async def aget_user(request): | ||||
|     """See get_user().""" | ||||
|     return await sync_to_async(get_user)(request) | ||||
|  | ||||
|  | ||||
| def get_permission_codename(action, opts): | ||||
|     """ | ||||
|     Return the codename of the permission for the specified action. | ||||
| @@ -242,3 +265,8 @@ def update_session_auth_hash(request, user): | ||||
|     request.session.cycle_key() | ||||
|     if hasattr(user, "get_session_auth_hash") and request.user == user: | ||||
|         request.session[HASH_SESSION_KEY] = user.get_session_auth_hash() | ||||
|  | ||||
|  | ||||
| async def aupdate_session_auth_hash(request, user): | ||||
|     """See update_session_auth_hash().""" | ||||
|     return await sync_to_async(update_session_auth_hash)(request, user) | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| from functools import partial | ||||
|  | ||||
| from asgiref.sync import sync_to_async | ||||
|  | ||||
| from django.contrib import auth | ||||
| from django.contrib.auth import load_backend | ||||
| from django.contrib.auth.backends import RemoteUserBackend | ||||
| @@ -18,7 +16,7 @@ def get_user(request): | ||||
|  | ||||
| async def auser(request): | ||||
|     if not hasattr(request, "_acached_user"): | ||||
|         request._acached_user = await sync_to_async(auth.get_user)(request) | ||||
|         request._acached_user = await auth.aget_user(request) | ||||
|     return request._acached_user | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -747,6 +747,9 @@ class ClientMixin: | ||||
|         self.cookies[settings.SESSION_COOKIE_NAME] = session.session_key | ||||
|         return session | ||||
|  | ||||
|     async def asession(self): | ||||
|         return await sync_to_async(lambda: self.session)() | ||||
|  | ||||
|     def login(self, **credentials): | ||||
|         """ | ||||
|         Set the Factory to appear as if it has successfully logged into a site. | ||||
| @@ -762,8 +765,29 @@ class ClientMixin: | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     async def alogin(self, **credentials): | ||||
|         """See login().""" | ||||
|         from django.contrib.auth import aauthenticate | ||||
|  | ||||
|         user = await aauthenticate(**credentials) | ||||
|         if user: | ||||
|             await self._alogin(user) | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     def force_login(self, user, backend=None): | ||||
|         def get_backend(): | ||||
|         if backend is None: | ||||
|             backend = self._get_backend() | ||||
|         user.backend = backend | ||||
|         self._login(user, backend) | ||||
|  | ||||
|     async def aforce_login(self, user, backend=None): | ||||
|         if backend is None: | ||||
|             backend = self._get_backend() | ||||
|         user.backend = backend | ||||
|         await self._alogin(user, backend) | ||||
|  | ||||
|     def _get_backend(self): | ||||
|         from django.contrib.auth import load_backend | ||||
|  | ||||
|         for backend_path in settings.AUTHENTICATION_BACKENDS: | ||||
| @@ -771,11 +795,6 @@ class ClientMixin: | ||||
|             if hasattr(backend, "get_user"): | ||||
|                 return backend_path | ||||
|  | ||||
|         if backend is None: | ||||
|             backend = get_backend() | ||||
|         user.backend = backend | ||||
|         self._login(user, backend) | ||||
|  | ||||
|     def _login(self, user, backend=None): | ||||
|         from django.contrib.auth import login | ||||
|  | ||||
| @@ -789,6 +808,26 @@ class ClientMixin: | ||||
|         login(request, user, backend) | ||||
|         # Save the session values. | ||||
|         request.session.save() | ||||
|         self._set_login_cookies(request) | ||||
|  | ||||
|     async def _alogin(self, user, backend=None): | ||||
|         from django.contrib.auth import alogin | ||||
|  | ||||
|         # Create a fake request to store login details. | ||||
|         request = HttpRequest() | ||||
|         session = await self.asession() | ||||
|         if session: | ||||
|             request.session = session | ||||
|         else: | ||||
|             engine = import_module(settings.SESSION_ENGINE) | ||||
|             request.session = engine.SessionStore() | ||||
|  | ||||
|         await alogin(request, user, backend) | ||||
|         # Save the session values. | ||||
|         await sync_to_async(request.session.save)() | ||||
|         self._set_login_cookies(request) | ||||
|  | ||||
|     def _set_login_cookies(self, request): | ||||
|         # Set the cookie to represent the session. | ||||
|         session_cookie = settings.SESSION_COOKIE_NAME | ||||
|         self.cookies[session_cookie] = request.session.session_key | ||||
| @@ -815,6 +854,21 @@ class ClientMixin: | ||||
|         logout(request) | ||||
|         self.cookies = SimpleCookie() | ||||
|  | ||||
|     async def alogout(self): | ||||
|         """See logout().""" | ||||
|         from django.contrib.auth import aget_user, alogout | ||||
|  | ||||
|         request = HttpRequest() | ||||
|         session = await self.asession() | ||||
|         if session: | ||||
|             request.session = session | ||||
|             request.user = await aget_user(request) | ||||
|         else: | ||||
|             engine = import_module(settings.SESSION_ENGINE) | ||||
|             request.session = engine.SessionStore() | ||||
|         await alogout(request) | ||||
|         self.cookies = SimpleCookie() | ||||
|  | ||||
|     def _parse_json(self, response, **extra): | ||||
|         if not hasattr(response, "_json"): | ||||
|             if not JSON_CONTENT_TYPE_RE.match(response.get("Content-Type")): | ||||
|   | ||||
| @@ -693,6 +693,9 @@ Utility functions | ||||
| .. currentmodule:: django.contrib.auth | ||||
|  | ||||
| .. function:: get_user(request) | ||||
| .. function:: aget_user(request) | ||||
|  | ||||
|     *Asynchronous version*: ``aget_user()`` | ||||
|  | ||||
|     Returns the user model instance associated with the given ``request``’s | ||||
|     session. | ||||
| @@ -716,3 +719,7 @@ Utility functions | ||||
|     .. versionchanged:: 4.1.8 | ||||
|  | ||||
|         Fallback verification with :setting:`SECRET_KEY_FALLBACKS` was added. | ||||
|  | ||||
|     .. versionchanged:: 5.0 | ||||
|  | ||||
|         ``aget_user()`` function was added. | ||||
|   | ||||
| @@ -150,6 +150,12 @@ Minor features | ||||
| * The default iteration count for the PBKDF2 password hasher is increased from | ||||
|   600,000 to 720,000. | ||||
|  | ||||
| * The new asynchronous functions are now provided, using an | ||||
|   ``a`` prefix: :func:`django.contrib.auth.aauthenticate`, | ||||
|   :func:`~.django.contrib.auth.aget_user`, | ||||
|   :func:`~.django.contrib.auth.alogin`, :func:`~.django.contrib.auth.alogout`, | ||||
|   and :func:`~.django.contrib.auth.aupdate_session_auth_hash`. | ||||
|  | ||||
| * ``AuthenticationMiddleware`` now adds an :meth:`.HttpRequest.auser` | ||||
|   asynchronous method that returns the currently logged-in user. | ||||
|  | ||||
| @@ -366,7 +372,11 @@ Templates | ||||
| Tests | ||||
| ~~~~~ | ||||
|  | ||||
| * ... | ||||
| * :class:`~django.test.Client` and :class:`~django.test.AsyncClient` now | ||||
|   provide asynchronous methods, using an ``a`` prefix: | ||||
|   :meth:`~django.test.Client.asession`, :meth:`~django.test.Client.alogin`, | ||||
|   :meth:`~django.test.Client.aforce_login`, and | ||||
|   :meth:`~django.test.Client.alogout`. | ||||
|  | ||||
| URLs | ||||
| ~~~~ | ||||
|   | ||||
| @@ -120,6 +120,9 @@ Authenticating users | ||||
| -------------------- | ||||
|  | ||||
| .. function:: authenticate(request=None, **credentials) | ||||
| .. function:: aauthenticate(request=None, **credentials) | ||||
|  | ||||
|     *Asynchronous version*: ``aauthenticate()`` | ||||
|  | ||||
|     Use :func:`~django.contrib.auth.authenticate()` to verify a set of | ||||
|     credentials. It takes credentials as keyword arguments, ``username`` and | ||||
| @@ -152,6 +155,10 @@ Authenticating users | ||||
|         this. Rather if you're looking for a way to login a user, use the | ||||
|         :class:`~django.contrib.auth.views.LoginView`. | ||||
|  | ||||
|     .. versionchanged:: 5.0 | ||||
|  | ||||
|         ``aauthenticate()`` function was added. | ||||
|  | ||||
| .. _topic-authorization: | ||||
|  | ||||
| Permissions and Authorization | ||||
| @@ -407,6 +414,9 @@ If you have an authenticated user you want to attach to the current session | ||||
| - this is done with a :func:`~django.contrib.auth.login` function. | ||||
|  | ||||
| .. function:: login(request, user, backend=None) | ||||
| .. function:: alogin(request, user, backend=None) | ||||
|  | ||||
|     *Asynchronous version*: ``alogin()`` | ||||
|  | ||||
|     To log a user in, from a view, use :func:`~django.contrib.auth.login()`. It | ||||
|     takes an :class:`~django.http.HttpRequest` object and a | ||||
| @@ -436,6 +446,10 @@ If you have an authenticated user you want to attach to the current session | ||||
|                 # Return an 'invalid login' error message. | ||||
|                 ... | ||||
|  | ||||
|     .. versionchanged:: 5.0 | ||||
|  | ||||
|         ``alogin()`` function was added. | ||||
|  | ||||
| Selecting the authentication backend | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| @@ -463,6 +477,9 @@ How to log a user out | ||||
| --------------------- | ||||
|  | ||||
| .. function:: logout(request) | ||||
| .. function:: alogout(request) | ||||
|  | ||||
|     *Asynchronous version*: ``alogout()`` | ||||
|  | ||||
|     To log out a user who has been logged in via | ||||
|     :func:`django.contrib.auth.login()`, use | ||||
| @@ -488,6 +505,10 @@ How to log a user out | ||||
|     immediately after logging out, do that *after* calling | ||||
|     :func:`django.contrib.auth.logout()`. | ||||
|  | ||||
|     .. versionchanged:: 5.0 | ||||
|  | ||||
|         ``alogout()`` function was added. | ||||
|  | ||||
| Limiting access to logged-in users | ||||
| ---------------------------------- | ||||
|  | ||||
| @@ -935,6 +956,9 @@ and wish to have similar behavior, use the :func:`update_session_auth_hash` | ||||
| function. | ||||
|  | ||||
| .. function:: update_session_auth_hash(request, user) | ||||
| .. function:: aupdate_session_auth_hash(request, user) | ||||
|  | ||||
|     *Asynchronous version*: ``aupdate_session_auth_hash()`` | ||||
|  | ||||
|     This function takes the current request and the updated user object from | ||||
|     which the new session hash will be derived and updates the session hash | ||||
| @@ -955,6 +979,10 @@ function. | ||||
|             else: | ||||
|                 ... | ||||
|  | ||||
|     .. versionchanged:: 5.0 | ||||
|  | ||||
|         ``aupdate_session_auth_hash()`` function was added. | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|     Since | ||||
|   | ||||
| @@ -440,6 +440,9 @@ Use the ``django.test.Client`` class to make requests. | ||||
|             The ``headers`` parameter was added. | ||||
|  | ||||
|     .. method:: Client.login(**credentials) | ||||
|     .. method:: Client.alogin(**credentials) | ||||
|  | ||||
|         *Asynchronous version*: ``alogin()`` | ||||
|  | ||||
|         If your site uses Django's :doc:`authentication system</topics/auth/index>` | ||||
|         and you deal with logging in users, you can use the test client's | ||||
| @@ -485,7 +488,14 @@ Use the ``django.test.Client`` class to make requests. | ||||
|         :meth:`~django.contrib.auth.models.UserManager.create_user` helper | ||||
|         method to create a new user with a correctly hashed password. | ||||
|  | ||||
|         .. versionchanged:: 5.0 | ||||
|  | ||||
|             ``alogin()`` method was added. | ||||
|  | ||||
|     .. method:: Client.force_login(user, backend=None) | ||||
|     .. method:: Client.aforce_login(user, backend=None) | ||||
|  | ||||
|         *Asynchronous version*: ``aforce_login()`` | ||||
|  | ||||
|         If your site uses Django's :doc:`authentication | ||||
|         system</topics/auth/index>`, you can use the ``force_login()`` method | ||||
| @@ -509,7 +519,14 @@ Use the ``django.test.Client`` class to make requests. | ||||
|         ``login()`` by :ref:`using a weaker hasher while testing | ||||
|         <speeding-up-tests-auth-hashers>`. | ||||
|  | ||||
|         .. versionchanged:: 5.0 | ||||
|  | ||||
|             ``aforce_login()`` method was added. | ||||
|  | ||||
|     .. method:: Client.logout() | ||||
|     .. method:: Client.alogout() | ||||
|  | ||||
|         *Asynchronous version*: ``alogout()`` | ||||
|  | ||||
|         If your site uses Django's :doc:`authentication system</topics/auth/index>`, | ||||
|         the ``logout()`` method can be used to simulate the effect of a user | ||||
| @@ -519,6 +536,10 @@ Use the ``django.test.Client`` class to make requests. | ||||
|         and session data cleared to defaults. Subsequent requests will appear | ||||
|         to come from an :class:`~django.contrib.auth.models.AnonymousUser`. | ||||
|  | ||||
|         .. versionchanged:: 5.0 | ||||
|  | ||||
|             ``alogout()`` method was added. | ||||
|  | ||||
| Testing responses | ||||
| ----------------- | ||||
|  | ||||
| @@ -703,6 +724,13 @@ access these properties as part of a test condition. | ||||
|             session["somekey"] = "test" | ||||
|             session.save() | ||||
|  | ||||
| .. method:: Client.asession() | ||||
|  | ||||
|     .. versionadded:: 5.0 | ||||
|  | ||||
|     This is similar to the :attr:`session` attribute but it works in async | ||||
|     contexts. | ||||
|  | ||||
| Setting the language | ||||
| -------------------- | ||||
|  | ||||
|   | ||||
							
								
								
									
										98
									
								
								tests/async/test_async_auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								tests/async/test_async_auth.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| from django.contrib.auth import ( | ||||
|     aauthenticate, | ||||
|     aget_user, | ||||
|     alogin, | ||||
|     alogout, | ||||
|     aupdate_session_auth_hash, | ||||
| ) | ||||
| from django.contrib.auth.models import AnonymousUser, User | ||||
| from django.http import HttpRequest | ||||
| from django.test import TestCase, override_settings | ||||
|  | ||||
|  | ||||
| class AsyncAuthTest(TestCase): | ||||
|     @classmethod | ||||
|     def setUpTestData(cls): | ||||
|         cls.test_user = User.objects.create_user( | ||||
|             "testuser", "test@example.com", "testpw" | ||||
|         ) | ||||
|  | ||||
|     async def test_aauthenticate(self): | ||||
|         user = await aauthenticate(username="testuser", password="testpw") | ||||
|         self.assertIsInstance(user, User) | ||||
|         self.assertEqual(user.username, self.test_user.username) | ||||
|         user.is_active = False | ||||
|         await user.asave() | ||||
|         self.assertIsNone(await aauthenticate(username="testuser", password="testpw")) | ||||
|  | ||||
|     async def test_alogin(self): | ||||
|         request = HttpRequest() | ||||
|         request.session = await self.client.asession() | ||||
|         await alogin(request, self.test_user) | ||||
|         user = await aget_user(request) | ||||
|         self.assertIsInstance(user, User) | ||||
|         self.assertEqual(user.username, self.test_user.username) | ||||
|  | ||||
|     async def test_alogin_without_user(self): | ||||
|         request = HttpRequest() | ||||
|         request.user = self.test_user | ||||
|         request.session = await self.client.asession() | ||||
|         await alogin(request, None) | ||||
|         user = await aget_user(request) | ||||
|         self.assertIsInstance(user, User) | ||||
|         self.assertEqual(user.username, self.test_user.username) | ||||
|  | ||||
|     async def test_alogout(self): | ||||
|         await self.client.alogin(username="testuser", password="testpw") | ||||
|         request = HttpRequest() | ||||
|         request.session = await self.client.asession() | ||||
|         await alogout(request) | ||||
|         user = await aget_user(request) | ||||
|         self.assertIsInstance(user, AnonymousUser) | ||||
|  | ||||
|     async def test_client_alogout(self): | ||||
|         await self.client.alogin(username="testuser", password="testpw") | ||||
|         request = HttpRequest() | ||||
|         request.session = await self.client.asession() | ||||
|         await self.client.alogout() | ||||
|         user = await aget_user(request) | ||||
|         self.assertIsInstance(user, AnonymousUser) | ||||
|  | ||||
|     async def test_change_password(self): | ||||
|         await self.client.alogin(username="testuser", password="testpw") | ||||
|         request = HttpRequest() | ||||
|         request.session = await self.client.asession() | ||||
|         request.user = self.test_user | ||||
|         await aupdate_session_auth_hash(request, self.test_user) | ||||
|         user = await aget_user(request) | ||||
|         self.assertIsInstance(user, User) | ||||
|  | ||||
|     async def test_invalid_login(self): | ||||
|         self.assertEqual( | ||||
|             await self.client.alogin(username="testuser", password=""), False | ||||
|         ) | ||||
|  | ||||
|     async def test_client_aforce_login(self): | ||||
|         await self.client.aforce_login(self.test_user) | ||||
|         request = HttpRequest() | ||||
|         request.session = await self.client.asession() | ||||
|         user = await aget_user(request) | ||||
|         self.assertEqual(user.username, self.test_user.username) | ||||
|  | ||||
|     @override_settings( | ||||
|         AUTHENTICATION_BACKENDS=[ | ||||
|             "django.contrib.auth.backends.ModelBackend", | ||||
|             "django.contrib.auth.backends.AllowAllUsersModelBackend", | ||||
|         ] | ||||
|     ) | ||||
|     async def test_client_aforce_login_backend(self): | ||||
|         self.test_user.is_active = False | ||||
|         await self.test_user.asave() | ||||
|         await self.client.aforce_login( | ||||
|             self.test_user, | ||||
|             backend="django.contrib.auth.backends.AllowAllUsersModelBackend", | ||||
|         ) | ||||
|         request = HttpRequest() | ||||
|         request.session = await self.client.asession() | ||||
|         user = await aget_user(request) | ||||
|         self.assertEqual(user.username, self.test_user.username) | ||||
| @@ -6,6 +6,7 @@ from django.contrib.auth import ( | ||||
|     BACKEND_SESSION_KEY, | ||||
|     SESSION_KEY, | ||||
|     _clean_credentials, | ||||
|     aauthenticate, | ||||
|     authenticate, | ||||
|     get_user, | ||||
|     signals, | ||||
| @@ -764,6 +765,28 @@ class AuthenticateTests(TestCase): | ||||
|             status_code=500, | ||||
|         ) | ||||
|  | ||||
|     @override_settings( | ||||
|         AUTHENTICATION_BACKENDS=["auth_tests.test_auth_backends.TypeErrorBackend"] | ||||
|     ) | ||||
|     async def test_aauthenticate_sensitive_variables(self): | ||||
|         try: | ||||
|             await aauthenticate( | ||||
|                 username="testusername", password=self.sensitive_password | ||||
|             ) | ||||
|         except TypeError: | ||||
|             exc_info = sys.exc_info() | ||||
|         rf = RequestFactory() | ||||
|         response = technical_500_response(rf.get("/"), *exc_info) | ||||
|         self.assertNotContains(response, self.sensitive_password, status_code=500) | ||||
|         self.assertContains(response, "TypeErrorBackend", status_code=500) | ||||
|         self.assertContains( | ||||
|             response, | ||||
|             '<tr><td>credentials</td><td class="code">' | ||||
|             "<pre>'********************'</pre></td></tr>", | ||||
|             html=True, | ||||
|             status_code=500, | ||||
|         ) | ||||
|  | ||||
|     def test_clean_credentials_sensitive_variables(self): | ||||
|         try: | ||||
|             # Passing in a list to cause an exception | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| from asgiref.sync import sync_to_async | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth import get_user, get_user_model | ||||
| from django.contrib.auth import aget_user, get_user, get_user_model | ||||
| from django.contrib.auth.models import AnonymousUser, User | ||||
| from django.core.exceptions import ImproperlyConfigured | ||||
| from django.db import IntegrityError | ||||
| @@ -129,6 +131,12 @@ class TestGetUser(TestCase): | ||||
|         user = get_user(request) | ||||
|         self.assertIsInstance(user, AnonymousUser) | ||||
|  | ||||
|     async def test_aget_user_anonymous(self): | ||||
|         request = HttpRequest() | ||||
|         request.session = await self.client.asession() | ||||
|         user = await aget_user(request) | ||||
|         self.assertIsInstance(user, AnonymousUser) | ||||
|  | ||||
|     def test_get_user(self): | ||||
|         created_user = User.objects.create_user( | ||||
|             "testuser", "test@example.com", "testpw" | ||||
| @@ -162,3 +170,14 @@ class TestGetUser(TestCase): | ||||
|             user = get_user(request) | ||||
|             self.assertIsInstance(user, User) | ||||
|             self.assertEqual(user.username, created_user.username) | ||||
|  | ||||
|     async def test_aget_user(self): | ||||
|         created_user = await sync_to_async(User.objects.create_user)( | ||||
|             "testuser", "test@example.com", "testpw" | ||||
|         ) | ||||
|         await self.client.alogin(username="testuser", password="testpw") | ||||
|         request = HttpRequest() | ||||
|         request.session = await self.client.asession() | ||||
|         user = await aget_user(request) | ||||
|         self.assertIsInstance(user, User) | ||||
|         self.assertEqual(user.username, created_user.username) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user