from typing import Any, Iterable, Mapping, Union, Tuple, Protocol from django.apps import apps from django.conf import settings from django.http import HttpRequest, HttpResponse from django.http.response import HttpResponseRedirectBase, StreamingHttpResponse from django.shortcuts import resolve_url from django.template import Context from django.template.response import TemplateResponse from django.test import Client, SimpleTestCase from django.urls import reverse, ResolverMatch from django.utils.http import urlencode FormData = dict JSONDict = dict JSONList = list RequestData = Union[FormData, JSONDict, JSONList] QueryParams = Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] class TestClientResponse(Protocol): client: Client request: HttpRequest templates: list context: Context resolver_match: ResolverMatch def json(self) -> Union[JSONList, JSONDict]: ... Response = Union[ HttpResponse, HttpResponseRedirectBase, StreamingHttpResponse, TemplateResponse, TestClientResponse, ] class ObjectWithGetAbsoluteURLMethod(Protocol): def get_absolute_url(self) -> str: ... URL = Union[str, ObjectWithGetAbsoluteURLMethod] URLArgs = Union[tuple, list] URLKwargs = dict def get_url(url: URL, args: URLArgs = None, kwargs: URLKwargs = None) -> str: """ Helper to reverse the given url name. """ if args or kwargs: return reverse(url, args=args, kwargs=kwargs) return resolve_url(url) def get_handler(test_case: SimpleTestCase, method: str = None, data=None): if data: method = str.lower(method or 'POST') else: method = str.lower(method or 'GET') return getattr(test_case.client, method) def request( test_case: SimpleTestCase, url: URL, status_code: int = None, expected_url: URL = None, args: URLArgs = None, kwargs: URLKwargs = None, headers: dict = None, msg: str = None, query_params: QueryParams = None, method: str = None, data: RequestData = None, **options, ) -> Response: """ A helper to make a request with the test case's http client. The given args and kwargs are used to reverse the url but not the expected url. When expected url needs args/kwargs pass an absolute url instead. All additional kwargs are passed as post parameters. When posting without parameters just pass post=True. """ data = data or options or None handler = get_handler(test_case, method, data) url = get_url(url, args, kwargs) if query_params: url = f'{url}?%s' % urlencode(query_params, doseq=True) headers = headers or {} status_code = status_code or 200 response = handler(url, data=data, **headers) msg = msg or getattr(response, 'content', None) if expected_url: test_case.assertRedirects( response=response, expected_url=get_url(expected_url), target_status_code=status_code, ) else: test_case.assertEqual(response.status_code, status_code, msg=msg) return response def login(test_case: SimpleTestCase, user=None, password: str = None) -> bool: """ Logs in the user trying to use the raw password or the given password. Force logs in the user when no password is found. """ user = user or getattr(test_case, 'user') password = password or getattr(user, 'raw_password', password) if password is None: return test_case.client.force_login(user=user) or True return test_case.client.login(username=user.username, password=password) def create_user(username: str, *, model=None, **kwargs): model = model or apps.get_model(settings.AUTH_USER_MODEL) password = kwargs.setdefault('password', 'P4sSW0rD') kwargs.setdefault('email', f'{username}@test.case') kwargs.setdefault(model.USERNAME_FIELD, username) user = model.objects.create_user(**kwargs) user.raw_password = password return user def create_superuser(username: str, **kwargs): kwargs['is_superuser'] = True kwargs['is_staff'] = True return create_user(username, **kwargs)