99import pytest
1010from django .contrib .auth .models import User # lint-amnesty, pylint: disable=imported-auth-user
1111from django .core .management import CommandError , call_command
12+ from django .db import connection
1213from django .db .models .signals import pre_delete
14+ from django .test .utils import CaptureQueriesContext
1315from social_django .models import UserSocialAuth
1416
1517from common .djangoapps .student .tests .factories import UserFactory # lint-amnesty, pylint: disable=wrong-import-order
18+ from openedx .core .djangoapps .user_api .accounts .signals import ( # lint-amnesty, pylint: disable=wrong-import-order
19+ redact_social_auth_pii_before_deletion ,
20+ )
1621from openedx .core .djangoapps .user_api .accounts .tests .retirement_helpers import ( # lint-amnesty, pylint: disable=unused-import, wrong-import-order
1722 setup_retirement_states , # noqa: F401
1823)
1924from openedx .core .djangolib .testing .utils import skip_unless_lms # lint-amnesty, pylint: disable=wrong-import-order
2025
21- from ...accounts .signals import get_redacted_social_auth_uid
2226from ...models import UserRetirementStatus
2327
2428pytestmark = pytest .mark .django_db
@@ -117,8 +121,8 @@ def test_retire_user_redacts_sso_pii_before_deletion(setup_retirement_states):
117121 """
118122 Test that SSO PII is redacted before UserSocialAuth records are deleted during retirement.
119123
120- This test verifies the order of operations by capturing the record's state
121- at the moment of deletion to ensure it was already redacted .
124+ The safety-net pre_delete signal handler is disconnected for this test so that
125+ we verify the redaction comes from retire_user itself, not the fallback signal .
122126 """
123127 user = UserFactory .create (username = 'sso-user' , email = 'sso-user@example.com' )
124128 social_auth = UserSocialAuth .objects .create (
@@ -133,32 +137,22 @@ def test_retire_user_redacts_sso_pii_before_deletion(setup_retirement_states):
133137 )
134138 social_auth_id = social_auth .id
135139
136- captured_states = []
137-
138- def capture_state_before_delete (sender , instance , ** kwargs ): # pylint: disable=unused-argument
139- """Capture the database state seen by the pre_delete signal."""
140- instance .refresh_from_db ()
141- captured_states .append ({
142- 'id' : instance .id ,
143- 'uid' : instance .uid ,
144- 'extra_data' : dict (instance .extra_data ) if instance .extra_data else {},
145- })
146-
147- pre_delete .connect (capture_state_before_delete , sender = UserSocialAuth )
140+ # Disconnect the safety-net signal so we prove retire_user itself redacts first.
141+ pre_delete .disconnect (redact_social_auth_pii_before_deletion , sender = UserSocialAuth )
148142 try :
149- call_command ('retire_user' , username = user .username , user_email = user .email )
143+ with CaptureQueriesContext (connection ) as ctx :
144+ call_command ('retire_user' , username = user .username , user_email = user .email )
150145 finally :
151- pre_delete .disconnect ( capture_state_before_delete , sender = UserSocialAuth )
146+ pre_delete .connect ( redact_social_auth_pii_before_deletion , sender = UserSocialAuth )
152147
153- # Verify that at the moment of deletion, the record was already redacted
154- assert captured_states == [{
155- 'id' : social_auth_id ,
156- 'uid' : get_redacted_social_auth_uid ( social_auth_id ),
157- 'extra_data' : {},
158- }], \
159- "SSO records should be redacted before deletion"
148+ sql_list = [ q [ 'sql' ] for q in ctx ]
149+ table_key = 'SOCIAL_AUTH_USERSOCIALAUTH'
150+ update_indices = [ i for i , s in enumerate ( sql_list ) if 'UPDATE' in s . upper () and table_key in s . upper ()]
151+ delete_indices = [ i for i , s in enumerate ( sql_list ) if 'DELETE' in s . upper () and table_key in s . upper ()]
152+ assert update_indices , 'Expected at least one UPDATE (redaction) on social_auth_usersocialauth'
153+ assert delete_indices , 'Expected at least one DELETE on social_auth_usersocialauth'
154+ assert update_indices [ 0 ] < delete_indices [ 0 ], 'Expected UPDATE (redaction) to precede DELETE'
160155
161- # Verify deletion completed
162156 assert not UserSocialAuth .objects .filter (id = social_auth_id ).exists ()
163157
164158 retired_user_status = UserRetirementStatus .objects .filter (original_username = user .username ).first ()
@@ -169,7 +163,10 @@ def capture_state_before_delete(sender, instance, **kwargs): # pylint: disable=
169163@skip_unless_lms
170164def test_retire_user_redacts_each_social_auth_before_bulk_deletion (setup_retirement_states ): # lint-amnesty, pylint: disable=redefined-outer-name, unused-argument # noqa: F811
171165 """
172- Test that each UserSocialAuth record is redacted before bulk deletion during retirement.
166+ Test that all UserSocialAuth records are redacted before bulk deletion during retirement.
167+
168+ The safety-net pre_delete signal handler is disconnected for this test so that
169+ we verify the redaction comes from retire_user itself, not the fallback signal.
173170 """
174171 user = UserFactory .create (username = 'multi-sso-user' , email = 'multi-sso@example.com' )
175172 google_auth = UserSocialAuth .objects .create (
@@ -184,25 +181,24 @@ def test_retire_user_redacts_each_social_auth_before_bulk_deletion(setup_retirem
184181 uid = 'saml-multi@example.com' ,
185182 extra_data = {'email' : 'saml-multi@example.com' , 'name' : 'SAML User' , 'uid' : 'saml-123' }
186183 )
187- # Save IDs before deletion (they become None after delete)
188184 google_auth_id = google_auth .id
189185 saml_auth_id = saml_auth .id
190186
191- captured_states = []
192-
193- def capture_state_before_delete (sender , instance , ** kwargs ): # pylint: disable=unused-argument
194- """Capture the database state seen by the pre_delete signal."""
195- instance .refresh_from_db ()
196- extra = dict (instance .extra_data ) if instance .extra_data else {}
197- captured_states .append ((instance .provider , instance .uid , extra ))
198-
199- pre_delete .connect (capture_state_before_delete , sender = UserSocialAuth )
187+ # Disconnect the safety-net signal so we prove retire_user itself redacts first.
188+ pre_delete .disconnect (redact_social_auth_pii_before_deletion , sender = UserSocialAuth )
200189 try :
201- call_command ('retire_user' , username = user .username , user_email = user .email )
190+ with CaptureQueriesContext (connection ) as ctx :
191+ call_command ('retire_user' , username = user .username , user_email = user .email )
202192 finally :
203- pre_delete .disconnect (capture_state_before_delete , sender = UserSocialAuth )
204-
205- assert sorted (captured_states ) == sorted ([
206- ('google-oauth2' , get_redacted_social_auth_uid (google_auth_id ), {}),
207- ('tpa-saml' , get_redacted_social_auth_uid (saml_auth_id ), {}),
208- ])
193+ pre_delete .connect (redact_social_auth_pii_before_deletion , sender = UserSocialAuth )
194+
195+ sql_list = [q ['sql' ] for q in ctx ]
196+ table_key = 'SOCIAL_AUTH_USERSOCIALAUTH'
197+ update_indices = [i for i , s in enumerate (sql_list ) if 'UPDATE' in s .upper () and table_key in s .upper ()]
198+ delete_indices = [i for i , s in enumerate (sql_list ) if 'DELETE' in s .upper () and table_key in s .upper ()]
199+ assert update_indices , 'Expected at least one UPDATE (redaction) on social_auth_usersocialauth'
200+ assert delete_indices , 'Expected at least one DELETE on social_auth_usersocialauth'
201+ assert update_indices [0 ] < delete_indices [0 ], 'Expected UPDATE (redaction) to precede DELETE'
202+
203+ assert not UserSocialAuth .objects .filter (id = google_auth_id ).exists ()
204+ assert not UserSocialAuth .objects .filter (id = saml_auth_id ).exists ()
0 commit comments