From bfda973c90d7d837feb4ed59658e189e7eec3499 Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Mon, 5 Feb 2024 09:35:37 -0500 Subject: [PATCH] feat: management command to truncate social auth (#2376) truncates all entries in this table were modified outside of the last 90 days. Doesn't take any arguments because YAGNI; can easily be modified in the future if the situation changes. FIXES: APER-3160 --- .../tests/test_truncate_social_auth.py | 45 +++++++++++++++++++ .../commands/truncate_social_auth.py | 38 ++++++++++++++++ credentials/apps/core/tests/factories.py | 10 +++++ 3 files changed, 93 insertions(+) create mode 100644 credentials/apps/core/management/commands/tests/test_truncate_social_auth.py create mode 100644 credentials/apps/core/management/commands/truncate_social_auth.py diff --git a/credentials/apps/core/management/commands/tests/test_truncate_social_auth.py b/credentials/apps/core/management/commands/tests/test_truncate_social_auth.py new file mode 100644 index 000000000..067259003 --- /dev/null +++ b/credentials/apps/core/management/commands/tests/test_truncate_social_auth.py @@ -0,0 +1,45 @@ +""" +Tests for the truncate_social_auth management command +""" + +from datetime import datetime, timedelta, timezone +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.core.management import call_command +from django.test import TestCase +from social_django.models import UserSocialAuth + +from credentials.apps.core.tests.factories import UserFactory, UserSocialAuthFactory + + +User = get_user_model() + +JSON = "application/json" + + +class TruncateUserSocialAuthTest(TestCase): + def setUp(self): + """Create social auth records for test""" + super().setUp() + now = datetime.now(timezone.utc) + long_ago = now - timedelta(days=180) + + self.user_young = UserFactory() + self.user_old = UserFactory() + + self.auth_young = UserSocialAuthFactory(user_id=self.user_young.id, modified=now) + with patch("django.utils.timezone.now") as mock_now: + mock_now.return_value = long_ago + self.auth_old = UserSocialAuthFactory(user_id=self.user_old.id, modified=long_ago) + + def test_delete_old_rows(self): + """verify that only old auth records are deleted.""" + auth_records = UserSocialAuth.objects.all() + self.assertEqual(len(auth_records), 2) + + call_command("truncate_social_auth") + + auth_records = UserSocialAuth.objects.all() + self.assertEqual(len(auth_records), 1) + self.assertEqual(auth_records[0], self.auth_young) diff --git a/credentials/apps/core/management/commands/truncate_social_auth.py b/credentials/apps/core/management/commands/truncate_social_auth.py new file mode 100644 index 000000000..815ea6eb1 --- /dev/null +++ b/credentials/apps/core/management/commands/truncate_social_auth.py @@ -0,0 +1,38 @@ +""" +Django managment command to truncate the social_auth_usersocialauth table. + +The social-auth-app-django plugin can have migrations on upgrade, and those +migrations can fail when the social_auth_usersocialauth table is too large. +It's safe to truncate the table; it doesn't affect logged in users at all. +However, to avoid any risk, this keeps a window of learners who have logged +in in the last 90 days. +""" + +import logging +from datetime import datetime, timedelta, timezone + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand +from social_django.models import UserSocialAuth + + +logger = logging.getLogger(__name__) +User = get_user_model() + + +class Command(BaseCommand): + def handle(self, *args, **options): + """Truncate the social_auth_usersocialauth table.""" + now = datetime.now(timezone.utc) + error_message = "truncate_social_auth deleted failed" + # This is unlikely to be a run-more-than-once management command. + # If the need rearises, this timedelta could become an argument. + window_to_keep = timedelta(days=90) + try: + deleted = UserSocialAuth.objects.filter(modified__lte=now - window_to_keep).delete() + except: # pylint: disable=bare-except + logger.exception(error_message) + try: + logger.info(f"truncate_social_auth deleted {deleted[0]} rows") + except IndexError: + logger.error(error_message) diff --git a/credentials/apps/core/tests/factories.py b/credentials/apps/core/tests/factories.py index 0ddf25f0b..d9855995c 100644 --- a/credentials/apps/core/tests/factories.py +++ b/credentials/apps/core/tests/factories.py @@ -4,6 +4,7 @@ from django.contrib.sites.models import Site from factory import Faker, PostGenerationMethodCall, Sequence, SubFactory, django, sequence +from social_django.models import UserSocialAuth from credentials.apps.core.models import SiteConfiguration, User @@ -56,3 +57,12 @@ class Meta: certificate_help_url = Faker("url") records_help_url = Faker("url") twitter_username = Faker("word") + + +class UserSocialAuthFactory(django.DjangoModelFactory): + class Meta: + model = UserSocialAuth + + user_id = Faker("random_int") + provider = Faker("word") + extra_data = Faker("json")