From 2c8c0ca0a0b73b167c29db506f810c318f915587 Mon Sep 17 00:00:00 2001 From: babbitt Date: Tue, 19 May 2026 11:11:29 -0700 Subject: [PATCH 1/2] Fixed #36225 -- Coped with lack of get_by_natural_key() in createsuperuser. --- .../management/commands/createsuperuser.py | 6 +- tests/auth_tests/models/__init__.py | 2 + tests/auth_tests/models/no_natural_key.py | 22 +++++++ tests/auth_tests/test_management.py | 62 +++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/auth_tests/models/no_natural_key.py diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index d5d5d193c845..5fecb4c0ed8b 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -298,9 +298,13 @@ def username_is_unique(self): for unique_constraint in self.UserModel._meta.total_unique_constraints ) + @cached_property + def natural_key_defined(self): + return hasattr(self.UserModel._default_manager, "get_by_natural_key") + def _validate_username(self, username, verbose_field_name, database): """Validate username. If invalid, return a string error message.""" - if self.username_is_unique: + if self.username_is_unique and self.natural_key_defined: try: self.UserModel._default_manager.db_manager(database).get_by_natural_key( username diff --git a/tests/auth_tests/models/__init__.py b/tests/auth_tests/models/__init__.py index 185b34d85716..7d3c27522215 100644 --- a/tests/auth_tests/models/__init__.py +++ b/tests/auth_tests/models/__init__.py @@ -8,6 +8,7 @@ from .invalid_models import CustomUserNonUniqueUsername from .is_active import IsActiveTestUser1 from .minimal import MinimalUser +from .no_natural_key import CustomUserNoNaturalKey from .no_password import NoPasswordUser from .proxy import Proxy, UserProxy from .uuid_pk import UUIDUser @@ -23,6 +24,7 @@ "CustomPermissionsUser", "CustomUser", "CustomUserCompositePrimaryKey", + "CustomUserNoNaturalKey", "CustomUserNonUniqueUsername", "CustomUserWithFK", "CustomUserWithM2M", diff --git a/tests/auth_tests/models/no_natural_key.py b/tests/auth_tests/models/no_natural_key.py new file mode 100644 index 000000000000..e7082b441b87 --- /dev/null +++ b/tests/auth_tests/models/no_natural_key.py @@ -0,0 +1,22 @@ +from django.contrib.auth.hashers import make_password +from django.db import models + + +class CustomUserNoNaturalKeyManager(models.Manager): + def create_superuser(self, email, password=None, **extra_fields): + user = self.model(email=email, is_superuser=True, **extra_fields) + user.password = make_password(password) + user.save(using=self._db) + return user + + +class CustomUserNoNaturalKey(models.Model): + email = models.EmailField(max_length=255, unique=True) + password = models.CharField(max_length=128) + is_superuser = models.BooleanField(default=False) + is_staff = models.BooleanField(default=False) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + objects = CustomUserNoNaturalKeyManager() diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py index 985dfd79f8e7..6d009e32d9ab 100644 --- a/tests/auth_tests/test_management.py +++ b/tests/auth_tests/test_management.py @@ -19,12 +19,14 @@ from django.core.management import call_command from django.core.management.base import CommandError from django.db import migrations +from django.db.utils import IntegrityError from django.test import TestCase, override_settings from django.test.testcases import TransactionTestCase from django.utils.translation import gettext_lazy as _ from .models import ( CustomUser, + CustomUserNoNaturalKey, CustomUserNonUniqueUsername, CustomUserWithFK, CustomUserWithM2M, @@ -470,6 +472,66 @@ def createsuperuser(): users = CustomUserNonUniqueUsername.objects.filter(username="joe") self.assertEqual(users.count(), 2) + @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNoNaturalKey") + def test_swappable_user_no_natural_key_non_interactive(self): + new_io = StringIO() + call_command( + "createsuperuser", + interactive=False, + email="joe@somewhere.org", + stdout=new_io, + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, "Superuser created successfully.") + + user = CustomUserNoNaturalKey._default_manager.get(email="joe@somewhere.org") + self.assertIsNotNone(user) + + @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNoNaturalKey") + def test_swappable_user_no_natural_key_interactive(self): + @mock_inputs( + { + "Email: ": "joe@somewhere.org", + "password": "nopasswd", + } + ) + def createsuperuser(): + new_io = StringIO() + call_command( + "createsuperuser", + interactive=True, + stdout=new_io, + stdin=MockTTY(), + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, "Superuser created successfully.") + + createsuperuser() + user = CustomUserNoNaturalKey._default_manager.get(email="joe@somewhere.org") + self.assertIsNotNone(user) + + @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNoNaturalKey") + def test_swappable_user_no_natural_key_duplicate_allowed(self): + """Without get_by_natural_key, + duplicate username validation is skipped.""" + new_io = StringIO() + call_command( + "createsuperuser", + interactive=False, + email="joe@somewhere.org", + stdout=new_io, + ) + # Creating a second user with the same email won't be caught by + # _validate_username since there's no get_by_natural_key; it will + # fail at the database level instead due to the unique constraint. + with self.assertRaises(IntegrityError): + call_command( + "createsuperuser", + interactive=False, + email="joe@somewhere.org", + stdout=new_io, + ) + def test_skip_if_not_in_TTY(self): """ If the command is not called from a TTY, it should be skipped and a From 5b3109329936e37432b1795f4b355ed60c4efd0a Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 28 May 2026 15:28:07 -0400 Subject: [PATCH 2/2] Removed unnecessary direct dev dependency on puppeteer. Checked the PR discussion, and this was added in an attempt to pin the version of puppeteer, but this would have been done with adding an `overrides` entry, not by adding a direct dependency. Bug in d407340e7f8463ee885aaa37789d8aef657b73f5. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 2c79999414ce..2a0c8726f3b6 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ }, "devDependencies": { "@biomejs/biome": "2.4.15", - "puppeteer": "^25.0.4", "grunt": "^1.6.2", "grunt-cli": "^1.5.0", "grunt-contrib-qunit": "^10.2.0",