|
1 | 1 | """ Unit tests for custom UserProfile properties. """ |
2 | 2 |
|
| 3 | +from contextlib import contextmanager |
3 | 4 |
|
4 | 5 | import ddt |
5 | 6 | from completion import models |
6 | 7 | from completion.test_utils import CompletionWaffleTestMixin |
| 8 | +from django.db import connection |
| 9 | +from django.db.models.signals import pre_delete |
7 | 10 | from django.test import TestCase |
8 | | -from django.test.utils import override_settings |
| 11 | +from django.test.utils import CaptureQueriesContext, override_settings |
| 12 | +from social_django.models import UserSocialAuth |
9 | 13 |
|
10 | 14 | from common.djangoapps.student.models import CourseEnrollment |
11 | 15 | from common.djangoapps.student.tests.factories import UserFactory |
12 | | -from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed |
| 16 | +from openedx.core.djangoapps.user_api.accounts.signals import redact_social_auth_pii_before_deletion |
| 17 | +from openedx.core.djangoapps.user_api.accounts.utils import ( |
| 18 | + redact_and_delete_social_auth, |
| 19 | + retrieve_last_sitewide_block_completed, |
| 20 | +) |
13 | 21 | from openedx.core.djangolib.testing.utils import skip_unless_lms |
14 | 22 | from xmodule.modulestore.tests.django_utils import ( |
15 | 23 | SharedModuleStoreTestCase, # pylint: disable=wrong-import-order |
|
22 | 30 | from ..utils import format_social_link, validate_social_link |
23 | 31 |
|
24 | 32 |
|
| 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 | + |
25 | 66 | @ddt.ddt |
26 | 67 | class UserAccountSettingsTest(TestCase): |
27 | 68 | """Unit tests for setting Social Media Links.""" |
@@ -133,3 +174,71 @@ def test_retrieve_last_sitewide_block_completed(self): |
133 | 174 | ) |
134 | 175 |
|
135 | 176 | assert empty_block_url is None |
| 177 | + |
| 178 | + |
| 179 | +@ddt.ddt |
| 180 | +@skip_unless_lms |
| 181 | +class RedactAndDeleteSocialAuthTest(TestCase): |
| 182 | + """ |
| 183 | + Tests for the redact_and_delete_social_auth utility function. |
| 184 | + """ |
| 185 | + |
| 186 | + def setUp(self): |
| 187 | + super().setUp() |
| 188 | + self.user = UserFactory.create(username='testuser', email='testuser@example.com') |
| 189 | + |
| 190 | + def create_social_auth(self, provider='google-oauth2', uid='user@example.com', extra_data=None): |
| 191 | + """ |
| 192 | + Helper method to create UserSocialAuth instances for testing. |
| 193 | + """ |
| 194 | + extra_data = extra_data or { |
| 195 | + 'email': f'{provider}@example.com', |
| 196 | + 'name': f'{provider.capitalize()} User', |
| 197 | + 'id': '123456789', |
| 198 | + } |
| 199 | + return UserSocialAuth.objects.create( |
| 200 | + user=self.user, |
| 201 | + provider=provider, |
| 202 | + uid=uid, |
| 203 | + extra_data=extra_data, |
| 204 | + ) |
| 205 | + |
| 206 | + def test_redact_and_delete_redacts_single_sso_record(self): |
| 207 | + """ |
| 208 | + Test that redact_and_delete_social_auth redacts and deletes a single SSO record. |
| 209 | + """ |
| 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 | + ) |
| 215 | + social_auth_id = social_auth.pk |
| 216 | + |
| 217 | + with disconnected_social_auth_redaction_signal(), CaptureQueriesContext(connection) as ctx: |
| 218 | + redact_and_delete_social_auth(self.user.id) |
| 219 | + |
| 220 | + assert_update_before_delete([query['sql'] for query in ctx]) |
| 221 | + 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