summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorClifford Gama <cliffygamy@gmail.com>2025-05-16 23:48:38 +0200
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-06-18 08:36:49 +0200
commit74b31cd26b9ad4ad85f131850a734f02aae988bb (patch)
treeceabf0b4c985ae8159d44cf969e17c31bd38a337 /tests
parent8c56e939750ae785b806dfa4e043590760c90ab6 (diff)
Fixed #32770 -- Added system check to ensure django.contrib.postgres is installed when using its features.
Added postgres.E005 to validate 'django.contrib.postgres' is in INSTALLED_APPS when using: * PostgreSQL-specific fields (ArrayField, HStoreField, range fields, SearchVectorField), * PostgreSQL indexes (PostgresIndex and all subclasses), and * ExclusionConstraint The check provides immediate feedback during system checks rather than failing later with obscure runtime and database errors. Thanks to Simon Charette and Sarah Boyce for reviews.
Diffstat (limited to 'tests')
-rw-r--r--tests/invalid_models_tests/test_deprecated_fields.py3
-rw-r--r--tests/postgres_tests/test_app_installed_check.py142
-rwxr-xr-xtests/runtests.py1
3 files changed, 145 insertions, 1 deletions
diff --git a/tests/invalid_models_tests/test_deprecated_fields.py b/tests/invalid_models_tests/test_deprecated_fields.py
index 65f65e7528..e3c7d2e12f 100644
--- a/tests/invalid_models_tests/test_deprecated_fields.py
+++ b/tests/invalid_models_tests/test_deprecated_fields.py
@@ -3,7 +3,7 @@ from unittest import skipUnless
from django.core import checks
from django.db import connection, models
from django.test import SimpleTestCase
-from django.test.utils import isolate_apps
+from django.test.utils import isolate_apps, modify_settings
@isolate_apps("invalid_models_tests")
@@ -87,6 +87,7 @@ class DeprecatedFieldsTests(SimpleTestCase):
)
@skipUnless(connection.vendor == "postgresql", "PostgreSQL specific SQL")
+ @modify_settings(INSTALLED_APPS={"append": "django.contrib.postgres"})
def test_postgres_ci_fields_deprecated(self):
from django.contrib.postgres.fields import (
ArrayField,
diff --git a/tests/postgres_tests/test_app_installed_check.py b/tests/postgres_tests/test_app_installed_check.py
new file mode 100644
index 0000000000..95315384e6
--- /dev/null
+++ b/tests/postgres_tests/test_app_installed_check.py
@@ -0,0 +1,142 @@
+from django.core import checks
+from django.db import models
+from django.test import modify_settings
+from django.test.utils import isolate_apps
+
+from . import PostgreSQLTestCase
+from .fields import (
+ BigIntegerRangeField,
+ DateRangeField,
+ DateTimeRangeField,
+ DecimalRangeField,
+ HStoreField,
+ IntegerRangeField,
+ SearchVectorField,
+)
+from .models import IntegerArrayModel, NestedIntegerArrayModel, PostgreSQLModel
+
+try:
+ from django.contrib.postgres.constraints import ExclusionConstraint
+ from django.contrib.postgres.fields.ranges import RangeOperators
+ from django.contrib.postgres.indexes import GinIndex, PostgresIndex
+ from django.contrib.postgres.search import SearchQueryField
+except ImportError:
+ pass
+
+
+@isolate_apps("postgres_tests")
+class TestPostgresAppInstalledCheck(PostgreSQLTestCase):
+
+ def _make_error(self, obj, klass_name):
+ """Helper to create postgres.E005 error for specific objects."""
+ return checks.Error(
+ "'django.contrib.postgres' must be in INSTALLED_APPS in order to "
+ f"use {klass_name}.",
+ obj=obj,
+ id="postgres.E005",
+ )
+
+ def assert_model_check_errors(self, model_class, expected_errors):
+ errors = model_class.check(databases=self.databases)
+ self.assertEqual(errors, [])
+ with modify_settings(INSTALLED_APPS={"remove": "django.contrib.postgres"}):
+ errors = model_class.check(databases=self.databases)
+ self.assertEqual(errors, expected_errors)
+
+ def test_indexes(self):
+ class IndexModel(PostgreSQLModel):
+ field = models.IntegerField()
+
+ class Meta:
+ indexes = [
+ PostgresIndex(fields=["id"], name="postgres_index_test"),
+ GinIndex(fields=["field"], name="gin_index_test"),
+ ]
+
+ self.assert_model_check_errors(
+ IndexModel,
+ [
+ self._make_error(IndexModel, "PostgresIndex"),
+ self._make_error(IndexModel, "GinIndex"),
+ ],
+ )
+
+ def test_exclusion_constraint(self):
+ class ExclusionModel(PostgreSQLModel):
+ value = models.IntegerField()
+
+ class Meta:
+ constraints = [
+ ExclusionConstraint(
+ name="exclude_equal",
+ expressions=[("value", RangeOperators.EQUAL)],
+ )
+ ]
+
+ self.assert_model_check_errors(
+ ExclusionModel, [self._make_error(ExclusionModel, "ExclusionConstraint")]
+ )
+
+ def test_array_field(self):
+ field = IntegerArrayModel._meta.get_field("field")
+ self.assert_model_check_errors(
+ IntegerArrayModel,
+ [self._make_error(field, "ArrayField")],
+ )
+
+ def test_nested_array_field(self):
+ """Inner ArrayField does not cause a postgres.E001 error."""
+ field = NestedIntegerArrayModel._meta.get_field("field")
+ self.assert_model_check_errors(
+ NestedIntegerArrayModel,
+ [
+ self._make_error(field, "ArrayField"),
+ ],
+ )
+
+ def test_hstore_field(self):
+ class HStoreFieldModel(PostgreSQLModel):
+ field = HStoreField()
+
+ field = HStoreFieldModel._meta.get_field("field")
+ self.assert_model_check_errors(
+ HStoreFieldModel,
+ [
+ self._make_error(field, "HStoreField"),
+ ],
+ )
+
+ def test_range_fields(self):
+ class RangeFieldsModel(PostgreSQLModel):
+ int_range = IntegerRangeField()
+ bigint_range = BigIntegerRangeField()
+ decimal_range = DecimalRangeField()
+ datetime_range = DateTimeRangeField()
+ date_range = DateRangeField()
+
+ expected_errors = [
+ self._make_error(field, field.__class__.__name__)
+ for field in [
+ RangeFieldsModel._meta.get_field("int_range"),
+ RangeFieldsModel._meta.get_field("bigint_range"),
+ RangeFieldsModel._meta.get_field("decimal_range"),
+ RangeFieldsModel._meta.get_field("datetime_range"),
+ RangeFieldsModel._meta.get_field("date_range"),
+ ]
+ ]
+ self.assert_model_check_errors(RangeFieldsModel, expected_errors)
+
+ def test_search_vector_field(self):
+ class SearchModel(PostgreSQLModel):
+ search_vector = SearchVectorField()
+ search_query = SearchQueryField()
+
+ vector_field = SearchModel._meta.get_field("search_vector")
+ query_field = SearchModel._meta.get_field("search_query")
+ self.assert_model_check_errors(
+ SearchModel,
+ [
+ self._make_error(vector_field, "SearchVectorField"),
+ self._make_error(query_field, "SearchQueryField"),
+ ],
+ )
diff --git a/tests/runtests.py b/tests/runtests.py
index e9052ca4a9..ceb88853b7 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -247,6 +247,7 @@ def setup_collect_tests(start_at, start_after, test_labels=None):
settings.LOGGING = log_config
settings.SILENCED_SYSTEM_CHECKS = [
"fields.W342", # ForeignKey(unique=True) -> OneToOneField
+ "postgres.E005", # django.contrib.postgres must be installed to use feature.
]
# Load all the ALWAYS_INSTALLED_APPS.