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):