diff --git a/auctions/forms.py b/auctions/forms.py index ff00f34e..07a361a7 100755 --- a/auctions/forms.py +++ b/auctions/forms.py @@ -41,6 +41,7 @@ UserData, UserLabelPrefs, ) +from .validators import validate_username_no_at_symbol # Distance conversion constant MILES_TO_KM = 1.60934 @@ -2713,6 +2714,11 @@ class Meta: "user_permissions", ) + def clean_username(self): + username = self.cleaned_data.get("username", "") + validate_username_no_at_symbol(username) + return username + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() diff --git a/auctions/tests.py b/auctions/tests.py index 3439cec5..cbe3e963 100644 --- a/auctions/tests.py +++ b/auctions/tests.py @@ -8,6 +8,7 @@ from django import forms from django.contrib.auth.models import User +from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.test import TestCase, TransactionTestCase, override_settings @@ -15,7 +16,7 @@ from django.urls import reverse from django.utils import timezone -from .forms import AuctionEditForm +from .forms import AuctionEditForm, ChangeUsernameForm from .models import ( Auction, AuctionHistory, @@ -10964,3 +10965,40 @@ def test_image_permission_check_blocks_when_dependent_any_auction_lot_sold(self) self.assertFalse(dependent_lot.can_add_images) # source_lot should be blocked regardless of auction type self.assertFalse(source_lot.image_permission_check(self.user)) + + +class ChangeUsernameFormTest(TestCase): + """Tests for ChangeUsernameForm to ensure @ symbol is disallowed in usernames""" + + def setUp(self): + self.user = User.objects.create_user(username="testuser", password="testpassword", email="test@example.com") + + def test_username_with_at_symbol_is_invalid(self): + form = ChangeUsernameForm(data={"username": "user@name"}, instance=self.user) + self.assertFalse(form.is_valid()) + self.assertIn("username", form.errors) + + def test_username_without_at_symbol_is_valid(self): + form = ChangeUsernameForm(data={"username": "validusername"}, instance=self.user) + self.assertTrue(form.is_valid()) + + def test_username_with_only_at_symbol_is_invalid(self): + form = ChangeUsernameForm(data={"username": "@"}, instance=self.user) + self.assertFalse(form.is_valid()) + self.assertIn("username", form.errors) + + +class CustomSignupFormTest(TestCase): + """Tests that the allauth adapter rejects usernames with @ via ACCOUNT_USERNAME_VALIDATORS""" + + def test_username_with_at_symbol_rejected_by_adapter(self): + from allauth.account.adapter import get_adapter + + with self.assertRaises(ValidationError): + get_adapter().clean_username("user@name") + + def test_username_without_at_symbol_accepted_by_adapter(self): + from allauth.account.adapter import get_adapter + + result = get_adapter().clean_username("validuser", shallow=True) + self.assertEqual(result, "validuser") diff --git a/auctions/validators.py b/auctions/validators.py new file mode 100644 index 00000000..38ac5e3c --- /dev/null +++ b/auctions/validators.py @@ -0,0 +1,19 @@ +from django.contrib.auth.validators import UnicodeUsernameValidator +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator + + +def validate_username_no_at_symbol(value): + """Disallow the @ symbol in usernames to avoid confusion with email addresses.""" + if "@" in value: + msg = "Usernames cannot contain the @ symbol." + raise ValidationError(msg) + + +# Used by ACCOUNT_USERNAME_VALIDATORS in settings to apply to all allauth signup flows. +# Includes Django's default username validators plus the @ restriction. +USERNAME_VALIDATORS = [ + UnicodeUsernameValidator(), + MaxLengthValidator(150), + validate_username_no_at_symbol, +] diff --git a/fishauctions/settings.py b/fishauctions/settings.py index d5ea0b4f..dd07a864 100755 --- a/fishauctions/settings.py +++ b/fishauctions/settings.py @@ -323,6 +323,7 @@ "signup": "auctions.forms.CustomSignupForm", "reset_password": "auctions.forms.CustomResetPasswordForm", } +ACCOUNT_USERNAME_VALIDATORS = "auctions.validators.USERNAME_VALIDATORS" # ACCOUNT_AUTHENTICATION_METHOD = "username_email" ACCOUNT_LOGIN_METHODS = {"username", "email"} ACCOUNT_CONFIRM_EMAIL_ON_GET = True