|
35 | 35 | from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories |
36 | 36 | from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers |
37 | 37 | from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH |
38 | | -from openedx.core.djangoapps.user_api.models import UserRetirementRequest |
| 38 | +from openedx.core.djangoapps.user_api.accounts.utils import create_retirement_request_and_deactivate_account |
| 39 | +from openedx.core.djangoapps.user_api.models import RetirementState |
39 | 40 | from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase |
40 | 41 | from openedx.core.djangoapps.user_authn.views.password_reset import ( |
41 | 42 | PASSWORD_RESET_INITIATED, |
@@ -79,6 +80,14 @@ def setUp(self): # pylint: disable=arguments-differ |
79 | 80 | self.user_bad_passwd.password = UNUSABLE_PASSWORD_PREFIX |
80 | 81 | self.user_bad_passwd.save() |
81 | 82 |
|
| 83 | + # Create PENDING retirement state for tests that need it |
| 84 | + RetirementState.objects.create( |
| 85 | + state_name='PENDING', |
| 86 | + state_execution_order=1, |
| 87 | + is_dead_end_state=False, |
| 88 | + required=True, |
| 89 | + ) |
| 90 | + |
82 | 91 | def setup_request_session_with_token(self, request): |
83 | 92 | """ |
84 | 93 | Internal helper to setup request session and add token in session. |
@@ -318,6 +327,7 @@ def test_reset_password_email(self, body_type, expected_output): |
318 | 327 | obj = json.loads(good_resp.content.decode('utf-8')) |
319 | 328 | assert obj['success'] |
320 | 329 | assert 'e-mailed you instructions for setting your password' in obj['value'] |
| 330 | + assert len(mail.outbox) > 0 |
321 | 331 |
|
322 | 332 | from_email = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) |
323 | 333 | sent_message = mail.outbox[0] |
@@ -536,23 +546,54 @@ def test_password_reset_fail(self): |
536 | 546 | assert resp.status_code == 200 |
537 | 547 | assert not User.objects.get(pk=self.user.pk).is_active |
538 | 548 |
|
539 | | - def test_password_reset_retired_user_fail(self): |
| 549 | + def test_password_reset_initiation_fails_for_retired_user(self): |
540 | 550 | """ |
541 | | - Tests that if a retired user attempts to reset their password, it fails. |
| 551 | + Tests that a retired user cannot initiate a password reset. |
542 | 552 | """ |
| 553 | + create_retirement_request_and_deactivate_account(self.user) |
| 554 | + self.user.refresh_from_db() |
543 | 555 | assert not self.user.is_active |
| 556 | + assert not self.user.has_usable_password() |
| 557 | + |
| 558 | + reset_request = self.request_factory.post('/password_reset/', {'email': self.user.email}) |
| 559 | + reset_request.user = AnonymousUser() |
| 560 | + response = password_reset(reset_request) |
544 | 561 |
|
545 | | - # Retire the user. |
546 | | - UserRetirementRequest.create_retirement_request(self.user) |
| 562 | + # Always return 200 OK to prevent user enumeration while leaving the password unchanged and unusable. |
| 563 | + assert response.status_code == 200 |
| 564 | + response_data = json.loads(response.content.decode('utf-8')) |
| 565 | + assert response_data['success'] is True |
| 566 | + assert len(mail.outbox) == 0 |
| 567 | + self.user.refresh_from_db() |
| 568 | + assert not self.user.is_active |
547 | 569 |
|
548 | | - reset_req = self.request_factory.get(self.password_reset_confirm_url) |
549 | | - reset_req.user = self.user |
550 | | - resp = PasswordResetConfirmWrapper.as_view()(reset_req, uidb36=self.uidb36, token=self.token) |
| 570 | + def test_password_reset_completion_fails_for_retired_user(self): |
| 571 | + """ |
| 572 | + Tests that password reset completion fails if retirement happens after reset initiation. |
551 | 573 |
|
552 | | - # Verify the response status code is: 200 with password reset fail and also verify that |
553 | | - # the user is not marked as active. |
554 | | - assert resp.status_code == 200 |
555 | | - assert not User.objects.get(pk=self.user.pk).is_active |
| 574 | + This simulates a user who initiated password reset before retirement |
| 575 | + and then attempts to submit a completed reset form after retirement. |
| 576 | + """ |
| 577 | + # Retire the user after they have initiated a reset (using the token set up in setUp). |
| 578 | + create_retirement_request_and_deactivate_account(self.user) |
| 579 | + self.user.refresh_from_db() |
| 580 | + assert not self.user.is_active |
| 581 | + assert not self.user.has_usable_password() |
| 582 | + old_password_hash = self.user.password |
| 583 | + |
| 584 | + request_params = {'new_password1': 'new_password1', 'new_password2': 'new_password1'} |
| 585 | + confirm_request = self.request_factory.post(self.password_reset_confirm_url, data=request_params) |
| 586 | + self.setup_request_session_with_token(confirm_request) |
| 587 | + confirm_request.user = self.user |
| 588 | + |
| 589 | + response = PasswordResetConfirmWrapper.as_view()(confirm_request, uidb36=self.uidb36, token=self.token) |
| 590 | + |
| 591 | + # Always return 200 OK to prevent user enumeration while leaving the password unchanged and unusable. |
| 592 | + assert response.status_code == 200 |
| 593 | + self.user.refresh_from_db() |
| 594 | + assert not self.user.is_active |
| 595 | + assert not self.user.has_usable_password() |
| 596 | + assert self.user.password == old_password_hash |
556 | 597 |
|
557 | 598 | def test_password_reset_normalize_password(self): |
558 | 599 | # pylint: disable=anomalous-unicode-escape-in-string |
|
0 commit comments