11""" Unit tests for custom UserProfile properties. """
22
3+ from contextlib import contextmanager
4+
35import ddt
46from completion import models
57from completion .test_utils import CompletionWaffleTestMixin
2830from ..utils import format_social_link , validate_social_link
2931
3032
33+ def assert_update_before_delete (sql_list , num_redact_delete_pairs = 1 , table = 'social_auth_usersocialauth' ):
34+ """
35+ Assert that UPDATE and DELETE queries for ``table`` occur in consecutive pairs.
36+ """
37+ table_key = table .upper ()
38+ expected_sql_list = [
39+ sql for sql in sql_list
40+ if table_key in sql .upper () and ('UPDATE' in sql .upper () or 'DELETE' in sql .upper ())
41+ ]
42+ assert len (expected_sql_list ) == num_redact_delete_pairs * 2 , (
43+ f'Expected { num_redact_delete_pairs * 2 } UPDATE/DELETE queries on { table } , '
44+ f'got { len (expected_sql_list )} '
45+ )
46+
47+ for index in range (0 , len (expected_sql_list ), 2 ):
48+ update_sql = expected_sql_list [index ]
49+ delete_sql = expected_sql_list [index + 1 ]
50+ assert 'UPDATE' in update_sql .upper (), f'Expected UPDATE at position { index } for { table } '
51+ assert 'DELETE' in delete_sql .upper (), f'Expected DELETE at position { index + 1 } for { table } '
52+
53+ # Use a context manager to guarantee signal reconnection between tests.
54+ @contextmanager
55+ def disconnected_social_auth_redaction_signal ():
56+ """
57+ Temporarily disconnect the fallback signal so tests exercise the helper path.
58+ """
59+ pre_delete .disconnect (redact_social_auth_pii_before_deletion , sender = UserSocialAuth )
60+ try :
61+ yield
62+ finally :
63+ pre_delete .connect (redact_social_auth_pii_before_deletion , sender = UserSocialAuth )
64+
65+
3166@ddt .ddt
3267class UserAccountSettingsTest (TestCase ):
3368 """Unit tests for setting Social Media Links."""
@@ -146,45 +181,12 @@ def test_retrieve_last_sitewide_block_completed(self):
146181class RedactAndDeleteSocialAuthTest (TestCase ):
147182 """
148183 Tests for the redact_and_delete_social_auth utility function.
149-
150- The safety-net pre_delete signal handler is disconnected for all tests in this class
151- to verify that redact_and_delete_social_auth itself redacts before deleting,
152- not the fallback signal.
153184 """
154185
155- @classmethod
156- def setUpClass (cls ):
157- super ().setUpClass ()
158- # Disconnect the safety-net signal so tests verify redact_and_delete_social_auth
159- # itself issues the UPDATE before DELETE, not the signal.
160- pre_delete .disconnect (redact_social_auth_pii_before_deletion , sender = UserSocialAuth )
161-
162- @classmethod
163- def tearDownClass (cls ):
164- super ().tearDownClass ()
165- # Reconnect the signal after the test class completes.
166- pre_delete .connect (redact_social_auth_pii_before_deletion , sender = UserSocialAuth )
167-
168186 def setUp (self ):
169187 super ().setUp ()
170188 self .user = UserFactory .create (username = 'testuser' , email = 'testuser@example.com' )
171189
172- def _assert_update_before_delete (self , sql_list , table = 'social_auth_usersocialauth' ):
173- """
174- Assert that an UPDATE on ``table`` runs before the DELETE on ``table``.
175-
176- This verifies that redaction happens in the helper itself, not via the
177- fallback pre_delete signal.
178- """
179- table_key = table .upper ()
180- update_indices = [i for i , sql in enumerate (sql_list ) if 'UPDATE' in sql .upper () and table_key in sql .upper ()]
181- delete_indices = [i for i , sql in enumerate (sql_list ) if 'DELETE' in sql .upper () and table_key in sql .upper ()]
182- assert update_indices , f'Expected at least one UPDATE on { table } '
183- assert delete_indices , f'Expected at least one DELETE on { table } '
184- assert any (update_idx < delete_idx for update_idx in update_indices for delete_idx in delete_indices ), (
185- 'Expected at least one UPDATE to precede at least one DELETE'
186- )
187-
188190 def create_social_auth (self , provider = 'google-oauth2' , uid = 'user@example.com' , extra_data = None ):
189191 """
190192 Helper method to create UserSocialAuth instances for testing.
@@ -201,29 +203,42 @@ def create_social_auth(self, provider='google-oauth2', uid='user@example.com', e
201203 extra_data = extra_data ,
202204 )
203205
204- @ddt .data (
205- {
206- 'provider' : 'google-oauth2' ,
207- 'uid' : 'google@example.com' ,
208- 'extra_data' : {'email' : 'google@example.com' , 'name' : 'Google User' }
209- },
210- {
211- 'provider' : 'tpa-saml' ,
212- 'uid' : 'saml@example.com' ,
213- 'extra_data' : {'email' : 'saml@example.com' , 'name' : 'SAML User' , 'uid' : 'saml-uid' }
214- }
215- )
216- @ddt .unpack
217- def test_redact_and_delete_redacts_multiple_sso_providers (self , provider , uid , extra_data ):
206+ def test_redact_and_delete_redacts_single_sso_record (self ):
218207 """
219- Test that redact_and_delete_social_auth redacts and deletes records for
220- multiple SSO providers in a single call.
208+ Test that redact_and_delete_social_auth redacts and deletes a single SSO record.
221209 """
222- social_auth = self .create_social_auth (provider = provider , uid = uid , extra_data = extra_data )
210+ social_auth = self .create_social_auth (
211+ provider = 'google-oauth2' ,
212+ uid = 'google@example.com' ,
213+ extra_data = {'email' : 'google@example.com' , 'name' : 'Google User' },
214+ )
223215 social_auth_id = social_auth .pk
224216
225- with CaptureQueriesContext (connection ) as ctx :
217+ with disconnected_social_auth_redaction_signal (), CaptureQueriesContext (connection ) as ctx :
226218 redact_and_delete_social_auth (self .user .id )
227219
228- self . _assert_update_before_delete ([query ['sql' ] for query in ctx ])
220+ assert_update_before_delete ([query ['sql' ] for query in ctx ])
229221 assert not UserSocialAuth .objects .filter (id = social_auth_id ).exists ()
222+
223+ def test_redact_and_delete_redacts_multiple_sso_records (self ):
224+ """
225+ Test that redact_and_delete_social_auth redacts and deletes all SSO records for a user.
226+ """
227+ social_auth_ids = [
228+ self .create_social_auth (
229+ provider = 'google-oauth2' ,
230+ uid = 'google@example.com' ,
231+ extra_data = {'email' : 'google@example.com' , 'name' : 'Google User' },
232+ ).pk ,
233+ self .create_social_auth (
234+ provider = 'tpa-saml' ,
235+ uid = 'saml@example.com' ,
236+ extra_data = {'email' : 'saml@example.com' , 'name' : 'SAML User' , 'uid' : 'saml-uid' },
237+ ).pk ,
238+ ]
239+
240+ with disconnected_social_auth_redaction_signal (), CaptureQueriesContext (connection ) as ctx :
241+ redact_and_delete_social_auth (self .user .id )
242+
243+ assert_update_before_delete ([query ['sql' ] for query in ctx ])
244+ assert not UserSocialAuth .objects .filter (id__in = social_auth_ids ).exists ()
0 commit comments