diff options
author | Gagaro <gagaro42@gmail.com> | 2022-01-31 16:04:13 +0100 |
---|---|---|
committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-05-10 11:22:23 +0200 |
commit | 667105877e6723c6985399803a364848891513cc (patch) | |
tree | b6b3a9fe9f2c8767bc6f6a68f0580eef021b2b55 /tests/constraints | |
parent | 441103a04d1d167dc870eaaf90e3fba974f67c93 (diff) | |
download | django-667105877e6723c6985399803a364848891513cc.tar.gz |
Fixed #30581 -- Added support for Meta.constraints validation.
Thanks Simon Charette, Keryn Knight, and Mariusz Felisiak for reviews.
Diffstat (limited to 'tests/constraints')
-rw-r--r-- | tests/constraints/models.py | 4 | ||||
-rw-r--r-- | tests/constraints/tests.py | 203 |
2 files changed, 197 insertions, 10 deletions
diff --git a/tests/constraints/models.py b/tests/constraints/models.py index 245693e847..3b349d204e 100644 --- a/tests/constraints/models.py +++ b/tests/constraints/models.py @@ -38,6 +38,10 @@ class UniqueConstraintProduct(models.Model): ] +class ChildUniqueConstraintProduct(UniqueConstraintProduct): + pass + + class UniqueConstraintConditionProduct(models.Model): name = models.CharField(max_length=255) color = models.CharField(max_length=32, null=True) diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index 58960fa5a3..d9e377438e 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -10,6 +10,7 @@ from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from .models import ( ChildModel, + ChildUniqueConstraintProduct, Product, UniqueConstraintConditionProduct, UniqueConstraintDeferrable, @@ -46,6 +47,24 @@ class BaseConstraintTests(SimpleTestCase): with self.assertRaisesMessage(NotImplementedError, msg): c.remove_sql(None, None) + def test_validate(self): + c = BaseConstraint("name") + msg = "This method must be implemented by a subclass." + with self.assertRaisesMessage(NotImplementedError, msg): + c.validate(None, None) + + def test_default_violation_error_message(self): + c = BaseConstraint("name") + self.assertEqual( + c.get_violation_error_message(), "Constraint “name” is violated." + ) + + def test_custom_violation_error_message(self): + c = BaseConstraint( + "base_name", violation_error_message="custom %(name)s message" + ) + self.assertEqual(c.get_violation_error_message(), "custom base_name message") + class CheckConstraintTests(TestCase): def test_eq(self): @@ -122,16 +141,60 @@ class CheckConstraintTests(TestCase): constraints = get_constraints(ChildModel._meta.db_table) self.assertIn("constraints_childmodel_adult", constraints) + def test_validate(self): + check = models.Q(price__gt=models.F("discounted_price")) + constraint = models.CheckConstraint(check=check, name="price") + # Invalid product. + invalid_product = Product(price=10, discounted_price=42) + with self.assertRaises(ValidationError): + constraint.validate(Product, invalid_product) + with self.assertRaises(ValidationError): + constraint.validate(Product, invalid_product, exclude={"unit"}) + # Fields used by the check constraint are excluded. + constraint.validate(Product, invalid_product, exclude={"price"}) + constraint.validate(Product, invalid_product, exclude={"discounted_price"}) + constraint.validate( + Product, + invalid_product, + exclude={"discounted_price", "price"}, + ) + # Valid product. + constraint.validate(Product, Product(price=10, discounted_price=5)) + + def test_validate_boolean_expressions(self): + constraint = models.CheckConstraint( + check=models.expressions.ExpressionWrapper( + models.Q(price__gt=500) | models.Q(price__lt=500), + output_field=models.BooleanField(), + ), + name="price_neq_500_wrap", + ) + msg = f"Constraint “{constraint.name}” is violated." + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate(Product, Product(price=500, discounted_price=5)) + constraint.validate(Product, Product(price=501, discounted_price=5)) + constraint.validate(Product, Product(price=499, discounted_price=5)) + + def test_validate_rawsql_expressions_noop(self): + constraint = models.CheckConstraint( + check=models.expressions.RawSQL( + "price < %s OR price > %s", + (500, 500), + output_field=models.BooleanField(), + ), + name="price_neq_500_raw", + ) + # RawSQL can not be checked and is always considered valid. + constraint.validate(Product, Product(price=500, discounted_price=5)) + constraint.validate(Product, Product(price=501, discounted_price=5)) + constraint.validate(Product, Product(price=499, discounted_price=5)) + class UniqueConstraintTests(TestCase): @classmethod def setUpTestData(cls): - cls.p1, cls.p2 = UniqueConstraintProduct.objects.bulk_create( - [ - UniqueConstraintProduct(name="p1", color="red"), - UniqueConstraintProduct(name="p2"), - ] - ) + cls.p1 = UniqueConstraintProduct.objects.create(name="p1", color="red") + cls.p2 = UniqueConstraintProduct.objects.create(name="p2") def test_eq(self): self.assertEqual( @@ -415,15 +478,135 @@ class UniqueConstraintTests(TestCase): with self.assertRaisesMessage(ValidationError, msg): UniqueConstraintProduct( name=self.p1.name, color=self.p1.color - ).validate_unique() + ).validate_constraints() @skipUnlessDBFeature("supports_partial_indexes") def test_model_validation_with_condition(self): - """Partial unique constraints are ignored by Model.validate_unique().""" + """ + Partial unique constraints are not ignored by + Model.validate_constraints(). + """ obj1 = UniqueConstraintConditionProduct.objects.create(name="p1", color="red") obj2 = UniqueConstraintConditionProduct.objects.create(name="p2") - UniqueConstraintConditionProduct(name=obj1.name, color="blue").validate_unique() - UniqueConstraintConditionProduct(name=obj2.name).validate_unique() + UniqueConstraintConditionProduct( + name=obj1.name, color="blue" + ).validate_constraints() + msg = "Constraint “name_without_color_uniq” is violated." + with self.assertRaisesMessage(ValidationError, msg): + UniqueConstraintConditionProduct(name=obj2.name).validate_constraints() + + def test_validate(self): + constraint = UniqueConstraintProduct._meta.constraints[0] + msg = "Unique constraint product with this Name and Color already exists." + non_unique_product = UniqueConstraintProduct( + name=self.p1.name, color=self.p1.color + ) + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate(UniqueConstraintProduct, non_unique_product) + # Null values are ignored. + constraint.validate( + UniqueConstraintProduct, + UniqueConstraintProduct(name=self.p2.name, color=None), + ) + # Existing instances have their existing row excluded. + constraint.validate(UniqueConstraintProduct, self.p1) + # Unique fields are excluded. + constraint.validate( + UniqueConstraintProduct, + non_unique_product, + exclude={"name"}, + ) + constraint.validate( + UniqueConstraintProduct, + non_unique_product, + exclude={"color"}, + ) + constraint.validate( + UniqueConstraintProduct, + non_unique_product, + exclude={"name", "color"}, + ) + # Validation on a child instance. + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate( + UniqueConstraintProduct, + ChildUniqueConstraintProduct(name=self.p1.name, color=self.p1.color), + ) + + @skipUnlessDBFeature("supports_partial_indexes") + def test_validate_condition(self): + p1 = UniqueConstraintConditionProduct.objects.create(name="p1") + constraint = UniqueConstraintConditionProduct._meta.constraints[0] + msg = "Constraint “name_without_color_uniq” is violated." + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate( + UniqueConstraintConditionProduct, + UniqueConstraintConditionProduct(name=p1.name, color=None), + ) + # Values not matching condition are ignored. + constraint.validate( + UniqueConstraintConditionProduct, + UniqueConstraintConditionProduct(name=p1.name, color="anything-but-none"), + ) + # Existing instances have their existing row excluded. + constraint.validate(UniqueConstraintConditionProduct, p1) + # Unique field is excluded. + constraint.validate( + UniqueConstraintConditionProduct, + UniqueConstraintConditionProduct(name=p1.name, color=None), + exclude={"name"}, + ) + + def test_validate_expression(self): + constraint = models.UniqueConstraint(Lower("name"), name="name_lower_uniq") + msg = "Constraint “name_lower_uniq” is violated." + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate( + UniqueConstraintProduct, + UniqueConstraintProduct(name=self.p1.name.upper()), + ) + constraint.validate( + UniqueConstraintProduct, + UniqueConstraintProduct(name="another-name"), + ) + # Existing instances have their existing row excluded. + constraint.validate(UniqueConstraintProduct, self.p1) + # Unique field is excluded. + constraint.validate( + UniqueConstraintProduct, + UniqueConstraintProduct(name=self.p1.name.upper()), + exclude={"name"}, + ) + + def test_validate_expression_condition(self): + constraint = models.UniqueConstraint( + Lower("name"), + name="name_lower_without_color_uniq", + condition=models.Q(color__isnull=True), + ) + non_unique_product = UniqueConstraintProduct(name=self.p2.name.upper()) + msg = "Constraint “name_lower_without_color_uniq” is violated." + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate(UniqueConstraintProduct, non_unique_product) + # Values not matching condition are ignored. + constraint.validate( + UniqueConstraintProduct, + UniqueConstraintProduct(name=self.p1.name, color=self.p1.color), + ) + # Existing instances have their existing row excluded. + constraint.validate(UniqueConstraintProduct, self.p2) + # Unique field is excluded. + constraint.validate( + UniqueConstraintProduct, + non_unique_product, + exclude={"name"}, + ) + # Field from a condition is excluded. + constraint.validate( + UniqueConstraintProduct, + non_unique_product, + exclude={"color"}, + ) def test_name(self): constraints = get_constraints(UniqueConstraintProduct._meta.db_table) |