688 lines
28 KiB
Python
688 lines
28 KiB
Python
import json
|
|
import unittest
|
|
from string import capwords
|
|
from unittest.mock import Mock, patch
|
|
|
|
import ddt
|
|
import pytest
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
|
from django.core import mail
|
|
from django.db import transaction
|
|
from django.http import HttpResponse
|
|
from django.test import TransactionTestCase, override_settings
|
|
from django.test.client import RequestFactory
|
|
from django.urls import reverse
|
|
from django.utils.html import escape
|
|
from edx_toggles.toggles.testutils import override_waffle_flag
|
|
|
|
from common.djangoapps.edxmako.shortcuts import marketing_link
|
|
from common.djangoapps.student.email_helpers import generate_proctoring_requirements_email_context
|
|
from common.djangoapps.student.emails import send_proctoring_requirements_email
|
|
from common.djangoapps.student.models import PendingEmailChange, Registration, UserProfile
|
|
from common.djangoapps.student.tests.factories import PendingEmailChangeFactory, UserFactory
|
|
from common.djangoapps.student.views import (
|
|
SETTING_CHANGE_INITIATED,
|
|
confirm_email_change,
|
|
do_email_change_request,
|
|
validate_new_email
|
|
)
|
|
from common.djangoapps.third_party_auth.views import inactive_user_view
|
|
from common.djangoapps.util.testing import EventTestMixin
|
|
from lms.djangoapps.courseware.toggles import COURSEWARE_PROCTORING_IMPROVEMENTS
|
|
from lms.djangoapps.verify_student.services import IDVerificationService
|
|
from openedx.core.djangoapps.ace_common.tests.mixins import EmailTemplateTagMixin
|
|
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
|
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
|
|
from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory
|
|
|
|
|
|
class TestException(Exception):
|
|
"""
|
|
Exception used for testing that nothing will catch explicitly
|
|
"""
|
|
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
|
|
|
|
|
def mock_render_to_string(template_name, context):
|
|
"""
|
|
Return a string that encodes template_name and context
|
|
"""
|
|
return str((template_name, sorted(context.items())))
|
|
|
|
|
|
def mock_render_to_response(template_name, context):
|
|
"""
|
|
Return an HttpResponse with content that encodes template_name and context
|
|
"""
|
|
# This simulates any db access in the templates.
|
|
UserProfile.objects.exists()
|
|
return HttpResponse(mock_render_to_string(template_name, context))
|
|
|
|
|
|
class EmailTestMixin:
|
|
"""
|
|
Adds useful assertions for testing `email_user`
|
|
"""
|
|
|
|
def assertEmailUser(self, email_user, subject_template, subject_context, body_template, body_context):
|
|
"""
|
|
Assert that `email_user` was used to send and email with the supplied subject and body
|
|
|
|
`email_user`: The mock `django.contrib.auth.models.User.email_user` function
|
|
to verify
|
|
`subject_template`: The template to have been used for the subject
|
|
`subject_context`: The context to have been used for the subject
|
|
`body_template`: The template to have been used for the body
|
|
`body_context`: The context to have been used for the body
|
|
"""
|
|
email_user.assert_called_with(
|
|
mock_render_to_string(subject_template, subject_context),
|
|
mock_render_to_string(body_template, body_context),
|
|
configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
|
|
)
|
|
|
|
def append_allowed_hosts(self, hostname):
|
|
"""
|
|
Append hostname to settings.ALLOWED_HOSTS
|
|
"""
|
|
settings.ALLOWED_HOSTS.append(hostname)
|
|
self.addCleanup(settings.ALLOWED_HOSTS.pop)
|
|
|
|
|
|
@ddt.ddt
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
class ActivationEmailTests(EmailTemplateTagMixin, CacheIsolationTestCase):
|
|
"""
|
|
Test sending of the activation email.
|
|
"""
|
|
|
|
ACTIVATION_SUBJECT = f"Action Required: Activate your {settings.PLATFORM_NAME} account"
|
|
|
|
# Text fragments we expect in the body of an email
|
|
# sent from an OpenEdX installation.
|
|
OPENEDX_FRAGMENTS = [
|
|
(
|
|
"Use the link below to activate your account to access engaging, "
|
|
"high-quality {platform_name} courses. Note that you will not be able to log back into your "
|
|
"account until you have activated it.".format(
|
|
platform_name=settings.PLATFORM_NAME
|
|
)
|
|
),
|
|
f"{settings.LMS_ROOT_URL}/activate/",
|
|
"If you need help, please use our web form at ", (
|
|
settings.ACTIVATION_EMAIL_SUPPORT_LINK or settings.SUPPORT_SITE_LINK
|
|
),
|
|
settings.CONTACT_EMAIL,
|
|
"This email message was automatically sent by ",
|
|
settings.LMS_ROOT_URL,
|
|
" because someone attempted to create an account on {platform_name}".format(
|
|
platform_name=settings.PLATFORM_NAME
|
|
),
|
|
" using this email address."
|
|
]
|
|
|
|
@ddt.data('plain_text', 'html')
|
|
def test_activation_email(self, test_body_type):
|
|
self._create_account()
|
|
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.OPENEDX_FRAGMENTS, test_body_type)
|
|
|
|
@with_comprehensive_theme("edx.org")
|
|
@ddt.data('plain_text', 'html')
|
|
def test_activation_email_edx_domain(self, test_body_type):
|
|
self._create_account()
|
|
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.OPENEDX_FRAGMENTS, test_body_type)
|
|
|
|
def _create_account(self):
|
|
"""
|
|
Create an account, triggering the activation email.
|
|
"""
|
|
url = reverse('create_account')
|
|
params = {
|
|
'username': 'test_user',
|
|
'email': 'test_user@example.com',
|
|
'password': 'edx',
|
|
'name': 'Test User',
|
|
'honor_code': True,
|
|
'terms_of_service': True
|
|
}
|
|
resp = self.client.post(url, params)
|
|
assert resp.status_code == 200, "Could not create account (status {status}). The response was {response}"\
|
|
.format(status=resp.status_code, response=resp.content)
|
|
|
|
def _assert_activation_email(self, subject, body_fragments, test_body_type):
|
|
"""
|
|
Verify that the activation email was sent.
|
|
"""
|
|
assert len(mail.outbox) == 1
|
|
msg = mail.outbox[0]
|
|
assert msg.subject == subject
|
|
|
|
body_text = {
|
|
'plain_text': msg.body,
|
|
'html': msg.alternatives[0][0]
|
|
}
|
|
assert test_body_type in body_text
|
|
body_to_be_tested = body_text[test_body_type]
|
|
|
|
for fragment in body_fragments:
|
|
assert fragment in body_to_be_tested
|
|
|
|
def test_do_not_send_email_and_do_activate(self):
|
|
"""
|
|
Tests that when an inactive user logs-in using the social auth,
|
|
an activation email is not sent.
|
|
"""
|
|
pipeline_partial = {
|
|
'kwargs': {
|
|
'social': {
|
|
'uid': 'fake uid'
|
|
}
|
|
}
|
|
}
|
|
user = UserFactory(is_active=False)
|
|
Registration().register(user)
|
|
request = RequestFactory().get(settings.SOCIAL_AUTH_INACTIVE_USER_URL)
|
|
request.user = user
|
|
with patch('common.djangoapps.student.views.management.compose_and_send_activation_email') as email:
|
|
with patch('common.djangoapps.third_party_auth.provider.Registry.get_from_pipeline') as reg:
|
|
with patch('common.djangoapps.third_party_auth.pipeline.get', return_value=pipeline_partial):
|
|
with patch('common.djangoapps.third_party_auth.pipeline.running', return_value=True):
|
|
with patch('common.djangoapps.third_party_auth.is_enabled', return_value=True):
|
|
reg.skip_email_verification = True
|
|
inactive_user_view(request)
|
|
assert user.is_active
|
|
assert email.called is False, 'method should not have been called'
|
|
|
|
@patch('common.djangoapps.student.views.management.compose_activation_email')
|
|
def test_send_email_to_inactive_user(self, email):
|
|
"""
|
|
Tests that when an inactive user logs-in using the social auth, system
|
|
sends an activation email to the user.
|
|
"""
|
|
inactive_user = UserFactory(is_active=False)
|
|
Registration().register(inactive_user)
|
|
request = RequestFactory().get(settings.SOCIAL_AUTH_INACTIVE_USER_URL)
|
|
request.user = inactive_user
|
|
with patch('common.djangoapps.edxmako.request_context.get_current_request', return_value=request):
|
|
with patch('common.djangoapps.third_party_auth.pipeline.running', return_value=False):
|
|
inactive_user_view(request)
|
|
assert email.called is True, 'method should have been called'
|
|
|
|
|
|
@ddt.ddt
|
|
@override_waffle_flag(COURSEWARE_PROCTORING_IMPROVEMENTS, active=True)
|
|
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
|
|
@override_settings(ACCOUNT_MICROFRONTEND_URL='http://account-mfe')
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
|
class ProctoringRequirementsEmailTests(EmailTemplateTagMixin, ModuleStoreTestCase):
|
|
"""
|
|
Test sending of the proctoring requirements email.
|
|
"""
|
|
|
|
# pylint: disable=no-member
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.course = CourseFactory(enable_proctored_exams=True)
|
|
self.user = UserFactory()
|
|
|
|
def test_send_proctoring_requirements_email(self):
|
|
context = generate_proctoring_requirements_email_context(self.user, self.course.id)
|
|
send_proctoring_requirements_email(context)
|
|
self._assert_email()
|
|
|
|
def _assert_email(self):
|
|
"""
|
|
Verify that the email was sent.
|
|
"""
|
|
assert len(mail.outbox) == 1
|
|
|
|
message = mail.outbox[0]
|
|
text = message.body
|
|
html = message.alternatives[0][0]
|
|
|
|
assert message.subject == f"Proctoring requirements for {self.course.display_name}"
|
|
|
|
for fragment in self._get_fragments():
|
|
assert fragment in text
|
|
assert escape(fragment) in html
|
|
|
|
def _get_fragments(self):
|
|
course_module = modulestore().get_course(self.course.id)
|
|
proctoring_provider = capwords(course_module.proctoring_provider.replace('_', ' '))
|
|
id_verification_url = IDVerificationService.get_verify_location()
|
|
return [
|
|
(
|
|
"You are enrolled in {} at {}. This course contains proctored exams.".format(
|
|
self.course.display_name,
|
|
settings.PLATFORM_NAME
|
|
)
|
|
),
|
|
(
|
|
"Proctored exams are timed exams that you take while proctoring software monitors "
|
|
"your computer's desktop, webcam video, and audio."
|
|
),
|
|
proctoring_provider,
|
|
(
|
|
"Carefully review the system requirements as well as the steps to take a proctored "
|
|
"exam in order to ensure that you are prepared."
|
|
),
|
|
settings.PROCTORING_SETTINGS.get('LINK_URLS', {}).get('faq', ''),
|
|
("Before taking a graded proctored exam, you must have approved ID verification photos."),
|
|
id_verification_url
|
|
]
|
|
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
|
class EmailChangeRequestTests(EventTestMixin, EmailTemplateTagMixin, CacheIsolationTestCase):
|
|
"""
|
|
Test changing a user's email address
|
|
"""
|
|
|
|
def setUp(self, tracker='common.djangoapps.student.views.management.tracker'):
|
|
super().setUp(tracker)
|
|
self.user = UserFactory.create()
|
|
self.new_email = 'new.email@edx.org'
|
|
self.req_factory = RequestFactory()
|
|
self.request = self.req_factory.post('unused_url', data={
|
|
'password': 'test',
|
|
'new_email': self.new_email
|
|
})
|
|
self.request.user = self.user
|
|
self.user.email_user = Mock()
|
|
|
|
def do_email_validation(self, email):
|
|
"""
|
|
Executes validate_new_email, returning any resulting error message.
|
|
"""
|
|
try:
|
|
validate_new_email(self.request.user, email)
|
|
except ValueError as err:
|
|
return str(err)
|
|
|
|
def do_email_change(self, user, email, activation_key=None):
|
|
"""
|
|
Executes do_email_change_request, returning any resulting error message.
|
|
"""
|
|
with patch('crum.get_current_request', return_value=self.fake_request):
|
|
do_email_change_request(user, email, activation_key)
|
|
|
|
def assertFailedRequest(self, response_data, expected_error):
|
|
"""
|
|
Assert that `response_data` indicates a failed request that returns `expected_error`
|
|
"""
|
|
assert response_data['success'] is False
|
|
assert expected_error == response_data['error']
|
|
assert self.user.email_user.called is False
|
|
|
|
@patch('common.djangoapps.student.views.management.render_to_string',
|
|
Mock(side_effect=mock_render_to_string, autospec=True)) # lint-amnesty, pylint: disable=line-too-long
|
|
def test_duplicate_activation_key(self):
|
|
"""
|
|
Assert that if two users change Email address simultaneously, no error is thrown
|
|
"""
|
|
|
|
# New emails for the users
|
|
user1_new_email = "valid_user1_email@example.com"
|
|
user2_new_email = "valid_user2_email@example.com"
|
|
|
|
# Create a another user 'user2' & make request for change email
|
|
user2 = UserFactory.create(email=self.new_email, password="test2")
|
|
|
|
# Send requests & ensure no error was thrown
|
|
self.do_email_change(self.user, user1_new_email)
|
|
self.do_email_change(user2, user2_new_email)
|
|
|
|
def test_invalid_emails(self):
|
|
"""
|
|
Assert the expected error message from the email validation method for an invalid
|
|
(improperly formatted) email address.
|
|
"""
|
|
for email in ('bad_email', 'bad_email@', '@bad_email'):
|
|
assert self.do_email_validation(email) == 'Valid e-mail address required.'
|
|
|
|
def test_change_email_to_existing_value(self):
|
|
"""
|
|
Test the error message if user attempts to change email to the existing value.
|
|
"""
|
|
assert self.do_email_validation(self.user.email) == 'Old email is the same as the new email.'
|
|
|
|
@patch('django.core.mail.EmailMultiAlternatives.send')
|
|
def test_email_failure(self, send_mail):
|
|
"""
|
|
Test the return value if sending the email for the user to click fails.
|
|
"""
|
|
send_mail.side_effect = [Exception, None]
|
|
with self.assertRaisesRegex(ValueError, 'Unable to send email activation link. Please try again later.'):
|
|
self.do_email_change(self.user, "valid@email.com")
|
|
|
|
self.assert_no_events_were_emitted()
|
|
|
|
def test_email_success(self):
|
|
"""
|
|
Test email was sent if no errors encountered.
|
|
"""
|
|
old_email = self.user.email
|
|
new_email = "valid@example.com"
|
|
registration_key = "test-registration-key"
|
|
|
|
self.do_email_change(self.user, new_email, registration_key)
|
|
|
|
self._assert_email(
|
|
subject='Request to change édX account e-mail',
|
|
body_fragments=[
|
|
'We received a request to change the e-mail associated with',
|
|
'your édX account from {old_email} to {new_email}.'.format(
|
|
old_email=old_email,
|
|
new_email=new_email,
|
|
),
|
|
'If this is correct, please confirm your new e-mail address by visiting:',
|
|
f'http://edx.org/email_confirm/{registration_key}',
|
|
'Please do not reply to this e-mail; if you require assistance,',
|
|
'check the help section of the édX web site.',
|
|
],
|
|
)
|
|
|
|
self.assert_event_emitted(
|
|
SETTING_CHANGE_INITIATED, user_id=self.user.id, setting='email', old=old_email, new=new_email
|
|
)
|
|
|
|
def _assert_email(self, subject, body_fragments):
|
|
"""
|
|
Verify that the email was sent.
|
|
"""
|
|
assert len(mail.outbox) == 1
|
|
assert len(body_fragments) > 1, 'Should provide at least two body fragments'
|
|
|
|
message = mail.outbox[0]
|
|
text = message.body
|
|
html = message.alternatives[0][0]
|
|
|
|
assert message.subject == subject
|
|
for body in text, html:
|
|
for fragment in body_fragments:
|
|
assert fragment in body
|
|
|
|
|
|
@ddt.ddt
|
|
@patch('common.djangoapps.student.views.management.render_to_response',
|
|
Mock(side_effect=mock_render_to_response, autospec=True)) # lint-amnesty, pylint: disable=line-too-long
|
|
@patch('common.djangoapps.student.views.management.render_to_string',
|
|
Mock(side_effect=mock_render_to_string, autospec=True)) # lint-amnesty, pylint: disable=line-too-long
|
|
class EmailChangeConfirmationTests(EmailTestMixin, EmailTemplateTagMixin, CacheIsolationMixin, TransactionTestCase):
|
|
"""
|
|
Test that confirmation of email change requests function even in the face of exceptions thrown while sending email
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.clear_caches()
|
|
self.addCleanup(self.clear_caches)
|
|
self.user = UserFactory.create()
|
|
self.profile = UserProfile.objects.get(user=self.user)
|
|
self.req_factory = RequestFactory()
|
|
self.request = self.req_factory.get('unused_url')
|
|
self.request.user = self.user
|
|
self.pending_change_request = PendingEmailChangeFactory.create(user=self.user)
|
|
self.key = self.pending_change_request.activation_key
|
|
|
|
# Expected subject of the email
|
|
self.email_subject = "Email Change Confirmation for {platform_name}".format(
|
|
platform_name=settings.PLATFORM_NAME
|
|
)
|
|
|
|
# Text fragments we expect in the body of the confirmation email
|
|
self.email_fragments = [
|
|
"This is to confirm that you changed the e-mail associated with {platform_name}"
|
|
" from {old_email} to {new_email}. If you did not make this request, please contact us immediately."
|
|
" Contact information is listed at:".format(
|
|
platform_name=settings.PLATFORM_NAME,
|
|
old_email=self.user.email,
|
|
new_email=PendingEmailChange.objects.get(activation_key=self.key).new_email
|
|
),
|
|
"We keep a log of old e-mails, so if this request was unintentional, we can investigate."
|
|
]
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.start_cache_isolation()
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.end_cache_isolation()
|
|
super().tearDownClass()
|
|
|
|
def assertRolledBack(self):
|
|
"""
|
|
Assert that no changes to user, profile, or pending email have been made to the db
|
|
"""
|
|
assert self.user.email == User.objects.get(username=self.user.username).email
|
|
assert self.profile.meta == UserProfile.objects.get(user=self.user).meta
|
|
assert PendingEmailChange.objects.count() == 1
|
|
|
|
def assertFailedBeforeEmailing(self):
|
|
"""
|
|
Assert that the function failed before emailing a user
|
|
"""
|
|
self.assertRolledBack()
|
|
assert len(mail.outbox) == 0
|
|
|
|
def check_confirm_email_change(self, expected_template, expected_context):
|
|
"""
|
|
Call `confirm_email_change` and assert that the content was generated as expected
|
|
|
|
`expected_template`: The name of the template that should have been used
|
|
to generate the content
|
|
`expected_context`: The context dictionary that should have been used to
|
|
generate the content
|
|
"""
|
|
response = confirm_email_change(self.request, self.key)
|
|
assert response.status_code == 200
|
|
assert mock_render_to_response(expected_template, expected_context).content.decode('utf-8') \
|
|
== response.content.decode('utf-8')
|
|
|
|
def assertChangeEmailSent(self, test_body_type):
|
|
"""
|
|
Assert that the correct email was sent to confirm an email change, the same
|
|
email contents should be sent to both old and new addresses
|
|
"""
|
|
self.check_confirm_email_change('email_change_successful.html', {
|
|
'old_email': self.user.email,
|
|
'new_email': self.pending_change_request.new_email
|
|
})
|
|
|
|
# Must have two items in outbox: one for old email, another for new email
|
|
assert len(mail.outbox) == 2
|
|
|
|
use_https = self.request.is_secure()
|
|
if settings.FEATURES['ENABLE_MKTG_SITE']:
|
|
contact_link = marketing_link('CONTACT')
|
|
else:
|
|
contact_link = '{protocol}://{site}{link}'.format(
|
|
protocol='https' if use_https else 'http',
|
|
site=settings.SITE_NAME,
|
|
link=reverse('contact'),
|
|
)
|
|
|
|
# Verifying contents
|
|
for msg in mail.outbox:
|
|
assert msg.subject == self.email_subject
|
|
|
|
body_text = {
|
|
'plain_text': msg.body,
|
|
'html': msg.alternatives[0][0]
|
|
}
|
|
assert test_body_type in body_text
|
|
|
|
body_to_be_tested = body_text[test_body_type]
|
|
for fragment in self.email_fragments:
|
|
assert fragment in body_to_be_tested
|
|
|
|
assert contact_link in body_to_be_tested
|
|
|
|
def test_not_pending(self):
|
|
self.key = 'not_a_key'
|
|
self.check_confirm_email_change('invalid_email_key.html', {})
|
|
self.assertFailedBeforeEmailing()
|
|
|
|
def test_duplicate_email(self):
|
|
UserFactory.create(email=self.pending_change_request.new_email)
|
|
self.check_confirm_email_change('email_exists.html', {})
|
|
self.assertFailedBeforeEmailing()
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
|
@patch('common.djangoapps.student.views.management.ace')
|
|
def test_old_email_fails(self, ace_mail):
|
|
ace_mail.send.side_effect = [Exception, None]
|
|
self.check_confirm_email_change('email_change_failed.html', {
|
|
'email': self.user.email,
|
|
})
|
|
assert ace_mail.send.call_count == 1
|
|
self.assertRolledBack()
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
|
@patch('common.djangoapps.student.views.management.ace')
|
|
def test_new_email_fails(self, ace_mail):
|
|
ace_mail.send.side_effect = [None, Exception]
|
|
self.check_confirm_email_change('email_change_failed.html', {
|
|
'email': self.pending_change_request.new_email
|
|
})
|
|
assert ace_mail.send.call_count == 2
|
|
self.assertRolledBack()
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
|
@override_settings(MKTG_URLS={'ROOT': 'https://dummy-root', 'CONTACT': '/help/contact-us'})
|
|
@ddt.data(
|
|
('plain_text', False),
|
|
('plain_text', True),
|
|
('html', False),
|
|
('html', True)
|
|
)
|
|
@ddt.unpack
|
|
def test_successful_email_change(self, test_body_type, test_marketing_enabled):
|
|
with patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': test_marketing_enabled}):
|
|
self.assertChangeEmailSent(test_body_type)
|
|
|
|
meta = json.loads(UserProfile.objects.get(user=self.user).meta)
|
|
assert 'old_emails' in meta
|
|
assert self.user.email == meta['old_emails'][0][0]
|
|
assert self.pending_change_request.new_email == User.objects.get(username=self.user.username).email
|
|
assert PendingEmailChange.objects.count() == 0
|
|
|
|
@patch('common.djangoapps.student.views.PendingEmailChange.objects.get', Mock(side_effect=TestException))
|
|
def test_always_rollback(self):
|
|
connection = transaction.get_connection()
|
|
with patch.object(connection, 'rollback', wraps=connection.rollback) as mock_rollback:
|
|
with pytest.raises(TestException):
|
|
confirm_email_change(self.request, self.key)
|
|
|
|
mock_rollback.assert_called_with()
|
|
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
|
|
class SecondaryEmailChangeRequestTests(EventTestMixin, EmailTemplateTagMixin, CacheIsolationTestCase):
|
|
"""
|
|
Test changing a user's email address
|
|
"""
|
|
|
|
def setUp(self, tracker='common.djangoapps.student.views.management.tracker'):
|
|
super().setUp(tracker)
|
|
self.user = UserFactory.create()
|
|
self.new_secondary_email = 'new.secondary.email@edx.org'
|
|
self.req_factory = RequestFactory()
|
|
self.request = self.req_factory.post('unused_url', data={
|
|
'password': 'test',
|
|
'new_email': self.new_secondary_email
|
|
})
|
|
self.request.user = self.user
|
|
self.user.email_user = Mock()
|
|
|
|
def do_email_validation(self, email):
|
|
"""
|
|
Executes validate_new_secondary_email, returning any resulting error message.
|
|
"""
|
|
try:
|
|
validate_new_email(self.request.user, email)
|
|
except ValueError as err:
|
|
return str(err)
|
|
|
|
def do_secondary_email_change(self, user, email, activation_key=None):
|
|
"""
|
|
Executes do_secondary_email_change_request, returning any resulting error message.
|
|
"""
|
|
with patch('crum.get_current_request', return_value=self.fake_request):
|
|
do_email_change_request(
|
|
user=user,
|
|
new_email=email,
|
|
activation_key=activation_key,
|
|
secondary_email_change_request=True
|
|
)
|
|
|
|
def assertFailedRequest(self, response_data, expected_error):
|
|
"""
|
|
Assert that `response_data` indicates a failed request that returns `expected_error`
|
|
"""
|
|
assert not response_data['success']
|
|
assert expected_error == response_data['error']
|
|
assert not self.user.email_user.called
|
|
|
|
def test_invalid_emails(self):
|
|
"""
|
|
Assert the expected error message from the email validation method for an invalid
|
|
(improperly formatted) email address.
|
|
"""
|
|
for email in ('bad_email', 'bad_email@', '@bad_email'):
|
|
assert self.do_email_validation(email) == 'Valid e-mail address required.'
|
|
|
|
@patch('django.core.mail.EmailMultiAlternatives.send')
|
|
def test_email_failure(self, send_mail):
|
|
"""
|
|
Test the return value if sending the email for the user to click fails.
|
|
"""
|
|
send_mail.side_effect = [Exception, None]
|
|
with self.assertRaisesRegex(ValueError, 'Unable to send email activation link. Please try again later.'):
|
|
self.do_secondary_email_change(self.user, "valid@email.com")
|
|
|
|
self.assert_no_events_were_emitted()
|
|
|
|
def test_email_success(self):
|
|
"""
|
|
Test email was sent if no errors encountered.
|
|
"""
|
|
new_email = "valid@example.com"
|
|
registration_key = "test-registration-key"
|
|
|
|
self.do_secondary_email_change(self.user, new_email, registration_key)
|
|
|
|
self._assert_email(
|
|
subject='Confirm your recovery email for édX',
|
|
body_fragments=[
|
|
'You\'ve registered this recovery email address for édX.',
|
|
'If you set this email address, click "confirm email."',
|
|
'If you didn\'t request this change, you can disregard this email.',
|
|
f'http://edx.org/activate_secondary_email/{registration_key}',
|
|
|
|
],
|
|
)
|
|
|
|
def _assert_email(self, subject, body_fragments):
|
|
"""
|
|
Verify that the email was sent.
|
|
"""
|
|
assert len(mail.outbox) == 1
|
|
assert len(body_fragments) > 1, 'Should provide at least two body fragments'
|
|
|
|
message = mail.outbox[0]
|
|
text = message.body
|
|
html = message.alternatives[0][0]
|
|
|
|
assert message.subject == subject
|
|
|
|
for fragment in body_fragments:
|
|
assert fragment in text
|
|
assert escape(fragment) in html
|