diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js index 27223e281569..8e3548ce5225 100644 --- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js @@ -1,4 +1,4 @@ -/*global Calendar, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/ +/*global Calendar, CalendarNamespace, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/ // Inserts shortcut buttons after all of the following: // // @@ -156,15 +156,19 @@ DateTimeShortcuts.openClock(num); }); + const clockIconId = DateTimeShortcuts.clockLinkName + num + "_icon"; quickElement( "span", clock_link, "", + "id", + clockIconId, "class", "clock-icon", "title", gettext("Choose a Time"), ); + clock_link.setAttribute("aria-labelledby", clockIconId); shortcuts_span.appendChild(document.createTextNode("\u00A0")); shortcuts_span.appendChild(now_link); shortcuts_span.appendChild( @@ -175,16 +179,19 @@ // Create clock link div // // Markup looks like: - //
+ // const clock_box = document.createElement("div"); @@ -192,6 +199,8 @@ clock_box.style.position = "absolute"; clock_box.className = "clockbox module"; clock_box.id = DateTimeShortcuts.clockDivName + num; + clock_box.setAttribute("role", "dialog"); + clock_box.setAttribute("aria-labelledby", clockIconId); document.body.appendChild(clock_box); clock_box.addEventListener("click", function (e) { e.stopPropagation(); @@ -305,6 +314,15 @@ return true; }; + function getFormattedDate(offset) { + const d = DateTimeShortcuts.now(); + d.setDate(d.getDate() + offset); + return CalendarNamespace.formatDate( + d.getDate(), + d.getMonth() + 1, + d.getFullYear(), + ); + } // Shortcut links (calendar icon and "Today" link) const shortcuts_span = document.createElement("span"); shortcuts_span.className = DateTimeShortcuts.shortCutsClass; @@ -313,6 +331,14 @@ today_link.href = "#"; today_link.role = "button"; today_link.appendChild(document.createTextNode(gettext("Today"))); + today_link.setAttribute( + "aria-label", + interpolate( + gettext("Today (%(date)s)"), + { date: getFormattedDate(0) }, + true, + ), + ); today_link.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, 0); @@ -326,15 +352,20 @@ e.stopPropagation(); DateTimeShortcuts.openCalendar(num); }); + const calIconId = + DateTimeShortcuts.calendarLinkName + num + "_icon"; quickElement( "span", cal_link, "", + "id", + calIconId, "class", "date-icon", "title", gettext("Choose a Date"), ); + cal_link.setAttribute("aria-labelledby", calIconId); shortcuts_span.appendChild(document.createTextNode("\u00A0")); shortcuts_span.appendChild(today_link); shortcuts_span.appendChild( @@ -346,24 +377,38 @@ // // Markup looks like: // - //
- //

- // - // February 2003 - //

+ // const cal_box = document.createElement("div"); cal_box.style.display = "none"; cal_box.style.position = "absolute"; cal_box.className = "calendarbox module"; cal_box.id = DateTimeShortcuts.calendarDivName1 + num; + cal_box.setAttribute("role", "dialog"); + cal_box.setAttribute("aria-labelledby", calIconId); document.body.appendChild(cal_box); cal_box.addEventListener("click", function (e) { e.stopPropagation(); @@ -412,6 +457,14 @@ "href", "#", ); + day_link.setAttribute( + "aria-label", + interpolate( + gettext("Yesterday (%(date)s)"), + { date: getFormattedDate(-1) }, + true, + ), + ); day_link.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, -1); @@ -426,6 +479,14 @@ "href", "#", ); + day_link.setAttribute( + "aria-label", + interpolate( + gettext("Today (%(date)s)"), + { date: getFormattedDate(0) }, + true, + ), + ); day_link.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, 0); @@ -440,6 +501,14 @@ "href", "#", ); + day_link.setAttribute( + "aria-label", + interpolate( + gettext("Tomorrow (%(date)s)"), + { date: getFormattedDate(1) }, + true, + ), + ); day_link.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, +1); @@ -469,6 +538,41 @@ } }); }, + updateNavAriaLabels: function (num) { + const cal = DateTimeShortcuts.calendars[num]; + const cal_box = document.getElementById( + DateTimeShortcuts.calendarDivName1 + num, + ); + const prevMonth = + CalendarNamespace.monthsOfYear[(cal.currentMonth + 10) % 12]; + const prevYear = + cal.currentMonth === 1 ? cal.currentYear - 1 : cal.currentYear; + cal_box + .querySelector(".calendarnav-previous") + .setAttribute( + "aria-label", + interpolate( + gettext("Previous (%(month)s %(year)s)"), + { month: prevMonth, year: prevYear }, + true, + ), + ); + + const nextMonth = + CalendarNamespace.monthsOfYear[cal.currentMonth % 12]; + const nextYear = + cal.currentMonth === 12 ? cal.currentYear + 1 : cal.currentYear; + cal_box + .querySelector(".calendarnav-next") + .setAttribute( + "aria-label", + interpolate( + gettext("Next (%(month)s %(year)s)"), + { month: nextMonth, year: nextYear }, + true, + ), + ); + }, openCalendar: function (num) { const cal_box = document.getElementById( DateTimeShortcuts.calendarDivName1 + num, @@ -507,6 +611,7 @@ cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + "px"; cal_box.style.display = "block"; + DateTimeShortcuts.updateNavAriaLabels(num); document.addEventListener( "click", DateTimeShortcuts.dismissCalendarFunc[num], @@ -523,9 +628,11 @@ }, drawPrev: function (num) { DateTimeShortcuts.calendars[num].drawPreviousMonth(); + DateTimeShortcuts.updateNavAriaLabels(num); }, drawNext: function (num) { DateTimeShortcuts.calendars[num].drawNextMonth(); + DateTimeShortcuts.updateNavAriaLabels(num); }, handleCalendarCallback: function (num) { const format = get_format("DATE_INPUT_FORMATS")[0]; @@ -536,9 +643,7 @@ d, ).strftime(format); DateTimeShortcuts.calendarInputs[num].focus(); - document.getElementById( - DateTimeShortcuts.calendarDivName1 + num, - ).style.display = "none"; + DateTimeShortcuts.dismissCalendar(num); }; }, handleCalendarQuickLink: function (num, offset) { diff --git a/django/contrib/admin/static/admin/js/calendar.js b/django/contrib/admin/static/admin/js/calendar.js index f10a584fa991..9cef8d0c81cc 100644 --- a/django/contrib/admin/static/admin/js/calendar.js +++ b/django/contrib/admin/static/admin/js/calendar.js @@ -1,4 +1,4 @@ -/*global gettext, pgettext, get_format, quickElement, removeChildren*/ +/*global gettext, pgettext, get_format, interpolate, quickElement, removeChildren*/ /* calendar.js - Calendar functions by Adrian Holovaty depends on core.js for utility functions like removeChildren or quickElement @@ -92,6 +92,17 @@ depends on core.js for utility functions like removeChildren or quickElement } return days; }, + formatDate: function (day, month, year) { + return interpolate( + gettext("%(month)s %(day)s, %(year)s"), + { + month: CalendarNamespace.monthsOfYear[month - 1], + day: day, + year: year, + }, + true, + ); + }, draw: function (month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999 const today = new Date(); @@ -133,13 +144,16 @@ depends on core.js for utility functions like removeChildren or quickElement // Draw days-of-week header let tableRow = quickElement("tr", tableBody); for (let i = 0; i < 7; i++) { - quickElement( - "th", - tableRow, + const dayAbbrev = + CalendarNamespace.daysOfWeekAbbrev[ + (i + CalendarNamespace.firstDayOfWeek) % 7 + ]; + const dayInitial = CalendarNamespace.daysOfWeekInitial[ (i + CalendarNamespace.firstDayOfWeek) % 7 - ], - ); + ]; + const th = quickElement("th", tableRow, dayInitial); + th.setAttribute("aria-label", dayAbbrev); } const startingPos = new Date( @@ -206,6 +220,30 @@ depends on core.js for utility functions like removeChildren or quickElement "href", "#", ); + let ariaLabel = CalendarNamespace.formatDate( + currentDay, + month, + year, + ); + const isToday = + currentDay === todayDay && + month === todayMonth && + year === todayYear; + const isSelected = + isSelectedMonth && currentDay === selected.getUTCDate(); + if (isToday && isSelected) { + ariaLabel = interpolate( + gettext("%s (today, current selection)"), + [ariaLabel], + ); + } else if (isToday) { + ariaLabel = interpolate(gettext("%s (today)"), [ariaLabel]); + } else if (isSelected) { + ariaLabel = interpolate(gettext("%s (current selection)"), [ + ariaLabel, + ]); + } + link.setAttribute("aria-label", ariaLabel); link.addEventListener("click", calendarMonth(year, month)); currentDay++; } diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py index bedb749d9237..ca8187a364b1 100644 --- a/django/contrib/auth/middleware.py +++ b/django/contrib/auth/middleware.py @@ -8,6 +8,7 @@ from django.contrib.auth.backends import RemoteUserBackend from django.contrib.auth.views import redirect_to_login from django.core.exceptions import ImproperlyConfigured +from django.core.handlers.asgi import ASGIRequest from django.shortcuts import resolve_url from django.utils.deprecation import MiddlewareMixin from django.utils.functional import SimpleLazyObject @@ -141,7 +142,7 @@ def process_request(self, request): f" before the {self.__class__.__name__} class." ) try: - username = request.META[self.header] + username = self.get_username(request) except KeyError: # If specified header doesn't exist then remove any existing # authenticated remote-user, or return (leaving request.user set to @@ -183,7 +184,7 @@ async def aprocess_request(self, request): f" before the {self.__class__.__name__} class." ) try: - username = request.META["HTTP_" + self.header] + username = self.get_username(request) except KeyError: # If specified header doesn't exist then remove any existing # authenticated remote-user, or return (leaving request.user set to @@ -236,6 +237,11 @@ async def aclean_username(self, username, request): pass return username + def get_username(self, request): + if isinstance(request, ASGIRequest): + return request.META["HTTP_" + self.header] + return request.META[self.header] + def _remove_invalid_user(self, request): """ Remove the current authenticated user in the request which is invalid diff --git a/django/core/signing.py b/django/core/signing.py index ed56ce0908be..86740edc27c4 100644 --- a/django/core/signing.py +++ b/django/core/signing.py @@ -38,10 +38,12 @@ import datetime import json import time +import warnings import zlib from django.conf import settings from django.utils.crypto import constant_time_compare, salted_hmac +from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes from django.utils.encoding import force_bytes from django.utils.module_loading import import_string from django.utils.regex_helper import _lazy_re_compile @@ -96,7 +98,17 @@ def b64_decode(s): return base64.urlsafe_b64decode(s + pad) -def base64_hmac(salt, value, key, algorithm="sha1"): +# RemovedInDjango70Warning: algorithm="sha256" +def base64_hmac(salt, value, key, algorithm=None): + if algorithm is None: + warnings.warn( + "The default argument for algorithm in base64_hmac() will change " + "from 'sha1' to 'sha256' in Django 7.0. Pass an explicit " + "algorithm to silence this warning.", + category=RemovedInDjango70Warning, + skip_file_prefixes=django_file_prefixes(), + ) + algorithm = "sha1" return b64_encode( salted_hmac(salt, value, key, algorithm=algorithm).digest() ).decode() diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 4b8146695ae3..beadb146cb0c 100644 --- a/django/utils/crypto.py +++ b/django/utils/crypto.py @@ -5,8 +5,10 @@ import hashlib import hmac import secrets +import warnings from django.conf import settings +from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes from django.utils.encoding import force_bytes @@ -16,14 +18,27 @@ class InvalidAlgorithm(ValueError): pass -def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"): +# RemovedInDjango70Warning: algorithm="sha256" +def salted_hmac(key_salt, value, secret=None, *, algorithm=None): """ Return the HMAC of 'value', using a key generated from key_salt and a secret (which defaults to settings.SECRET_KEY). Default algorithm is SHA1, but any algorithm name supported by hashlib can be passed. + Removed in Django70Warning: The default algorithm will change to SHA256 + in Django 7.0, so provide an explicit algorithm to silence the warning. + A different key_salt should be passed in for every application of HMAC. """ + if algorithm is None: + warnings.warn( + "The default argument for algorithm in salted_hmac() will change " + "from 'sha1' to 'sha256' in Django 7.0. Pass an explicit " + "algorithm to silence this warning.", + category=RemovedInDjango70Warning, + skip_file_prefixes=django_file_prefixes(), + ) + algorithm = "sha1" if secret is None: secret = settings.SECRET_KEY diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 3cda71933b6b..a45b5cdad49b 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -80,6 +80,11 @@ details on these changes. * Support for double-dot variable lookup, like ``{{ book..title }}``, is removed. +* The default value of the ``algorithm`` argument for + ``django.utils.crypto.salted_hmac()`` and + ``django.core.signing.base64_hmac()`` will change from ``"sha1"`` to + ``"sha256"``. + .. _deprecation-removed-in-6.1: 6.1 diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 987d46874aec..9003e01e1b71 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -589,6 +589,12 @@ Miscellaneous deprecated. This syntax maps to a lookup of the empty string, which is normally a mistake. +* The default value of the ``algorithm`` argument for + ``django.utils.crypto.salted_hmac()`` and + ``django.core.signing.base64_hmac()`` is deprecated and will change from + ``"sha1"`` to ``"sha256"`` in Django 7.0. Pass an explicit ``algorithm`` + to silence the deprecation warning. + Features removed in 6.1 ======================= diff --git a/js_tests/admin/DateTimeShortcuts.test.js b/js_tests/admin/DateTimeShortcuts.test.js index 28d07fb918a3..bf09b711d4e3 100644 --- a/js_tests/admin/DateTimeShortcuts.test.js +++ b/js_tests/admin/DateTimeShortcuts.test.js @@ -93,3 +93,47 @@ QUnit.test("time zone offset warning - date and time field", function (assert) { "id_updated_at_timezone_warning_helptext", ); }); + +QUnit.test("update aria labels - previous and next months", function (assert) { + const $ = django.jQuery; + const dateField = $(''); + $("#qunit-fixture").append(dateField); + DateTimeShortcuts.init(); + const num = DateTimeShortcuts.calendars.length - 1; + const cal = DateTimeShortcuts.calendars[num]; + // Set to January 2026 + cal.currentMonth = 1; + cal.currentYear = 2026; + DateTimeShortcuts.updateNavAriaLabels(num); + const cal_box = document.getElementById( + DateTimeShortcuts.calendarDivName1 + num, + ); + const prevLabel = cal_box + .querySelector(".calendarnav-previous") + .getAttribute("aria-label"); + const nextLabel = cal_box + .querySelector(".calendarnav-next") + .getAttribute("aria-label"); + assert.equal(prevLabel, "Previous (December 2025)"); + assert.equal(nextLabel, "Next (February 2026)"); +}); + +QUnit.test("today link has aria-label with current date", function (assert) { + const $ = django.jQuery; + const dateField = $( + '
', + ); + $("#qunit-fixture").append(dateField); + DateTimeShortcuts.init(); + const todayLink = $(".datetimeshortcuts a:first"); + assert.equal(todayLink.text(), "Today"); + // "Today (April 12, 2026)" + const today = new Date(); + const formattedDate = today.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }); + const expectedAriaLabel = `Today (${formattedDate})`; + assert.equal(todayLink.attr("aria-label"), expectedAriaLabel); +}); diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py index b94f44ef222c..42fccca7d6eb 100644 --- a/tests/annotations/tests.py +++ b/tests/annotations/tests.py @@ -742,8 +742,7 @@ def test_column_field_ordering(self): 4. model_related_fields """ store = Store.objects.first() - Employee.objects.create( - id=1, + e1 = Employee.objects.create( first_name="Max", manager=True, last_name="Paine", @@ -751,8 +750,7 @@ def test_column_field_ordering(self): age=23, salary=Decimal(50000.00), ) - Employee.objects.create( - id=2, + e2 = Employee.objects.create( first_name="Buffy", manager=False, last_name="Summers", @@ -770,8 +768,18 @@ def test_column_field_ordering(self): ) rows = [ - (1, "Max", True, 42, "Paine", 23, Decimal(50000.00), store.name, 17), - (2, "Buffy", False, 42, "Summers", 18, Decimal(40000.00), store.name, 17), + (e1.pk, "Max", True, 42, "Paine", 23, Decimal(50000.00), store.name, 17), + ( + e2.pk, + "Buffy", + False, + 42, + "Summers", + 18, + Decimal(40000.00), + store.name, + 17, + ), ] self.assertQuerySetEqual( @@ -792,8 +800,7 @@ def test_column_field_ordering(self): def test_column_field_ordering_with_deferred(self): store = Store.objects.first() - Employee.objects.create( - id=1, + e1 = Employee.objects.create( first_name="Max", manager=True, last_name="Paine", @@ -801,8 +808,7 @@ def test_column_field_ordering_with_deferred(self): age=23, salary=Decimal(50000.00), ) - Employee.objects.create( - id=2, + e2 = Employee.objects.create( first_name="Buffy", manager=False, last_name="Summers", @@ -820,8 +826,18 @@ def test_column_field_ordering_with_deferred(self): ) rows = [ - (1, "Max", True, 42, "Paine", 23, Decimal(50000.00), store.name, 17), - (2, "Buffy", False, 42, "Summers", 18, Decimal(40000.00), store.name, 17), + (e1.pk, "Max", True, 42, "Paine", 23, Decimal(50000.00), store.name, 17), + ( + e2.pk, + "Buffy", + False, + 42, + "Summers", + 18, + Decimal(40000.00), + store.name, + 17, + ), ] # and we respect deferred columns! diff --git a/tests/auth_tests/test_remote_user.py b/tests/auth_tests/test_remote_user.py index f88d966bad40..8c85c651968a 100644 --- a/tests/auth_tests/test_remote_user.py +++ b/tests/auth_tests/test_remote_user.py @@ -1,5 +1,7 @@ from datetime import UTC, datetime +import asgiref.sync + from django.conf import settings from django.contrib.auth import aauthenticate, authenticate from django.contrib.auth.backends import RemoteUserBackend @@ -14,6 +16,15 @@ modify_settings, override_settings, ) +from django.utils.decorators import sync_only_middleware + + +@sync_only_middleware +def sync_middleware(get_response): + def middleware(request): + return get_response(request) + + return middleware @override_settings(ROOT_URLCONF="auth_tests.urls") @@ -470,6 +481,20 @@ async def test_unknown_user_async(self): self.assertEqual(newuser.email, "user@example.com") +class ASGISyncPathRemoteUserTest(RemoteUserTest): + """Later sync-only middleware forces sync execution even under ASGI.""" + + middleware = [ + RemoteUserTest.middleware, + "auth_tests.test_remote_user.sync_middleware", + ] + + def setUp(self): + method = getattr(self, self._testMethodName) + if not isinstance(method, asgiref.sync.AsyncToSync): + self.skipTest("This test covers async-only functionality") + + class CustomHeaderMiddleware(RemoteUserMiddleware): """ Middleware that overrides custom HTTP auth user header. @@ -488,6 +513,11 @@ class CustomHeaderRemoteUserTest(RemoteUserTest): header = "HTTP_AUTHUSER" +class CustomHeaderASGISyncPathRemoteUserTest(ASGISyncPathRemoteUserTest): + middleware = "auth_tests.test_remote_user.CustomHeaderMiddleware" + header = "HTTP_AUTHUSER" + + class PersistentRemoteUserTest(RemoteUserTest): """ PersistentRemoteUserMiddleware keeps the user logged in even if the diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py index ce5a0db8ab86..dc1039f7b98b 100644 --- a/tests/delete_regress/tests.py +++ b/tests/delete_regress/tests.py @@ -58,9 +58,9 @@ def setUp(self): def test_concurrent_delete(self): """Concurrent deletes don't collide and lock the database (#9479).""" with transaction.atomic(): - Book.objects.create(id=1, pagecount=100) - Book.objects.create(id=2, pagecount=200) - Book.objects.create(id=3, pagecount=300) + Book.objects.create(pagecount=100) + book = Book.objects.create(pagecount=200) + Book.objects.create(pagecount=300) with transaction.atomic(): # Start a transaction on the main connection. @@ -68,7 +68,9 @@ def test_concurrent_delete(self): # Delete something using another database connection. with self.conn2.cursor() as cursor2: - cursor2.execute("DELETE from delete_regress_book WHERE id = 1") + cursor2.execute( + "DELETE from delete_regress_book WHERE id = %s", [book.pk] + ) self.conn2.commit() # In the same transaction on the main connection, perform a diff --git a/tests/runtests.py b/tests/runtests.py index 60dc03fea39b..c24481f8cb5c 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -28,7 +28,10 @@ from django.test.runner import get_max_test_processes, parallel_type from django.test.selenium import SeleniumTestCase, SeleniumTestCaseBase from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner - from django.utils.deprecation import RemovedInDjango70Warning + from django.utils.deprecation import ( + RemovedAfterNextVersionWarning, + RemovedInNextVersionWarning, + ) from django.utils.functional import classproperty from django.utils.log import DEFAULT_LOGGING from django.utils.version import PYPY @@ -43,7 +46,8 @@ warnings.filterwarnings("ignore", r"\(1003, *", category=MySQLdb.Warning) # Make deprecation warnings errors to ensure no usage of deprecated features. -warnings.simplefilter("error", RemovedInDjango70Warning) +warnings.simplefilter("error", RemovedInNextVersionWarning) +warnings.simplefilter("error", RemovedAfterNextVersionWarning) # Make resource and runtime warning errors to ensure no usage of error prone # patterns. warnings.simplefilter("error", ResourceWarning) diff --git a/tests/signing/tests.py b/tests/signing/tests.py index 0aadba12a123..963e8db52b57 100644 --- a/tests/signing/tests.py +++ b/tests/signing/tests.py @@ -4,6 +4,7 @@ from django.test import SimpleTestCase, override_settings from django.test.utils import freeze_time from django.utils.crypto import InvalidAlgorithm +from django.utils.deprecation import RemovedInDjango70Warning class TestSigner(SimpleTestCase): @@ -43,6 +44,15 @@ def test_signature_with_salt(self): signing.Signer(key="predictable-secret", salt="two").signature("hello"), ) + def test_base64_hmac_default_algorithm_deprecation(self): + msg = ( + "The default argument for algorithm in base64_hmac() will change " + "from 'sha1' to 'sha256' in Django 7.0. Pass an explicit " + "algorithm to silence this warning." + ) + with self.assertWarnsMessage(RemovedInDjango70Warning, msg): + signing.base64_hmac("salt", "value", "key") + def test_custom_algorithm(self): signer = signing.Signer(key="predictable-secret", algorithm="sha512") self.assertEqual( diff --git a/tests/utils_tests/test_crypto.py b/tests/utils_tests/test_crypto.py index 6fb4bfa4534c..61d9bb047896 100644 --- a/tests/utils_tests/test_crypto.py +++ b/tests/utils_tests/test_crypto.py @@ -8,6 +8,7 @@ pbkdf2, salted_hmac, ) +from django.utils.deprecation import RemovedInDjango70Warning class TestUtilsCryptoMisc(SimpleTestCase): @@ -24,20 +25,30 @@ def test_constant_time_compare(self): def test_salted_hmac(self): tests = [ - ((b"salt", b"value"), {}, "b51a2e619c43b1ca4f91d15c57455521d71d61eb"), - (("salt", "value"), {}, "b51a2e619c43b1ca4f91d15c57455521d71d61eb"), + ( + (b"salt", b"value"), + {"algorithm": "sha1"}, + "b51a2e619c43b1ca4f91d15c57455521d71d61eb", + ), ( ("salt", "value"), - {"secret": "abcdefg"}, + {"algorithm": "sha1"}, + "b51a2e619c43b1ca4f91d15c57455521d71d61eb", + ), + ( + ("salt", "value"), + {"secret": "abcdefg", "algorithm": "sha1"}, "8bbee04ccddfa24772d1423a0ba43bd0c0e24b76", ), ( ("salt", "value"), - {"secret": "x" * hashlib.sha1().block_size}, + {"secret": "x" * hashlib.sha1().block_size, "algorithm": "sha1"}, "bd3749347b412b1b0a9ea65220e55767ac8e96b0", ), ( ("salt", "value"), + # RemovedInDjango70Warning: Remove the explicit algorithm to + # test the new default value. {"algorithm": "sha256"}, "ee0bf789e4e009371a5372c90f73fcf17695a8439c9108b0480f14e347b3f9ec", ), @@ -56,6 +67,15 @@ def test_salted_hmac(self): with self.subTest(args=args, kwargs=kwargs): self.assertEqual(salted_hmac(*args, **kwargs).hexdigest(), digest) + def test_salted_hmac_default_algorithm_deprecation(self): + msg = ( + "The default argument for algorithm in salted_hmac() will change " + "from 'sha1' to 'sha256' in Django 7.0. Pass an explicit " + "algorithm to silence this warning." + ) + with self.assertWarnsMessage(RemovedInDjango70Warning, msg): + salted_hmac("salt", "value") + def test_invalid_algorithm(self): msg = "'whatever' is not an algorithm accepted by the hashlib module." with self.assertRaisesMessage(InvalidAlgorithm, msg):