test_api.py 18.6 KB
Newer Older
1 2 3
"""Tests for the certificates Python API. """
from contextlib import contextmanager
import ddt
4
from functools import wraps
5 6 7

from django.test import TestCase, RequestFactory
from django.test.utils import override_settings
8
from django.conf import settings
Ned Batchelder committed
9
from mock import patch
10
from nose.plugins.attrib import attr
11 12 13 14 15 16

from opaque_keys.edx.locator import CourseLocator
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
17
from course_modes.models import CourseMode
18 19
from course_modes.tests.factories import CourseModeFactory
from config_models.models import cache
20
from util.testing import EventTestMixin
21 22 23 24 25

from certificates import api as certs_api
from certificates.models import (
    CertificateStatuses,
    CertificateGenerationConfiguration,
26
    ExampleCertificate,
27 28
    GeneratedCertificate,
    certificate_status_for_student,
29
)
30
from certificates.queue import XQueueCertInterface, XQueueAddToQueueError
31 32
from certificates.tests.factories import GeneratedCertificateFactory

33 34
from microsite_configuration import microsite

35 36 37
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True

38

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
class WebCertificateTestMixin(object):
    """
    Mixin with helpers for testing Web Certificates.
    """
    @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

    @contextmanager
    def _mock_queue(self, is_successful=True):
        """
        Mock the "send to XQueue" method to return either success or an error.
        """
        symbol = 'capa.xqueue_interface.XQueueInterface.send_to_queue'
        with patch(symbol) as mock_send_to_queue:
            if is_successful:
                mock_send_to_queue.return_value = (0, "Successfully queued")
            else:
                mock_send_to_queue.side_effect = XQueueAddToQueueError(1, self.ERROR_REASON)

            yield mock_send_to_queue

    def _setup_course_certificate(self):
        """
        Creates certificate configuration for course
        """
        certificates = [
            {
                'id': 1,
                'name': 'Test Certificate Name',
                'description': 'Test Certificate Description',
                'course_title': 'tes_course_title',
                'signatories': [],
                'version': 1,
                'is_active': True
            }
        ]
        self.course.certificates = {'certificates': certificates}
        self.course.cert_html_view_enabled = True
        self.course.save()
        self.store.update_item(self.course, self.user.id)


88
@attr('shard_1')
89
class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTestCase):
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
    """Tests for the `certificate_downloadable_status` helper function. """

    def setUp(self):
        super(CertificateDownloadableStatusTests, self).setUp()

        self.student = UserFactory()
        self.student_no_cert = UserFactory()
        self.course = CourseFactory.create(
            org='edx',
            number='verified',
            display_name='Verified Course'
        )

        self.request_factory = RequestFactory()

105
    def test_cert_status_with_generating(self):
106 107 108 109 110 111 112 113 114 115 116
        GeneratedCertificateFactory.create(
            user=self.student,
            course_id=self.course.id,
            status=CertificateStatuses.generating,
            mode='verified'
        )
        self.assertEqual(
            certs_api.certificate_downloadable_status(self.student, self.course.id),
            {
                'is_downloadable': False,
                'is_generating': True,
117 118
                'download_url': None,
                'uuid': None,
119 120 121
            }
        )

122
    def test_cert_status_with_error(self):
123 124 125 126 127 128 129 130 131 132 133 134
        GeneratedCertificateFactory.create(
            user=self.student,
            course_id=self.course.id,
            status=CertificateStatuses.error,
            mode='verified'
        )

        self.assertEqual(
            certs_api.certificate_downloadable_status(self.student, self.course.id),
            {
                'is_downloadable': False,
                'is_generating': True,
135 136
                'download_url': None,
                'uuid': None
137 138 139
            }
        )

140
    def test_without_cert(self):
141 142 143 144 145
        self.assertEqual(
            certs_api.certificate_downloadable_status(self.student_no_cert, self.course.id),
            {
                'is_downloadable': False,
                'is_generating': False,
146 147
                'download_url': None,
                'uuid': None,
148 149 150
            }
        )

151 152 153 154 155
    def verify_downloadable_pdf_cert(self):
        """
        Verifies certificate_downloadable_status returns the
        correct response for PDF certificates.
        """
156
        cert = GeneratedCertificateFactory.create(
157 158 159 160
            user=self.student,
            course_id=self.course.id,
            status=CertificateStatuses.downloadable,
            mode='verified',
161
            download_url='www.google.com',
162 163 164 165 166 167 168
        )

        self.assertEqual(
            certs_api.certificate_downloadable_status(self.student, self.course.id),
            {
                'is_downloadable': True,
                'is_generating': False,
169 170
                'download_url': 'www.google.com',
                'uuid': cert.verify_uuid
171 172 173
            }
        )

174
    @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
175 176 177 178 179 180 181
    def test_pdf_cert_with_html_enabled(self):
        self.verify_downloadable_pdf_cert()

    def test_pdf_cert_with_html_disabled(self):
        self.verify_downloadable_pdf_cert()

    @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
182 183 184 185 186 187
    def test_with_downloadable_web_cert(self):
        CourseEnrollment.enroll(self.student, self.course.id, mode='honor')
        self._setup_course_certificate()
        with self._mock_passing_grade():
            certs_api.generate_user_certificates(self.student, self.course.id)

188
        cert_status = certificate_status_for_student(self.student, self.course.id)
189 190 191 192 193 194 195 196 197
        self.assertEqual(
            certs_api.certificate_downloadable_status(self.student, self.course.id),
            {
                'is_downloadable': True,
                'is_generating': False,
                'download_url': '/certificates/user/{user_id}/course/{course_id}'.format(
                    user_id=self.student.id,  # pylint: disable=no-member
                    course_id=self.course.id,
                ),
198
                'uuid': cert_status['uuid']
199 200 201
            }
        )

202

203
@attr('shard_1')
204
@override_settings(CERT_QUEUE='certificates')
205
class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, ModuleStoreTestCase):
206 207 208
    """Tests for generating certificates for students. """

    ERROR_REASON = "Kaboom!"
209

210
    def setUp(self):  # pylint: disable=arguments-differ
211
        super(GenerateUserCertificatesTest, self).setUp('certificates.api.tracker')
212

213 214 215 216 217
        self.student = UserFactory.create(
            email='joe_user@edx.org',
            username='joeuser',
            password='foo'
        )
218 219 220 221 222 223 224 225 226 227 228
        self.student_no_cert = UserFactory()
        self.course = CourseFactory.create(
            org='edx',
            number='verified',
            display_name='Verified Course',
            grade_cutoffs={'cutoff': 0.75, 'Pass': 0.5}
        )
        self.enrollment = CourseEnrollment.enroll(self.student, self.course.id, mode='honor')
        self.request_factory = RequestFactory()

    def test_new_cert_requests_into_xqueue_returns_generating(self):
229 230 231 232 233
        with self._mock_passing_grade():
            with self._mock_queue():
                certs_api.generate_user_certificates(self.student, self.course.id)

        # Verify that the certificate has status 'generating'
234
        cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
235
        self.assertEqual(cert.status, CertificateStatuses.generating)
236 237 238 239
        self.assert_event_emitted(
            'edx.certificate.created',
            user_id=self.student.id,
            course_id=unicode(self.course.id),
240
            certificate_url=certs_api.get_certificate_url(self.student.id, self.course.id),
241 242 243 244
            certificate_id=cert.verify_uuid,
            enrollment_mode=cert.mode,
            generation_mode='batch'
        )
245 246 247 248 249 250 251

    def test_xqueue_submit_task_error(self):
        with self._mock_passing_grade():
            with self._mock_queue(is_successful=False):
                certs_api.generate_user_certificates(self.student, self.course.id)

        # Verify that the certificate has been marked with status error
252
        cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
253 254 255
        self.assertEqual(cert.status, 'error')
        self.assertIn(self.ERROR_REASON, cert.error_reason)

256
    @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
257 258 259 260 261 262 263 264 265
    def test_new_cert_requests_returns_generating_for_html_certificate(self):
        """
        Test no message sent to Xqueue if HTML certificate view is enabled
        """
        self._setup_course_certificate()
        with self._mock_passing_grade():
            certs_api.generate_user_certificates(self.student, self.course.id)

        # Verify that the certificate has status 'downloadable'
266
        cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
267 268
        self.assertEqual(cert.status, CertificateStatuses.downloadable)

269 270 271 272 273 274 275 276
    @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': False})
    def test_cert_url_empty_with_invalid_certificate(self):
        """
        Test certificate url is empty if html view is not enabled and certificate is not yet generated
        """
        url = certs_api.get_certificate_url(self.student.id, self.course.id)
        self.assertEqual(url, "")

277

278
@attr('shard_1')
279
@ddt.ddt
280
class CertificateGenerationEnabledTest(EventTestMixin, TestCase):
281 282 283 284
    """Test enabling/disabling self-generated certificates for a course. """

    COURSE_KEY = CourseLocator(org='test', course='test', run='test')

285
    def setUp(self):  # pylint: disable=arguments-differ
286
        super(CertificateGenerationEnabledTest, self).setUp('certificates.api.tracker')
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306

        # Since model-based configuration is cached, we need
        # to clear the cache before each test.
        cache.clear()

    @ddt.data(
        (None, None, False),
        (False, None, False),
        (False, True, False),
        (True, None, False),
        (True, False, False),
        (True, True, True)
    )
    @ddt.unpack
    def test_cert_generation_enabled(self, is_feature_enabled, is_course_enabled, expect_enabled):
        if is_feature_enabled is not None:
            CertificateGenerationConfiguration.objects.create(enabled=is_feature_enabled)

        if is_course_enabled is not None:
            certs_api.set_cert_generation_enabled(self.COURSE_KEY, is_course_enabled)
307 308 309 310 311 312
            cert_event_type = 'enabled' if is_course_enabled else 'disabled'
            event_name = '.'.join(['edx', 'certificate', 'generation', cert_event_type])
            self.assert_event_emitted(
                event_name,
                course_id=unicode(self.COURSE_KEY),
            )
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 345

        self._assert_enabled_for_course(self.COURSE_KEY, expect_enabled)

    def test_latest_setting_used(self):
        # Enable the feature
        CertificateGenerationConfiguration.objects.create(enabled=True)

        # Enable for the course
        certs_api.set_cert_generation_enabled(self.COURSE_KEY, True)
        self._assert_enabled_for_course(self.COURSE_KEY, True)

        # Disable for the course
        certs_api.set_cert_generation_enabled(self.COURSE_KEY, False)
        self._assert_enabled_for_course(self.COURSE_KEY, False)

    def test_setting_is_course_specific(self):
        # Enable the feature
        CertificateGenerationConfiguration.objects.create(enabled=True)

        # Enable for one course
        certs_api.set_cert_generation_enabled(self.COURSE_KEY, True)
        self._assert_enabled_for_course(self.COURSE_KEY, True)

        # Should be disabled for another course
        other_course = CourseLocator(org='other', course='other', run='other')
        self._assert_enabled_for_course(other_course, False)

    def _assert_enabled_for_course(self, course_key, expect_enabled):
        """Check that self-generated certificates are enabled or disabled for the course. """
        actual_enabled = certs_api.cert_generation_enabled(course_key)
        self.assertEqual(expect_enabled, actual_enabled)


346
@attr('shard_1')
347 348 349 350 351 352 353 354 355 356
class GenerateExampleCertificatesTest(TestCase):
    """Test generation of example certificates. """

    COURSE_KEY = CourseLocator(org='test', course='test', run='test')

    def setUp(self):
        super(GenerateExampleCertificatesTest, self).setUp()

    def test_generate_example_certs(self):
        # Generate certificates for the course
357
        CourseModeFactory.create(course_id=self.COURSE_KEY, mode_slug=CourseMode.HONOR)
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
        with self._mock_xqueue() as mock_queue:
            certs_api.generate_example_certificates(self.COURSE_KEY)

        # Verify that the appropriate certs were added to the queue
        self._assert_certs_in_queue(mock_queue, 1)

        # Verify that the certificate status is "started"
        self._assert_cert_status({
            'description': 'honor',
            'status': 'started'
        })

    def test_generate_example_certs_with_verified_mode(self):
        # Create verified and honor modes for the course
        CourseModeFactory(course_id=self.COURSE_KEY, mode_slug='honor')
        CourseModeFactory(course_id=self.COURSE_KEY, mode_slug='verified')

        # Generate certificates for the course
        with self._mock_xqueue() as mock_queue:
            certs_api.generate_example_certificates(self.COURSE_KEY)

        # Verify that the appropriate certs were added to the queue
        self._assert_certs_in_queue(mock_queue, 2)

        # Verify that the certificate status is "started"
        self._assert_cert_status(
            {
                'description': 'verified',
                'status': 'started'
            },
            {
                'description': 'honor',
                'status': 'started'
            }
        )

    @contextmanager
    def _mock_xqueue(self):
        """Mock the XQueue method for adding a task to the queue. """
        with patch.object(XQueueCertInterface, 'add_example_cert') as mock_queue:
            yield mock_queue

    def _assert_certs_in_queue(self, mock_queue, expected_num):
        """Check that the certificate generation task was added to the queue. """
        certs_in_queue = [call_args[0] for (call_args, __) in mock_queue.call_args_list]
        self.assertEqual(len(certs_in_queue), expected_num)
        for cert in certs_in_queue:
            self.assertTrue(isinstance(cert, ExampleCertificate))

    def _assert_cert_status(self, *expected_statuses):
        """Check the example certificate status. """
        actual_status = certs_api.example_certificates_status(self.COURSE_KEY)
        self.assertEqual(list(expected_statuses), actual_status)
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501


def set_microsite(domain):
    """
    returns a decorator that can be used on a test_case to set a specific microsite for the current test case.
    :param domain: Domain of the new microsite
    """
    def decorator(func):
        """
        Decorator to set current microsite according to domain
        """
        @wraps(func)
        def inner(request, *args, **kwargs):
            """
            Execute the function after setting up the microsite.
            """
            microsite.set_by_domain(domain)
            return func(request, *args, **kwargs)
        return inner
    return decorator


@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
@attr('shard_1')
class CertificatesBrandingTest(TestCase):
    """Test certificates branding. """

    COURSE_KEY = CourseLocator(org='test', course='test', run='test')

    def setUp(self):
        super(CertificatesBrandingTest, self).setUp()

    @set_microsite(settings.MICROSITE_CONFIGURATION['test_microsite']['domain_prefix'])
    def test_certificate_header_data(self):
        """
        Test that get_certificate_header_context from certificates api
        returns data customized according to site branding.
        """
        # Generate certificates for the course
        CourseModeFactory.create(course_id=self.COURSE_KEY, mode_slug=CourseMode.HONOR)
        data = certs_api.get_certificate_header_context(is_secure=True)

        # Make sure there are not unexpected keys in dict returned by 'get_certificate_header_context'
        self.assertItemsEqual(
            data.keys(),
            ['logo_src', 'logo_url']
        )
        self.assertIn(
            settings.MICROSITE_CONFIGURATION['test_microsite']['logo_image_url'],
            data['logo_src']
        )

        self.assertIn(
            settings.MICROSITE_CONFIGURATION['test_microsite']['SITE_NAME'],
            data['logo_url']
        )

    @set_microsite(settings.MICROSITE_CONFIGURATION['test_microsite']['domain_prefix'])
    def test_certificate_footer_data(self):
        """
        Test that get_certificate_footer_context from certificates api returns
        data customized according to site branding.
        """
        # Generate certificates for the course
        CourseModeFactory.create(course_id=self.COURSE_KEY, mode_slug=CourseMode.HONOR)
        data = certs_api.get_certificate_footer_context()

        # Make sure there are not unexpected keys in dict returned by 'get_certificate_footer_context'
        self.assertItemsEqual(
            data.keys(),
            ['company_about_url', 'company_privacy_url', 'company_tos_url']
        )

        # ABOUT is present in MICROSITE_CONFIGURATION['test_microsite']["urls"] so web certificate will use that url
        self.assertIn(
            settings.MICROSITE_CONFIGURATION['test_microsite']["urls"]['ABOUT'],
            data['company_about_url']
        )

        # PRIVACY is present in MICROSITE_CONFIGURATION['test_microsite']["urls"] so web certificate will use that url
        self.assertIn(
            settings.MICROSITE_CONFIGURATION['test_microsite']["urls"]['PRIVACY'],
            data['company_privacy_url']
        )

        # TOS_AND_HONOR is present in MICROSITE_CONFIGURATION['test_microsite']["urls"],
        # so web certificate will use that url
        self.assertIn(
            settings.MICROSITE_CONFIGURATION['test_microsite']["urls"]['TOS_AND_HONOR'],
            data['company_tos_url']
        )