text: """
  Tests the execution of forum notification tasks.
  """
  
  
  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.site_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
          ):
              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,
                  )
  