test_cert_management.py 10.5 KB
Newer Older
1 2
"""Tests for the resubmit_error_certificates management command. """
import ddt
3
from contextlib import contextmanager
4
from django.core.management.base import CommandError
5
from nose.plugins.attrib import attr
6 7
from django.test.utils import override_settings
from mock import patch
8

9
from course_modes.models import CourseMode
10
from opaque_keys.edx.locator import CourseLocator
11

12
from badges.events.course_complete import get_completion_badge
13 14
from badges.models import BadgeAssertion
from badges.tests.factories import BadgeAssertionFactory, CourseCompleteImageConfigurationFactory
15 16 17
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from student.tests.factories import UserFactory, CourseEnrollmentFactory
18
from certificates.management.commands import resubmit_error_certificates, regenerate_user, ungenerated_certs
19
from certificates.models import GeneratedCertificate, CertificateStatuses
20 21


22 23 24 25 26 27
class CertificateManagementTest(ModuleStoreTestCase):
    """
    Base test class for Certificate Management command tests.
    """
    # Override with the command module you wish to test.
    command = resubmit_error_certificates
28 29

    def setUp(self):
30
        super(CertificateManagementTest, self).setUp()
31 32 33 34 35
        self.user = UserFactory.create()
        self.courses = [
            CourseFactory.create()
            for __ in range(3)
        ]
36
        CourseCompleteImageConfigurationFactory.create()
37

38
    def _create_cert(self, course_key, user, status, mode=CourseMode.HONOR):
39 40 41 42
        """Create a certificate entry. """
        # Enroll the user in the course
        CourseEnrollmentFactory.create(
            user=user,
43 44
            course_id=course_key,
            mode=mode
45 46 47
        )

        # Create the certificate
48
        GeneratedCertificate.eligible_certificates.create(
49 50 51 52 53 54 55 56 57 58 59 60
            user=user,
            course_id=course_key,
            status=status
        )

    def _run_command(self, *args, **kwargs):
        """Run the management command to generate a fake cert. """
        command = self.command.Command()
        return command.handle(*args, **kwargs)

    def _assert_cert_status(self, course_key, user, expected_status):
        """Check the status of a certificate. """
61
        cert = GeneratedCertificate.eligible_certificates.get(user=user, course_id=course_key)
62 63 64 65 66 67 68 69
        self.assertEqual(cert.status, expected_status)


@attr('shard_1')
@ddt.ddt
class ResubmitErrorCertificatesTest(CertificateManagementTest):
    """Tests for the resubmit_error_certificates management command. """

70 71
    @ddt.data(CourseMode.HONOR, CourseMode.VERIFIED)
    def test_resubmit_error_certificate(self, mode):
72
        # Create a certificate with status 'error'
73
        self._create_cert(self.courses[0].id, self.user, CertificateStatuses.error, mode)
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151

        # Re-submit all certificates with status 'error'
        with check_mongo_calls(1):
            self._run_command()

        # Expect that the certificate was re-submitted
        self._assert_cert_status(self.courses[0].id, self.user, CertificateStatuses.notpassing)

    def test_resubmit_error_certificate_in_a_course(self):
        # Create a certificate with status 'error'
        # in three courses.
        for idx in range(3):
            self._create_cert(self.courses[idx].id, self.user, CertificateStatuses.error)

        # Re-submit certificates for two of the courses
        self._run_command(course_key_list=[
            unicode(self.courses[0].id),
            unicode(self.courses[1].id)
        ])

        # Expect that the first two courses have been re-submitted,
        # but not the third course.
        self._assert_cert_status(self.courses[0].id, self.user, CertificateStatuses.notpassing)
        self._assert_cert_status(self.courses[1].id, self.user, CertificateStatuses.notpassing)
        self._assert_cert_status(self.courses[2].id, self.user, CertificateStatuses.error)

    @ddt.data(
        CertificateStatuses.deleted,
        CertificateStatuses.deleting,
        CertificateStatuses.downloadable,
        CertificateStatuses.generating,
        CertificateStatuses.notpassing,
        CertificateStatuses.restricted,
        CertificateStatuses.unavailable,
    )
    def test_resubmit_error_certificate_skips_non_error_certificates(self, other_status):
        # Create certificates with an error status and some other status
        self._create_cert(self.courses[0].id, self.user, CertificateStatuses.error)
        self._create_cert(self.courses[1].id, self.user, other_status)

        # Re-submit certificates for all courses
        self._run_command()

        # Only the certificate with status "error" should have been re-submitted
        self._assert_cert_status(self.courses[0].id, self.user, CertificateStatuses.notpassing)
        self._assert_cert_status(self.courses[1].id, self.user, other_status)

    def test_resubmit_error_certificate_none_found(self):
        self._create_cert(self.courses[0].id, self.user, CertificateStatuses.downloadable)
        self._run_command()
        self._assert_cert_status(self.courses[0].id, self.user, CertificateStatuses.downloadable)

    def test_course_caching(self):
        # Create multiple certificates for the same course
        self._create_cert(self.courses[0].id, UserFactory.create(), CertificateStatuses.error)
        self._create_cert(self.courses[0].id, UserFactory.create(), CertificateStatuses.error)
        self._create_cert(self.courses[0].id, UserFactory.create(), CertificateStatuses.error)

        # Verify that we make only one Mongo query
        # because the course is cached.
        with check_mongo_calls(1):
            self._run_command()

    def test_invalid_course_key(self):
        invalid_key = u"invalid/"
        with self.assertRaisesRegexp(CommandError, invalid_key):
            self._run_command(course_key_list=[invalid_key])

    def test_course_does_not_exist(self):
        phantom_course = CourseLocator(org='phantom', course='phantom', run='phantom')
        self._create_cert(phantom_course, self.user, 'error')
        self._run_command()

        # Expect that the certificate was NOT resubmitted
        # since the course doesn't actually exist.
        self._assert_cert_status(phantom_course, self.user, CertificateStatuses.error)


152
@ddt.ddt
153 154 155 156 157 158
@attr('shard_1')
class RegenerateCertificatesTest(CertificateManagementTest):
    """
    Tests for regenerating certificates.
    """
    command = regenerate_user
159

160 161 162 163 164 165 166
    def setUp(self):
        """
        We just need one course here.
        """
        super(RegenerateCertificatesTest, self).setUp()
        self.course = self.courses[0]

167
    @ddt.data(True, False)
168
    @override_settings(CERT_QUEUE='test-queue')
169
    @patch('certificates.api.XQueueCertInterface', spec=True)
170
    def test_clear_badge(self, issue_badges, xqueue):
171 172 173 174
        """
        Given that I have a user with a badge
        If I run regeneration for a user
        Then certificate generation will be requested
175
        And the badge will be deleted if badge issuing is enabled
176 177 178
        """
        key = self.course.location.course_key
        self._create_cert(key, self.user, CertificateStatuses.downloadable)
179 180 181
        badge_class = get_completion_badge(key, self.user)
        BadgeAssertionFactory(badge_class=badge_class, user=self.user)
        self.assertTrue(BadgeAssertion.objects.filter(user=self.user, badge_class=badge_class))
182 183
        self.course.issue_badges = issue_badges
        self.store.update_item(self.course, None)
184 185 186 187 188
        self._run_command(
            username=self.user.email, course=unicode(key), noop=False, insecure=False, template_file=None,
            grade_value=None
        )
        xqueue.return_value.regen_cert.assert_called_with(
189 190 191 192 193 194
            self.user,
            key,
            course=self.course,
            forced_grade=None,
            template_file=None,
            generate_pdf=True
195
        )
196 197 198
        self.assertEquals(
            bool(BadgeAssertion.objects.filter(user=self.user, badge_class=badge_class)), not issue_badges
        )
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213

    @override_settings(CERT_QUEUE='test-queue')
    @patch('capa.xqueue_interface.XQueueInterface.send_to_queue', spec=True)
    def test_regenerating_certificate(self, mock_send_to_queue):
        """
        Given that I have a user who has not passed course
        If I run regeneration for that user
        Then certificate generation will be not be requested
        """
        key = self.course.location.course_key
        self._create_cert(key, self.user, CertificateStatuses.downloadable)
        self._run_command(
            username=self.user.email, course=unicode(key), noop=False, insecure=True, template_file=None,
            grade_value=None
        )
214
        certificate = GeneratedCertificate.eligible_certificates.get(
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
            user=self.user,
            course_id=key
        )
        self.assertEqual(certificate.status, CertificateStatuses.notpassing)
        self.assertFalse(mock_send_to_queue.called)


@attr('shard_1')
class UngenerateCertificatesTest(CertificateManagementTest):
    """
    Tests for generating certificates.
    """
    command = ungenerated_certs

    def setUp(self):
        """
        We just need one course here.
        """
        super(UngenerateCertificatesTest, self).setUp()
        self.course = self.courses[0]

    @override_settings(CERT_QUEUE='test-queue')
    @patch('capa.xqueue_interface.XQueueInterface.send_to_queue', spec=True)
    def test_ungenerated_certificate(self, mock_send_to_queue):
        """
        Given that I have ended course
        If I run ungenerated certs command
        Then certificates should be generated for all users who passed course
        """
        mock_send_to_queue.return_value = (0, "Successfully queued")
        key = self.course.location.course_key
        self._create_cert(key, self.user, CertificateStatuses.unavailable)
        with self._mock_passing_grade():
            self._run_command(
                course=unicode(key), noop=False, insecure=True, force=False
            )
        self.assertTrue(mock_send_to_queue.called)
252
        certificate = GeneratedCertificate.eligible_certificates.get(
253 254 255 256 257 258 259 260 261 262 263 264
            user=self.user,
            course_id=key
        )
        self.assertEqual(certificate.status, CertificateStatuses.generating)

    @contextmanager
    def _mock_passing_grade(self):
        """Mock the grading function to always return a passing grade. """
        symbol = 'courseware.grades.grade'
        with patch(symbol) as mock_grade:
            mock_grade.return_value = {'grade': 'Pass', 'percent': 0.75}
            yield