Files
edx-platform/lms/djangoapps/discussion/tests/test_tasks.py
2019-05-21 11:44:00 -04:00

334 lines
14 KiB
Python

"""
Tests the execution of forum notification tasks.
"""
from __future__ import absolute_import
import json
import math
from datetime import datetime, timedelta
import ddt
import mock
import six
from django.contrib.sites.models import Site
from edx_ace.channel import ChannelType, get_channel_for_message
from edx_ace.recipient import Recipient
from edx_ace.renderers import EmailRenderer
from edx_ace.utils import date
import openedx.core.djangoapps.django_comment_common.comment_client as cc
from lms.djangoapps.discussion.signals.handlers import ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY
from lms.djangoapps.discussion.tasks import _should_send_message, _track_notification_sent
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.django_comment_common.models import ForumsConfig
from openedx.core.djangoapps.django_comment_common.signals import comment_created
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from openedx.core.lib.celery.task_utils import emulate_http_request
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
NOW = datetime.utcnow()
ONE_HOUR_AGO = NOW - timedelta(hours=1)
TWO_HOURS_AGO = NOW - timedelta(hours=2)
def make_mock_responder(subscribed_thread_ids=None, thread_data=None, comment_data=None, per_page=1):
def mock_subscribed_threads(method, url, **kwargs):
subscribed_thread_collection = [
{'id': thread_id} for thread_id in subscribed_thread_ids
]
page = kwargs.get('params', {}).get('page', 1)
start_index = per_page * (page - 1)
end_index = per_page * page
data = {
'collection': subscribed_thread_collection[start_index: end_index],
'page': page,
'num_pages': int(math.ceil(len(subscribed_thread_collection) / float(per_page))),
'thread_count': len(subscribed_thread_collection)
}
return mock.Mock(status_code=200, text=json.dumps(data), json=mock.Mock(return_value=data))
def mock_comment_find(method, url, **kwargs):
return mock.Mock(status_code=200, text=json.dumps(comment_data), json=mock.Mock(return_value=comment_data))
def mock_thread_find(method, url, **kwargs):
return mock.Mock(status_code=200, text=json.dumps(thread_data), json=mock.Mock(return_value=thread_data))
def mock_request(method, url, **kwargs):
if '/subscribed_threads' in url:
return mock_subscribed_threads(method, url, **kwargs)
if '/comments' in url:
return mock_comment_find(method, url, **kwargs)
if '/threads' in url:
return mock_thread_find(method, url, **kwargs)
return mock_request
@ddt.ddt
class TaskTestCase(ModuleStoreTestCase):
@classmethod
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUpClass(cls):
super(TaskTestCase, cls).setUpClass()
cls.discussion_id = 'dummy_discussion_id'
cls.course = CourseOverviewFactory.create(language='fr')
# Patch the comment client user save method so it does not try
# to create a new cc user when creating a django user
with mock.patch('student.models.cc.User.save'):
cls.thread_author = UserFactory(
username='thread_author',
password='password',
email='email'
)
cls.comment_author = UserFactory(
username='comment_author',
password='password',
email='email'
)
CourseEnrollmentFactory(
user=cls.thread_author,
course_id=cls.course.id
)
CourseEnrollmentFactory(
user=cls.comment_author,
course_id=cls.course.id
)
config = ForumsConfig.current()
config.enabled = True
config.save()
cls.create_thread_and_comments()
@classmethod
def create_thread_and_comments(cls):
cls.thread = {
'id': cls.discussion_id,
'course_id': six.text_type(cls.course.id),
'created_at': date.serialize(TWO_HOURS_AGO),
'title': 'thread-title',
'user_id': cls.thread_author.id,
'username': cls.thread_author.username,
'commentable_id': 'thread-commentable-id',
}
cls.comment = {
'id': 'comment',
'body': 'comment-body',
'created_at': date.serialize(ONE_HOUR_AGO),
'thread_id': cls.thread['id'],
'parent_id': None,
'user_id': cls.comment_author.id,
'username': cls.comment_author.username,
}
cls.comment2 = {
'id': 'comment2',
'body': 'comment2-body',
'created_at': date.serialize(NOW),
'thread_id': cls.thread['id'],
'parent_id': None,
'user_id': cls.comment_author.id,
'username': cls.comment_author.username
}
cls.subcomment = {
'id': 'subcomment',
'body': 'subcomment-body',
'created_at': date.serialize(NOW),
'thread_id': cls.thread['id'],
'parent_id': cls.comment['id'],
'user_id': cls.comment_author.id,
'username': cls.comment_author.username,
}
cls.thread['children'] = [cls.comment, cls.comment2]
cls.comment['child_count'] = 1
cls.thread2 = {
'id': cls.discussion_id,
'course_id': six.text_type(cls.course.id),
'created_at': date.serialize(TWO_HOURS_AGO),
'title': 'thread-title',
'user_id': cls.thread_author.id,
'username': cls.thread_author.username,
'commentable_id': 'thread-commentable-id-2',
}
def setUp(self):
super(TaskTestCase, self).setUp()
self.request_patcher = mock.patch('requests.request')
self.mock_request = self.request_patcher.start()
self.ace_send_patcher = mock.patch('edx_ace.ace.send')
self.mock_ace_send = self.ace_send_patcher.start()
thread_permalink = '/courses/discussion/dummy_discussion_id'
self.permalink_patcher = mock.patch('lms.djangoapps.discussion.tasks.permalink', return_value=thread_permalink)
self.mock_permalink = self.permalink_patcher.start()
def tearDown(self):
super(TaskTestCase, self).tearDown()
self.request_patcher.stop()
self.ace_send_patcher.stop()
self.permalink_patcher.stop()
@ddt.data(True, False)
def test_send_discussion_email_notification(self, user_subscribed):
if user_subscribed:
non_matching_id = 'not-a-match'
# with per_page left with a default value of 1, this ensures
# that we test a multiple page result when calling
# comment_client.User.subscribed_threads()
subscribed_thread_ids = [non_matching_id, self.discussion_id]
else:
subscribed_thread_ids = []
self.mock_request.side_effect = make_mock_responder(
subscribed_thread_ids=subscribed_thread_ids,
comment_data=self.comment,
thread_data=self.thread,
)
user = mock.Mock()
comment = cc.Comment.find(id=self.comment['id']).retrieve()
site = Site.objects.get_current()
site_config = SiteConfigurationFactory.create(site=site)
site_config.values[ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY] = True
site_config.save()
with mock.patch('lms.djangoapps.discussion.signals.handlers.get_current_site', return_value=site):
comment_created.send(sender=None, user=user, post=comment)
if user_subscribed:
expected_message_context = get_base_template_context(site)
expected_message_context.update({
'comment_author_id': self.comment_author.id,
'comment_body': self.comment['body'],
'comment_created_at': ONE_HOUR_AGO,
'comment_id': self.comment['id'],
'comment_username': self.comment_author.username,
'course_id': self.course.id,
'thread_author_id': self.thread_author.id,
'thread_created_at': TWO_HOURS_AGO,
'thread_id': self.discussion_id,
'thread_title': 'thread-title',
'thread_username': self.thread_author.username,
'thread_commentable_id': self.thread['commentable_id'],
'post_link': self.mock_permalink.return_value,
'site': site,
'site_id': site.id
})
expected_recipient = Recipient(self.thread_author.username, self.thread_author.email)
actual_message = self.mock_ace_send.call_args_list[0][0][0]
self.assertEqual(expected_message_context, actual_message.context)
self.assertEqual(expected_recipient, actual_message.recipient)
self.assertEqual(self.course.language, actual_message.language)
self._assert_rendered_email(actual_message)
else:
self.assertFalse(self.mock_ace_send.called)
def _assert_rendered_email(self, message):
# check that we can actually render the message
with emulate_http_request(
site=message.context['site'], user=self.thread_author
):
# pylint: disable=unsupported-membership-test
rendered_email = EmailRenderer().render(get_channel_for_message(ChannelType.EMAIL, message), message)
assert self.comment['body'] in rendered_email.body_html
assert self.comment_author.username in rendered_email.body_html
assert self.mock_permalink.return_value in rendered_email.body_html
assert message.context['site'].domain in rendered_email.body_html
def run_should_not_send_email_test(self, thread, comment_dict):
"""
assert email is not sent
"""
self.mock_request.side_effect = make_mock_responder(
subscribed_thread_ids=[self.discussion_id],
comment_data=comment_dict,
thread_data=thread,
)
user = mock.Mock()
comment = cc.Comment.find(id=comment_dict['id']).retrieve()
comment_created.send(sender=None, user=user, post=comment)
actual_result = _should_send_message({
'thread_author_id': self.thread_author.id,
'course_id': self.course.id,
'comment_id': comment_dict['id'],
'thread_id': thread['id'],
})
self.assertEqual(actual_result, False)
self.assertFalse(self.mock_ace_send.called)
def test_subcomment_should_not_send_email(self):
self.run_should_not_send_email_test(self.thread, self.subcomment)
def test_second_comment_should_not_send_email(self):
self.run_should_not_send_email_test(self.thread, self.comment2)
def test_thread_without_children_should_not_send_email(self):
"""
test that email notification will not be sent for the thread
that doesn't have attribute 'children'
"""
self.run_should_not_send_email_test(self.thread2, self.comment)
@ddt.data((
{
'thread_id': 'dummy_discussion_id',
'thread_title': 'thread-title',
'thread_created_at': date.serialize(datetime(2000, 1, 1, 0, 0, 0)),
'course_id': 'fake_course_edx',
'thread_author_id': 'a_fake_dude'
},
{
'app_label': 'discussion',
'name': 'responsenotification',
'language': 'en',
'uuid': 'uuid1',
'send_uuid': 'uuid2',
'thread_id': 'dummy_discussion_id',
'course_id': 'fake_course_edx',
'thread_created_at': datetime(2000, 1, 1, 0, 0, 0)
}
), (
{
'thread_id': 'dummy_discussion_id2',
'thread_title': 'thread-title2',
'thread_created_at': date.serialize(datetime(2000, 1, 1, 0, 0, 0)),
'course_id': 'fake_course_edx2',
'thread_author_id': 'a_fake_dude2'
},
{
'app_label': 'discussion',
'name': 'responsenotification',
'language': 'en',
'uuid': 'uuid3',
'send_uuid': 'uuid4',
'thread_id': 'dummy_discussion_id2',
'course_id': 'fake_course_edx2',
'thread_created_at': datetime(2000, 1, 1, 0, 0, 0)
}
))
@ddt.unpack
def test_track_notification_sent(self, context, test_props):
with mock.patch('edx_ace.ace.send').start() as message:
# Populate mock message (
# There are some cruft attrs, but they're harmless.
for key, entry in test_props.items():
setattr(message, key, entry)
test_props['nonInteraction'] = True
# Also augment context with site object, for setting segment context.
site = Site.objects.get_current()
context['site'] = site
with mock.patch('lms.djangoapps.discussion.tasks.segment.track') as mock_segment_track:
_track_notification_sent(message, context)
mock_segment_track.assert_called_once_with(
user_id=context['thread_author_id'],
event_name='edx.bi.email.sent',
properties=test_props,
)