Files
edx-platform/lms/djangoapps/certificates/tests/test_queue.py

438 lines
17 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for the XQueue certificates interface. """
from __future__ import absolute_import
import json
from contextlib import contextmanager
from datetime import datetime, timedelta
import ddt
import freezegun
import pytz
import six
from django.test import TestCase
from django.test.utils import override_settings
from mock import Mock, patch
from opaque_keys.edx.locator import CourseLocator
from testfixtures import LogCapture
# It is really unfortunate that we are using the XQueue client
# code from the capa library. In the future, we should move this
# into a shared library. We import it here so we can mock it
# and verify that items are being correctly added to the queue
# in our `XQueueCertInterface` implementation.
from capa.xqueue_interface import XQueueInterface
from course_modes.models import CourseMode
from lms.djangoapps.certificates.models import (
CertificateStatuses,
ExampleCertificate,
ExampleCertificateSet,
GeneratedCertificate
)
from lms.djangoapps.certificates.queue import LOGGER, XQueueCertInterface
from lms.djangoapps.certificates.tests.factories import CertificateWhitelistFactory, GeneratedCertificateFactory
from lms.djangoapps.grades.tests.utils import mock_passing_grade
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
@override_settings(CERT_QUEUE='certificates')
class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
"""Test the "add to queue" operation of the XQueue interface. """
def setUp(self):
super(XQueueCertInterfaceAddCertificateTest, self).setUp()
self.user = UserFactory.create()
self.course = CourseFactory.create()
self.enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=self.course.id,
is_active=True,
mode="honor",
)
self.xqueue = XQueueCertInterface()
self.user_2 = UserFactory.create()
SoftwareSecurePhotoVerificationFactory.create(user=self.user_2, status='approved')
def test_add_cert_callback_url(self):
with mock_passing_grade():
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None)
self.xqueue.add_cert(self.user, self.course.id)
# Verify that the task was sent to the queue with the correct callback URL
self.assertTrue(mock_send.called)
__, kwargs = mock_send.call_args_list[0]
actual_header = json.loads(kwargs['header'])
self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url'])
def test_no_create_action_in_queue_for_html_view_certs(self):
"""
Tests there is no certificate create message in the queue if generate_pdf is False
"""
with mock_passing_grade():
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
self.xqueue.add_cert(self.user, self.course.id, generate_pdf=False)
# Verify that add_cert method does not add message to queue
self.assertFalse(mock_send.called)
certificate = GeneratedCertificate.eligible_certificates.get(user=self.user, course_id=self.course.id)
self.assertEqual(certificate.status, CertificateStatuses.downloadable)
self.assertIsNotNone(certificate.verify_uuid)
@ddt.data('honor', 'audit')
@override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1))
def test_add_cert_with_honor_certificates(self, mode):
"""Test certificates generations for honor and audit modes."""
template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format(
id=self.course.id
)
mock_send = self.add_cert_to_queue(mode)
if CourseMode.is_eligible_for_certificate(mode):
self.assert_certificate_generated(mock_send, mode, template_name)
else:
self.assert_ineligible_certificate_generated(mock_send, mode)
@ddt.data('credit', 'verified')
def test_add_cert_with_verified_certificates(self, mode):
"""Test if enrollment mode is verified or credit along with valid
software-secure verification than verified certificate should be generated.
"""
template_name = 'certificate-template-{id.org}-{id.course}-verified.pdf'.format(
id=self.course.id
)
mock_send = self.add_cert_to_queue(mode)
self.assert_certificate_generated(mock_send, 'verified', template_name)
def test_ineligible_cert_whitelisted(self):
"""Test that audit mode students can receive a certificate if they are whitelisted."""
# Enroll as audit
CourseEnrollmentFactory(
user=self.user_2,
course_id=self.course.id,
is_active=True,
mode='audit'
)
# Whitelist student
CertificateWhitelistFactory(course_id=self.course.id, user=self.user_2)
# Generate certs
with mock_passing_grade():
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None)
self.xqueue.add_cert(self.user_2, self.course.id)
# Assert cert generated correctly
self.assertTrue(mock_send.called)
certificate = GeneratedCertificate.certificate_for_student(self.user_2, self.course.id)
self.assertIsNotNone(certificate)
self.assertEqual(certificate.mode, 'audit')
def add_cert_to_queue(self, mode):
"""
Dry method for course enrollment and adding request to
queue. Returns a mock object containing information about the
`XQueueInterface.send_to_queue` method, which can be used in other
assertions.
"""
CourseEnrollmentFactory(
user=self.user_2,
course_id=self.course.id,
is_active=True,
mode=mode,
)
with mock_passing_grade():
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None)
self.xqueue.add_cert(self.user_2, self.course.id)
return mock_send
def assert_certificate_generated(self, mock_send, expected_mode, expected_template_name):
"""
Assert that a certificate was generated with the correct mode and
template type.
"""
# Verify that the task was sent to the queue with the correct callback URL
self.assertTrue(mock_send.called)
__, kwargs = mock_send.call_args_list[0]
actual_header = json.loads(kwargs['header'])
self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url'])
body = json.loads(kwargs['body'])
self.assertIn(expected_template_name, body['template_pdf'])
certificate = GeneratedCertificate.eligible_certificates.get(user=self.user_2, course_id=self.course.id)
self.assertEqual(certificate.mode, expected_mode)
def assert_ineligible_certificate_generated(self, mock_send, expected_mode):
"""
Assert that an ineligible certificate was generated with the
correct mode.
"""
# Ensure the certificate was not generated
self.assertFalse(mock_send.called)
certificate = GeneratedCertificate.objects.get(
user=self.user_2,
course_id=self.course.id
)
self.assertIn(certificate.status, (CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing))
self.assertEqual(certificate.mode, expected_mode)
@ddt.data(
(CertificateStatuses.restricted, False),
(CertificateStatuses.deleting, False),
(CertificateStatuses.generating, True),
(CertificateStatuses.unavailable, True),
(CertificateStatuses.deleted, True),
(CertificateStatuses.error, True),
(CertificateStatuses.notpassing, True),
(CertificateStatuses.downloadable, True),
(CertificateStatuses.auditing, True),
)
@ddt.unpack
def test_add_cert_statuses(self, status, should_generate):
"""
Test that certificates can or cannot be generated with the given
certificate status.
"""
with patch(
'lms.djangoapps.certificates.queue.certificate_status_for_student',
Mock(return_value={'status': status})
):
mock_send = self.add_cert_to_queue('verified')
if should_generate:
self.assertTrue(mock_send.called)
else:
self.assertFalse(mock_send.called)
@ddt.data(
# Eligible and should stay that way
(
CertificateStatuses.downloadable,
timedelta(days=-2),
'Pass',
CertificateStatuses.generating
),
# Ensure that certs in the wrong state can be fixed by regeneration
(
CertificateStatuses.downloadable,
timedelta(hours=-1),
'Pass',
CertificateStatuses.audit_passing
),
# Ineligible and should stay that way
(
CertificateStatuses.audit_passing,
timedelta(hours=-1),
'Pass',
CertificateStatuses.audit_passing
),
# As above
(
CertificateStatuses.audit_notpassing,
timedelta(hours=-1),
'Pass',
CertificateStatuses.audit_passing
),
# As above
(
CertificateStatuses.audit_notpassing,
timedelta(hours=-1),
None,
CertificateStatuses.audit_notpassing
),
)
@ddt.unpack
@override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1))
def test_regen_audit_certs_eligibility(self, status, created_delta, grade, expected_status):
"""
Test that existing audit certificates remain eligible even if cert
generation is re-run.
"""
# Create an existing audit enrollment and certificate
CourseEnrollmentFactory(
user=self.user_2,
course_id=self.course.id,
is_active=True,
mode=CourseMode.AUDIT,
)
created_date = datetime.now(pytz.UTC) + created_delta
with freezegun.freeze_time(created_date):
GeneratedCertificateFactory(
user=self.user_2,
course_id=self.course.id,
grade='1.0',
status=status,
mode=GeneratedCertificate.MODES.audit,
)
# Run grading/cert generation again
with mock_passing_grade(letter_grade=grade):
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None)
self.xqueue.add_cert(self.user_2, self.course.id)
self.assertEqual(
GeneratedCertificate.objects.get(user=self.user_2, course_id=self.course.id).status,
expected_status
)
def test_regen_cert_with_pdf_certificate(self):
"""
Test that regenerating PDF certifcate log warning message and certificate
status remains unchanged.
"""
download_url = 'http://www.example.com/certificate.pdf'
# Create an existing verifed enrollment and certificate
CourseEnrollmentFactory(
user=self.user_2,
course_id=self.course.id,
is_active=True,
mode=CourseMode.VERIFIED,
)
GeneratedCertificateFactory(
user=self.user_2,
course_id=self.course.id,
grade='1.0',
status=CertificateStatuses.downloadable,
mode=GeneratedCertificate.MODES.verified,
download_url=download_url
)
self._assert_pdf_cert_generation_dicontinued_logs(download_url)
def test_add_cert_with_existing_pdf_certificate(self):
"""
Test that add certifcate for existing PDF certificate log warning
message and certificate status remains unchanged.
"""
download_url = 'http://www.example.com/certificate.pdf'
# Create an existing verifed enrollment and certificate
CourseEnrollmentFactory(
user=self.user_2,
course_id=self.course.id,
is_active=True,
mode=CourseMode.VERIFIED,
)
GeneratedCertificateFactory(
user=self.user_2,
course_id=self.course.id,
grade='1.0',
status=CertificateStatuses.downloadable,
mode=GeneratedCertificate.MODES.verified,
download_url=download_url
)
self._assert_pdf_cert_generation_dicontinued_logs(download_url, add_cert=True)
def _assert_pdf_cert_generation_dicontinued_logs(self, download_url, add_cert=False):
"""Assert PDF certificate generation discontinued logs."""
with LogCapture(LOGGER.name) as log:
if add_cert:
self.xqueue.add_cert(self.user_2, self.course.id)
else:
self.xqueue.regen_cert(self.user_2, self.course.id)
log.check_present(
(
LOGGER.name,
'WARNING',
(
u"PDF certificate generation discontinued, canceling "
u"PDF certificate generation for student {student_id} "
u"in course '{course_id}' "
u"with status '{status}' "
u"and download_url '{download_url}'."
).format(
student_id=self.user_2.id,
course_id=six.text_type(self.course.id),
status=CertificateStatuses.downloadable,
download_url=download_url
)
)
)
@override_settings(CERT_QUEUE='certificates')
class XQueueCertInterfaceExampleCertificateTest(TestCase):
"""Tests for the XQueue interface for certificate generation. """
COURSE_KEY = CourseLocator(org='test', course='test', run='test')
TEMPLATE = 'test.pdf'
DESCRIPTION = 'test'
ERROR_MSG = 'Kaboom!'
def setUp(self):
super(XQueueCertInterfaceExampleCertificateTest, self).setUp()
self.xqueue = XQueueCertInterface()
def test_add_example_cert(self):
cert = self._create_example_cert()
with self._mock_xqueue() as mock_send:
self.xqueue.add_example_cert(cert)
# Verify that the correct payload was sent to the XQueue
self._assert_queue_task(mock_send, cert)
# Verify the certificate status
self.assertEqual(cert.status, ExampleCertificate.STATUS_STARTED)
def test_add_example_cert_error(self):
cert = self._create_example_cert()
with self._mock_xqueue(success=False):
self.xqueue.add_example_cert(cert)
# Verify the error status of the certificate
self.assertEqual(cert.status, ExampleCertificate.STATUS_ERROR)
self.assertIn(self.ERROR_MSG, cert.error_reason)
def _create_example_cert(self):
"""Create an example certificate. """
cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY)
return ExampleCertificate.objects.create(
example_cert_set=cert_set,
description=self.DESCRIPTION,
template=self.TEMPLATE
)
@contextmanager
def _mock_xqueue(self, success=True):
"""Mock the XQueue method for sending a task to the queue. """
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None) if success else (1, self.ERROR_MSG)
yield mock_send
def _assert_queue_task(self, mock_send, cert):
"""Check that the task was added to the queue. """
expected_header = {
'lms_key': cert.access_key,
'lms_callback_url': 'https://edx.org/update_example_certificate?key={key}'.format(key=cert.uuid),
'queue_name': 'certificates'
}
expected_body = {
'action': 'create',
'username': cert.uuid,
'name': u'John Doë',
'course_id': six.text_type(self.COURSE_KEY),
'template_pdf': 'test.pdf',
'example_certificate': True
}
self.assertTrue(mock_send.called)
__, kwargs = mock_send.call_args_list[0]
actual_header = json.loads(kwargs['header'])
actual_body = json.loads(kwargs['body'])
self.assertEqual(expected_header, actual_header)
self.assertEqual(expected_body, actual_body)