[MICROBA-1109] * Remove the requirement for the `auto_certificate_generation` waffle switch to be enabled in order for a learner to be eligible for a certificate. This switch should control _when_ a certificate is generated, not _if_ a certificate will be generated for a learner. * Move the `auto_certificate_generation_enabled` check to happen before we try to generate v2 certificates in signals.py * Update unit tests * Update documentation
445 lines
16 KiB
Python
445 lines
16 KiB
Python
"""
|
|
Tests for certificate generation handler
|
|
"""
|
|
import logging
|
|
from unittest import mock
|
|
|
|
import ddt
|
|
from edx_toggles.toggles.testutils import override_waffle_flag
|
|
|
|
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
|
from lms.djangoapps.certificates.generation_handler import (
|
|
CERTIFICATES_USE_ALLOWLIST,
|
|
CERTIFICATES_USE_UPDATED,
|
|
is_using_certificate_allowlist,
|
|
_is_using_v2_course_certificates,
|
|
_can_generate_allowlist_certificate,
|
|
_can_generate_certificate_for_status,
|
|
_can_generate_v2_certificate,
|
|
can_generate_certificate_task,
|
|
generate_allowlist_certificate_task,
|
|
generate_certificate_task,
|
|
generate_regular_certificate_task,
|
|
is_using_certificate_allowlist_and_is_on_allowlist
|
|
)
|
|
from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate
|
|
from lms.djangoapps.certificates.tests.factories import (
|
|
CertificateInvalidationFactory,
|
|
CertificateWhitelistFactory,
|
|
GeneratedCertificateFactory
|
|
)
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
BETA_TESTER_METHOD = 'lms.djangoapps.certificates.generation_handler._is_beta_tester'
|
|
CCX_COURSE_METHOD = 'lms.djangoapps.certificates.generation_handler._is_ccx_course'
|
|
ID_VERIFIED_METHOD = 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
|
|
PASSING_GRADE_METHOD = 'lms.djangoapps.certificates.generation_handler._has_passing_grade'
|
|
WEB_CERTS_METHOD = 'lms.djangoapps.certificates.generation_handler.has_html_certificates_enabled'
|
|
|
|
|
|
@override_waffle_flag(CERTIFICATES_USE_ALLOWLIST, active=True)
|
|
@mock.patch(ID_VERIFIED_METHOD, mock.Mock(return_value=True))
|
|
@mock.patch(WEB_CERTS_METHOD, mock.Mock(return_value=True))
|
|
@ddt.ddt
|
|
class AllowlistTests(ModuleStoreTestCase):
|
|
"""
|
|
Tests for handling allowlist certificates
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Create user, a course run, and an enrollment
|
|
self.user = UserFactory()
|
|
self.course_run = CourseFactory()
|
|
self.course_run_key = self.course_run.id # pylint: disable=no-member
|
|
self.enrollment = CourseEnrollmentFactory(
|
|
user=self.user,
|
|
course_id=self.course_run_key,
|
|
is_active=True,
|
|
mode="verified",
|
|
)
|
|
|
|
# Whitelist user
|
|
CertificateWhitelistFactory.create(course_id=self.course_run_key, user=self.user)
|
|
|
|
def test_is_using_allowlist_true(self):
|
|
"""
|
|
Test the allowlist flag
|
|
"""
|
|
assert is_using_certificate_allowlist(self.course_run_key)
|
|
|
|
@override_waffle_flag(CERTIFICATES_USE_ALLOWLIST, active=False)
|
|
def test_is_using_allowlist_false(self):
|
|
"""
|
|
Test the allowlist flag without the override
|
|
"""
|
|
assert not is_using_certificate_allowlist(self.course_run_key)
|
|
|
|
def test_is_using_allowlist_and_is_on_list(self):
|
|
"""
|
|
Test the allowlist flag and the presence of the user on the list
|
|
"""
|
|
assert is_using_certificate_allowlist_and_is_on_allowlist(self.user, self.course_run_key)
|
|
|
|
@override_waffle_flag(CERTIFICATES_USE_ALLOWLIST, active=False)
|
|
def test_is_using_allowlist_and_is_on_list_with_flag_off(self):
|
|
"""
|
|
Test the allowlist flag and the presence of the user on the list when the flag is off
|
|
"""
|
|
assert not is_using_certificate_allowlist_and_is_on_allowlist(self.user, self.course_run_key)
|
|
|
|
def test_is_using_allowlist_and_is_on_list_true(self):
|
|
"""
|
|
Test the allowlist flag and the presence of the user on the list when the user is not on the list
|
|
"""
|
|
u = UserFactory()
|
|
CourseEnrollmentFactory(
|
|
user=u,
|
|
course_id=self.course_run_key,
|
|
is_active=True,
|
|
mode="verified",
|
|
)
|
|
CertificateWhitelistFactory.create(course_id=self.course_run_key, user=u, whitelist=False)
|
|
assert not is_using_certificate_allowlist_and_is_on_allowlist(u, self.course_run_key)
|
|
|
|
@ddt.data(
|
|
(CertificateStatuses.deleted, True),
|
|
(CertificateStatuses.deleting, True),
|
|
(CertificateStatuses.downloadable, False),
|
|
(CertificateStatuses.error, True),
|
|
(CertificateStatuses.generating, True),
|
|
(CertificateStatuses.notpassing, True),
|
|
(CertificateStatuses.restricted, True),
|
|
(CertificateStatuses.unavailable, True),
|
|
(CertificateStatuses.audit_passing, True),
|
|
(CertificateStatuses.audit_notpassing, True),
|
|
(CertificateStatuses.honor_passing, True),
|
|
(CertificateStatuses.unverified, True),
|
|
(CertificateStatuses.invalidated, True),
|
|
(CertificateStatuses.requesting, True))
|
|
@ddt.unpack
|
|
def test_generation_status(self, status, expected_response):
|
|
"""
|
|
Test handling of certificate statuses
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
GeneratedCertificateFactory(
|
|
user=u,
|
|
course_id=key,
|
|
mode=GeneratedCertificate.MODES.verified,
|
|
status=status,
|
|
)
|
|
|
|
assert _can_generate_certificate_for_status(u, key) == expected_response
|
|
|
|
def test_generation_status_for_none(self):
|
|
"""
|
|
Test handling of certificate statuses for a non-existent cert
|
|
"""
|
|
assert _can_generate_certificate_for_status(None, None)
|
|
|
|
@override_waffle_flag(CERTIFICATES_USE_ALLOWLIST, active=False)
|
|
def test_handle_invalid(self):
|
|
"""
|
|
Test handling of an invalid user/course run combo
|
|
"""
|
|
assert not _can_generate_allowlist_certificate(self.user, self.course_run_key)
|
|
assert not generate_allowlist_certificate_task(self.user, self.course_run_key)
|
|
assert not can_generate_certificate_task(self.user, self.course_run_key)
|
|
assert not generate_certificate_task(self.user, self.course_run_key)
|
|
|
|
def test_handle_valid(self):
|
|
"""
|
|
Test handling of a valid user/course run combo
|
|
"""
|
|
assert _can_generate_allowlist_certificate(self.user, self.course_run_key)
|
|
assert generate_allowlist_certificate_task(self.user, self.course_run_key)
|
|
|
|
def test_handle_valid_general_methods(self):
|
|
"""
|
|
Test handling of a valid user/course run combo for the general (non-allowlist) generation methods
|
|
"""
|
|
assert can_generate_certificate_task(self.user, self.course_run_key)
|
|
assert generate_certificate_task(self.user, self.course_run_key)
|
|
|
|
def test_can_generate_not_verified(self):
|
|
"""
|
|
Test handling when the user's id is not verified
|
|
"""
|
|
with mock.patch(ID_VERIFIED_METHOD, return_value=False):
|
|
assert not _can_generate_allowlist_certificate(self.user, self.course_run_key)
|
|
|
|
def test_can_generate_not_enrolled(self):
|
|
"""
|
|
Test handling when user is not enrolled
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
CertificateWhitelistFactory.create(course_id=key, user=u)
|
|
assert not _can_generate_allowlist_certificate(u, key)
|
|
|
|
def test_can_generate_audit(self):
|
|
"""
|
|
Test handling when user is enrolled in audit mode
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
CourseEnrollmentFactory(
|
|
user=u,
|
|
course_id=key,
|
|
is_active=True,
|
|
mode="audit",
|
|
)
|
|
CertificateWhitelistFactory.create(course_id=key, user=u)
|
|
|
|
assert not _can_generate_allowlist_certificate(u, key)
|
|
|
|
def test_can_generate_not_whitelisted(self):
|
|
"""
|
|
Test handling when user is not whitelisted
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
CourseEnrollmentFactory(
|
|
user=u,
|
|
course_id=key,
|
|
is_active=True,
|
|
mode="verified",
|
|
)
|
|
assert not _can_generate_allowlist_certificate(u, key)
|
|
|
|
def test_can_generate_invalidated(self):
|
|
"""
|
|
Test handling when user is on the invalidate list
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
CourseEnrollmentFactory(
|
|
user=u,
|
|
course_id=key,
|
|
is_active=True,
|
|
mode="verified",
|
|
)
|
|
cert = GeneratedCertificateFactory(
|
|
user=u,
|
|
course_id=key,
|
|
mode=GeneratedCertificate.MODES.verified,
|
|
status=CertificateStatuses.downloadable
|
|
)
|
|
CertificateWhitelistFactory.create(course_id=key, user=u)
|
|
CertificateInvalidationFactory.create(
|
|
generated_certificate=cert,
|
|
invalidated_by=self.user,
|
|
active=True
|
|
)
|
|
|
|
assert not _can_generate_allowlist_certificate(u, key)
|
|
|
|
def test_can_generate_web_cert_disabled(self):
|
|
"""
|
|
Test handling when web certs are not enabled
|
|
"""
|
|
with mock.patch(WEB_CERTS_METHOD, return_value=False):
|
|
assert not _can_generate_allowlist_certificate(self.user, self.course_run_key)
|
|
|
|
|
|
@override_waffle_flag(CERTIFICATES_USE_UPDATED, active=True)
|
|
@mock.patch(ID_VERIFIED_METHOD, mock.Mock(return_value=True))
|
|
@mock.patch(CCX_COURSE_METHOD, mock.Mock(return_value=False))
|
|
@mock.patch(PASSING_GRADE_METHOD, mock.Mock(return_value=True))
|
|
@mock.patch(WEB_CERTS_METHOD, mock.Mock(return_value=True))
|
|
@ddt.ddt
|
|
class CertificateTests(ModuleStoreTestCase):
|
|
"""
|
|
Tests for handling course certificates
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Create user, a course run, and an enrollment
|
|
self.user = UserFactory()
|
|
self.course_run = CourseFactory()
|
|
self.course_run_key = self.course_run.id # pylint: disable=no-member
|
|
self.enrollment = CourseEnrollmentFactory(
|
|
user=self.user,
|
|
course_id=self.course_run_key,
|
|
is_active=True,
|
|
mode="verified",
|
|
)
|
|
|
|
def test_handle_valid(self):
|
|
"""
|
|
Test handling of a valid user/course run combo.
|
|
"""
|
|
assert _can_generate_v2_certificate(self.user, self.course_run_key)
|
|
assert can_generate_certificate_task(self.user, self.course_run_key)
|
|
assert generate_certificate_task(self.user, self.course_run_key)
|
|
|
|
def test_handle_valid_task(self):
|
|
"""
|
|
Test handling of a valid user/course run combo.
|
|
|
|
We test generate_certificate_task() and generate_regular_certificate_task() separately since they both
|
|
generate a cert.
|
|
"""
|
|
assert generate_regular_certificate_task(self.user, self.course_run_key)
|
|
|
|
@override_waffle_flag(CERTIFICATES_USE_UPDATED, active=False)
|
|
def test_handle_invalid(self):
|
|
"""
|
|
Test handling of an invalid user/course run combo
|
|
"""
|
|
assert not _can_generate_v2_certificate(self.user, self.course_run_key)
|
|
assert not can_generate_certificate_task(self.user, self.course_run_key)
|
|
assert not generate_certificate_task(self.user, self.course_run_key)
|
|
assert not generate_regular_certificate_task(self.user, self.course_run_key)
|
|
|
|
def test_is_using_updated_true(self):
|
|
"""
|
|
Test the updated flag
|
|
"""
|
|
assert _is_using_v2_course_certificates(self.course_run_key)
|
|
|
|
@override_waffle_flag(CERTIFICATES_USE_UPDATED, active=False)
|
|
def test_is_using_updated_false(self):
|
|
"""
|
|
Test the updated flag without the override
|
|
"""
|
|
assert not _is_using_v2_course_certificates(self.course_run_key)
|
|
|
|
@ddt.data(
|
|
(CertificateStatuses.deleted, True),
|
|
(CertificateStatuses.deleting, True),
|
|
(CertificateStatuses.downloadable, False),
|
|
(CertificateStatuses.error, True),
|
|
(CertificateStatuses.generating, True),
|
|
(CertificateStatuses.notpassing, True),
|
|
(CertificateStatuses.restricted, True),
|
|
(CertificateStatuses.unavailable, True),
|
|
(CertificateStatuses.audit_passing, True),
|
|
(CertificateStatuses.audit_notpassing, True),
|
|
(CertificateStatuses.honor_passing, True),
|
|
(CertificateStatuses.unverified, True),
|
|
(CertificateStatuses.invalidated, True),
|
|
(CertificateStatuses.requesting, True))
|
|
@ddt.unpack
|
|
def test_generation_status(self, status, expected_response):
|
|
"""
|
|
Test handling of certificate statuses
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
GeneratedCertificateFactory(
|
|
user=u,
|
|
course_id=key,
|
|
mode=GeneratedCertificate.MODES.verified,
|
|
status=status,
|
|
)
|
|
|
|
assert _can_generate_certificate_for_status(u, key) == expected_response
|
|
|
|
def test_generation_status_for_none(self):
|
|
"""
|
|
Test handling of certificate statuses for a non-existent cert
|
|
"""
|
|
assert _can_generate_certificate_for_status(None, None)
|
|
|
|
def test_can_generate_not_verified(self):
|
|
"""
|
|
Test handling when the user's id is not verified
|
|
"""
|
|
with mock.patch(ID_VERIFIED_METHOD, return_value=False):
|
|
assert not _can_generate_v2_certificate(self.user, self.course_run_key)
|
|
|
|
def test_can_generate_ccx(self):
|
|
"""
|
|
Test handling when the course is a CCX (custom edX) course
|
|
"""
|
|
with mock.patch(CCX_COURSE_METHOD, return_value=True):
|
|
assert not _can_generate_v2_certificate(self.user, self.course_run_key)
|
|
|
|
def test_can_generate_beta_tester(self):
|
|
"""
|
|
Test handling when the user is a beta tester
|
|
"""
|
|
with mock.patch(BETA_TESTER_METHOD, return_value=True):
|
|
assert not _can_generate_v2_certificate(self.user, self.course_run_key)
|
|
|
|
def test_can_generate_failing_grade(self):
|
|
"""
|
|
Test handling when the user does not have a passing grade
|
|
"""
|
|
with mock.patch(PASSING_GRADE_METHOD, return_value=False):
|
|
assert not _can_generate_v2_certificate(self.user, self.course_run_key)
|
|
|
|
def test_can_generate_not_enrolled(self):
|
|
"""
|
|
Test handling when user is not enrolled
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
assert not _can_generate_v2_certificate(u, key)
|
|
|
|
def test_can_generate_audit(self):
|
|
"""
|
|
Test handling when user is enrolled in audit mode
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
CourseEnrollmentFactory(
|
|
user=u,
|
|
course_id=key,
|
|
is_active=True,
|
|
mode="audit",
|
|
)
|
|
|
|
assert not _can_generate_v2_certificate(u, key)
|
|
|
|
def test_can_generate_invalidated(self):
|
|
"""
|
|
Test handling when user is on the invalidate list
|
|
"""
|
|
u = UserFactory()
|
|
cr = CourseFactory()
|
|
key = cr.id # pylint: disable=no-member
|
|
CourseEnrollmentFactory(
|
|
user=u,
|
|
course_id=key,
|
|
is_active=True,
|
|
mode="verified",
|
|
)
|
|
cert = GeneratedCertificateFactory(
|
|
user=u,
|
|
course_id=key,
|
|
mode=GeneratedCertificate.MODES.verified,
|
|
status=CertificateStatuses.downloadable
|
|
)
|
|
CertificateInvalidationFactory.create(
|
|
generated_certificate=cert,
|
|
invalidated_by=self.user,
|
|
active=True
|
|
)
|
|
|
|
assert not _can_generate_v2_certificate(u, key)
|
|
|
|
def test_can_generate_web_cert_disabled(self):
|
|
"""
|
|
Test handling when web certs are not enabled
|
|
"""
|
|
with mock.patch(WEB_CERTS_METHOD, return_value=False):
|
|
assert not _can_generate_v2_certificate(self.user, self.course_run_key)
|