Browse Source

Fixed #34074 -- Added headers argument to RequestFactory and Client classes.

David Wobrock 2 years ago
parent
commit
67da22f08e

+ 2 - 0
django/http/__init__.py

@@ -1,5 +1,6 @@
 from django.http.cookie import SimpleCookie, parse_cookie
 from django.http.request import (
+    HttpHeaders,
     HttpRequest,
     QueryDict,
     RawPostDataException,
@@ -27,6 +28,7 @@ from django.http.response import (
 __all__ = [
     "SimpleCookie",
     "parse_cookie",
+    "HttpHeaders",
     "HttpRequest",
     "QueryDict",
     "RawPostDataException",

+ 25 - 0
django/http/request.py

@@ -461,6 +461,31 @@ class HttpHeaders(CaseInsensitiveMapping):
             return None
         return header.replace("_", "-").title()
 
+    @classmethod
+    def to_wsgi_name(cls, header):
+        header = header.replace("-", "_").upper()
+        if header in cls.UNPREFIXED_HEADERS:
+            return header
+        return f"{cls.HTTP_PREFIX}{header}"
+
+    @classmethod
+    def to_asgi_name(cls, header):
+        return header.replace("-", "_").upper()
+
+    @classmethod
+    def to_wsgi_names(cls, headers):
+        return {
+            cls.to_wsgi_name(header_name): value
+            for header_name, value in headers.items()
+        }
+
+    @classmethod
+    def to_asgi_names(cls, headers):
+        return {
+            cls.to_asgi_name(header_name): value
+            for header_name, value in headers.items()
+        }
+
 
 class QueryDict(MultiValueDict):
     """

+ 1 - 2
django/middleware/csrf.py

@@ -11,8 +11,7 @@ from urllib.parse import urlparse
 
 from django.conf import settings
 from django.core.exceptions import DisallowedHost, ImproperlyConfigured
-from django.http import UnreadablePostError
-from django.http.request import HttpHeaders
+from django.http import HttpHeaders, UnreadablePostError
 from django.urls import get_callable
 from django.utils.cache import patch_vary_headers
 from django.utils.crypto import constant_time_compare, get_random_string

+ 172 - 37
django/test/client.py

@@ -18,7 +18,7 @@ from django.core.handlers.wsgi import LimitedStream, WSGIRequest
 from django.core.serializers.json import DjangoJSONEncoder
 from django.core.signals import got_request_exception, request_finished, request_started
 from django.db import close_old_connections
-from django.http import HttpRequest, QueryDict, SimpleCookie
+from django.http import HttpHeaders, HttpRequest, QueryDict, SimpleCookie
 from django.test import signals
 from django.test.utils import ContextList
 from django.urls import resolve
@@ -346,11 +346,13 @@ class RequestFactory:
     just as if that view had been hooked up using a URLconf.
     """
 
-    def __init__(self, *, json_encoder=DjangoJSONEncoder, **defaults):
+    def __init__(self, *, json_encoder=DjangoJSONEncoder, headers=None, **defaults):
         self.json_encoder = json_encoder
         self.defaults = defaults
         self.cookies = SimpleCookie()
         self.errors = BytesIO()
+        if headers:
+            self.defaults.update(HttpHeaders.to_wsgi_names(headers))
 
     def _base_environ(self, **request):
         """
@@ -422,13 +424,14 @@ class RequestFactory:
         # Refs comment in `get_bytes_from_wsgi()`.
         return path.decode("iso-8859-1")
 
-    def get(self, path, data=None, secure=False, **extra):
+    def get(self, path, data=None, secure=False, *, headers=None, **extra):
         """Construct a GET request."""
         data = {} if data is None else data
         return self.generic(
             "GET",
             path,
             secure=secure,
+            headers=headers,
             **{
                 "QUERY_STRING": urlencode(data, doseq=True),
                 **extra,
@@ -436,32 +439,46 @@ class RequestFactory:
         )
 
     def post(
-        self, path, data=None, content_type=MULTIPART_CONTENT, secure=False, **extra
+        self,
+        path,
+        data=None,
+        content_type=MULTIPART_CONTENT,
+        secure=False,
+        *,
+        headers=None,
+        **extra,
     ):
         """Construct a POST request."""
         data = self._encode_json({} if data is None else data, content_type)
         post_data = self._encode_data(data, content_type)
 
         return self.generic(
-            "POST", path, post_data, content_type, secure=secure, **extra
+            "POST",
+            path,
+            post_data,
+            content_type,
+            secure=secure,
+            headers=headers,
+            **extra,
         )
 
-    def head(self, path, data=None, secure=False, **extra):
+    def head(self, path, data=None, secure=False, *, headers=None, **extra):
         """Construct a HEAD request."""
         data = {} if data is None else data
         return self.generic(
             "HEAD",
             path,
             secure=secure,
+            headers=headers,
             **{
                 "QUERY_STRING": urlencode(data, doseq=True),
                 **extra,
             },
         )
 
-    def trace(self, path, secure=False, **extra):
+    def trace(self, path, secure=False, *, headers=None, **extra):
         """Construct a TRACE request."""
-        return self.generic("TRACE", path, secure=secure, **extra)
+        return self.generic("TRACE", path, secure=secure, headers=headers, **extra)
 
     def options(
         self,
@@ -469,10 +486,14 @@ class RequestFactory:
         data="",
         content_type="application/octet-stream",
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         "Construct an OPTIONS request."
-        return self.generic("OPTIONS", path, data, content_type, secure=secure, **extra)
+        return self.generic(
+            "OPTIONS", path, data, content_type, secure=secure, headers=headers, **extra
+        )
 
     def put(
         self,
@@ -480,11 +501,15 @@ class RequestFactory:
         data="",
         content_type="application/octet-stream",
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Construct a PUT request."""
         data = self._encode_json(data, content_type)
-        return self.generic("PUT", path, data, content_type, secure=secure, **extra)
+        return self.generic(
+            "PUT", path, data, content_type, secure=secure, headers=headers, **extra
+        )
 
     def patch(
         self,
@@ -492,11 +517,15 @@ class RequestFactory:
         data="",
         content_type="application/octet-stream",
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Construct a PATCH request."""
         data = self._encode_json(data, content_type)
-        return self.generic("PATCH", path, data, content_type, secure=secure, **extra)
+        return self.generic(
+            "PATCH", path, data, content_type, secure=secure, headers=headers, **extra
+        )
 
     def delete(
         self,
@@ -504,11 +533,15 @@ class RequestFactory:
         data="",
         content_type="application/octet-stream",
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Construct a DELETE request."""
         data = self._encode_json(data, content_type)
-        return self.generic("DELETE", path, data, content_type, secure=secure, **extra)
+        return self.generic(
+            "DELETE", path, data, content_type, secure=secure, headers=headers, **extra
+        )
 
     def generic(
         self,
@@ -517,6 +550,8 @@ class RequestFactory:
         data="",
         content_type="application/octet-stream",
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Construct an arbitrary HTTP request."""
@@ -536,6 +571,8 @@ class RequestFactory:
                     "wsgi.input": FakePayload(data),
                 }
             )
+        if headers:
+            extra.update(HttpHeaders.to_wsgi_names(headers))
         r.update(extra)
         # If QUERY_STRING is absent or empty, we want to extract it from the URL.
         if not r.get("QUERY_STRING"):
@@ -611,6 +648,8 @@ class AsyncRequestFactory(RequestFactory):
         data="",
         content_type="application/octet-stream",
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Construct an arbitrary HTTP request."""
@@ -636,6 +675,8 @@ class AsyncRequestFactory(RequestFactory):
             s["follow"] = follow
         if query_string := extra.pop("QUERY_STRING", None):
             s["query_string"] = query_string
+        if headers:
+            extra.update(HttpHeaders.to_asgi_names(headers))
         s["headers"] += [
             (key.lower().encode("ascii"), value.encode("latin1"))
             for key, value in extra.items()
@@ -782,9 +823,14 @@ class Client(ClientMixin, RequestFactory):
     """
 
     def __init__(
-        self, enforce_csrf_checks=False, raise_request_exception=True, **defaults
+        self,
+        enforce_csrf_checks=False,
+        raise_request_exception=True,
+        *,
+        headers=None,
+        **defaults,
     ):
-        super().__init__(**defaults)
+        super().__init__(headers=headers, **defaults)
         self.handler = ClientHandler(enforce_csrf_checks)
         self.raise_request_exception = raise_request_exception
         self.exc_info = None
@@ -837,12 +883,23 @@ class Client(ClientMixin, RequestFactory):
             self.cookies.update(response.cookies)
         return response
 
-    def get(self, path, data=None, follow=False, secure=False, **extra):
+    def get(
+        self,
+        path,
+        data=None,
+        follow=False,
+        secure=False,
+        *,
+        headers=None,
+        **extra,
+    ):
         """Request a response from the server using GET."""
         self.extra = extra
-        response = super().get(path, data=data, secure=secure, **extra)
+        response = super().get(path, data=data, secure=secure, headers=headers, **extra)
         if follow:
-            response = self._handle_redirects(response, data=data, **extra)
+            response = self._handle_redirects(
+                response, data=data, headers=headers, **extra
+            )
         return response
 
     def post(
@@ -852,25 +909,45 @@ class Client(ClientMixin, RequestFactory):
         content_type=MULTIPART_CONTENT,
         follow=False,
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Request a response from the server using POST."""
         self.extra = extra
         response = super().post(
-            path, data=data, content_type=content_type, secure=secure, **extra
+            path,
+            data=data,
+            content_type=content_type,
+            secure=secure,
+            headers=headers,
+            **extra,
         )
         if follow:
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, **extra
+                response, data=data, content_type=content_type, headers=headers, **extra
             )
         return response
 
-    def head(self, path, data=None, follow=False, secure=False, **extra):
+    def head(
+        self,
+        path,
+        data=None,
+        follow=False,
+        secure=False,
+        *,
+        headers=None,
+        **extra,
+    ):
         """Request a response from the server using HEAD."""
         self.extra = extra
-        response = super().head(path, data=data, secure=secure, **extra)
+        response = super().head(
+            path, data=data, secure=secure, headers=headers, **extra
+        )
         if follow:
-            response = self._handle_redirects(response, data=data, **extra)
+            response = self._handle_redirects(
+                response, data=data, headers=headers, **extra
+            )
         return response
 
     def options(
@@ -880,16 +957,23 @@ class Client(ClientMixin, RequestFactory):
         content_type="application/octet-stream",
         follow=False,
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Request a response from the server using OPTIONS."""
         self.extra = extra
         response = super().options(
-            path, data=data, content_type=content_type, secure=secure, **extra
+            path,
+            data=data,
+            content_type=content_type,
+            secure=secure,
+            headers=headers,
+            **extra,
         )
         if follow:
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, **extra
+                response, data=data, content_type=content_type, headers=headers, **extra
             )
         return response
 
@@ -900,16 +984,23 @@ class Client(ClientMixin, RequestFactory):
         content_type="application/octet-stream",
         follow=False,
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Send a resource to the server using PUT."""
         self.extra = extra
         response = super().put(
-            path, data=data, content_type=content_type, secure=secure, **extra
+            path,
+            data=data,
+            content_type=content_type,
+            secure=secure,
+            headers=headers,
+            **extra,
         )
         if follow:
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, **extra
+                response, data=data, content_type=content_type, headers=headers, **extra
             )
         return response
 
@@ -920,16 +1011,23 @@ class Client(ClientMixin, RequestFactory):
         content_type="application/octet-stream",
         follow=False,
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Send a resource to the server using PATCH."""
         self.extra = extra
         response = super().patch(
-            path, data=data, content_type=content_type, secure=secure, **extra
+            path,
+            data=data,
+            content_type=content_type,
+            secure=secure,
+            headers=headers,
+            **extra,
         )
         if follow:
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, **extra
+                response, data=data, content_type=content_type, headers=headers, **extra
             )
         return response
 
@@ -940,28 +1038,55 @@ class Client(ClientMixin, RequestFactory):
         content_type="application/octet-stream",
         follow=False,
         secure=False,
+        *,
+        headers=None,
         **extra,
     ):
         """Send a DELETE request to the server."""
         self.extra = extra
         response = super().delete(
-            path, data=data, content_type=content_type, secure=secure, **extra
+            path,
+            data=data,
+            content_type=content_type,
+            secure=secure,
+            headers=headers,
+            **extra,
         )
         if follow:
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, **extra
+                response, data=data, content_type=content_type, headers=headers, **extra
             )
         return response
 
-    def trace(self, path, data="", follow=False, secure=False, **extra):
+    def trace(
+        self,
+        path,
+        data="",
+        follow=False,
+        secure=False,
+        *,
+        headers=None,
+        **extra,
+    ):
         """Send a TRACE request to the server."""
         self.extra = extra
-        response = super().trace(path, data=data, secure=secure, **extra)
+        response = super().trace(
+            path, data=data, secure=secure, headers=headers, **extra
+        )
         if follow:
-            response = self._handle_redirects(response, data=data, **extra)
+            response = self._handle_redirects(
+                response, data=data, headers=headers, **extra
+            )
         return response
 
-    def _handle_redirects(self, response, data="", content_type="", **extra):
+    def _handle_redirects(
+        self,
+        response,
+        data="",
+        content_type="",
+        headers=None,
+        **extra,
+    ):
         """
         Follow any redirects by requesting responses from the server using GET.
         """
@@ -1010,7 +1135,12 @@ class Client(ClientMixin, RequestFactory):
                 content_type = None
 
             response = request_method(
-                path, data=data, content_type=content_type, follow=False, **extra
+                path,
+                data=data,
+                content_type=content_type,
+                follow=False,
+                headers=headers,
+                **extra,
             )
             response.redirect_chain = redirect_chain
 
@@ -1038,9 +1168,14 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
     """
 
     def __init__(
-        self, enforce_csrf_checks=False, raise_request_exception=True, **defaults
+        self,
+        enforce_csrf_checks=False,
+        raise_request_exception=True,
+        *,
+        headers=None,
+        **defaults,
     ):
-        super().__init__(**defaults)
+        super().__init__(headers=headers, **defaults)
         self.handler = AsyncClientHandler(enforce_csrf_checks)
         self.raise_request_exception = raise_request_exception
         self.exc_info = None

+ 16 - 0
docs/releases/4.2.txt

@@ -279,6 +279,22 @@ Tests
 * The :option:`test --debug-sql` option now formats SQL queries with
   ``sqlparse``.
 
+* The :class:`~django.test.RequestFactory`,
+  :class:`~django.test.AsyncRequestFactory`, :class:`~django.test.Client`, and
+  :class:`~django.test.AsyncClient` classes now support the ``headers``
+  parameter, which accepts a dictionary of header names and values. This allows
+  a more natural syntax for declaring headers.
+
+  .. code-block:: python
+
+     # Before:
+     self.client.get("/home/", HTTP_ACCEPT_LANGUAGE="fr")
+     await self.async_client.get("/home/", ACCEPT_LANGUAGE="fr")
+
+     # After:
+     self.client.get("/home/", headers={"accept-language": "fr"})
+     await self.async_client.get("/home/", headers={"accept-language": "fr"})
+
 URLs
 ~~~~
 

+ 8 - 0
docs/topics/testing/advanced.txt

@@ -32,6 +32,10 @@ restricted subset of the test client API:
   attributes must be supplied by the test itself if required
   for the view to function properly.
 
+.. versionchanged:: 4.2
+
+    The ``headers`` parameter was added.
+
 Example
 -------
 
@@ -83,6 +87,10 @@ difference being that it returns ``ASGIRequest`` instances rather than
 Arbitrary keyword arguments in ``defaults`` are added directly into the ASGI
 scope.
 
+.. versionchanged:: 4.2
+
+    The ``headers`` parameter was added.
+
 Testing class-based views
 =========================
 

+ 89 - 38
docs/topics/testing/tools.txt

@@ -112,15 +112,27 @@ Making requests
 
 Use the ``django.test.Client`` class to make requests.
 
-.. class:: Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, **defaults)
+.. class:: Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, **defaults)
 
-    It requires no arguments at time of construction. However, you can use
-    keyword arguments to specify some default headers. For example, this will
-    send a ``User-Agent`` HTTP header in each request::
+    A testing HTTP client. Takes several arguments that can customize behavior.
 
-        >>> c = Client(HTTP_USER_AGENT='Mozilla/5.0')
+    ``headers`` allows you to specify default headers that will be sent with
+    every request. For example, to set a ``User-Agent`` header::
 
-    The values from the ``extra`` keyword arguments passed to
+        client = Client(headers={"user-agent": "curl/7.79.1"})
+
+    Arbitrary keyword arguments in ``**defaults`` set WSGI
+    :pep:`environ variables <3333#environ-variables>`. For example, to set the
+    script name::
+
+        client = Client(SCRIPT_NAME="/app/")
+
+    .. note::
+
+        Keyword arguments starting with a ``HTTP_`` prefix are set as headers,
+        but the ``headers`` parameter should be preferred for readability.
+
+    The values from the ``headers`` and ``extra`` keyword arguments passed to
     :meth:`~django.test.Client.get()`,
     :meth:`~django.test.Client.post()`, etc. have precedence over
     the defaults passed to the class constructor.
@@ -138,7 +150,11 @@ Use the ``django.test.Client`` class to make requests.
     Once you have a ``Client`` instance, you can call any of the following
     methods:
 
-    .. method:: Client.get(path, data=None, follow=False, secure=False, **extra)
+    .. versionchanged:: 4.2
+
+        The ``headers`` parameter was added.
+
+    .. method:: Client.get(path, data=None, follow=False, secure=False, *, headers=None, **extra)
 
         Makes a GET request on the provided ``path`` and returns a ``Response``
         object, which is documented below.
@@ -153,25 +169,23 @@ Use the ``django.test.Client`` class to make requests.
 
             /customers/details/?name=fred&age=7
 
-        The ``extra`` keyword arguments parameter can be used to specify
-        headers to be sent in the request. For example::
+        The ``headers`` parameter can be used to specify headers to be sent in
+        the request. For example::
 
             >>> c = Client()
             >>> c.get('/customers/details/', {'name': 'fred', 'age': 7},
-            ...       HTTP_ACCEPT='application/json')
+            ...       headers={'accept': 'application/json'})
 
         ...will send the HTTP header ``HTTP_ACCEPT`` to the details view, which
         is a good way to test code paths that use the
         :meth:`django.http.HttpRequest.accepts()` method.
 
-        .. admonition:: CGI specification
+        Arbitrary keyword arguments set WSGI
+        :pep:`environ variables <3333#environ-variables>`. For example, headers
+        to set the script name::
 
-            The headers sent via ``**extra`` should follow CGI_ specification.
-            For example, emulating a different "Host" header as sent in the
-            HTTP request from the browser to the server should be passed
-            as ``HTTP_HOST``.
-
-            .. _CGI: https://www.w3.org/CGI/
+            >>> c = Client()
+            >>> c.get("/", SCRIPT_NAME="/app/")
 
         If you already have the GET arguments in URL-encoded form, you can
         use that encoding instead of using the data argument. For example,
@@ -197,7 +211,11 @@ Use the ``django.test.Client`` class to make requests.
         If you set ``secure`` to ``True`` the client will emulate an HTTPS
         request.
 
-    .. method:: Client.post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra)
+        .. versionchanged:: 4.2
+
+            The ``headers`` parameter was added.
+
+    .. method:: Client.post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, **extra)
 
         Makes a POST request on the provided ``path`` and returns a
         ``Response`` object, which is documented below.
@@ -277,7 +295,8 @@ Use the ``django.test.Client`` class to make requests.
         such as an image, this means you will need to open the file in
         ``rb`` (read binary) mode.
 
-        The ``extra`` argument acts the same as for :meth:`Client.get`.
+        The ``headers`` and ``extra`` parameters acts the same as for
+        :meth:`Client.get`.
 
         If the URL you request with a POST contains encoded parameters, these
         parameters will be made available in the request.GET data. For example,
@@ -296,14 +315,22 @@ Use the ``django.test.Client`` class to make requests.
         If you set ``secure`` to ``True`` the client will emulate an HTTPS
         request.
 
-    .. method:: Client.head(path, data=None, follow=False, secure=False, **extra)
+        .. versionchanged:: 4.2
+
+            The ``headers`` parameter was added.
+
+    .. method:: Client.head(path, data=None, follow=False, secure=False, *, headers=None, **extra)
 
         Makes a HEAD request on the provided ``path`` and returns a
         ``Response`` object. This method works just like :meth:`Client.get`,
-        including the ``follow``, ``secure`` and ``extra`` arguments, except
-        it does not return a message body.
+        including the ``follow``, ``secure``, ``headers``, and ``extra``
+        parameters, except it does not return a message body.
+
+        .. versionchanged:: 4.2
 
-    .. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
+            The ``headers`` parameter was added.
+
+    .. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)
 
         Makes an OPTIONS request on the provided ``path`` and returns a
         ``Response`` object. Useful for testing RESTful interfaces.
@@ -311,10 +338,14 @@ Use the ``django.test.Client`` class to make requests.
         When ``data`` is provided, it is used as the request body, and
         a ``Content-Type`` header is set to ``content_type``.
 
-        The ``follow``, ``secure`` and ``extra`` arguments act the same as for
-        :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        the same as for :meth:`Client.get`.
 
-    .. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
+        .. versionchanged:: 4.2
+
+            The ``headers`` parameter was added.
+
+    .. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)
 
         Makes a PUT request on the provided ``path`` and returns a
         ``Response`` object. Useful for testing RESTful interfaces.
@@ -322,18 +353,26 @@ Use the ``django.test.Client`` class to make requests.
         When ``data`` is provided, it is used as the request body, and
         a ``Content-Type`` header is set to ``content_type``.
 
-        The ``follow``, ``secure`` and ``extra`` arguments act the same as for
-        :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        the same as for :meth:`Client.get`.
+
+        .. versionchanged:: 4.2
 
-    .. method:: Client.patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
+            The ``headers`` parameter was added.
+
+    .. method:: Client.patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)
 
         Makes a PATCH request on the provided ``path`` and returns a
         ``Response`` object. Useful for testing RESTful interfaces.
 
-        The ``follow``, ``secure`` and ``extra`` arguments act the same as for
-        :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        the same as for :meth:`Client.get`.
+
+        .. versionchanged:: 4.2
+
+            The ``headers`` parameter was added.
 
-    .. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
+    .. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)
 
         Makes a DELETE request on the provided ``path`` and returns a
         ``Response`` object. Useful for testing RESTful interfaces.
@@ -341,10 +380,14 @@ Use the ``django.test.Client`` class to make requests.
         When ``data`` is provided, it is used as the request body, and
         a ``Content-Type`` header is set to ``content_type``.
 
-        The ``follow``, ``secure`` and ``extra`` arguments act the same as for
-        :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        the same as for :meth:`Client.get`.
+
+        .. versionchanged:: 4.2
 
-    .. method:: Client.trace(path, follow=False, secure=False, **extra)
+            The ``headers`` parameter was added.
+
+    .. method:: Client.trace(path, follow=False, secure=False, *, headers=None, **extra)
 
         Makes a TRACE request on the provided ``path`` and returns a
         ``Response`` object. Useful for simulating diagnostic probes.
@@ -353,8 +396,12 @@ Use the ``django.test.Client`` class to make requests.
         parameter in order to comply with :rfc:`9110#section-9.3.8`, which
         mandates that TRACE requests must not have a body.
 
-        The ``follow``, ``secure``, and ``extra`` arguments act the same as for
-        :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        the same as for :meth:`Client.get`.
+
+        .. versionchanged:: 4.2
+
+            The ``headers`` parameter was added.
 
     .. method:: Client.login(**credentials)
 
@@ -1905,7 +1952,7 @@ If you are testing from an asynchronous function, you must also use the
 asynchronous test client. This is available as ``django.test.AsyncClient``,
 or as ``self.async_client`` on any test.
 
-.. class:: AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, **defaults)
+.. class:: AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, **defaults)
 
 ``AsyncClient`` has the same methods and signatures as the synchronous (normal)
 test client, with two exceptions:
@@ -1924,6 +1971,10 @@ test client, with two exceptions:
     ...     ACCEPT='application/json'
     ... )
 
+.. versionchanged:: 4.2
+
+    The ``headers`` parameter was added.
+
 Using ``AsyncClient`` any method that makes a request must be awaited::
 
     async def test_my_thing(self):

+ 2 - 2
tests/i18n/tests.py

@@ -2139,7 +2139,7 @@ class UnprefixedDefaultLanguageTests(SimpleTestCase):
 
     def test_unprefixed_language_with_accept_language(self):
         """'Accept-Language' is respected."""
-        response = self.client.get("/simple/", HTTP_ACCEPT_LANGUAGE="fr")
+        response = self.client.get("/simple/", headers={"accept-language": "fr"})
         self.assertRedirects(response, "/fr/simple/")
 
     def test_unprefixed_language_with_cookie_language(self):
@@ -2149,7 +2149,7 @@ class UnprefixedDefaultLanguageTests(SimpleTestCase):
         self.assertRedirects(response, "/fr/simple/")
 
     def test_unprefixed_language_with_non_valid_language(self):
-        response = self.client.get("/simple/", HTTP_ACCEPT_LANGUAGE="fi")
+        response = self.client.get("/simple/", headers={"accept-language": "fi"})
         self.assertEqual(response.content, b"Yes")
         self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: "fi"})
         response = self.client.get("/simple/")

+ 7 - 2
tests/requests/tests.py

@@ -5,9 +5,14 @@ from urllib.parse import urlencode
 
 from django.core.exceptions import DisallowedHost
 from django.core.handlers.wsgi import LimitedStream, WSGIRequest
-from django.http import HttpRequest, RawPostDataException, UnreadablePostError
+from django.http import (
+    HttpHeaders,
+    HttpRequest,
+    RawPostDataException,
+    UnreadablePostError,
+)
 from django.http.multipartparser import MultiPartParserError
-from django.http.request import HttpHeaders, split_domain_port
+from django.http.request import split_domain_port
 from django.test import RequestFactory, SimpleTestCase, override_settings
 from django.test.client import FakePayload
 

+ 58 - 0
tests/test_client/tests.py

@@ -1066,6 +1066,52 @@ class RequestFactoryTest(SimpleTestCase):
         echoed_request_line = "TRACE {} {}".format(url_path, protocol)
         self.assertContains(response, echoed_request_line)
 
+    def test_request_factory_default_headers(self):
+        request = RequestFactory(
+            HTTP_AUTHORIZATION="Bearer faketoken",
+            HTTP_X_ANOTHER_HEADER="some other value",
+        ).get("/somewhere/")
+        self.assertEqual(request.headers["authorization"], "Bearer faketoken")
+        self.assertIn("HTTP_AUTHORIZATION", request.META)
+        self.assertEqual(request.headers["x-another-header"], "some other value")
+        self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
+
+        request = RequestFactory(
+            headers={
+                "Authorization": "Bearer faketoken",
+                "X-Another-Header": "some other value",
+            }
+        ).get("/somewhere/")
+        self.assertEqual(request.headers["authorization"], "Bearer faketoken")
+        self.assertIn("HTTP_AUTHORIZATION", request.META)
+        self.assertEqual(request.headers["x-another-header"], "some other value")
+        self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
+
+    def test_request_factory_sets_headers(self):
+        for method_name, view in self.http_methods_and_views:
+            method = getattr(self.request_factory, method_name)
+            request = method(
+                "/somewhere/",
+                HTTP_AUTHORIZATION="Bearer faketoken",
+                HTTP_X_ANOTHER_HEADER="some other value",
+            )
+            self.assertEqual(request.headers["authorization"], "Bearer faketoken")
+            self.assertIn("HTTP_AUTHORIZATION", request.META)
+            self.assertEqual(request.headers["x-another-header"], "some other value")
+            self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
+
+            request = method(
+                "/somewhere/",
+                headers={
+                    "Authorization": "Bearer faketoken",
+                    "X-Another-Header": "some other value",
+                },
+            )
+            self.assertEqual(request.headers["authorization"], "Bearer faketoken")
+            self.assertIn("HTTP_AUTHORIZATION", request.META)
+            self.assertEqual(request.headers["x-another-header"], "some other value")
+            self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
+
 
 @override_settings(ROOT_URLCONF="test_client.urls")
 class AsyncClientTest(TestCase):
@@ -1176,6 +1222,18 @@ class AsyncRequestFactoryTest(SimpleTestCase):
         self.assertEqual(request.headers["x-another-header"], "some other value")
         self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
 
+        request = self.request_factory.get(
+            "/somewhere/",
+            headers={
+                "Authorization": "Bearer faketoken",
+                "X-Another-Header": "some other value",
+            },
+        )
+        self.assertEqual(request.headers["authorization"], "Bearer faketoken")
+        self.assertIn("HTTP_AUTHORIZATION", request.META)
+        self.assertEqual(request.headers["x-another-header"], "some other value")
+        self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
+
     def test_request_factory_query_string(self):
         request = self.request_factory.get("/somewhere/", {"example": "data"})
         self.assertNotIn("Query-String", request.headers)