test_email.py 14.2 KB
Newer Older
1
# -*- coding: utf-8 -*-
2 3 4
"""
Unit tests for sending course email
"""
5
import json
6 7 8
from mock import patch, Mock
import os
from unittest import skipIf
9

10 11
from django.conf import settings
from django.core import mail
12
from django.core.urlresolvers import reverse
13 14
from django.core.management import call_command
from django.test.utils import override_settings
15

16
from bulk_email.models import Optout
17
from courseware.tests.factories import StaffFactory, InstructorFactory
18
from instructor_task.subtasks import update_subtask_status
19 20
from student.roles import CourseStaffRole
from student.models import CourseEnrollment
21 22 23
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
24

25 26
STAFF_COUNT = 3
STUDENT_COUNT = 10
27 28 29 30 31 32 33 34 35 36
LARGE_NUM_EMAILS = 137


class MockCourseEmailResult(object):
    """
    A small closure-like class to keep count of emails sent over all tasks, recorded
    by mock object side effects
    """
    emails_sent = 0

37
    def get_mock_update_subtask_status(self):
38
        """Wrapper for mock email function."""
39
        def mock_update_subtask_status(entry_id, current_task_id, new_subtask_status):  # pylint: disable=unused-argument
40
            """Increments count of number of emails sent."""
41 42 43
            self.emails_sent += new_subtask_status.succeeded
            return update_subtask_status(entry_id, current_task_id, new_subtask_status)
        return mock_update_subtask_status
44 45


Christine Lytwynec committed
46
class EmailSendFromDashboardTestCase(ModuleStoreTestCase):
47 48 49 50
    """
    Test that emails send correctly.
    """

51
    @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
52
    def setUp(self):
53
        super(EmailSendFromDashboardTestCase, self).setUp()
54 55
        course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ"
        self.course = CourseFactory.create(display_name=course_title)
56

57
        self.instructor = InstructorFactory(course_key=self.course.id)
58

59
        # Create staff
60
        self.staff = [StaffFactory(course_key=self.course.id)
61
                      for _ in xrange(STAFF_COUNT)]
62

63
        # Create students
64 65 66 67
        self.students = [UserFactory() for _ in xrange(STUDENT_COUNT)]
        for student in self.students:
            CourseEnrollmentFactory.create(user=student, course_id=self.course.id)

68 69 70
        # load initial content (since we don't run migrations as part of tests):
        call_command("loaddata", "course_email_template.json")

71 72
        self.client.login(username=self.instructor.username, password="test")

73
        # Pull up email view on instructor dashboard
74
        self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
75 76
        # Response loads the whole instructor dashboard, so no need to explicitly
        # navigate to a particular email section
77
        response = self.client.get(self.url)
78
        email_section = '<div class="vert-left send-email" id="section-send-email">'
79
        # If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
80
        self.assertTrue(email_section in response.content)
Calen Pennington committed
81
        self.send_mail_url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
82
        self.success_content = {
Calen Pennington committed
83
            'course_id': self.course.id.to_deprecated_string(),
84 85
            'success': True,
        }
86

Christine Lytwynec committed
87 88 89 90 91 92 93

@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message'))
class TestEmailSendFromDashboardMockedHtmlToText(EmailSendFromDashboardTestCase):
    """
    Tests email sending with mocked html_to_text.
    """
94 95 96 97 98 99 100
    @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
    def test_email_disabled(self):
        """
        Test response when email is disabled for course.
        """
        test_email = {
            'action': 'Send email',
101
            'send_to': 'myself',
102 103 104
            'subject': 'test subject for myself',
            'message': 'test message for myself'
        }
105 106 107
        response = self.client.post(self.send_mail_url, test_email)
        # We should get back a HttpResponseForbidden (status code 403)
        self.assertContains(response, "Email is not enabled for this course.", status_code=403)
108

109
    @patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message'))
110 111 112 113
    def test_send_to_self(self):
        """
        Make sure email send to myself goes to myself.
        """
114 115 116
        # Now we know we have pulled up the instructor dash's email view
        # (in the setUp method), we can test sending an email.
        test_email = {
117 118
            'action': 'send',
            'send_to': 'myself',
119 120 121
            'subject': 'test subject for myself',
            'message': 'test message for myself'
        }
122 123 124
        # Post the email to the instructor dashboard API
        response = self.client.post(self.send_mail_url, test_email)
        self.assertEquals(json.loads(response.content), self.success_content)
125

126
        # Check that outbox is as expected
127 128 129
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(len(mail.outbox[0].to), 1)
        self.assertEquals(mail.outbox[0].to[0], self.instructor.email)
130 131 132 133
        self.assertEquals(
            mail.outbox[0].subject,
            '[' + self.course.display_name + ']' + ' test subject for myself'
        )
134 135 136 137 138

    def test_send_to_staff(self):
        """
        Make sure email send to staff and instructors goes there.
        """
139 140 141 142
        # Now we know we have pulled up the instructor dash's email view
        # (in the setUp method), we can test sending an email.
        test_email = {
            'action': 'Send email',
143
            'send_to': 'staff',
144 145 146
            'subject': 'test subject for staff',
            'message': 'test message for subject'
        }
147 148 149
        # Post the email to the instructor dashboard API
        response = self.client.post(self.send_mail_url, test_email)
        self.assertEquals(json.loads(response.content), self.success_content)
150

151
        # the 1 is for the instructor in this test and others
152
        self.assertEquals(len(mail.outbox), 1 + len(self.staff))
153 154 155 156
        self.assertItemsEqual(
            [e.to[0] for e in mail.outbox],
            [self.instructor.email] + [s.email for s in self.staff]
        )
157 158 159 160 161

    def test_send_to_all(self):
        """
        Make sure email send to all goes there.
        """
162 163 164 165 166
        # Now we know we have pulled up the instructor dash's email view
        # (in the setUp method), we can test sending an email.

        test_email = {
            'action': 'Send email',
167
            'send_to': 'all',
168 169 170
            'subject': 'test subject for all',
            'message': 'test message for all'
        }
171 172 173
        # Post the email to the instructor dashboard API
        response = self.client.post(self.send_mail_url, test_email)
        self.assertEquals(json.loads(response.content), self.success_content)
174

175
        # the 1 is for the instructor
176
        self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students))
177 178 179 180 181
        self.assertItemsEqual(
            [e.to[0] for e in mail.outbox],
            [self.instructor.email] + [s.email for s in self.staff] + [s.email for s in self.students]
        )

182 183 184 185 186 187 188 189 190 191
    def test_no_duplicate_emails_staff_instructor(self):
        """
        Test that no duplicate emails are sent to a course instructor that is
        also course staff
        """
        CourseStaffRole(self.course.id).add_users(self.instructor)
        self.test_send_to_all()

    def test_no_duplicate_emails_enrolled_staff(self):
        """
192
        Test that no duplicate emails are sent to a course instructor that is
193 194 195 196 197
        also enrolled in the course
        """
        CourseEnrollment.enroll(self.instructor, self.course.id)
        self.test_send_to_all()

198 199 200 201 202 203 204 205 206 207 208 209 210
    def test_no_duplicate_emails_unenrolled_staff(self):
        """
        Test that no duplicate emails are sent to a course staff that is
        not enrolled in the course, but is enrolled in other courses
        """
        course_1 = CourseFactory.create()
        course_2 = CourseFactory.create()
        # make sure self.instructor isn't enrolled in the course
        self.assertFalse(CourseEnrollment.is_enrolled(self.instructor, self.course.id))
        CourseEnrollment.enroll(self.instructor, course_1.id)
        CourseEnrollment.enroll(self.instructor, course_2.id)
        self.test_send_to_all()

211 212 213 214 215 216 217
    def test_unicode_subject_send_to_all(self):
        """
        Make sure email (with Unicode characters) send to all goes there.
        """
        # Now we know we have pulled up the instructor dash's email view
        # (in the setUp method), we can test sending an email.

218
        uni_subject = u'téśt śúbjéćt főŕ áĺĺ'
219 220
        test_email = {
            'action': 'Send email',
221
            'send_to': 'all',
222
            'subject': uni_subject,
223 224
            'message': 'test message for all'
        }
225 226 227
        # Post the email to the instructor dashboard API
        response = self.client.post(self.send_mail_url, test_email)
        self.assertEquals(json.loads(response.content), self.success_content)
228 229 230 231 232 233 234 235

        self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students))
        self.assertItemsEqual(
            [e.to[0] for e in mail.outbox],
            [self.instructor.email] + [s.email for s in self.staff] + [s.email for s in self.students]
        )
        self.assertEquals(
            mail.outbox[0].subject,
236
            '[' + self.course.display_name + '] ' + uni_subject
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
        )

    def test_unicode_students_send_to_all(self):
        """
        Make sure email (with Unicode characters) send to all goes there.
        """
        # Now we know we have pulled up the instructor dash's email view
        # (in the setUp method), we can test sending an email.

        # Create a student with Unicode in their first & last names
        unicode_user = UserFactory(first_name=u'Ⓡⓞⓑⓞⓣ', last_name=u'ՇﻉรՇ')
        CourseEnrollmentFactory.create(user=unicode_user, course_id=self.course.id)
        self.students.append(unicode_user)

        test_email = {
            'action': 'Send email',
253
            'send_to': 'all',
254 255 256
            'subject': 'test subject for all',
            'message': 'test message for all'
        }
257 258 259
        # Post the email to the instructor dashboard API
        response = self.client.post(self.send_mail_url, test_email)
        self.assertEquals(json.loads(response.content), self.success_content)
260 261 262 263 264 265 266

        self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students))

        self.assertItemsEqual(
            [e.to[0] for e in mail.outbox],
            [self.instructor.email] + [s.email for s in self.staff] + [s.email for s in self.students]
        )
267

268
    @override_settings(BULK_EMAIL_EMAILS_PER_TASK=3)
269
    @patch('bulk_email.tasks.update_subtask_status')
270 271 272 273 274
    def test_chunked_queries_send_numerous_emails(self, email_mock):
        """
        Test sending a large number of emails, to test the chunked querying
        """
        mock_factory = MockCourseEmailResult()
275
        email_mock.side_effect = mock_factory.get_mock_update_subtask_status()
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
        added_users = []
        for _ in xrange(LARGE_NUM_EMAILS):
            user = UserFactory()
            added_users.append(user)
            CourseEnrollmentFactory.create(user=user, course_id=self.course.id)

        optouts = []
        for i in [1, 3, 9, 10, 18]:  # 5 random optouts
            user = added_users[i]
            optouts.append(user)
            optout = Optout(user=user, course_id=self.course.id)
            optout.save()

        test_email = {
            'action': 'Send email',
291
            'send_to': 'all',
292 293 294
            'subject': 'test subject for all',
            'message': 'test message for all'
        }
295 296 297 298
        # Post the email to the instructor dashboard API
        response = self.client.post(self.send_mail_url, test_email)
        self.assertEquals(json.loads(response.content), self.success_content)

299 300
        self.assertEquals(mock_factory.emails_sent,
                          1 + len(self.staff) + len(self.students) + LARGE_NUM_EMAILS - len(optouts))
301 302 303 304 305 306
        outbox_contents = [e.to[0] for e in mail.outbox]
        should_send_contents = ([self.instructor.email] +
                                [s.email for s in self.staff] +
                                [s.email for s in self.students] +
                                [s.email for s in added_users if s not in optouts])
        self.assertItemsEqual(outbox_contents, should_send_contents)
Christine Lytwynec committed
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344


@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
@skipIf(os.environ.get("TRAVIS") == 'true', "Skip this test in Travis CI.")
class TestEmailSendFromDashboard(EmailSendFromDashboardTestCase):
    """
    Tests email sending without mocked html_to_text.

    Note that these tests are skipped on Travis because we can't use the
    function `html_to_text` as it is currently implemented on Travis.
    """

    def test_unicode_message_send_to_all(self):
        """
        Make sure email (with Unicode characters) send to all goes there.
        """
        # Now we know we have pulled up the instructor dash's email view
        # (in the setUp method), we can test sending an email.

        uni_message = u'ẗëṡẗ ṁëṡṡäġë ḟöṛ äḷḷ イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ fоѓ аll'
        test_email = {
            'action': 'Send email',
            'send_to': 'all',
            'subject': 'test subject for all',
            'message': uni_message
        }
        # Post the email to the instructor dashboard API
        response = self.client.post(self.send_mail_url, test_email)
        self.assertEquals(json.loads(response.content), self.success_content)

        self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students))
        self.assertItemsEqual(
            [e.to[0] for e in mail.outbox],
            [self.instructor.email] + [s.email for s in self.staff] + [s.email for s in self.students]
        )

        message_body = mail.outbox[0].body
        self.assertIn(uni_message, message_body)