""" Tests for certificate app views used by the support team. """ import json from unittest.mock import patch from uuid import uuid4 import ddt from django.conf import settings from django.test.utils import override_settings from django.urls import reverse from opaque_keys.edx.keys import CourseKey from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.roles import GlobalStaff, SupportStaffRole from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.certificates.api import regenerate_user_certificates from lms.djangoapps.certificates.models import CertificateInvalidation, CertificateStatuses, GeneratedCertificate from lms.djangoapps.certificates.tests.factories import CertificateInvalidationFactory, GeneratedCertificateFactory from lms.djangoapps.grades.tests.utils import mock_passing_grade from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True class CertificateSupportTestCase(ModuleStoreTestCase): """ Base class for tests of the certificate support views. """ SUPPORT_USERNAME = "support" SUPPORT_EMAIL = "support@example.com" SUPPORT_PASSWORD = "support" STUDENT_USERNAME = "student" STUDENT_EMAIL = "student@example.com" STUDENT_PASSWORD = "student" CERT_COURSE_KEY = CourseKey.from_string("edX/DemoX/Demo_Course") COURSE_NOT_EXIST_KEY = CourseKey.from_string("test/TestX/Test_Course_Not_Exist") EXISTED_COURSE_KEY_1 = CourseKey.from_string("test1/Test1X/Test_Course_Exist_1") EXISTED_COURSE_KEY_2 = CourseKey.from_string("test2/Test2X/Test_Course_Exist_2") CERT_GRADE = 0.89 CERT_STATUS = CertificateStatuses.downloadable CERT_MODE = "verified" CERT_DOWNLOAD_URL = "http://www.example.com/cert.pdf" def setUp(self): """ Create a support team member and a student with a certificate. Log in as the support team member. """ super().setUp() CourseFactory( org=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.org, course=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.course, run=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.run, ) # Create the support staff user self.support = UserFactory( username=self.SUPPORT_USERNAME, email=self.SUPPORT_EMAIL, password=self.SUPPORT_PASSWORD, ) SupportStaffRole().add_users(self.support) # Create a student self.student = UserFactory( username=self.STUDENT_USERNAME, email=self.STUDENT_EMAIL, password=self.STUDENT_PASSWORD, ) # Create certificates for the student self.cert = GeneratedCertificateFactory( user=self.student, course_id=self.CERT_COURSE_KEY, grade=self.CERT_GRADE, status=self.CERT_STATUS, mode=self.CERT_MODE, download_url=self.CERT_DOWNLOAD_URL, verify_uuid=uuid4().hex ) # Login as support staff success = self.client.login(username=self.SUPPORT_USERNAME, password=self.SUPPORT_PASSWORD) assert success, "Couldn't log in as support staff" @ddt.ddt class CertificateSearchTests(CertificateSupportTestCase): """ Tests for the certificate search end-point used by the support team. """ def setUp(self): """ Create a course """ super().setUp() self.course = CourseFactory( org=self.CERT_COURSE_KEY.org, course=self.CERT_COURSE_KEY.course, run=self.CERT_COURSE_KEY.run, ) self.course.cert_html_view_enabled = True self.course_key = self.course.id # pylint: disable=no-member # Course certificate configurations certificates = [ { 'id': 1, 'name': 'Name 1', 'description': 'Description 1', 'course_title': 'course_title_1', 'signatories': [], 'version': 1, 'is_active': True } ] self.course.certificates = {'certificates': certificates} self.course.save() # pylint: disable=no-member self.store.update_item(self.course, self.user.id) self.course_overview = CourseOverviewFactory( id=self.course_key, cert_html_view_enabled=True, ) @ddt.data( (GlobalStaff, True), (SupportStaffRole, True), (None, False), ) @ddt.unpack def test_access_control(self, role, has_access): # Create a user and log in user = UserFactory(username="foo", password="foo") success = self.client.login(username="foo", password="foo") assert success, 'Could not log in' # Assign the user to the role if role is not None: role().add_users(user) # Retrieve the page response = self._search("foo") if has_access: self.assertContains(response, json.dumps([])) else: assert response.status_code == 403 @ddt.data( (CertificateSupportTestCase.STUDENT_USERNAME, True), (CertificateSupportTestCase.STUDENT_EMAIL, True), ("bar", False), ("bar@example.com", False), ("", False), (CertificateSupportTestCase.STUDENT_USERNAME, False, 'invalid_key'), (CertificateSupportTestCase.STUDENT_USERNAME, False, str(CertificateSupportTestCase.COURSE_NOT_EXIST_KEY)), (CertificateSupportTestCase.STUDENT_USERNAME, True, str(CertificateSupportTestCase.EXISTED_COURSE_KEY_1)), ) @ddt.unpack def test_search(self, user_filter, expect_result, course_filter=None): response = self._search(user_filter, course_filter) if expect_result: assert response.status_code == 200 results = json.loads(response.content.decode('utf-8')) assert len(results) == 1 else: assert response.status_code == 400 def test_search_with_plus_sign(self): """ Test that email address that contains '+' accepted by student support """ self.student.email = "student+student@example.com" self.student.save() response = self._search(self.student.email) assert response.status_code == 200 results = json.loads(response.content.decode('utf-8')) assert len(results) == 1 retrieved_data = results[0] assert retrieved_data['username'] == self.STUDENT_USERNAME def test_results(self): response = self._search(self.STUDENT_USERNAME) assert response.status_code == 200 results = json.loads(response.content.decode('utf-8')) assert len(results) == 1 retrieved_cert = results[0] assert retrieved_cert['username'] == self.STUDENT_USERNAME assert retrieved_cert['course_key'] == str(self.CERT_COURSE_KEY) assert retrieved_cert['created'] == self.cert.created_date.isoformat() assert retrieved_cert['modified'] == self.cert.modified_date.isoformat() assert retrieved_cert['grade'] == str(self.CERT_GRADE) assert retrieved_cert['status'] == self.CERT_STATUS assert retrieved_cert['type'] == self.CERT_MODE assert retrieved_cert['download_url'] == self.CERT_DOWNLOAD_URL assert not retrieved_cert['regenerate'] @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_download_link(self): self.cert.course_id = self.course_key self.cert.download_url = '' self.cert.save() response = self._search(self.STUDENT_USERNAME) assert response.status_code == 200 results = json.loads(response.content.decode('utf-8')) assert len(results) == 1 retrieved_cert = results[0] assert retrieved_cert['download_url'] ==\ reverse('certificates:render_cert_by_uuid', kwargs={'certificate_uuid': self.cert.verify_uuid}) assert retrieved_cert['regenerate'] def _search(self, user_filter, course_filter=None): """Execute a search and return the response. """ url = reverse("certificates:search") + "?user=" + user_filter if course_filter: url += '&course_id=' + course_filter return self.client.get(url) @ddt.ddt class CertificateRegenerateTests(CertificateSupportTestCase): """ Tests for the certificate regeneration end-point used by the support team. """ def setUp(self): """ Create a course and enroll the student in the course. """ super().setUp() self.course = CourseFactory( org=self.CERT_COURSE_KEY.org, course=self.CERT_COURSE_KEY.course, run=self.CERT_COURSE_KEY.run, ) self.course_key = self.course.id # pylint: disable=no-member CourseEnrollment.enroll(self.student, self.CERT_COURSE_KEY, self.CERT_MODE) @ddt.data( (GlobalStaff, True), (SupportStaffRole, True), (None, False), ) @ddt.unpack def test_access_control(self, role, has_access): # Create a user and log in user = UserFactory(username="foo", password="foo") success = self.client.login(username="foo", password="foo") assert success, 'Could not log in' # Assign the user to the role if role is not None: role().add_users(user) # Make a POST request # Since we're not passing valid parameters, we'll get an error response # but at least we'll know we have access response = self._regenerate() if has_access: assert response.status_code == 400 else: assert response.status_code == 403 def test_regenerate_certificate(self): """Test web certificate regeneration.""" self.cert.download_url = '' self.cert.save() response = self._regenerate( course_key=self.course_key, username=self.STUDENT_USERNAME, ) assert response.status_code == 200 # Check that the user's certificate was updated # Since the student hasn't actually passed the course, # we'd expect that the certificate status will be "notpassing" cert = GeneratedCertificate.eligible_certificates.get(user=self.student) assert cert.status == CertificateStatuses.notpassing @patch('lms.djangoapps.certificates.queue.XQueueCertInterface._generate_cert') def test_regenerate_certificate_for_honor_mode(self, mock_generate_cert): """Test web certificate regeneration for the users who have earned the certificate in honor mode """ self.cert.mode = 'honor' self.cert.download_url = '' self.cert.save() with mock_passing_grade(percent=0.75): with patch('common.djangoapps.course_modes.models.CourseMode.mode_for_course') as mock_mode_for_course: mock_mode_for_course.return_value = 'honor' regenerate_user_certificates(self.student, self.course_key, course=self.course) mock_generate_cert.assert_called() def test_regenerate_certificate_missing_params(self): # Missing username response = self._regenerate(course_key=self.CERT_COURSE_KEY) assert response.status_code == 400 # Missing course key response = self._regenerate(username=self.STUDENT_USERNAME) assert response.status_code == 400 def test_regenerate_no_such_user(self): response = self._regenerate( course_key=str(self.CERT_COURSE_KEY), username="invalid_username", ) assert response.status_code == 400 def test_regenerate_no_such_course(self): response = self._regenerate( course_key=CourseKey.from_string("edx/invalid/course"), username=self.STUDENT_USERNAME ) assert response.status_code == 400 def test_regenerate_user_is_not_enrolled(self): # Unenroll the user CourseEnrollment.unenroll(self.student, self.CERT_COURSE_KEY) # Can no longer regenerate certificates for the user response = self._regenerate( course_key=self.CERT_COURSE_KEY, username=self.STUDENT_USERNAME ) assert response.status_code == 400 def test_regenerate_user_has_no_certificate(self): # Delete the user's certificate GeneratedCertificate.eligible_certificates.all().delete() # Should be able to regenerate response = self._regenerate( course_key=self.CERT_COURSE_KEY, username=self.STUDENT_USERNAME ) assert response.status_code == 200 # A new certificate is created num_certs = GeneratedCertificate.eligible_certificates.filter(user=self.student).count() assert num_certs == 1 def test_regenerate_cert_with_invalidated_record(self): """ If the certificate is marked as invalid, regenerate the certificate. """ # mark certificate as invalid self._invalidate_certificate(self.cert) self.assertInvalidatedCertExists() # after invalidation certificate status become un-available. self.assertGeneratedCertExists( user=self.student, status=CertificateStatuses.unavailable ) # Should be able to regenerate response = self._regenerate( course_key=self.CERT_COURSE_KEY, username=self.STUDENT_USERNAME ) assert response.status_code == 200 self.assertInvalidatedCertExists() # Check that the user's certificate was updated # Since the student hasn't actually passed the course, # we'd expect that the certificate status will be "notpassing" self.assertGeneratedCertExists( user=self.student, status=CertificateStatuses.notpassing ) def _regenerate(self, course_key=None, username=None): """Call the regeneration end-point and return the response. """ url = reverse("certificates:regenerate_certificate_for_user") params = {} if course_key is not None: params["course_key"] = course_key if username is not None: params["username"] = username return self.client.post(url, params) def _invalidate_certificate(self, certificate): """ Dry method to mark certificate as invalid. """ CertificateInvalidationFactory.create( generated_certificate=certificate, invalidated_by=self.support, active=True ) # Invalidate user certificate certificate.invalidate() assert not certificate.is_valid() def assertInvalidatedCertExists(self): """ Dry method to check certificate invalidated entry exists. """ assert CertificateInvalidation.objects.filter(generated_certificate__user=self.student, active=True).exists() def assertInvalidatedCertDoesNotExist(self): """ Dry method to check certificate invalidated entry does not exists. """ assert not CertificateInvalidation.objects\ .filter(generated_certificate__user=self.student, active=True).exists() def assertGeneratedCertExists(self, user, status): """ Dry method to check if certificate exists. """ assert GeneratedCertificate.objects.filter(user=user, status=status).exists() @ddt.ddt class CertificateGenerateTests(CertificateSupportTestCase): """ Tests for the certificate generation end-point used by the support team. """ def setUp(self): """ Create a course and enroll the student in the course. """ super().setUp() self.course = CourseFactory( org=self.EXISTED_COURSE_KEY_2.org, course=self.EXISTED_COURSE_KEY_2.course, run=self.EXISTED_COURSE_KEY_2.run ) self.course_key = self.course.id # pylint: disable=no-member CourseEnrollment.enroll(self.student, self.EXISTED_COURSE_KEY_2, self.CERT_MODE) @ddt.data( (GlobalStaff, True), (SupportStaffRole, True), (None, False), ) @ddt.unpack def test_access_control(self, role, has_access): # Create a user and log in user = UserFactory(username="foo", password="foo") success = self.client.login(username="foo", password="foo") assert success, 'Could not log in' # Assign the user to the role if role is not None: role().add_users(user) # Make a POST request # Since we're not passing valid parameters, we'll get an error response # but at least we'll know we have access response = self._generate() if has_access: assert response.status_code == 400 else: assert response.status_code == 403 def test_generate_certificate(self): response = self._generate( course_key=self.course_key, username=self.STUDENT_USERNAME, ) assert response.status_code == 200 def test_generate_certificate_missing_params(self): # Missing username response = self._generate(course_key=self.EXISTED_COURSE_KEY_2) assert response.status_code == 400 # Missing course key response = self._generate(username=self.STUDENT_USERNAME) assert response.status_code == 400 def test_generate_no_such_user(self): response = self._generate( course_key=str(self.EXISTED_COURSE_KEY_2), username="invalid_username", ) assert response.status_code == 400 def test_generate_no_such_course(self): response = self._generate( course_key=CourseKey.from_string("edx/invalid/course"), username=self.STUDENT_USERNAME ) assert response.status_code == 400 def test_generate_user_is_not_enrolled(self): # Unenroll the user CourseEnrollment.unenroll(self.student, self.EXISTED_COURSE_KEY_2) # Can no longer regenerate certificates for the user response = self._generate( course_key=self.EXISTED_COURSE_KEY_2, username=self.STUDENT_USERNAME ) assert response.status_code == 400 def test_generate_user_has_no_certificate(self): # Delete the user's certificate GeneratedCertificate.eligible_certificates.all().delete() # Should be able to generate response = self._generate( course_key=self.EXISTED_COURSE_KEY_2, username=self.STUDENT_USERNAME ) assert response.status_code == 200 # A new certificate is created num_certs = GeneratedCertificate.eligible_certificates.filter(user=self.student).count() assert num_certs == 1 def _generate(self, course_key=None, username=None): """Call the generation end-point and return the response. """ url = reverse("certificates:generate_certificate_for_user") params = {} if course_key is not None: params["course_key"] = course_key if username is not None: params["username"] = username return self.client.post(url, params)