11"""Tests for cached authentication middleware."""
2- from unittest .mock import patch
2+ from unittest .mock import call , patch
33
4+ import django
45from django .conf import settings
5- from django .contrib .auth .models import User , AnonymousUser # lint-amnesty, pylint: disable=imported-auth-user
6- from django .urls import reverse
7- from django .test import TestCase
86from django .contrib .auth import SESSION_KEY
7+ from django .contrib .auth .models import AnonymousUser , User # lint-amnesty, pylint: disable=imported-auth-user
98from django .http import HttpResponse , SimpleCookie
9+ from django .test import TestCase
10+ from django .urls import reverse
1011
12+ from common .djangoapps .student .tests .factories import UserFactory
1113from openedx .core .djangoapps .cache_toolbox .middleware import CacheBackedAuthenticationMiddleware
1214from openedx .core .djangoapps .safe_sessions .middleware import SafeCookieData , SafeSessionMiddleware
13- from openedx .core .djangolib .testing .utils import skip_unless_cms , skip_unless_lms , get_mock_request
14- from common .djangoapps .student .tests .factories import UserFactory
15+ from openedx .core .djangolib .testing .utils import get_mock_request , skip_unless_cms , skip_unless_lms
1516
1617
1718class CachedAuthMiddlewareTestCase (TestCase ):
@@ -36,9 +37,68 @@ def _test_change_session_hash(self, test_url, redirect_url, target_status_code=2
3637 """
3738 response = self .client .get (test_url )
3839 assert response .status_code == 200
39- with patch .object (User , 'get_session_auth_hash' , return_value = 'abc123' ):
40- response = self .client .get (test_url )
41- self .assertRedirects (response , redirect_url , target_status_code = target_status_code )
40+
41+ with patch (
42+ "openedx.core.djangoapps.cache_toolbox.middleware.set_custom_attribute"
43+ ) as mock_set_custom_attribute :
44+ with patch .object (User , 'get_session_auth_hash' , return_value = 'abc123' , autospec = True ):
45+ # Django 3.2 has _legacy_get_session_auth_hash, and Django 4 does not
46+ # Remove once we reach Django 4
47+ if hasattr (User , '_legacy_get_session_auth_hash' ):
48+ with patch .object (User , '_legacy_get_session_auth_hash' , return_value = 'abc123' ):
49+ response = self .client .get (test_url )
50+ else :
51+ response = self .client .get (test_url )
52+
53+ self .assertRedirects (response , redirect_url , target_status_code = target_status_code )
54+ mock_set_custom_attribute .assert_any_call ('failed_session_verification' , True )
55+
56+ def _test_custom_attribute_after_changing_hash (self , test_url , mock_set_custom_attribute ):
57+ """verify that set_custom_attribute is called with expected values"""
58+ password = 'test-password'
59+
60+ # Test DEFAULT_HASHING_ALGORITHM of 'sha1' for both login and client get
61+ with self .settings (DEFAULT_HASHING_ALGORITHM = 'sha1' ):
62+ self .client .login (username = self .user .username , password = password )
63+ self .client .get (test_url )
64+ # For Django 3.2, the setting 'sha1' applies and is the "default".
65+ # For Django 4, the setting no longer applies, and 'sha256' will be used for both as the "default".
66+ mock_set_custom_attribute .assert_has_calls ([
67+ call ('DEFAULT_HASHING_ALGORITHM' , 'sha1' ),
68+ call ('session_hash_verified' , "default" ),
69+ ])
70+ mock_set_custom_attribute .reset_mock ()
71+
72+ # Test DEFAULT_HASHING_ALGORITHM of 'sha1' for login and switch to 'sha256' for client get.
73+ with self .settings (DEFAULT_HASHING_ALGORITHM = 'sha1' ):
74+ self .client .login (username = self .user .username , password = password )
75+ with self .settings (DEFAULT_HASHING_ALGORITHM = 'sha256' ):
76+ self .client .get (test_url )
77+ if django .VERSION < (4 , 0 ):
78+ # For Django 3.2, the setting 'sha1' applies to login, and uses 'she256' for client get,
79+ # and should "fallback" to 'sha1".
80+ mock_set_custom_attribute .assert_has_calls ([
81+ call ('DEFAULT_HASHING_ALGORITHM' , 'sha256' ),
82+ call ('session_hash_verified' , "fallback" ),
83+ ])
84+ else :
85+ # For Django 4, the setting no longer applies, and again 'sha256' will be used for both as the "default".
86+ mock_set_custom_attribute .assert_has_calls ([
87+ call ('DEFAULT_HASHING_ALGORITHM' , 'sha256' ),
88+ call ('session_hash_verified' , "default" ),
89+ ])
90+ mock_set_custom_attribute .reset_mock ()
91+
92+ # Test DEFAULT_HASHING_ALGORITHM of 'sha256' for both login and client get
93+ with self .settings (DEFAULT_HASHING_ALGORITHM = 'sha256' ):
94+ self .client .login (username = self .user .username , password = password )
95+ self .client .get (test_url )
96+ # For Django 3.2, the setting 'sha256' applies and is the "default".
97+ # For Django 4, the setting no longer applies, and 'sha256' will be used for both as the "default".
98+ mock_set_custom_attribute .assert_has_calls ([
99+ call ('DEFAULT_HASHING_ALGORITHM' , 'sha256' ),
100+ call ('session_hash_verified' , "default" ),
101+ ])
42102
43103 @skip_unless_lms
44104 def test_session_change_lms (self ):
@@ -53,6 +113,20 @@ def test_session_change_cms(self):
53113 # Studio login redirects to LMS login
54114 self ._test_change_session_hash (home_url , settings .LOGIN_URL + '?next=' + home_url , target_status_code = 302 )
55115
116+ @skip_unless_lms
117+ @patch ("openedx.core.djangoapps.cache_toolbox.middleware.set_custom_attribute" )
118+ def test_custom_attribute_after_changing_hash_lms (self , mock_set_custom_attribute ):
119+ """Test set_custom_attribute is called with expected values in LMS"""
120+ test_url = reverse ('dashboard' )
121+ self ._test_custom_attribute_after_changing_hash (test_url , mock_set_custom_attribute )
122+
123+ @skip_unless_cms
124+ @patch ("openedx.core.djangoapps.cache_toolbox.middleware.set_custom_attribute" )
125+ def test_custom_attribute_after_changing_hash_cms (self , mock_set_custom_attribute ):
126+ """Test set_custom_attribute is called with expected values in CMS"""
127+ test_url = reverse ('home' )
128+ self ._test_custom_attribute_after_changing_hash (test_url , mock_set_custom_attribute )
129+
56130 def test_user_logout_on_session_hash_change (self ):
57131 """
58132 Verify that if a user's session auth hash and the request's hash
@@ -75,9 +149,18 @@ def test_user_logout_on_session_hash_change(self):
75149 assert self .client .response .cookies .get (settings .SESSION_COOKIE_NAME ).value == session_id
76150 assert self .client .response .cookies .get ('edx-jwt-cookie-header-payload' ).value == 'test-jwt-payload'
77151
78- with patch .object (User , 'get_session_auth_hash' , return_value = 'abc123' ):
79- CacheBackedAuthenticationMiddleware ().process_request (self .request )
80- SafeSessionMiddleware ().process_response (self .request , self .client .response )
152+ with patch .object (User , 'get_session_auth_hash' , return_value = 'abc123' , autospec = True ):
153+ # Django 3.2 has _legacy_get_session_auth_hash, and Django 4 does not
154+ # Remove once we reach Django 4
155+ if hasattr (User , '_legacy_get_session_auth_hash' ):
156+ with patch .object (User , '_legacy_get_session_auth_hash' , return_value = 'abc123' ):
157+ CacheBackedAuthenticationMiddleware (get_response = lambda request : None ).process_request (self .request )
158+
159+ else :
160+ CacheBackedAuthenticationMiddleware (get_response = lambda request : None ).process_request (self .request )
161+ SafeSessionMiddleware (get_response = lambda request : None ).process_response (
162+ self .request , self .client .response
163+ )
81164
82165 # asserts that user, session, and JWT cookies do not exist
83166 assert self .request .session .get (SESSION_KEY ) is None
0 commit comments