summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Wobrock <david.wobrock@gmail.com>2023-12-23 23:05:05 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2024-01-03 13:02:26 +0100
commit8fb0be3500cc7519a56985b1b6f415d75ac6fedb (patch)
tree718f67cf2b7a5b5c0a0b72e03f413423e63fc03a
parent45f778eded9dff59cfdd4dce8720daf87a08cfac (diff)
Fixed #33277 -- Disallowed database connections in threads in SimpleTestCase.
-rw-r--r--django/test/testcases.py31
-rw-r--r--docs/releases/5.1.txt3
-rw-r--r--tests/test_utils/tests.py32
3 files changed, 66 insertions, 0 deletions
diff --git a/django/test/testcases.py b/django/test/testcases.py
index b5d426f75f..7382f7f0f0 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -10,6 +10,7 @@ from contextlib import contextmanager
from copy import copy, deepcopy
from difflib import get_close_matches
from functools import wraps
+from unittest import mock
from unittest.suite import _DebugResult
from unittest.util import safe_repr
from urllib.parse import (
@@ -37,6 +38,7 @@ from django.core.management.sql import emit_post_migrate_signal
from django.core.servers.basehttp import ThreadedWSGIServer, WSGIRequestHandler
from django.core.signals import setting_changed
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
+from django.db.backends.base.base import NO_DB_ALIAS, BaseDatabaseWrapper
from django.forms.fields import CharField
from django.http import QueryDict
from django.http.request import split_domain_port, validate_host
@@ -255,6 +257,13 @@ class SimpleTestCase(unittest.TestCase):
}
method = getattr(connection, name)
setattr(connection, name, _DatabaseFailure(method, message))
+ cls.enterClassContext(
+ mock.patch.object(
+ BaseDatabaseWrapper,
+ "ensure_connection",
+ new=cls.ensure_connection_patch_method(),
+ )
+ )
@classmethod
def _remove_databases_failures(cls):
@@ -266,6 +275,28 @@ class SimpleTestCase(unittest.TestCase):
method = getattr(connection, name)
setattr(connection, name, method.wrapped)
+ @classmethod
+ def ensure_connection_patch_method(cls):
+ real_ensure_connection = BaseDatabaseWrapper.ensure_connection
+
+ def patched_ensure_connection(self, *args, **kwargs):
+ if (
+ self.connection is None
+ and self.alias not in cls.databases
+ and self.alias != NO_DB_ALIAS
+ ):
+ # Connection has not yet been established, but the alias is not allowed.
+ message = cls._disallowed_database_msg % {
+ "test": f"{cls.__module__}.{cls.__qualname__}",
+ "alias": self.alias,
+ "operation": "threaded connections",
+ }
+ return _DatabaseFailure(self.ensure_connection, message)()
+
+ real_ensure_connection(self, *args, **kwargs)
+
+ return patched_ensure_connection
+
def __call__(self, result=None):
"""
Wrapper around default __call__ method to perform common Django test
diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt
index f949b31ad2..544b1f5d08 100644
--- a/docs/releases/5.1.txt
+++ b/docs/releases/5.1.txt
@@ -250,6 +250,9 @@ Tests
* The new :meth:`.SimpleTestCase.assertNotInHTML` assertion allows testing that
an HTML fragment is not contained in the given HTML haystack.
+* In order to enforce test isolation, database connections inside threads are
+ no longer allowed in :class:`~django.test.SimpleTestCase`.
+
URLs
~~~~
diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py
index ce78ffc008..65a782bf87 100644
--- a/tests/test_utils/tests.py
+++ b/tests/test_utils/tests.py
@@ -1,5 +1,6 @@
import os
import sys
+import threading
import unittest
import warnings
from io import StringIO
@@ -2093,6 +2094,29 @@ class DisallowedDatabaseQueriesTests(SimpleTestCase):
with self.assertRaisesMessage(DatabaseOperationForbidden, expected_message):
next(Car.objects.iterator())
+ def test_disallowed_thread_database_connection(self):
+ expected_message = (
+ "Database threaded connections to 'default' are not allowed in "
+ "SimpleTestCase subclasses. Either subclass TestCase or TransactionTestCase"
+ " to ensure proper test isolation or add 'default' to "
+ "test_utils.tests.DisallowedDatabaseQueriesTests.databases to "
+ "silence this failure."
+ )
+
+ exceptions = []
+
+ def thread_func():
+ try:
+ Car.objects.first()
+ except DatabaseOperationForbidden as e:
+ exceptions.append(e)
+
+ t = threading.Thread(target=thread_func)
+ t.start()
+ t.join()
+ self.assertEqual(len(exceptions), 1)
+ self.assertEqual(exceptions[0].args[0], expected_message)
+
class AllowedDatabaseQueriesTests(SimpleTestCase):
databases = {"default"}
@@ -2103,6 +2127,14 @@ class AllowedDatabaseQueriesTests(SimpleTestCase):
def test_allowed_database_chunked_cursor_queries(self):
next(Car.objects.iterator(), None)
+ def test_allowed_threaded_database_queries(self):
+ def thread_func():
+ next(Car.objects.iterator(), None)
+
+ t = threading.Thread(target=thread_func)
+ t.start()
+ t.join()
+
class DatabaseAliasTests(SimpleTestCase):
def setUp(self):