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

This commit is contained in:
David Wobrock 2022-10-09 22:33:35 +02:00 committed by Mariusz Felisiak
parent b181cae2e3
commit 67da22f08e
10 changed files with 380 additions and 81 deletions

View File

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

View File

@ -461,6 +461,31 @@ class HttpHeaders(CaseInsensitiveMapping):
return None return None
return header.replace("_", "-").title() 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): class QueryDict(MultiValueDict):
""" """

View File

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

View File

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

View File

@ -279,6 +279,22 @@ Tests
* The :option:`test --debug-sql` option now formats SQL queries with * The :option:`test --debug-sql` option now formats SQL queries with
``sqlparse``. ``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 URLs
~~~~ ~~~~

View File

@ -32,6 +32,10 @@ restricted subset of the test client API:
attributes must be supplied by the test itself if required attributes must be supplied by the test itself if required
for the view to function properly. for the view to function properly.
.. versionchanged:: 4.2
The ``headers`` parameter was added.
Example 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 Arbitrary keyword arguments in ``defaults`` are added directly into the ASGI
scope. scope.
.. versionchanged:: 4.2
The ``headers`` parameter was added.
Testing class-based views Testing class-based views
========================= =========================

View File

@ -112,15 +112,27 @@ Making requests
Use the ``django.test.Client`` class to make 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 A testing HTTP client. Takes several arguments that can customize behavior.
keyword arguments to specify some default headers. For example, this will
send a ``User-Agent`` HTTP header in each request::
>>> 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.get()`,
:meth:`~django.test.Client.post()`, etc. have precedence over :meth:`~django.test.Client.post()`, etc. have precedence over
the defaults passed to the class constructor. 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 Once you have a ``Client`` instance, you can call any of the following
methods: 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`` Makes a GET request on the provided ``path`` and returns a ``Response``
object, which is documented below. object, which is documented below.
@ -153,25 +169,23 @@ Use the ``django.test.Client`` class to make requests.
/customers/details/?name=fred&age=7 /customers/details/?name=fred&age=7
The ``extra`` keyword arguments parameter can be used to specify The ``headers`` parameter can be used to specify headers to be sent in
headers to be sent in the request. For example:: the request. For example::
>>> c = Client() >>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7}, >>> 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 ...will send the HTTP header ``HTTP_ACCEPT`` to the details view, which
is a good way to test code paths that use the is a good way to test code paths that use the
:meth:`django.http.HttpRequest.accepts()` method. :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. >>> c = Client()
For example, emulating a different "Host" header as sent in the >>> c.get("/", SCRIPT_NAME="/app/")
HTTP request from the browser to the server should be passed
as ``HTTP_HOST``.
.. _CGI: https://www.w3.org/CGI/
If you already have the GET arguments in URL-encoded form, you can If you already have the GET arguments in URL-encoded form, you can
use that encoding instead of using the data argument. For example, 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 If you set ``secure`` to ``True`` the client will emulate an HTTPS
request. 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 Makes a POST request on the provided ``path`` and returns a
``Response`` object, which is documented below. ``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 such as an image, this means you will need to open the file in
``rb`` (read binary) mode. ``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 If the URL you request with a POST contains encoded parameters, these
parameters will be made available in the request.GET data. For example, 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 If you set ``secure`` to ``True`` the client will emulate an HTTPS
request. 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 Makes a HEAD request on the provided ``path`` and returns a
``Response`` object. This method works just like :meth:`Client.get`, ``Response`` object. This method works just like :meth:`Client.get`,
including the ``follow``, ``secure`` and ``extra`` arguments, except including the ``follow``, ``secure``, ``headers``, and ``extra``
it does not return a message body. parameters, except it does not return a message body.
.. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra) .. versionchanged:: 4.2
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 Makes an OPTIONS request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``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 When ``data`` is provided, it is used as the request body, and
a ``Content-Type`` header is set to ``content_type``. a ``Content-Type`` header is set to ``content_type``.
The ``follow``, ``secure`` and ``extra`` arguments act the same as for The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
:meth:`Client.get`. 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 Makes a PUT request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``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 When ``data`` is provided, it is used as the request body, and
a ``Content-Type`` header is set to ``content_type``. a ``Content-Type`` header is set to ``content_type``.
The ``follow``, ``secure`` and ``extra`` arguments act the same as for The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
:meth:`Client.get`. the same as for :meth:`Client.get`.
.. method:: Client.patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra) .. versionchanged:: 4.2
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 Makes a PATCH request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``Response`` object. Useful for testing RESTful interfaces.
The ``follow``, ``secure`` and ``extra`` arguments act the same as for The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
:meth:`Client.get`. the same as for :meth:`Client.get`.
.. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra) .. versionchanged:: 4.2
The ``headers`` parameter was added.
.. 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 Makes a DELETE request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``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 When ``data`` is provided, it is used as the request body, and
a ``Content-Type`` header is set to ``content_type``. a ``Content-Type`` header is set to ``content_type``.
The ``follow``, ``secure`` and ``extra`` arguments act the same as for The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
:meth:`Client.get`. the same as for :meth:`Client.get`.
.. method:: Client.trace(path, follow=False, secure=False, **extra) .. versionchanged:: 4.2
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 Makes a TRACE request on the provided ``path`` and returns a
``Response`` object. Useful for simulating diagnostic probes. ``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 parameter in order to comply with :rfc:`9110#section-9.3.8`, which
mandates that TRACE requests must not have a body. mandates that TRACE requests must not have a body.
The ``follow``, ``secure``, and ``extra`` arguments act the same as for The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
:meth:`Client.get`. the same as for :meth:`Client.get`.
.. versionchanged:: 4.2
The ``headers`` parameter was added.
.. method:: Client.login(**credentials) .. 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``, asynchronous test client. This is available as ``django.test.AsyncClient``,
or as ``self.async_client`` on any test. 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) ``AsyncClient`` has the same methods and signatures as the synchronous (normal)
test client, with two exceptions: test client, with two exceptions:
@ -1924,6 +1971,10 @@ test client, with two exceptions:
... ACCEPT='application/json' ... ACCEPT='application/json'
... ) ... )
.. versionchanged:: 4.2
The ``headers`` parameter was added.
Using ``AsyncClient`` any method that makes a request must be awaited:: Using ``AsyncClient`` any method that makes a request must be awaited::
async def test_my_thing(self): async def test_my_thing(self):

View File

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

View File

@ -5,9 +5,14 @@ from urllib.parse import urlencode
from django.core.exceptions import DisallowedHost from django.core.exceptions import DisallowedHost
from django.core.handlers.wsgi import LimitedStream, WSGIRequest 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.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 import RequestFactory, SimpleTestCase, override_settings
from django.test.client import FakePayload from django.test.client import FakePayload

View File

@ -1066,6 +1066,52 @@ class RequestFactoryTest(SimpleTestCase):
echoed_request_line = "TRACE {} {}".format(url_path, protocol) echoed_request_line = "TRACE {} {}".format(url_path, protocol)
self.assertContains(response, echoed_request_line) 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") @override_settings(ROOT_URLCONF="test_client.urls")
class AsyncClientTest(TestCase): class AsyncClientTest(TestCase):
@ -1176,6 +1222,18 @@ class AsyncRequestFactoryTest(SimpleTestCase):
self.assertEqual(request.headers["x-another-header"], "some other value") self.assertEqual(request.headers["x-another-header"], "some other value")
self.assertIn("HTTP_X_ANOTHER_HEADER", request.META) 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): def test_request_factory_query_string(self):
request = self.request_factory.get("/somewhere/", {"example": "data"}) request = self.request_factory.get("/somewhere/", {"example": "data"})
self.assertNotIn("Query-String", request.headers) self.assertNotIn("Query-String", request.headers)