diff --git a/lms/djangoapps/survey/tests/test_signals.py b/lms/djangoapps/survey/tests/test_signals.py index a78aefb0ae15da8d0bb471312eb8cbc033350be6..c22a258e639a7b89dcf4486cc52e8c3282990c2d 100644 --- a/lms/djangoapps/survey/tests/test_signals.py +++ b/lms/djangoapps/survey/tests/test_signals.py @@ -2,7 +2,7 @@ Test signal handlers for the survey app """ -from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import fake_retirement +from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import fake_completed_retirement from student.tests.factories import UserFactory from survey.models import SurveyAnswer from survey.tests.factories import SurveyAnswerFactory @@ -43,7 +43,7 @@ class SurveyRetireSignalTests(ModuleStoreTestCase): # Run twice to make sure no errors are raised _listen_for_lms_retire(sender=self.__class__, user=answer.user) - fake_retirement(answer.user) + fake_completed_retirement(answer.user) _listen_for_lms_retire(sender=self.__class__, user=answer.user) # All values for this user should still be here and just be an empty string diff --git a/lms/djangoapps/verify_student/tests/test_signals.py b/lms/djangoapps/verify_student/tests/test_signals.py index 294cff801de2c2168a6fdbf0f2cc1785756fc5b6..deaa5b41fa8f8a9cc2f13226fdc23deb04a2b0e3 100644 --- a/lms/djangoapps/verify_student/tests/test_signals.py +++ b/lms/djangoapps/verify_student/tests/test_signals.py @@ -9,7 +9,7 @@ from pytz import UTC from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline from lms.djangoapps.verify_student.signals import _listen_for_course_publish, _listen_for_lms_retire from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory -from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import fake_retirement +from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import fake_completed_retirement from student.tests.factories import UserFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -95,7 +95,7 @@ class RetirementSignalTest(ModuleStoreTestCase): # Run this twice to make sure there are no errors raised 2nd time through _listen_for_lms_retire(sender=self.__class__, user=verification.user) - fake_retirement(verification.user) + fake_completed_retirement(verification.user) _listen_for_lms_retire(sender=self.__class__, user=verification.user) ver_obj = SoftwareSecurePhotoVerification.objects.get(user=verification.user) diff --git a/openedx/core/djangoapps/credit/tests/test_models.py b/openedx/core/djangoapps/credit/tests/test_models.py index da957c1041da376eadf155ea636271d4a6b7f02f..e844a424d1ad03010398a2349188f83c5a2c2c28 100644 --- a/openedx/core/djangoapps/credit/tests/test_models.py +++ b/openedx/core/djangoapps/credit/tests/test_models.py @@ -15,7 +15,10 @@ from openedx.core.djangoapps.credit.models import ( CreditRequirement, CreditRequirementStatus ) -from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import RetirementTestCase +from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # pylint: disable=unused-import + RetirementTestCase, + setup_retirement_states +) from openedx.core.djangoapps.user_api.models import UserRetirementStatus from student.tests.factories import UserFactory @@ -97,7 +100,7 @@ class CreditEligibilityModelTests(TestCase): self.assertEqual(len(requirements), 1) -class CreditRequirementStatusTests(TestCase): +class CreditRequirementStatusTests(RetirementTestCase): """ Tests for credit requirement status models. """ @@ -105,7 +108,6 @@ class CreditRequirementStatusTests(TestCase): def setUp(self): super(CreditRequirementStatusTests, self).setUp() self.course_key = CourseKey.from_string("edX/DemoX/Demo_Course") - RetirementTestCase.setup_states() self.old_username = "username" self.user = UserFactory(username=self.old_username) self.retirement = UserRetirementStatus.create_retirement(self.user) @@ -170,14 +172,13 @@ class CreditRequirementStatusTests(TestCase): self.assertFalse(retirement_succeeded) -class CreditRequestTest(TestCase): +class CreditRequestTest(RetirementTestCase): """ The CreditRequest model's test suite. """ def setUp(self): super(CreditRequestTest, self).setUp() - RetirementTestCase.setup_states() self.user = UserFactory.create() self.retirement = UserRetirementStatus.create_retirement(self.user) self.credit_course = CreditCourse.objects.create() diff --git a/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py b/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py index 0953892ea635b7d343ec6388c3145a8a93cfd1b7..b2bf9d1004f9532f6ac8a529d8b7a4720c156257 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/retirement_helpers.py @@ -3,6 +3,7 @@ Helpers for testing retirement functionality """ import datetime +import pytest import pytz from django.test import TestCase from social_django.models import UserSocialAuth @@ -21,75 +22,92 @@ from student.tests.factories import UserFactory from ..views import AccountRetirementView -class RetirementTestCase(TestCase): +@pytest.fixture +def setup_retirement_states(scope="module"): # pylint: disable=unused-argument """ - Test case with a helper methods for retirement + Create basic states that mimic the retirement process. """ - @classmethod - def setUpClass(cls): - super(RetirementTestCase, cls).setUpClass() - cls.setup_states() - - @staticmethod - def setup_states(): - """ - Create basic states that mimic our current understanding of the retirement process - """ - default_states = [ - ('PENDING', 1, False, True), - ('LOCKING_ACCOUNT', 20, False, False), - ('LOCKING_COMPLETE', 30, False, False), - ('RETIRING_CREDENTIALS', 40, False, False), - ('CREDENTIALS_COMPLETE', 50, False, False), - ('RETIRING_ECOM', 60, False, False), - ('ECOM_COMPLETE', 70, False, False), - ('RETIRING_FORUMS', 80, False, False), - ('FORUMS_COMPLETE', 90, False, False), - ('RETIRING_EMAIL_LISTS', 100, False, False), - ('EMAIL_LISTS_COMPLETE', 110, False, False), - ('RETIRING_ENROLLMENTS', 120, False, False), - ('ENROLLMENTS_COMPLETE', 130, False, False), - ('RETIRING_NOTES', 140, False, False), - ('NOTES_COMPLETE', 150, False, False), - ('RETIRING_LMS', 160, False, False), - ('LMS_COMPLETE', 170, False, False), - ('ADDING_TO_PARTNER_QUEUE', 180, False, False), - ('PARTNER_QUEUE_COMPLETE', 190, False, False), - ('ERRORED', 200, True, True), - ('ABORTED', 210, True, True), - ('COMPLETE', 220, True, True), - ] - - for name, ex, dead, req in default_states: - RetirementState.objects.create( - state_name=name, - state_execution_order=ex, - is_dead_end_state=dead, - required=req - ) - - def _create_retirement(self, state, create_datetime=None): - """ - Helper method to create a RetirementStatus with useful defaults - """ - if create_datetime is None: - create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=8) - - user = UserFactory() - return UserRetirementStatus.objects.create( - user=user, - original_username=user.username, - original_email=user.email, - original_name=user.profile.name, - retired_username=get_retired_username_by_username(user.username), - retired_email=get_retired_email_by_email(user.email), - current_state=state, - last_state=state, - responses="", - created=create_datetime, - modified=create_datetime + default_states = [ + ('PENDING', 1, False, True), + ('LOCKING_ACCOUNT', 20, False, False), + ('LOCKING_COMPLETE', 30, False, False), + ('RETIRING_CREDENTIALS', 40, False, False), + ('CREDENTIALS_COMPLETE', 50, False, False), + ('RETIRING_ECOM', 60, False, False), + ('ECOM_COMPLETE', 70, False, False), + ('RETIRING_FORUMS', 80, False, False), + ('FORUMS_COMPLETE', 90, False, False), + ('RETIRING_EMAIL_LISTS', 100, False, False), + ('EMAIL_LISTS_COMPLETE', 110, False, False), + ('RETIRING_ENROLLMENTS', 120, False, False), + ('ENROLLMENTS_COMPLETE', 130, False, False), + ('RETIRING_NOTES', 140, False, False), + ('NOTES_COMPLETE', 150, False, False), + ('RETIRING_LMS', 160, False, False), + ('LMS_COMPLETE', 170, False, False), + ('ADDING_TO_PARTNER_QUEUE', 180, False, False), + ('PARTNER_QUEUE_COMPLETE', 190, False, False), + ('ERRORED', 200, True, True), + ('ABORTED', 210, True, True), + ('COMPLETE', 220, True, True), + ] + + for name, ex, dead, req in default_states: + RetirementState.objects.create( + state_name=name, + state_execution_order=ex, + is_dead_end_state=dead, + required=req ) + yield + + RetirementState.objects.all().delete() + + +def create_retirement_status(state=None, create_datetime=None): + """ + Helper method to create a RetirementStatus with useful defaults. + Assumes that retirement states have been setup before calling. + """ + if create_datetime is None: + create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=8) + + user = UserFactory() + retirement = UserRetirementStatus.create_retirement(user) + if state: + retirement.current_state = state + retirement.last_state = state + retirement.created = create_datetime + retirement.modified = create_datetime + retirement.save() + return retirement + + +def _fake_logged_out_user(user): + # Simulate the initial logout retirement endpoint. + user.username = get_retired_username_by_username(user.username) + user.email = get_retired_email_by_email(user.email) + user.set_unusable_password() + user.save() + + +@pytest.fixture +def logged_out_retirement_request(): + """ + Returns a UserRetirementStatus test fixture object that has been logged out and email-changed, + which is the first step which happens to a user being added to the retirement queue. + """ + retirement = create_retirement_status() + _fake_logged_out_user(retirement.user) + return retirement + + +@pytest.mark.usefixtures("setup_retirement_states") +class RetirementTestCase(TestCase): + """ + Test case with a helper methods for retirement + """ def _retirement_to_dict(self, retirement, all_fields=False): """ Return a dict format of this model to a consistent format for serialization, removing the long text field @@ -131,7 +149,7 @@ class RetirementTestCase(TestCase): return retirement_dict def _create_users_all_states(self): - return [self._create_retirement(state) for state in RetirementState.objects.all()] + return [create_retirement_status(state) for state in RetirementState.objects.all()] def _get_non_dead_end_states(self): return [state for state in RetirementState.objects.filter(is_dead_end_state=False)] @@ -140,7 +158,7 @@ class RetirementTestCase(TestCase): return [state for state in RetirementState.objects.filter(is_dead_end_state=True)] -def fake_retirement(user): +def fake_completed_retirement(user): """ Makes an attempt to put user for the given user into a "COMPLETED" retirement state by faking important parts of retirement. @@ -151,12 +169,10 @@ def fake_retirement(user): """ # Deactivate / logout and hash username & email UserSocialAuth.objects.filter(user_id=user.id).delete() + _fake_logged_out_user(user) user.first_name = '' user.last_name = '' user.is_active = False - user.username = get_retired_username_by_username(user.username) - user.email = get_retired_email_by_email(user.email) - user.set_unusable_password() user.save() # Clear profile diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_models.py b/openedx/core/djangoapps/user_api/accounts/tests/test_models.py index 1f59f86c311341ec422877b0b88f8da76ad99a58..e903ed0bd647c0f1a133059ead0d0246c66efda6 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_models.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_models.py @@ -11,38 +11,13 @@ from openedx.core.djangoapps.user_api.models import ( ) from student.models import get_retired_email_by_email, get_retired_username_by_username from student.tests.factories import UserFactory +from .retirement_helpers import setup_retirement_states # pylint: disable=unused-import # Tell pytest it's ok to use the database pytestmark = pytest.mark.django_db -@pytest.fixture -def setup_retirement_states(): - """ - Pytest fixture to create some basic states for testing. Duplicates functionality of the - Django test runner in test_views.py unfortunately, but they're not compatible. - """ - default_states = [ - ('PENDING', 1, False, True), - ('LOCKING_ACCOUNT', 20, False, False), - ('LOCKING_COMPLETE', 30, False, False), - ('RETIRING_LMS', 40, False, False), - ('LMS_COMPLETE', 50, False, False), - ('ERRORED', 60, True, True), - ('ABORTED', 70, True, True), - ('COMPLETE', 80, True, True), - ] - - for name, ex, dead, req in default_states: - RetirementState.objects.create( - state_name=name, - state_execution_order=ex, - is_dead_end_state=dead, - required=req - ) - - def _assert_retirementstatus_is_user(retirement, user): """ Helper function to compare a newly created UserRetirementStatus object to expected values for diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py index c3b8b718e48b218844879877fcb81017f6255c30..f820ca87e1643db50ed61cf9dcb6a28431ebe8cd 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py @@ -78,7 +78,12 @@ from student.tests.factories import ( from ..views import AccountRetirementView, USER_PROFILE_PII from ...tests.factories import UserOrgTagFactory -from .retirement_helpers import RetirementTestCase, fake_retirement +from .retirement_helpers import ( # pylint: disable=unused-import + RetirementTestCase, + fake_completed_retirement, + create_retirement_status, + setup_retirement_states +) def build_jwt_headers(user): @@ -255,8 +260,7 @@ class TestAccountRetireMailings(RetirementTestCase): # Should be created in parent setUpClass retiring_email_lists = RetirementState.objects.get(state_name='RETIRING_EMAIL_LISTS') - - self.retirement = self._create_retirement(retiring_email_lists) + self.retirement = create_retirement_status(retiring_email_lists) self.test_user = self.retirement.user self.url = reverse('accounts_retire_mailings') @@ -456,7 +460,7 @@ class TestPartnerReportingPut(RetirementTestCase, ModuleStoreTestCase): Checks the simple success case of creating a user, enrolling in a course, and doing the partner report PUT. User should then have the appropriate row in UserRetirementPartnerReportingStatus """ - retirement = self._create_retirement(self.partner_queue_state) + retirement = create_retirement_status(self.partner_queue_state) for course in self.courses: CourseEnrollment.enroll(user=retirement.user, course_key=course.id) @@ -467,7 +471,7 @@ class TestPartnerReportingPut(RetirementTestCase, ModuleStoreTestCase): """ Runs the success test twice to make sure that re-running the step still succeeds. """ - retirement = self._create_retirement(self.partner_queue_state) + retirement = create_retirement_status(self.partner_queue_state) for course in self.courses: CourseEnrollment.enroll(user=retirement.user, course_key=course.id) @@ -475,7 +479,7 @@ class TestPartnerReportingPut(RetirementTestCase, ModuleStoreTestCase): self.put_and_assert_status({'username': retirement.original_username}) # Do our basic other retirement step fakery - fake_retirement(retirement.user) + fake_completed_retirement(retirement.user) # Try running our step again self.put_and_assert_status({'username': retirement.original_username}) @@ -500,7 +504,7 @@ class TestPartnerReportingPut(RetirementTestCase, ModuleStoreTestCase): the enrollment.course.org. We now just use the enrollment.course_id.org since for this purpose we don't care if the course exists. """ - retirement = self._create_retirement(self.partner_queue_state) + retirement = create_retirement_status(self.partner_queue_state) user = retirement.user enrollment = CourseEnrollment.enroll(user=user, course_key=CourseKey.from_string('edX/Test201/2018_Fall')) @@ -717,7 +721,7 @@ class TestAccountRetirementList(RetirementTestCase): Verify that users in dead end states are not returned """ for state in self._get_dead_end_states(): - self._create_retirement(state) + create_retirement_status(state) self.assert_status_and_user_list([], states_to_request=self._get_non_dead_end_states()) def test_users_retrieved_in_multiple_states(self): @@ -726,7 +730,7 @@ class TestAccountRetirementList(RetirementTestCase): """ multiple_states = ['PENDING', 'FORUMS_COMPLETE'] for state in multiple_states: - self._create_retirement(RetirementState.objects.get(state_name=state)) + create_retirement_status(RetirementState.objects.get(state_name=state)) data = {'cool_off_days': 0, 'states': multiple_states} response = self.client.get(self.url, data, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -763,7 +767,7 @@ class TestAccountRetirementList(RetirementTestCase): pending_state = RetirementState.objects.get(state_name='PENDING') for days_back in range(1, days_back_to_test, -1): create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=days_back) - retirements.append(self._create_retirement(state=pending_state, create_datetime=create_datetime)) + retirements.append(create_retirement_status(state=pending_state, create_datetime=create_datetime)) # Confirm we get the correct number and data back for each day we add to cool off days # For each day we add to `cool_off_days` we expect to get one fewer retirement. @@ -886,7 +890,7 @@ class TestAccountRetirementsByStatusAndDate(RetirementTestCase): Verify that users in non-requested states are not returned """ state = RetirementState.objects.get(state_name='PENDING') - self._create_retirement(state=state) + create_retirement_status(state=state) self.assert_status_and_user_list([]) def test_users_exist(self): @@ -917,7 +921,7 @@ class TestAccountRetirementsByStatusAndDate(RetirementTestCase): # Create retirements for the last 10 days for days_back in range(0, 10): create_datetime = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=days_back) - ret = self._create_retirement(state=complete_state, create_datetime=create_datetime) + ret = create_retirement_status(state=complete_state, create_datetime=create_datetime) retirements.append(self._retirement_to_dict(ret)) # Go back in time adding days to the query, assert the correct retirements are present @@ -1010,7 +1014,7 @@ class TestAccountRetirementRetrieve(RetirementTestCase): retirements = [] for state in RetirementState.objects.all(): - retirements.append(self._create_retirement(state)) + retirements.append(create_retirement_status(state)) for retirement in retirements: values = self._retirement_to_dict(retirement) @@ -1021,7 +1025,7 @@ class TestAccountRetirementRetrieve(RetirementTestCase): Simulate retrieving a retirement by the old username, after the name has been changed to the hashed one """ pending_state = RetirementState.objects.get(state_name='PENDING') - retirement = self._create_retirement(pending_state) + retirement = create_retirement_status(pending_state) original_username = retirement.user.username hashed_username = get_retired_username_by_username(original_username) @@ -1044,7 +1048,7 @@ class TestAccountRetirementUpdate(RetirementTestCase): self.pending_state = RetirementState.objects.get(state_name='PENDING') self.locking_state = RetirementState.objects.get(state_name='LOCKING_ACCOUNT') - self.retirement = self._create_retirement(self.pending_state) + self.retirement = create_retirement_status(self.pending_state) self.test_user = self.retirement.user self.test_superuser = SuperuserFactory() self.headers = build_jwt_headers(self.test_superuser) @@ -1354,7 +1358,7 @@ class TestAccountRetirementPost(RetirementTestCase): def test_retire_user_twice_idempotent(self): data = {'username': self.original_username} self.post_and_assert_status(data) - fake_retirement(self.test_user) + fake_completed_retirement(self.test_user) self.post_and_assert_status(data) def test_deletes_pii_from_user_profile(self): @@ -1574,5 +1578,5 @@ class TestLMSAccountRetirementPost(RetirementTestCase, ModuleStoreTestCase): # check that a second call to the retire_misc endpoint will work data = {'username': self.original_username} self.post_and_assert_status(data) - fake_retirement(self.test_user) + fake_completed_retirement(self.test_user) self.post_and_assert_status(data) diff --git a/openedx/core/djangoapps/user_api/management/commands/cancel_user_retirement_request.py b/openedx/core/djangoapps/user_api/management/commands/cancel_user_retirement_request.py new file mode 100644 index 0000000000000000000000000000000000000000..0b7f8f85bcd7bdc7fbee2064d97e9c768225d2ee --- /dev/null +++ b/openedx/core/djangoapps/user_api/management/commands/cancel_user_retirement_request.py @@ -0,0 +1,59 @@ +""" +Use this mgmt command when a user requests retirement mistakenly, then requests +for the retirement request to be cancelled. The command can't cancel a retirement +that has already commenced - only pending retirements. +""" +from __future__ import print_function + +import logging + +from django.core.management.base import BaseCommand, CommandError +from openedx.core.djangoapps.user_api.models import UserRetirementStatus + + +LOGGER = logging.getLogger(__name__) + + +class Command(BaseCommand): + """ + Implementation of the cancel_user_retirement_request command. + """ + help = 'Cancels the retirement of a user who has requested retirement - but has not yet been retired.' + + def add_arguments(self, parser): + parser.add_argument('email_address', + help='Email address of user whose retirement request will be cancelled.') + + def handle(self, *args, **options): + """ + Execute the command. + """ + email_address = options['email_address'].lower() + + try: + # Load the user retirement status. + retirement_status = UserRetirementStatus.objects.select_related('current_state').select_related('user').get( + original_email=email_address + ) + except UserRetirementStatus.DoesNotExist: + raise CommandError("No retirement request with email address '{}' exists.".format(email_address)) + + # Check if the user has started the retirement process -or- not. + if retirement_status.current_state.state_name != 'PENDING': + raise CommandError( + "Retirement requests can only be cancelled for users in the PENDING state." + " Current request state for '{}': {}".format( + email_address, + retirement_status.current_state.state_name + ) + ) + + # Load the user record using the retired email address -and- change the email address back. + retirement_status.user.email = email_address + retirement_status.user.save() + + # Delete the user retirement status record. + # No need to delete the accompanying "permanent" retirement request record - it gets done via Django signal. + retirement_status.delete() + + print("Successfully cancelled retirement request for user with email address '{}'.") diff --git a/openedx/core/djangoapps/user_api/management/tests/test_cancel_retirement.py b/openedx/core/djangoapps/user_api/management/tests/test_cancel_retirement.py new file mode 100644 index 0000000000000000000000000000000000000000..609c8356549a68967f6e01929e2661f977d368c6 --- /dev/null +++ b/openedx/core/djangoapps/user_api/management/tests/test_cancel_retirement.py @@ -0,0 +1,50 @@ +""" +Test the cancel_user_retirement_request management command +""" +import pytest +from django.contrib.auth.models import User +from django.core.management import CommandError, call_command + +from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # pylint: disable=unused-import + logged_out_retirement_request, + setup_retirement_states +) +from openedx.core.djangoapps.user_api.models import RetirementState, UserRetirementRequest, UserRetirementStatus +from student.tests.factories import UserFactory + +pytestmark = pytest.mark.django_db + + +def test_successful_cancellation(setup_retirement_states, logged_out_retirement_request): # pylint: disable=redefined-outer-name, unused-argument + """ + Test a successfully cancelled retirement request. + """ + call_command('cancel_user_retirement_request', logged_out_retirement_request.original_email) + # Confirm that no retirement status exists for the user. + with pytest.raises(UserRetirementStatus.DoesNotExist): + UserRetirementStatus.objects.get(original_email=logged_out_retirement_request.user.email) + # Confirm that no retirement request exists for the user. + with pytest.raises(UserRetirementRequest.DoesNotExist): + UserRetirementRequest.objects.get(user=logged_out_retirement_request.user) + # Ensure user can be retrieved using the original email address. + User.objects.get(email=logged_out_retirement_request.original_email) + + +def test_cancellation_in_unrecoverable_state(setup_retirement_states, logged_out_retirement_request): # pylint: disable=redefined-outer-name, unused-argument + """ + Test a failed cancellation of a retirement request due to the retirement already beginning. + """ + retiring_lms_state = RetirementState.objects.get(state_name='RETIRING_LMS') + logged_out_retirement_request.current_state = retiring_lms_state + logged_out_retirement_request.save() + with pytest.raises(CommandError, match=r'Retirement requests can only be cancelled for users in the PENDING state'): + call_command('cancel_user_retirement_request', logged_out_retirement_request.original_email) + + +def test_cancellation_unknown_email_address(setup_retirement_states, logged_out_retirement_request): # pylint: disable=redefined-outer-name, unused-argument + """ + Test attempting to cancel a non-existent request of a user. + """ + user = UserFactory() + with pytest.raises(CommandError, match=r'No retirement request with email address'): + call_command('cancel_user_retirement_request', user.email) diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py index 0c2576a0e92e88481571b7e68cbcb5dad3fe6a32..d756eaa7f2951029273c79158d22c44f3c9f5ac9 100644 --- a/openedx/core/djangoapps/user_api/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/tests/test_views.py @@ -41,6 +41,7 @@ from ..accounts import ( NAME_MAX_LENGTH, EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, USERNAME_BAD_LENGTH_MSG ) +from ..accounts.tests.retirement_helpers import setup_retirement_states # pylint: disable=unused-import from ..accounts.api import get_account_settings from ..models import UserOrgTag from ..tests.factories import UserPreferenceFactory