move send_activation_email celery task (#28666)
- moved send_activation_email to user_authn app - registered task under both new and old name - exposed the old name for task invocation VAN-417
This commit is contained in:
80
openedx/core/djangoapps/user_authn/tasks.py
Normal file
80
openedx/core/djangoapps/user_authn/tasks.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
This file contains celery tasks for sending email
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
from celery import shared_task
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.contrib.sites.models import Site
|
||||
from edx_ace import ace
|
||||
from edx_ace.errors import RecoverableChannelDeliveryError
|
||||
from edx_ace.message import Message
|
||||
from edx_django_utils.monitoring import set_code_owner_attribute
|
||||
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.lib.celery.task_utils import emulate_http_request
|
||||
|
||||
log = logging.getLogger('edx.celery.task')
|
||||
|
||||
|
||||
@set_code_owner_attribute
|
||||
def _send_activation_email(self, msg_string, from_address=None):
|
||||
"""
|
||||
Sending an activation email to the user.
|
||||
"""
|
||||
msg = Message.from_string(msg_string)
|
||||
|
||||
max_retries = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS
|
||||
retries = self.request.retries
|
||||
|
||||
if from_address is None:
|
||||
from_address = configuration_helpers.get_value('ACTIVATION_EMAIL_FROM_ADDRESS') or (
|
||||
configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
|
||||
)
|
||||
msg.options['from_address'] = from_address
|
||||
|
||||
dest_addr = msg.recipient.email_address
|
||||
|
||||
site = Site.objects.get_current()
|
||||
user = User.objects.get(id=msg.recipient.lms_user_id)
|
||||
|
||||
try:
|
||||
with emulate_http_request(site=site, user=user):
|
||||
ace.send(msg)
|
||||
except RecoverableChannelDeliveryError:
|
||||
log.info('Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format(
|
||||
dest_addr=dest_addr,
|
||||
attempt=retries,
|
||||
max_attempts=max_retries
|
||||
))
|
||||
try:
|
||||
self.retry(countdown=settings.RETRY_ACTIVATION_EMAIL_TIMEOUT, max_retries=max_retries)
|
||||
except MaxRetriesExceededError:
|
||||
log.error(
|
||||
'Unable to send activation email to user from "%s" to "%s"',
|
||||
from_address,
|
||||
dest_addr,
|
||||
exc_info=True
|
||||
)
|
||||
except Exception:
|
||||
log.exception(
|
||||
'Unable to send activation email to user from "%s" to "%s"',
|
||||
from_address,
|
||||
dest_addr,
|
||||
)
|
||||
raise Exception # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
|
||||
_OLD_TASK_NAME = 'common.djangoapps.student.tasks.send_activation_email'
|
||||
_NEW_TASK_NAME = 'openedx.core.djangoapps.user_authn.tasks.send_activation_email'
|
||||
|
||||
|
||||
# Register task under both its old and new names,
|
||||
# but expose only the old-named task for invocation.
|
||||
# -> Next step: Once we deploy and teach Celery workers the new name,
|
||||
# set `send_activation_email` to the new-named task.
|
||||
send_activation_email = shared_task(bind=True, name=_OLD_TASK_NAME)(_send_activation_email)
|
||||
shared_task(bind=True, name=_NEW_TASK_NAME)(_send_activation_email)
|
||||
120
openedx/core/djangoapps/user_authn/tests/test_tasks.py
Normal file
120
openedx/core/djangoapps/user_authn/tests/test_tasks.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Tests for the Sending activation email celery tasks
|
||||
"""
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from edx_ace.errors import ChannelError, RecoverableChannelDeliveryError
|
||||
from unittest import mock
|
||||
|
||||
from common.djangoapps.student.models import Registration
|
||||
from common.djangoapps.student.views.management import compose_activation_email, compose_and_send_activation_email
|
||||
from lms.djangoapps.courseware.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
|
||||
from openedx.core.djangoapps.user_authn.tasks import send_activation_email
|
||||
|
||||
|
||||
class SendActivationEmailTestCase(TestCase):
|
||||
"""
|
||||
Test for send activation email to user
|
||||
"""
|
||||
def setUp(self):
|
||||
""" Setup components used by each test."""
|
||||
super().setUp()
|
||||
self.student = UserFactory()
|
||||
|
||||
registration = Registration()
|
||||
registration.register(self.student)
|
||||
|
||||
self.msg = compose_activation_email(self.student, registration)
|
||||
|
||||
def test_ComposeEmail(self):
|
||||
"""
|
||||
Tests that attributes of the message are being filled correctly in compose_activation_email
|
||||
"""
|
||||
# Check that variables used by the base template are present in generated context
|
||||
assert 'platform_name' in self.msg.context
|
||||
assert 'contact_mailing_address' in self.msg.context
|
||||
# Verify the presence of the activation-email specific attributes
|
||||
assert self.msg.recipient.lms_user_id == self.student.id
|
||||
assert self.msg.recipient.email_address == self.student.email
|
||||
assert self.msg.context['routed_user'] == self.student.username
|
||||
assert self.msg.context['routed_user_email'] == self.student.email
|
||||
assert self.msg.context['routed_profile_name'] == ''
|
||||
|
||||
@mock.patch('time.sleep', mock.Mock(return_value=None))
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.tasks.log')
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.tasks.ace.send', mock.Mock(side_effect=RecoverableChannelDeliveryError(None, None))) # lint-amnesty, pylint: disable=line-too-long
|
||||
def test_RetrySendUntilFail(self, mock_log):
|
||||
"""
|
||||
Tests retries when the activation email doesn't send
|
||||
"""
|
||||
from_address = 'task_testing@example.com'
|
||||
email_max_attempts = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS
|
||||
|
||||
send_activation_email.delay(str(self.msg), from_address=from_address)
|
||||
|
||||
# Asserts sending email retry logging.
|
||||
for attempt in range(email_max_attempts):
|
||||
mock_log.info.assert_any_call(
|
||||
'Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format(
|
||||
dest_addr=self.student.email,
|
||||
attempt=attempt,
|
||||
max_attempts=email_max_attempts
|
||||
))
|
||||
assert mock_log.info.call_count == 6
|
||||
|
||||
# Asserts that the error was logged on crossing max retry attempts.
|
||||
mock_log.error.assert_called_with(
|
||||
'Unable to send activation email to user from "%s" to "%s"',
|
||||
from_address,
|
||||
self.student.email,
|
||||
exc_info=True
|
||||
)
|
||||
assert mock_log.error.call_count == 1
|
||||
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.tasks.log')
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.tasks.ace.send', mock.Mock(side_effect=ChannelError))
|
||||
def test_UnrecoverableSendError(self, mock_log):
|
||||
"""
|
||||
Tests that a major failure of the send is logged
|
||||
"""
|
||||
from_address = 'task_testing@example.com'
|
||||
|
||||
send_activation_email.delay(str(self.msg), from_address=from_address)
|
||||
|
||||
# Asserts that the error was logged
|
||||
mock_log.exception.assert_called_with(
|
||||
'Unable to send activation email to user from "%s" to "%s"',
|
||||
from_address,
|
||||
self.student.email,
|
||||
)
|
||||
|
||||
# Assert that nothing else was logged
|
||||
assert mock_log.info.call_count == 0
|
||||
assert mock_log.error.call_count == 0
|
||||
assert mock_log.exception.call_count == 1
|
||||
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.tasks.log')
|
||||
@mock.patch('openedx.core.djangoapps.user_authn.tasks.ace.send', mock.Mock(side_effect=ChannelError))
|
||||
@mock.patch('common.djangoapps.student.views.management.theming_helpers.get_current_site')
|
||||
@mock.patch('openedx.core.djangoapps.site_configuration.helpers.get_current_site_configuration')
|
||||
def test_from_address_in_send_email(self, mock_site_configuration, mock_get_current_site, mock_log):
|
||||
"""
|
||||
Tests that the "from_address" is pulled from the site configuration.
|
||||
"""
|
||||
site = SiteFactory.create()
|
||||
mock_get_current_site.return_value = site
|
||||
expected_from_email_address = 'test-no-reply@example.com'
|
||||
site_config = SiteConfigurationFactory.create(site=site, site_values={
|
||||
'ACTIVATION_EMAIL_FROM_ADDRESS': expected_from_email_address
|
||||
})
|
||||
mock_site_configuration.return_value = site_config
|
||||
|
||||
compose_and_send_activation_email(self.student, self.student.profile, self.student.registration)
|
||||
mock_log.exception.assert_called_with(
|
||||
'Unable to send activation email to user from "%s" to "%s"',
|
||||
expected_from_email_address,
|
||||
self.student.email,
|
||||
)
|
||||
Reference in New Issue
Block a user