Files
edx-platform/lms/djangoapps/certificates/tests/test_generation.py
michaelroytman bb299c9521 feat: Remove Use of VERIFIED_NAME_FLAG Waffle Flag and is_verified_enabled Utility
The VERIFIED_NAME_FLAG, the VerifiedNameEnabledView, and the verified_name_enabled key removed from responses for both VerifiedNameView view and VerifiedNameHistoryView
were removed as part https://github.com/edx/edx-name-affirmation/pull/12. This was released in version 2.0.0 of the edx-name-affirmation PyPI package. Please see below for additional context for the removal, copied from the name-affirmation commit message.

The VERIFIED_NAME_FLAG was added as part https://github.com/edx/edx-name-affirmation/pull/12, [MST-801](https://openedx.atlassian.net/browse/MST-801) in order to control the release of the Verified Name project. It was used for a phased roll out by percentage of users.

The release reached a percentage of 50% before it was observed that, due to the way percentage roll out works in django-waffle, the code to create or update VerifiedName records was not working properly. The code was written such that any change to a SoftwareSecurePhotoVerification model instance sent a signal, which was received and handled by the Name Affirmation application. If the VERIFIED_NAME_FLAG was on for the requesting user, a Celery task was launched from the Name Affirmation application to perform the creation of or update to the appropriate VerifiedName model instances based on the verify_student application signal. However, we observed that when SoftwareSecurePhotoVerification records were moved into the "created" or "ready" status, a Celery task in Name Affirmation was created, but when SoftwareSecurePhotoVerification records were moved into the "submitted" status, the corresponding Celery task in Name Affirmation was not created. This caused VerifiedName records to stay in the "pending" state.

The django-waffle waffle flag used by the edx-toggle library implements percentage rollout by setting a cookie in a learner's browser session to assign them to the enabled or disabled group.
It turns out that the code that submits a SoftwareSecurePhotoVerification record, which moves it into the "submitted" state, happens as part of a Celery task in the verify_student application in the edx-platform. Therefore, we believe that because there is no request object in a Celery task, the edx-toggle code is defaulting to the case where there is no request object. In this case, the code checks whether the flag is enabled for everyone when determining whether the flag is enabled. Because of the percentage rollout (i.e. waffle flag not enabled for everyone), the Celery task in Name Affirmation is not created. This behavior was confirmed by logging added as part of https://github.com/edx/edx-name-affirmation/pull/62.

We have determined that we do not need the waffle flag, as we are comfortable that enabling the waffle flag for everyone will fix the issue and are comfortable releasing the feature to all users. For this reason, we are removing references to the flag.

[MST-1130](https://openedx.atlassian.net/browse/MST-1130)
2021-11-01 13:33:55 -04:00

227 lines
9.2 KiB
Python

"""
Tests for certificate generation
"""
import ddt
import logging
from unittest import mock
from edx_name_affirmation.api import create_verified_name, create_verified_name_config
from edx_name_affirmation.statuses import VerifiedNameStatus
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import UserProfile
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.util.testing import EventTestMixin
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.generation import generate_course_certificate
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
log = logging.getLogger(__name__)
PROFILE_NAME_METHOD = 'common.djangoapps.student.models_api.get_name'
@ddt.ddt
class CertificateTests(EventTestMixin, ModuleStoreTestCase):
"""
Tests for certificate generation
"""
def setUp(self): # pylint: disable=arguments-differ
super().setUp('lms.djangoapps.certificates.utils.tracker')
# Create user, a course run, and an enrollment
self.u = UserFactory()
self.profile = UserProfile.objects.get(user_id=self.u.id)
self.name = self.profile.name
self.cr = CourseFactory()
self.key = self.cr.id # pylint: disable=no-member
CourseEnrollmentFactory(
user=self.u,
course_id=self.key,
is_active=True,
mode=CourseMode.VERIFIED,
)
self.gen_mode = 'batch'
self.grade = '.85'
self.enrollment_mode = CourseMode.VERIFIED
def test_generation(self):
"""
Test certificate generation
"""
certs = GeneratedCertificate.objects.filter(user=self.u, course_id=self.key)
assert len(certs) == 0
generated_cert = generate_course_certificate(self.u, self.key, CertificateStatuses.downloadable,
self.enrollment_mode, self.grade, self.gen_mode)
self.assert_event_emitted(
'edx.certificate.created',
user_id=self.u.id,
course_id=str(self.key),
certificate_id=generated_cert.verify_uuid,
enrollment_mode=generated_cert.mode,
certificate_url='',
generation_mode=self.gen_mode
)
certs = GeneratedCertificate.objects.filter(user=self.u, course_id=self.key)
assert len(certs) == 1
cert = GeneratedCertificate.objects.get(user=self.u, course_id=self.key)
assert cert.status == CertificateStatuses.downloadable
assert cert.mode == self.enrollment_mode
assert cert.grade == self.grade
assert cert.name == self.name
def test_generation_existing_unverified(self):
"""
Test certificate generation when a certificate already exists and we want to mark it as unverified
"""
error_reason = 'Some PDF error'
GeneratedCertificateFactory(
user=self.u,
course_id=self.key,
mode=CourseMode.AUDIT,
status=CertificateStatuses.error,
error_reason=error_reason
)
cert = GeneratedCertificate.objects.get(user=self.u, course_id=self.key)
assert cert.error_reason == error_reason
assert cert.mode == CourseMode.AUDIT
assert cert.status == CertificateStatuses.error
generate_course_certificate(self.u, self.key, CertificateStatuses.unverified, self.enrollment_mode, self.grade,
self.gen_mode)
cert = GeneratedCertificate.objects.get(user=self.u, course_id=self.key)
assert cert.error_reason == ''
assert cert.status == CertificateStatuses.unverified
assert cert.mode == self.enrollment_mode
assert cert.grade == ''
def test_generation_existing_downloadable(self):
"""
Test certificate generation when a certificate already exists and we want to mark it as downloadable
"""
error_reason = 'Some PDF error'
GeneratedCertificateFactory(
user=self.u,
course_id=self.key,
mode=CourseMode.AUDIT,
status=CertificateStatuses.error,
error_reason=error_reason
)
generate_course_certificate(self.u, self.key, CertificateStatuses.downloadable, self.enrollment_mode,
self.grade, self.gen_mode)
cert = GeneratedCertificate.objects.get(user=self.u, course_id=self.key)
assert cert.error_reason == ''
assert cert.status == CertificateStatuses.downloadable
assert cert.mode == self.enrollment_mode
assert cert.grade == self.grade
def test_generation_uuid_persists_through_revocation(self):
"""
Test that the `verify_uuid` value of a certificate does not change when it is revoked and re-awarded.
"""
generated_cert = generate_course_certificate(self.u, self.key, CertificateStatuses.downloadable,
self.enrollment_mode, self.grade, self.gen_mode)
assert generated_cert.status, CertificateStatuses.downloadable
verify_uuid = generated_cert.verify_uuid
generated_cert.invalidate()
assert generated_cert.status, CertificateStatuses.unavailable
assert generated_cert.verify_uuid, verify_uuid
generated_cert = generate_course_certificate(self.u, self.key, CertificateStatuses.downloadable,
self.enrollment_mode, self.grade, self.gen_mode)
assert generated_cert.status, CertificateStatuses.downloadable
assert generated_cert.verify_uuid, verify_uuid
generated_cert.mark_notpassing(mode=generated_cert.mode, grade=50.00)
assert generated_cert.status, CertificateStatuses.notpassing
assert generated_cert.verify_uuid, verify_uuid
generated_cert = generate_course_certificate(self.u, self.key, CertificateStatuses.downloadable,
self.enrollment_mode, self.grade, self.gen_mode)
assert generated_cert.status, CertificateStatuses.downloadable
assert generated_cert.verify_uuid, verify_uuid
def test_generation_creates_verify_uuid_when_needed(self):
"""
Test that ensures we will create a verify_uuid when needed.
"""
GeneratedCertificateFactory(
user=self.u,
course_id=self.key,
mode=CourseMode.VERIFIED,
status=CertificateStatuses.unverified,
verify_uuid=''
)
generated_cert = generate_course_certificate(self.u, self.key, CertificateStatuses.downloadable,
self.enrollment_mode, self.grade, self.gen_mode)
assert generated_cert.status, CertificateStatuses.downloadable
assert generated_cert.verify_uuid != ''
def test_generation_missing_profile(self):
"""
Test certificate generation when the user profile is missing
"""
GeneratedCertificateFactory(
user=self.u,
course_id=self.key,
mode=CourseMode.AUDIT,
status=CertificateStatuses.unverified
)
with mock.patch(PROFILE_NAME_METHOD, return_value=None):
generate_course_certificate(self.u, self.key, CertificateStatuses.downloadable, self.enrollment_mode,
self.grade, self.gen_mode)
cert = GeneratedCertificate.objects.get(user=self.u, course_id=self.key)
assert cert.status == CertificateStatuses.downloadable
assert cert.mode == self.enrollment_mode
assert cert.grade == self.grade
assert cert.name == ''
@ddt.data((True, VerifiedNameStatus.APPROVED),
(True, VerifiedNameStatus.DENIED),
(False, VerifiedNameStatus.PENDING))
@ddt.unpack
def test_generation_verified_name(self, should_use_verified_name_for_certs, status):
"""
Test that if the user has their preference set to use
verified name for certificates, their verified name will appear on the certificate rather than
their profile name.
"""
verified_name = 'Jonathan Doe'
create_verified_name(self.u, verified_name, self.name, status=status)
create_verified_name_config(self.u, use_verified_name_for_certs=should_use_verified_name_for_certs)
GeneratedCertificateFactory(
user=self.u,
course_id=self.key,
mode=CourseMode.AUDIT,
status=CertificateStatuses.unverified
)
generate_course_certificate(
self.u, self.key, CertificateStatuses.downloadable, self.enrollment_mode, self.grade, self.gen_mode,
)
cert = GeneratedCertificate.objects.get(user=self.u, course_id=self.key)
if should_use_verified_name_for_certs and status == VerifiedNameStatus.APPROVED:
assert cert.name == verified_name
else:
assert cert.name == self.name