From 694dafc092e390fcb71e5801e5fb87c0a3b54680 Mon Sep 17 00:00:00 2001 From: Adeleke Omotayo Date: Fri, 20 Mar 2026 13:55:39 +0000 Subject: [PATCH] refactor: Extract get_username_from_data helper to remove duplication --- axes/helpers.py | 53 +++++++++++++++++++++++-------------------- tests/test_helpers.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/axes/helpers.py b/axes/helpers.py index 8c41e68..ed29bdf 100644 --- a/axes/helpers.py +++ b/axes/helpers.py @@ -132,6 +132,32 @@ def get_credentials(username: Optional[str] = None, **kwargs) -> dict: return credentials +def get_username_from_data(data: dict) -> Optional[str]: + """ + Extract username from a dict-like object (credentials or request data). + + Tries AXES_USERNAME_FORM_FIELD first, then falls back to Django's USERNAME_FIELD + for compatibility with Django's user_login_failed signal. + + See: https://github.com/jazzband/django-axes/issues/1159 + + :param data: dict-like object containing username (credentials or request.POST) + :return: username string or None if not found + """ + username = data.get(settings.AXES_USERNAME_FORM_FIELD, None) + if username is None: + # Fallback to Django's USERNAME_FIELD for compatibility with + # Django's user_login_failed signal which uses USERNAME_FIELD as the key + username_field = get_user_model().USERNAME_FIELD + if username_field != str(settings.AXES_USERNAME_FORM_FIELD): + log.debug( + "Falling back to USERNAME_FIELD '%s' for data lookup", + username_field, + ) + username = data.get(username_field, None) + return username + + def get_client_username( request: HttpRequest, credentials: Optional[dict] = None ) -> str: @@ -165,37 +191,14 @@ def get_client_username( log.debug( "Using parameter credentials to get username with key settings.AXES_USERNAME_FORM_FIELD" ) - username = credentials.get(settings.AXES_USERNAME_FORM_FIELD, None) - if username is None: - # Fallback to Django's USERNAME_FIELD for compatibility with - # Django's user_login_failed signal which uses USERNAME_FIELD as the key - # See: https://github.com/jazzband/django-axes/issues/1159 - username_field = get_user_model().USERNAME_FIELD - if username_field != str(settings.AXES_USERNAME_FORM_FIELD): - log.debug( - "Falling back to USERNAME_FIELD '%s' for credentials lookup", - username_field, - ) - username = credentials.get(username_field, None) - return username # type: ignore[return-value] + return get_username_from_data(credentials) # type: ignore[return-value] log.debug( "Using parameter request.POST to get username with key settings.AXES_USERNAME_FORM_FIELD" ) request_data = getattr(request, "data", request.POST) - username = request_data.get(settings.AXES_USERNAME_FORM_FIELD, None) - if username is None: - # Fallback to Django's USERNAME_FIELD for compatibility - # See: https://github.com/jazzband/django-axes/issues/1159 - username_field = get_user_model().USERNAME_FIELD - if username_field != str(settings.AXES_USERNAME_FORM_FIELD): - log.debug( - "Falling back to USERNAME_FIELD '%s' for request data lookup", - username_field, - ) - username = request_data.get(username_field, None) - return username # type: ignore[return-value] + return get_username_from_data(request_data) # type: ignore[return-value] def get_client_ip_address( diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 2e15a3f..09540d4 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -18,6 +18,7 @@ from axes.helpers import ( get_cool_off, get_cool_off_iso8601, get_lockout_response, + get_username_from_data, is_client_ip_address_blacklisted, is_client_ip_address_whitelisted, is_client_method_whitelisted, @@ -848,6 +849,46 @@ class UsernameTestCase(AxesTestCase): self.assertEqual(expected, actual) +class GetUsernameFromDataTestCase(AxesTestCase): + """Unit tests for the get_username_from_data helper function.""" + + @override_settings(AXES_USERNAME_FORM_FIELD="username") + def test_get_username_from_data_with_matching_field(self): + """Test that username is returned when AXES_USERNAME_FORM_FIELD matches.""" + data = {"username": "test-user"} + self.assertEqual(get_username_from_data(data), "test-user") + + @override_settings(AXES_USERNAME_FORM_FIELD="custom-field") + def test_get_username_from_data_fallback_to_username_field(self): + """ + Test that when AXES_USERNAME_FORM_FIELD doesn't exist in data, + we fallback to Django's USERNAME_FIELD. + """ + # Django's default USERNAME_FIELD is "username" + data = {"username": "fallback-user"} + self.assertEqual(get_username_from_data(data), "fallback-user") + + @override_settings(AXES_USERNAME_FORM_FIELD="custom-field") + def test_get_username_from_data_custom_field_takes_priority(self): + """Test that AXES_USERNAME_FORM_FIELD takes priority over USERNAME_FIELD.""" + data = { + "custom-field": "custom-user", + "username": "fallback-user", + } + self.assertEqual(get_username_from_data(data), "custom-user") + + @override_settings(AXES_USERNAME_FORM_FIELD="nonexistent") + def test_get_username_from_data_returns_none_when_not_found(self): + """Test that None is returned when username is not found in any field.""" + data = {"other-field": "some-value"} + self.assertIsNone(get_username_from_data(data)) + + @override_settings(AXES_USERNAME_FORM_FIELD="username") + def test_get_username_from_data_with_empty_dict(self): + """Test that None is returned for empty dict.""" + self.assertIsNone(get_username_from_data({})) + + def get_username(request, credentials: dict) -> str: return "username"