Fixed #33348 -- Changed SimpleTestCase.assertFormError()/assertFormsetErrors() to take form/formset.

Instead of taking a response object and a context name for
the form/formset, the two methods now take the object directly.
This commit is contained in:
Baptiste Mispelon 2021-12-10 12:22:23 +01:00 committed by Mariusz Felisiak
parent 1a7d75cf77
commit 50e1e7ef8e
8 changed files with 563 additions and 290 deletions

View File

@ -441,9 +441,8 @@ class InlineAdminFormSet:
def forms(self):
return self.formset.forms
@property
def non_form_errors(self):
return self.formset.non_form_errors
return self.formset.non_form_errors()
@property
def is_bound(self):

View File

@ -1,5 +1,6 @@
import asyncio
import difflib
import inspect
import json
import logging
import posixpath
@ -42,6 +43,7 @@ from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
from django.forms.fields import CharField
from django.http import QueryDict
from django.http.request import split_domain_port, validate_host
from django.http.response import HttpResponseBase
from django.test.client import AsyncClient, Client
from django.test.html import HTMLParseError, parse_html
from django.test.signals import template_rendered
@ -165,6 +167,127 @@ class _DatabaseFailure:
raise DatabaseOperationForbidden(self.message)
# RemovedInDjango50Warning
class _AssertFormErrorDeprecationHelper:
@staticmethod
def assertFormError(self, response, form, field, errors, msg_prefix=""):
"""
Search through all the rendered contexts of the `response` for a form named
`form` then dispatch to the new assertFormError() using that instance.
If multiple contexts contain the form, they're all checked in order and any
failure will abort (this matches the old behavior).
"""
warning_msg = (
f"Passing response to assertFormError() is deprecated. Use the form object "
f"directly: assertFormError(response.context[{form!r}], {field!r}, ...)"
)
warnings.warn(warning_msg, RemovedInDjango50Warning, stacklevel=2)
full_msg_prefix = f"{msg_prefix}: " if msg_prefix else ""
contexts = to_list(response.context) if response.context is not None else []
if not contexts:
self.fail(
f"{full_msg_prefix}Response did not use any contexts to render the "
f"response"
)
# Search all contexts for the error.
found_form = False
for i, context in enumerate(contexts):
if form not in context:
continue
found_form = True
self.assertFormError(context[form], field, errors, msg_prefix=msg_prefix)
if not found_form:
self.fail(
f"{full_msg_prefix}The form '{form}' was not used to render the "
f"response"
)
@staticmethod
def assertFormsetError(
self, response, formset, form_index, field, errors, msg_prefix=""
):
"""
Search for a formset named "formset" in the "response" and dispatch to
the new assertFormsetError() using that instance. If the name is found
in multiple contexts they're all checked in order and any failure will
abort the test.
"""
warning_msg = (
f"Passing response to assertFormsetError() is deprecated. Use the formset "
f"object directly: assertFormsetError(response.context[{formset!r}], "
f"{form_index!r}, ...)"
)
warnings.warn(warning_msg, RemovedInDjango50Warning, stacklevel=2)
full_msg_prefix = f"{msg_prefix}: " if msg_prefix else ""
contexts = to_list(response.context) if response.context is not None else []
if not contexts:
self.fail(
f"{full_msg_prefix}Response did not use any contexts to render the "
f"response"
)
found_formset = False
for i, context in enumerate(contexts):
if formset not in context or not hasattr(context[formset], "forms"):
continue
found_formset = True
self.assertFormsetError(
context[formset], form_index, field, errors, msg_prefix
)
if not found_formset:
self.fail(
f"{full_msg_prefix}The formset '{formset}' was not used to render the "
f"response"
)
@classmethod
def patch_signature(cls, new_method):
"""
Replace the decorated method with a new one that inspects the passed
args/kwargs and dispatch to the old implementation (with deprecation
warning) when it detects the old signature.
"""
@wraps(new_method)
def patched_method(self, *args, **kwargs):
old_method = getattr(cls, new_method.__name__)
old_signature = inspect.signature(old_method)
try:
old_bound_args = old_signature.bind(self, *args, **kwargs)
except TypeError:
# If old signature doesn't match then either:
# 1) new signature will match
# 2) or a TypeError will be raised showing the user information
# about the new signature.
return new_method(self, *args, **kwargs)
new_signature = inspect.signature(new_method)
try:
new_bound_args = new_signature.bind(self, *args, **kwargs)
except TypeError:
# Old signature matches but not the new one (because of
# previous try/except).
return old_method(self, *args, **kwargs)
# If both signatures match, decide on which method to call by
# inspecting the first arg (arg[0] = self).
assert old_bound_args.args[1] == new_bound_args.args[1]
if hasattr(
old_bound_args.args[1], "context"
): # Looks like a response object => old method.
return old_method(self, *args, **kwargs)
elif isinstance(old_bound_args.args[1], HttpResponseBase):
raise ValueError(
f"{old_method.__name__}() is only usable on responses fetched "
f"using the Django test Client."
)
else:
return new_method(self, *args, **kwargs)
return patched_method
class SimpleTestCase(unittest.TestCase):
# The class we'll use for the test client self.client.
@ -585,22 +708,19 @@ class SimpleTestCase(unittest.TestCase):
self.assertEqual(field_errors, errors, msg_prefix + failure_message)
def assertFormError(self, response, form, field, errors, msg_prefix=""):
# RemovedInDjango50Warning: When the deprecation ends, remove the
# decorator.
@_AssertFormErrorDeprecationHelper.patch_signature
def assertFormError(self, form, field, errors, msg_prefix=""):
"""
Assert that a form used to render the response has a specific field
error.
Assert that a field named "field" on the given form object has specific
errors.
errors can be either a single error message or a list of errors
messages. Using errors=[] test that the field has no errors.
You can pass field=None to check the form's non-field errors.
"""
self._check_test_client_response(response, "context", "assertFormError")
if msg_prefix:
msg_prefix += ": "
# Put context(s) into a list to simplify processing.
contexts = [] if response.context is None else to_list(response.context)
if not contexts:
self.fail(
msg_prefix + "Response did not use any contexts to render the response"
)
if errors is None:
warnings.warn(
"Passing errors=None to assertFormError() is deprecated, use "
@ -609,47 +729,25 @@ class SimpleTestCase(unittest.TestCase):
stacklevel=2,
)
errors = []
# Put error(s) into a list to simplify processing.
errors = to_list(errors)
# Search all contexts for the error.
found_form = False
for i, context in enumerate(contexts):
if form in context:
found_form = True
self._assert_form_error(
context[form], field, errors, msg_prefix, "form %r" % context[form]
)
if not found_form:
self.fail(
msg_prefix + "The form '%s' was not used to render the response" % form
)
def assertFormsetError(
self, response, formset, form_index, field, errors, msg_prefix=""
):
"""
Assert that a formset used to render the response has a specific error.
For field errors, specify the ``form_index`` and the ``field``.
For non-field errors, specify the ``form_index`` and the ``field`` as
None.
For non-form errors, specify ``form_index`` as None and the ``field``
as None.
"""
self._check_test_client_response(response, "context", "assertFormsetError")
# Add punctuation to msg_prefix
if msg_prefix:
msg_prefix += ": "
errors = to_list(errors)
self._assert_form_error(form, field, errors, msg_prefix, f"form {form!r}")
# Put context(s) into a list to simplify processing.
contexts = [] if response.context is None else to_list(response.context)
if not contexts:
self.fail(
msg_prefix + "Response did not use any contexts to "
"render the response"
)
# RemovedInDjango50Warning: When the deprecation ends, remove the
# decorator.
@_AssertFormErrorDeprecationHelper.patch_signature
def assertFormsetError(self, formset, form_index, field, errors, msg_prefix=""):
"""
Similar to assertFormError() but for formsets.
Use form_index=None to check the formset's non-form errors (in that
case, you must also use field=None).
Otherwise use an integer to check the formset's n-th form for errors.
Other parameters are the same as assertFormError().
"""
if errors is None:
warnings.warn(
"Passing errors=None to assertFormsetError() is deprecated, "
@ -662,50 +760,31 @@ class SimpleTestCase(unittest.TestCase):
if form_index is None and field is not None:
raise ValueError("You must use field=None with form_index=None.")
# Put error(s) into a list to simplify processing.
if msg_prefix:
msg_prefix += ": "
errors = to_list(errors)
# Search all contexts for the error.
found_formset = False
for i, context in enumerate(contexts):
if formset not in context or not hasattr(context[formset], "forms"):
continue
formset_repr = repr(context[formset])
if not context[formset].is_bound:
self.fail(
f"{msg_prefix}The formset {formset_repr} is not bound, it will "
f"never have any errors."
)
found_formset = True
if form_index is not None:
form_count = context[formset].total_form_count()
if form_index >= form_count:
form_or_forms = "forms" if form_count > 1 else "form"
self.fail(
f"{msg_prefix}The formset {formset_repr} only has "
f"{form_count} {form_or_forms}."
)
if form_index is not None:
form_repr = f"form {form_index} of formset {formset_repr}"
self._assert_form_error(
context[formset].forms[form_index],
field,
errors,
msg_prefix,
form_repr,
)
else:
failure_message = (
f"{msg_prefix}The non-form errors of formset {formset_repr} don't "
f"match."
)
self.assertEqual(
context[formset].non_form_errors(), errors, failure_message
)
if not found_formset:
if not formset.is_bound:
self.fail(
msg_prefix
+ "The formset '%s' was not used to render the response" % formset
f"{msg_prefix}The formset {formset!r} is not bound, it will never have "
f"any errors."
)
if form_index is not None and form_index >= formset.total_form_count():
form_count = formset.total_form_count()
form_or_forms = "forms" if form_count > 1 else "form"
self.fail(
f"{msg_prefix}The formset {formset!r} only has {form_count} "
f"{form_or_forms}."
)
if form_index is not None:
form_repr = f"form {form_index} of formset {formset!r}"
self._assert_form_error(
formset.forms[form_index], field, errors, msg_prefix, form_repr
)
else:
failure_message = f"The non-form errors of formset {formset!r} don't match."
self.assertEqual(
formset.non_form_errors(), errors, msg_prefix + failure_message
)
def _get_template_used(self, response, template_name, msg_prefix, method_name):

View File

@ -97,6 +97,10 @@ details on these changes.
* The ``django.utils.timezone.utc`` alias to ``datetime.timezone.utc`` will be
removed.
* Passing a response object and a form/formset name to
``SimpleTestCase.assertFormError()`` and ``assertFormsetError()`` will no
longer be allowed.
.. _deprecation-removed-in-4.1:
4.1

View File

@ -327,6 +327,10 @@ Tests
* A nested atomic block marked as durable in :class:`django.test.TestCase` now
raises a ``RuntimeError``, the same as outside of tests.
* :meth:`.SimpleTestCase.assertFormError` and
:meth:`~.SimpleTestCase.assertFormsetError` now support passing a
form/formset object directly.
URLs
~~~~
@ -449,6 +453,9 @@ Miscellaneous
* The admin log out UI now uses ``POST`` requests.
* The undocumented ``InlineAdminFormSet.non_form_errors`` property is replaced
by the ``non_form_errors()`` method. This is consistent with ``BaseFormSet``.
.. _deprecated-features-4.1:
Features deprecated in 4.1
@ -552,6 +559,15 @@ Miscellaneous
* The :data:`django.utils.timezone.utc` alias to :attr:`datetime.timezone.utc`
is deprecated. Use :attr:`datetime.timezone.utc` directly.
* Passing a response object and a form/formset name to
``SimpleTestCase.assertFormError()`` and ``assertFormsetError()`` is
deprecated. Use::
assertFormError(response.context['form_name'], …)
assertFormsetError(response.context['formset_name'], …)
or pass the form/formset object directly instead.
Features removed in 4.1
=======================

View File

@ -1473,47 +1473,65 @@ your test suite.
self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})
.. method:: SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='')
.. method:: SimpleTestCase.assertFormError(form, field, errors, msg_prefix='')
Asserts that a field on a form raises the provided list of errors when
rendered on the form.
Asserts that a field on a form raises the provided list of errors.
``response`` must be a response instance returned by the
:class:`test client <django.test.Response>`.
``form`` is a ``Form`` instance. The form must be
:ref:`bound <ref-forms-api-bound-unbound>` but not necessarily
validated (``assertFormError()`` will automatically call ``full_clean()``
on the form).
``form`` is the name the ``Form`` instance was given in the template
context of the response.
``field`` is the name of the field on the form to check. To check the form's
:meth:`non-field errors <django.forms.Form.non_field_errors>`, use
``field=None``.
``field`` is the name of the field on the form to check. If ``field``
has a value of ``None``, non-field errors (errors you can access via
:meth:`form.non_field_errors() <django.forms.Form.non_field_errors>`) will
be checked.
``errors`` is a list of all the error strings that the field is expected to
have. You can also pass a single error string if you only expect one error
which means that ``errors='error message'`` is the same as
``errors=['error message']``.
``errors`` is an error string, or a list of error strings, that are
expected as a result of form validation.
.. versionchanged:: 4.1
.. method:: SimpleTestCase.assertFormsetError(response, formset, form_index, field, errors, msg_prefix='')
In older versions, using an empty error list with ``assertFormError()``
would always pass, regardless of whether the field had any errors or
not. Starting from Django 4.1, using ``errors=[]`` will only pass if
the field actually has no errors.
Django 4.1 also changed the behavior of ``assertFormError()`` when a
field has multiple errors. In older versions, if a field had multiple
errors and you checked for only some of them, the test would pass.
Starting from Django 4.1, the error list must be an exact match to the
field's actual errors.
.. deprecated:: 4.1
Support for passing a response object and a form name to
``assertFormError()`` is deprecated and will be removed in Django 5.0.
Use the form instance directly instead.
.. method:: SimpleTestCase.assertFormsetError(formset, form_index, field, errors, msg_prefix='')
Asserts that the ``formset`` raises the provided list of errors when
rendered.
``response`` must be a response instance returned by the
:class:`test client <django.test.Response>`.
``formset`` is a ``Formset`` instance. The formset must be bound but not
necessarily validated (``assertFormsetError()`` will automatically call the
``full_clean()`` on the formset).
``formset`` is the name the ``Formset`` instance was given in the template
context of the response.
``form_index`` is the number of the form within the ``Formset`` (starting
from 0). Use ``form_index=None`` to check the formset's non-form errors,
i.e. the errors you get when calling ``formset.non_form_errors()``. In that
case you must also use ``field=None``.
``form_index`` is the number of the form within the ``Formset``. If
``form_index`` has a value of ``None``, non-form errors (errors you can
access via ``formset.non_form_errors()``) will be checked.
``field`` and ``errors`` have the same meaning as the parameters to
``assertFormError()``.
``field`` is the name of the field on the form to check. If ``field``
has a value of ``None``, non-field errors (errors you can access via
:meth:`form.non_field_errors() <django.forms.Form.non_field_errors>`) will
be checked.
.. deprecated:: 4.1
``errors`` is an error string, or a list of error strings, that are
expected as a result of form validation.
Support for passing a response object and a formset name to
``assertFormsetError()`` is deprecated and will be removed in Django
5.0. Use the formset instance directly instead.
.. method:: SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)

View File

@ -2128,7 +2128,9 @@ class AdminViewPermissionsTest(TestCase):
self.assertEqual(response.status_code, 302)
login = self.client.post(login_url, self.no_username_login)
self.assertEqual(login.status_code, 200)
self.assertFormError(login, "form", "username", ["This field is required."])
self.assertFormError(
login.context["form"], "username", ["This field is required."]
)
def test_login_redirect_for_direct_get(self):
"""
@ -6711,10 +6713,9 @@ class UserAdminTest(TestCase):
},
)
self.assertEqual(response.status_code, 200)
self.assertFormError(response, "adminform", "password1", [])
self.assertFormError(response.context["adminform"], "password1", [])
self.assertFormError(
response,
"adminform",
response.context["adminform"],
"password2",
["The two password fields didnt match."],
)
@ -7836,12 +7837,13 @@ class AdminViewOnSiteTests(TestCase):
reverse("admin:admin_views_parentwithdependentchildren_add"), post_data
)
self.assertFormError(
response, "adminform", "some_required_info", ["This field is required."]
response.context["adminform"],
"some_required_info",
["This field is required."],
)
self.assertFormError(response, "adminform", None, [])
self.assertFormError(response.context["adminform"], None, [])
self.assertFormsetError(
response,
"inline_admin_formset",
response.context["inline_admin_formset"],
0,
None,
[
@ -7849,7 +7851,9 @@ class AdminViewOnSiteTests(TestCase):
"contrived test case"
],
)
self.assertFormsetError(response, "inline_admin_formset", None, None, [])
self.assertFormsetError(
response.context["inline_admin_formset"], None, None, []
)
def test_change_view_form_and_formsets_run_validation(self):
"""
@ -7879,11 +7883,12 @@ class AdminViewOnSiteTests(TestCase):
post_data,
)
self.assertFormError(
response, "adminform", "some_required_info", ["This field is required."]
response.context["adminform"],
"some_required_info",
["This field is required."],
)
self.assertFormsetError(
response,
"inline_admin_formset",
response.context["inline_admin_formset"],
0,
None,
[

View File

@ -437,10 +437,10 @@ class ClientTest(TestCase):
response = self.client.post("/form_view/", post_data)
self.assertContains(response, "This field is required.", 3)
self.assertTemplateUsed(response, "Invalid POST Template")
self.assertFormError(response, "form", "email", "This field is required.")
self.assertFormError(response, "form", "single", "This field is required.")
self.assertFormError(response, "form", "multi", "This field is required.")
form = response.context["form"]
self.assertFormError(form, "email", "This field is required.")
self.assertFormError(form, "single", "This field is required.")
self.assertFormError(form, "multi", "This field is required.")
def test_form_error(self):
"POST erroneous data to a form"
@ -455,7 +455,9 @@ class ClientTest(TestCase):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "Invalid POST Template")
self.assertFormError(response, "form", "email", "Enter a valid email address.")
self.assertFormError(
response.context["form"], "email", "Enter a valid email address."
)
def test_valid_form_with_template(self):
"POST valid data to a form using multiple templates"
@ -480,10 +482,10 @@ class ClientTest(TestCase):
self.assertTemplateUsed(response, "form_view.html")
self.assertTemplateUsed(response, "base.html")
self.assertTemplateNotUsed(response, "Invalid POST Template")
self.assertFormError(response, "form", "email", "This field is required.")
self.assertFormError(response, "form", "single", "This field is required.")
self.assertFormError(response, "form", "multi", "This field is required.")
form = response.context["form"]
self.assertFormError(form, "email", "This field is required.")
self.assertFormError(form, "single", "This field is required.")
self.assertFormError(form, "multi", "This field is required.")
def test_form_error_with_template(self):
"POST erroneous data to a form using multiple templates"
@ -500,7 +502,9 @@ class ClientTest(TestCase):
self.assertTemplateUsed(response, "base.html")
self.assertTemplateNotUsed(response, "Invalid POST Template")
self.assertFormError(response, "form", "email", "Enter a valid email address.")
self.assertFormError(
response.context["form"], "email", "Enter a valid email address."
)
def test_unknown_page(self):
"GET an invalid URL"

View File

@ -1373,6 +1373,7 @@ class TestFormset(formset_factory(TestForm)):
class AssertFormErrorTests(SimpleTestCase):
@ignore_warnings(category=RemovedInDjango50Warning)
def test_non_client_response(self):
msg = (
"assertFormError() is only usable on responses fetched using the "
@ -1380,8 +1381,9 @@ class AssertFormErrorTests(SimpleTestCase):
)
response = HttpResponse()
with self.assertRaisesMessage(ValueError, msg):
self.assertFormError(response, "formset", 0, "field", "invalid value")
self.assertFormError(response, "form", "field", "invalid value")
@ignore_warnings(category=RemovedInDjango50Warning)
def test_response_with_no_context(self):
msg = "Response did not use any contexts to render the response"
response = mock.Mock(context=[])
@ -1397,6 +1399,7 @@ class AssertFormErrorTests(SimpleTestCase):
msg_prefix=msg_prefix,
)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_form_not_in_context(self):
msg = "The form 'form' was not used to render the response"
response = mock.Mock(context=[{}])
@ -1408,18 +1411,32 @@ class AssertFormErrorTests(SimpleTestCase):
response, "form", "field", "invalid value", msg_prefix=msg_prefix
)
def test_single_error(self):
self.assertFormError(TestForm.invalid(), "field", "invalid value")
def test_error_list(self):
self.assertFormError(TestForm.invalid(), "field", ["invalid value"])
def test_empty_errors_valid_form(self):
self.assertFormError(TestForm.valid(), "field", [])
def test_empty_errors_valid_form_non_field_errors(self):
self.assertFormError(TestForm.valid(), None, [])
def test_field_not_in_form(self):
msg = (
"The form <TestForm bound=True, valid=False, fields=(field)> does not "
"contain the field 'other_field'."
)
response = mock.Mock(context=[{"form": TestForm.invalid()}])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormError(response, "form", "other_field", "invalid value")
self.assertFormError(TestForm.invalid(), "other_field", "invalid value")
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormError(
response, "form", "other_field", "invalid value", msg_prefix=msg_prefix
TestForm.invalid(),
"other_field",
"invalid value",
msg_prefix=msg_prefix,
)
def test_field_with_no_errors(self):
@ -1427,14 +1444,13 @@ class AssertFormErrorTests(SimpleTestCase):
"The errors of field 'field' on form <TestForm bound=True, valid=True, "
"fields=(field)> don't match."
)
response = mock.Mock(context=[{"form": TestForm.valid()}])
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormError(response, "form", "field", "invalid value")
self.assertFormError(TestForm.valid(), "field", "invalid value")
self.assertIn("[] != ['invalid value']", str(ctx.exception))
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormError(
response, "form", "field", "invalid value", msg_prefix=msg_prefix
TestForm.valid(), "field", "invalid value", msg_prefix=msg_prefix
)
def test_field_with_different_error(self):
@ -1442,99 +1458,62 @@ class AssertFormErrorTests(SimpleTestCase):
"The errors of field 'field' on form <TestForm bound=True, valid=False, "
"fields=(field)> don't match."
)
response = mock.Mock(context=[{"form": TestForm.invalid()}])
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormError(response, "form", "field", "other error")
self.assertFormError(TestForm.invalid(), "field", "other error")
self.assertIn("['invalid value'] != ['other error']", str(ctx.exception))
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormError(
response, "form", "field", "other error", msg_prefix=msg_prefix
TestForm.invalid(), "field", "other error", msg_prefix=msg_prefix
)
def test_basic_positive_assertion(self):
response = mock.Mock(context=[{"form": TestForm.invalid()}])
self.assertFormError(response, "form", "field", "invalid value")
def test_basic_positive_assertion_multicontext(self):
response = mock.Mock(context=[{}, {"form": TestForm.invalid()}])
self.assertFormError(response, "form", "field", "invalid value")
def test_empty_errors_unbound_form(self):
def test_unbound_form(self):
msg = (
"The form <TestForm bound=False, valid=Unknown, fields=(field)> is not "
"bound, it will never have any errors."
)
response = mock.Mock(context=[{"form": TestForm()}])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormError(response, "form", "field", [])
self.assertFormError(TestForm(), "field", [])
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormError(response, "form", "field", [], msg_prefix=msg_prefix)
def test_empty_errors_valid_form(self):
response = mock.Mock(context=[{"form": TestForm.valid()}])
self.assertFormError(response, "form", "field", [])
self.assertFormError(TestForm(), "field", [], msg_prefix=msg_prefix)
def test_empty_errors_invalid_form(self):
msg = (
"The errors of field 'field' on form <TestForm bound=True, valid=False, "
"fields=(field)> don't match."
)
response = mock.Mock(context=[{"form": TestForm.invalid()}])
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormError(response, "form", "field", [])
self.assertFormError(TestForm.invalid(), "field", [])
self.assertIn("['invalid value'] != []", str(ctx.exception))
def test_non_field_errors(self):
response = mock.Mock(context=[{"form": TestForm.invalid(nonfield=True)}])
self.assertFormError(response, "form", None, "non-field error")
self.assertFormError(TestForm.invalid(nonfield=True), None, "non-field error")
def test_different_non_field_errors(self):
response = mock.Mock(context=[{"form": TestForm.invalid(nonfield=True)}])
msg = (
"The non-field errors of form <TestForm bound=True, valid=False, "
"fields=(field)> don't match."
)
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormError(response, "form", None, "other non-field error")
self.assertFormError(
TestForm.invalid(nonfield=True), None, "other non-field error"
)
self.assertIn(
"['non-field error'] != ['other non-field error']", str(ctx.exception)
)
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormError(
response, "form", None, "other non-field error", msg_prefix=msg_prefix
TestForm.invalid(nonfield=True),
None,
"other non-field error",
msg_prefix=msg_prefix,
)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_errors_none(self):
msg = (
"The errors of field 'field' on form <TestForm bound=True, valid=False, "
"fields=(field)> don't match."
)
response = mock.Mock(context=[{"form": TestForm.invalid()}])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormError(response, "form", "field", None)
def test_errors_none_warning(self):
response = mock.Mock(context=[{"form": TestForm.valid()}])
msg = (
"Passing errors=None to assertFormError() is deprecated, use "
"errors=[] instead."
)
with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
self.assertFormError(response, "form", "field", None)
class AssertFormsetErrorTests(SimpleTestCase):
def _get_formset_data(self, field_value):
return {
"form-TOTAL_FORMS": "1",
"form-INITIAL_FORMS": "0",
"form-0-field": field_value,
}
@ignore_warnings(category=RemovedInDjango50Warning)
def test_non_client_response(self):
msg = (
"assertFormsetError() is only usable on responses fetched using "
@ -1544,12 +1523,14 @@ class AssertFormsetErrorTests(SimpleTestCase):
with self.assertRaisesMessage(ValueError, msg):
self.assertFormsetError(response, "formset", 0, "field", "invalid value")
@ignore_warnings(category=RemovedInDjango50Warning)
def test_response_with_no_context(self):
msg = "Response did not use any contexts to render the response"
response = mock.Mock(context=[])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(response, "formset", 0, "field", "invalid value")
@ignore_warnings(category=RemovedInDjango50Warning)
def test_formset_not_in_context(self):
msg = "The formset 'formset' was not used to render the response"
response = mock.Mock(context=[{}])
@ -1561,25 +1542,41 @@ class AssertFormsetErrorTests(SimpleTestCase):
response, "formset", 0, "field", "invalid value", msg_prefix=msg_prefix
)
def test_single_error(self):
self.assertFormsetError(TestFormset.invalid(), 0, "field", "invalid value")
def test_error_list(self):
self.assertFormsetError(TestFormset.invalid(), 0, "field", ["invalid value"])
def test_empty_errors_valid_formset(self):
self.assertFormsetError(TestFormset.valid(), 0, "field", [])
def test_multiple_forms(self):
formset = TestFormset(
{
"form-TOTAL_FORMS": "2",
"form-INITIAL_FORMS": "0",
"form-0-field": "valid",
"form-1-field": "invalid",
}
)
formset.full_clean()
self.assertFormsetError(formset, 0, "field", [])
self.assertFormsetError(formset, 1, "field", ["invalid value"])
def test_field_not_in_form(self):
msg = (
"The form 0 of formset <TestFormset: bound=True valid=False total_forms=1> "
"does not contain the field 'other_field'."
)
response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(
response,
"formset",
0,
"other_field",
"invalid value",
TestFormset.invalid(), 0, "other_field", "invalid value"
)
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormsetError(
response,
"formset",
TestFormset.invalid(),
0,
"other_field",
"invalid value",
@ -1591,14 +1588,13 @@ class AssertFormsetErrorTests(SimpleTestCase):
"The errors of field 'field' on form 0 of formset <TestFormset: bound=True "
"valid=True total_forms=1> don't match."
)
response = mock.Mock(context=[{"formset": TestFormset.valid()}])
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormsetError(response, "formset", 0, "field", "invalid value")
self.assertFormsetError(TestFormset.valid(), 0, "field", "invalid value")
self.assertIn("[] != ['invalid value']", str(ctx.exception))
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormsetError(
response, "formset", 0, "field", "invalid value", msg_prefix=msg_prefix
TestFormset.valid(), 0, "field", "invalid value", msg_prefix=msg_prefix
)
def test_field_with_different_error(self):
@ -1606,67 +1602,45 @@ class AssertFormsetErrorTests(SimpleTestCase):
"The errors of field 'field' on form 0 of formset <TestFormset: bound=True "
"valid=False total_forms=1> don't match."
)
response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormsetError(response, "formset", 0, "field", "other error")
self.assertFormsetError(TestFormset.invalid(), 0, "field", "other error")
self.assertIn("['invalid value'] != ['other error']", str(ctx.exception))
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormsetError(
response, "formset", 0, "field", "other error", msg_prefix=msg_prefix
TestFormset.invalid(), 0, "field", "other error", msg_prefix=msg_prefix
)
def test_basic_positive_assertion(self):
response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
self.assertFormsetError(response, "formset", 0, "field", "invalid value")
def test_basic_positive_assertion_multicontext(self):
response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
self.assertFormsetError(response, "formset", 0, "field", "invalid value")
def test_empty_errors_unbound_formset(self):
def test_unbound_formset(self):
msg = (
"The formset <TestFormset: bound=False valid=Unknown total_forms=1> is not "
"bound, it will never have any errors."
)
response = mock.Mock(context=[{"formset": TestFormset()}])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(response, "formset", 0, "field", [])
def test_empty_errors_valid_formset(self):
response = mock.Mock(context=[{}, {"formset": TestFormset.valid()}])
self.assertFormsetError(response, "formset", 0, "field", [])
self.assertFormsetError(TestFormset(), 0, "field", [])
def test_empty_errors_invalid_formset(self):
msg = (
"The errors of field 'field' on form 0 of formset <TestFormset: bound=True "
"valid=False total_forms=1> don't match."
)
response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormsetError(response, "formset", 0, "field", [])
self.assertFormsetError(TestFormset.invalid(), 0, "field", [])
self.assertIn("['invalid value'] != []", str(ctx.exception))
def test_non_field_errors(self):
response = mock.Mock(
context=[
{},
{"formset": TestFormset.invalid(nonfield=True)},
]
self.assertFormsetError(
TestFormset.invalid(nonfield=True), 0, None, "non-field error"
)
self.assertFormsetError(response, "formset", 0, None, "non-field error")
def test_different_non_field_errors(self):
response = mock.Mock(
context=[{}, {"formset": TestFormset.invalid(nonfield=True)}],
)
msg = (
"The non-field errors of form 0 of formset <TestFormset: bound=True "
"valid=False total_forms=1> don't match."
)
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormsetError(
response, "formset", 0, None, "other non-field error"
TestFormset.invalid(nonfield=True), 0, None, "other non-field error"
)
self.assertIn(
"['non-field error'] != ['other non-field error']", str(ctx.exception)
@ -1674,8 +1648,7 @@ class AssertFormsetErrorTests(SimpleTestCase):
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormsetError(
response,
"formset",
TestFormset.invalid(nonfield=True),
0,
None,
"other non-field error",
@ -1683,80 +1656,74 @@ class AssertFormsetErrorTests(SimpleTestCase):
)
def test_no_non_field_errors(self):
response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
msg = (
"The non-field errors of form 0 of formset <TestFormset: bound=True "
"valid=False total_forms=1> don't match."
)
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormsetError(response, "formset", 0, None, "non-field error")
self.assertFormsetError(TestFormset.invalid(), 0, None, "non-field error")
self.assertIn("[] != ['non-field error']", str(ctx.exception))
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormsetError(
response, "formset", 0, None, "non-field error", msg_prefix=msg_prefix
TestFormset.invalid(), 0, None, "non-field error", msg_prefix=msg_prefix
)
def test_non_form_errors(self):
response = mock.Mock(
context=[
{},
{"formset": TestFormset.invalid(nonform=True)},
]
)
self.assertFormsetError(response, "formset", None, None, "error")
self.assertFormsetError(TestFormset.invalid(nonform=True), None, None, "error")
def test_different_non_form_errors(self):
response = mock.Mock(
context=[{}, {"formset": TestFormset.invalid(nonform=True)}],
)
msg = (
"The non-form errors of formset <TestFormset: bound=True valid=False "
"total_forms=0> don't match."
)
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormsetError(response, "formset", None, None, "other error")
self.assertFormsetError(
TestFormset.invalid(nonform=True), None, None, "other error"
)
self.assertIn("['error'] != ['other error']", str(ctx.exception))
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormsetError(
response, "formset", None, None, "other error", msg_prefix=msg_prefix
TestFormset.invalid(nonform=True),
None,
None,
"other error",
msg_prefix=msg_prefix,
)
def test_no_non_form_errors(self):
response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
msg = (
"The non-form errors of formset <TestFormset: bound=True valid=False "
"total_forms=1> don't match."
)
with self.assertRaisesMessage(AssertionError, msg) as ctx:
self.assertFormsetError(response, "formset", None, None, "error")
self.assertFormsetError(TestFormset.invalid(), None, None, "error")
self.assertIn("[] != ['error']", str(ctx.exception))
msg_prefix = "Custom prefix"
with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
self.assertFormsetError(
response, "formset", None, None, "error", msg_prefix=msg_prefix
TestFormset.invalid(),
None,
None,
"error",
msg_prefix=msg_prefix,
)
def test_non_form_errors_with_field(self):
response = mock.Mock(
context=[
{},
{"formset": TestFormset.invalid(nonform=True)},
]
)
msg = "You must use field=None with form_index=None."
with self.assertRaisesMessage(ValueError, msg):
self.assertFormsetError(response, "formset", None, "field", "error")
self.assertFormsetError(
TestFormset.invalid(nonform=True), None, "field", "error"
)
def test_form_index_too_big(self):
msg = (
"The formset <TestFormset: bound=True valid=False total_forms=1> only has "
"1 form."
)
response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(response, "formset", 2, "field", "error")
self.assertFormsetError(TestFormset.invalid(), 2, "field", "error")
def test_form_index_too_big_plural(self):
formset = TestFormset(
@ -1772,40 +1739,221 @@ class AssertFormsetErrorTests(SimpleTestCase):
"The formset <TestFormset: bound=True valid=True total_forms=2> only has 2 "
"forms."
)
response = mock.Mock(context=[{}, {"formset": formset}])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(response, "formset", 2, "field", "error")
self.assertFormsetError(formset, 2, "field", "error")
def test_formset_named_form(self):
formset = TestFormset.invalid()
# The mocked context emulates the template-based rendering of the
# formset.
response = mock.Mock(
context=[
{"form": formset},
{"form": formset.management_form},
]
)
self.assertFormsetError(response, "form", 0, "field", "invalid value")
# RemovedInDjango50Warning
class AssertFormErrorDeprecationTests(SimpleTestCase):
"""
Exhaustively test all possible combinations of args/kwargs for the old
signature.
"""
@ignore_warnings(category=RemovedInDjango50Warning)
def test_errors_none(self):
def test_assert_form_error_errors_none(self):
msg = (
"The errors of field 'field' on form <TestForm bound=True, valid=False, "
"fields=(field)> don't match."
)
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormError(TestForm.invalid(), "field", None)
def test_assert_form_error_errors_none_warning(self):
msg = (
"Passing errors=None to assertFormError() is deprecated, use "
"errors=[] instead."
)
with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
self.assertFormError(TestForm.valid(), "field", None)
def _assert_form_error_old_api_cases(self, form, field, errors, msg_prefix):
response = mock.Mock(context=[{"form": TestForm.invalid()}])
return (
((response, form, field, errors), {}),
((response, form, field, errors, msg_prefix), {}),
((response, form, field, errors), {"msg_prefix": msg_prefix}),
((response, form, field), {"errors": errors}),
((response, form, field), {"errors": errors, "msg_prefix": msg_prefix}),
((response, form), {"field": field, "errors": errors}),
(
(response, form),
{"field": field, "errors": errors, "msg_prefix": msg_prefix},
),
((response,), {"form": form, "field": field, "errors": errors}),
(
(response,),
{
"form": form,
"field": field,
"errors": errors,
"msg_prefix": msg_prefix,
},
),
(
(),
{"response": response, "form": form, "field": field, "errors": errors},
),
(
(),
{
"response": response,
"form": form,
"field": field,
"errors": errors,
"msg_prefix": msg_prefix,
},
),
)
def test_assert_form_error_old_api(self):
deprecation_msg = (
"Passing response to assertFormError() is deprecated. Use the form object "
"directly: assertFormError(response.context['form'], 'field', ...)"
)
for args, kwargs in self._assert_form_error_old_api_cases(
form="form",
field="field",
errors=["invalid value"],
msg_prefix="Custom prefix",
):
with self.subTest(args=args, kwargs=kwargs):
with self.assertWarnsMessage(RemovedInDjango50Warning, deprecation_msg):
self.assertFormError(*args, **kwargs)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_assert_form_error_old_api_assertion_error(self):
for args, kwargs in self._assert_form_error_old_api_cases(
form="form",
field="field",
errors=["other error"],
msg_prefix="Custom prefix",
):
with self.subTest(args=args, kwargs=kwargs):
with self.assertRaises(AssertionError):
self.assertFormError(*args, **kwargs)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_assert_formset_error_errors_none(self):
msg = (
"The errors of field 'field' on form 0 of formset <TestFormset: bound=True "
"valid=False total_forms=1> don't match."
)
response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
with self.assertRaisesMessage(AssertionError, msg):
self.assertFormsetError(response, "formset", 0, "field", None)
self.assertFormsetError(TestFormset.invalid(), 0, "field", None)
def test_errors_none_warning(self):
response = mock.Mock(context=[{"formset": TestFormset.valid()}])
def test_assert_formset_error_errors_none_warning(self):
msg = (
"Passing errors=None to assertFormsetError() is deprecated, use "
"errors=[] instead."
)
with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
self.assertFormsetError(response, "formset", 0, "field", None)
self.assertFormsetError(TestFormset.valid(), 0, "field", None)
def _assert_formset_error_old_api_cases(
self, formset, form_index, field, errors, msg_prefix
):
response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
return (
((response, formset, form_index, field, errors), {}),
((response, formset, form_index, field, errors, msg_prefix), {}),
(
(response, formset, form_index, field, errors),
{"msg_prefix": msg_prefix},
),
((response, formset, form_index, field), {"errors": errors}),
(
(response, formset, form_index, field),
{"errors": errors, "msg_prefix": msg_prefix},
),
((response, formset, form_index), {"field": field, "errors": errors}),
(
(response, formset, form_index),
{"field": field, "errors": errors, "msg_prefix": msg_prefix},
),
(
(response, formset),
{"form_index": form_index, "field": field, "errors": errors},
),
(
(response, formset),
{
"form_index": form_index,
"field": field,
"errors": errors,
"msg_prefix": msg_prefix,
},
),
(
(response,),
{
"formset": formset,
"form_index": form_index,
"field": field,
"errors": errors,
},
),
(
(response,),
{
"formset": formset,
"form_index": form_index,
"field": field,
"errors": errors,
"msg_prefix": msg_prefix,
},
),
(
(),
{
"response": response,
"formset": formset,
"form_index": form_index,
"field": field,
"errors": errors,
},
),
(
(),
{
"response": response,
"formset": formset,
"form_index": form_index,
"field": field,
"errors": errors,
"msg_prefix": msg_prefix,
},
),
)
def test_assert_formset_error_old_api(self):
deprecation_msg = (
"Passing response to assertFormsetError() is deprecated. Use the formset "
"object directly: assertFormsetError(response.context['formset'], 0, ...)"
)
for args, kwargs in self._assert_formset_error_old_api_cases(
formset="formset",
form_index=0,
field="field",
errors=["invalid value"],
msg_prefix="Custom prefix",
):
with self.subTest(args=args, kwargs=kwargs):
with self.assertWarnsMessage(RemovedInDjango50Warning, deprecation_msg):
self.assertFormsetError(*args, **kwargs)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_assert_formset_error_old_api_assertion_error(self):
for args, kwargs in self._assert_formset_error_old_api_cases(
formset="formset",
form_index=0,
field="field",
errors=["other error"],
msg_prefix="Custom prefix",
):
with self.subTest(args=args, kwargs=kwargs):
with self.assertRaises(AssertionError):
self.assertFormsetError(*args, **kwargs)
class FirstUrls: