diff --git a/openedx/core/djangoapps/programs/tests/test_tasks.py b/openedx/core/djangoapps/programs/tests/test_tasks.py index 7e0db4fc6a..5a2518af78 100644 --- a/openedx/core/djangoapps/programs/tests/test_tasks.py +++ b/openedx/core/djangoapps/programs/tests/test_tasks.py @@ -19,23 +19,31 @@ from django.test import TestCase, override_settings from edx_rest_api_client.auth import SuppliedJwtAuth from requests.exceptions import HTTPError from testfixtures import LogCapture -from xmodule.data import CertificatesDisplayBehaviors from common.djangoapps.course_modes.tests.factories import CourseModeFactory from common.djangoapps.student.tests.factories import UserFactory -from lms.djangoapps.certificates.tests.factories import CertificateDateOverrideFactory, GeneratedCertificateFactory +from lms.djangoapps.certificates.tests.factories import ( + CertificateDateOverrideFactory, + GeneratedCertificateFactory, +) from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin -from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from openedx.core.djangoapps.content.course_overviews.tests.factories import ( + CourseOverviewFactory, +) from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFactory from openedx.core.djangoapps.programs import tasks -from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory +from openedx.core.djangoapps.site_configuration.tests.factories import ( + SiteConfigurationFactory, + SiteFactory, +) from openedx.core.djangolib.testing.utils import skip_unless_lms +from xmodule.data import CertificatesDisplayBehaviors log = logging.getLogger(__name__) -CREDENTIALS_INTERNAL_SERVICE_URL = 'https://credentials.example.com' -TASKS_MODULE = 'openedx.core.djangoapps.programs.tasks' +CREDENTIALS_INTERNAL_SERVICE_URL = "https://credentials.example.com" +TASKS_MODULE = "openedx.core.djangoapps.programs.tasks" @skip_unless_lms @@ -49,32 +57,36 @@ class GetAwardedCertificateProgramsTestCase(TestCase): Helper to make dummy results from the credentials API """ result = { - 'id': 1, - 'username': 'dummy-username', - 'credential': { - 'credential_id': None, - 'program_uuid': None, + "id": 1, + "username": "dummy-username", + "credential": { + "credential_id": None, + "program_uuid": None, }, - 'status': 'dummy-status', - 'uuid': 'dummy-uuid', - 'certificate_url': 'http://credentials.edx.org/credentials/dummy-uuid/' + "status": "dummy-status", + "uuid": "dummy-uuid", + "certificate_url": "http://credentials.edx.org/credentials/dummy-uuid/", } result.update(**kwargs) return result - @mock.patch(TASKS_MODULE + '.get_credentials') + @mock.patch(TASKS_MODULE + ".get_credentials") def test_get_certified_programs(self, mock_get_credentials): """ Ensure the API is called and results handled correctly. """ - student = UserFactory(username='test-username') + student = UserFactory(username="test-username") mock_get_credentials.return_value = [ - self.make_credential_result(status='awarded', credential={'program_uuid': 1}), + self.make_credential_result( + status="awarded", credential={"program_uuid": 1} + ), ] result = tasks.get_certified_programs(student) assert mock_get_credentials.call_args[0] == (student,) - assert mock_get_credentials.call_args[1] == {'credential_type': 'program'} + assert ( + mock_get_credentials.call_args[1].get("credential_type", None) == "program" + ) assert result == [1] @@ -83,50 +95,55 @@ class AwardProgramCertificateTestCase(TestCase): """ Test the award_program_certificate function """ + @httpretty.activate - @mock.patch('openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url') + @mock.patch("openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url") def test_award_program_certificate(self, mock_get_api_base_url): """ Ensure the correct API call gets made """ - mock_get_api_base_url.return_value = 'http://test-server/' - student = UserFactory(username='test-username', email='test-email@email.com') + mock_get_api_base_url.return_value = "http://test-server/" + student = UserFactory(username="test-username", email="test-email@email.com") test_client = requests.Session() - test_client.auth = SuppliedJwtAuth('test-token') + test_client.auth = SuppliedJwtAuth("test-token") httpretty.register_uri( httpretty.POST, - 'http://test-server/credentials/', + "http://test-server/credentials/", ) - tasks.award_program_certificate(test_client, student, 123, datetime(2010, 5, 30)) + tasks.award_program_certificate( + test_client, student, 123, datetime(2010, 5, 30) + ) expected_body = { - 'username': student.username, - 'lms_user_id': student.id, - 'credential': { - 'program_uuid': 123, - 'type': tasks.PROGRAM_CERTIFICATE, + "username": student.username, + "lms_user_id": student.id, + "credential": { + "program_uuid": 123, + "type": tasks.PROGRAM_CERTIFICATE, }, - 'attributes': [ + "attributes": [ { - 'name': 'visible_date', - 'value': '2010-05-30T00:00:00Z', + "name": "visible_date", + "value": "2010-05-30T00:00:00Z", } - ] + ], } - last_request_body = httpretty.last_request().body.decode('utf-8') + last_request_body = httpretty.last_request().body.decode("utf-8") assert json.loads(last_request_body) == expected_body @skip_unless_lms @ddt.ddt -@mock.patch(TASKS_MODULE + '.award_program_certificate') -@mock.patch(TASKS_MODULE + '.get_certified_programs') -@mock.patch(TASKS_MODULE + '.get_completed_programs') -@override_settings(CREDENTIALS_SERVICE_USERNAME='test-service-username') -class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase): +@mock.patch(TASKS_MODULE + ".award_program_certificate") +@mock.patch(TASKS_MODULE + ".get_certified_programs") +@mock.patch(TASKS_MODULE + ".get_completed_programs") +@override_settings(CREDENTIALS_SERVICE_USERNAME="test-service-username") +class AwardProgramCertificatesTestCase( + CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase +): """ Tests for the 'award_program_certificates' celery task. """ @@ -134,11 +151,11 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo def setUp(self): super().setUp() self.create_credentials_config() - self.student = UserFactory.create(username='test-student') + self.student = UserFactory.create(username="test-student") self.site = SiteFactory() self.site_configuration = SiteConfigurationFactory(site=self.site) self.catalog_integration = self.create_catalog_integration() - ApplicationFactory.create(name='credentials') + ApplicationFactory.create(name="credentials") UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) def test_completion_check( @@ -177,20 +194,26 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo tasks.award_program_certificates.delay(self.student.username).get() - actual_program_uuids = [call[0][2] for call in mock_award_program_certificate.call_args_list] + actual_program_uuids = [ + call[0][2] for call in mock_award_program_certificate.call_args_list + ] assert actual_program_uuids == expected_awarded_program_uuids - actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list] + actual_visible_dates = [ + call[0][3] for call in mock_award_program_certificate.call_args_list + ] assert actual_visible_dates == expected_awarded_program_uuids # program uuids are same as mock dates - @mock.patch('openedx.core.djangoapps.site_configuration.helpers.get_current_site_configuration') + @mock.patch( + "openedx.core.djangoapps.site_configuration.helpers.get_current_site_configuration" + ) def test_awarding_certs_with_skip_program_certificate( - self, - mocked_get_current_site_configuration, - mock_get_completed_programs, - mock_get_certified_programs, - mock_award_program_certificate, + self, + mocked_get_current_site_configuration, + mock_get_completed_programs, + mock_get_certified_programs, + mock_award_program_certificate, ): """ Checks that the Credentials API is used to award certificates for @@ -204,9 +227,7 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo mock_get_certified_programs.return_value = [1] # programs to be skipped - self.site_configuration.site_values = { - "programs_without_certificates": [2] - } + self.site_configuration.site_values = {"programs_without_certificates": [2]} self.site_configuration.save() mocked_get_current_site_configuration.return_value = self.site_configuration @@ -215,28 +236,31 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo expected_awarded_program_uuids = [3, 4] tasks.award_program_certificates.delay(self.student.username).get() - actual_program_uuids = [call[0][2] for call in mock_award_program_certificate.call_args_list] + actual_program_uuids = [ + call[0][2] for call in mock_award_program_certificate.call_args_list + ] assert actual_program_uuids == expected_awarded_program_uuids - actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list] + actual_visible_dates = [ + call[0][3] for call in mock_award_program_certificate.call_args_list + ] assert actual_visible_dates == expected_awarded_program_uuids # program uuids are same as mock dates @ddt.data( - ('credentials', 'enable_learner_issuance'), + ("credentials", "enable_learner_issuance"), ) @ddt.unpack def test_retry_if_config_disabled( - self, - disabled_config_type, - disabled_config_attribute, - *mock_helpers + self, disabled_config_type, disabled_config_attribute, *mock_helpers ): """ Checks that the task is aborted if any relevant api configs are disabled. """ - getattr(self, f'create_{disabled_config_type}_config')(**{disabled_config_attribute: False}) - with mock.patch(TASKS_MODULE + '.LOGGER.warning') as mock_warning: + getattr(self, f"create_{disabled_config_type}_config")( + **{disabled_config_attribute: False} + ) + with mock.patch(TASKS_MODULE + ".LOGGER.warning") as mock_warning: with pytest.raises(MaxRetriesExceededError): tasks.award_program_certificates.delay(self.student.username).get() assert mock_warning.called @@ -248,8 +272,8 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo Checks that the task will be aborted and not retried if the username passed was not found, and that an exception is logged. """ - with mock.patch(TASKS_MODULE + '.LOGGER.exception') as mock_exception: - tasks.award_program_certificates.delay('nonexistent-username').get() + with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception: + tasks.award_program_certificates.delay("nonexistent-username").get() assert mock_exception.called for mock_helper in mock_helpers: assert not mock_helper.called @@ -270,13 +294,13 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo assert not mock_get_certified_programs.called assert not mock_award_program_certificate.called - @mock.patch('openedx.core.djangoapps.site_configuration.helpers.get_value') + @mock.patch("openedx.core.djangoapps.site_configuration.helpers.get_value") def test_programs_without_certificates( self, mock_get_value, mock_get_completed_programs, mock_get_certified_programs, - mock_award_program_certificate + mock_award_program_certificate, ): """ Checks that the task will be aborted without further action if there exists a list @@ -289,22 +313,22 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo assert not mock_get_certified_programs.called assert not mock_award_program_certificate.called - @mock.patch(TASKS_MODULE + '.get_credentials_api_client') + @mock.patch(TASKS_MODULE + ".get_credentials_api_client") def test_failure_to_create_api_client_retries( self, mock_get_api_client, mock_get_completed_programs, mock_get_certified_programs, - mock_award_program_certificate + mock_award_program_certificate, ): """ Checks that we log an exception and retry if the API client isn't creating. """ - mock_get_api_client.side_effect = Exception('boom') + mock_get_api_client.side_effect = Exception("boom") mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_get_certified_programs.return_value = [2] - with mock.patch(TASKS_MODULE + '.LOGGER.exception') as mock_exception: + with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception: with pytest.raises(MaxRetriesExceededError): tasks.award_program_certificates.delay(self.student.username).get() @@ -347,25 +371,30 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo """ mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_get_certified_programs.side_effect = [[], [2]] - mock_award_program_certificate.side_effect = self._make_side_effect([Exception('boom'), None]) + mock_award_program_certificate.side_effect = self._make_side_effect( + [Exception("boom"), None] + ) - with mock.patch(TASKS_MODULE + '.LOGGER.info') as mock_info, \ - mock.patch(TASKS_MODULE + '.LOGGER.exception') as mock_warning: + with mock.patch(TASKS_MODULE + ".LOGGER.info") as mock_info, mock.patch( + TASKS_MODULE + ".LOGGER.exception" + ) as mock_warning: tasks.award_program_certificates.delay(self.student.username).get() assert mock_award_program_certificate.call_count == 3 mock_warning.assert_called_once_with( - 'Failed to award certificate for program {uuid} to user {username}.'.format( - uuid=1, - username=self.student.username) + "Failed to award certificate for program {uuid} to user {username}.".format( + uuid=1, username=self.student.username + ) + ) + mock_info.assert_any_call( + f"Awarded certificate for program {1} to user {self.student.username}" + ) + mock_info.assert_any_call( + f"Awarded certificate for program {2} to user {self.student.username}" ) - mock_info.assert_any_call(f"Awarded certificate for program {1} to user {self.student.username}") - mock_info.assert_any_call(f"Awarded certificate for program {2} to user {self.student.username}") def test_retry_on_programs_api_errors( - self, - mock_get_completed_programs, - *_mock_helpers + self, mock_get_completed_programs, *_mock_helpers ): """ Ensures that any otherwise-unhandled errors that arise while trying @@ -373,7 +402,9 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo transient API errors) will cause the task to be failed and queued for retry. """ - mock_get_completed_programs.side_effect = self._make_side_effect([Exception('boom'), None]) + mock_get_completed_programs.side_effect = self._make_side_effect( + [Exception("boom"), None] + ) tasks.award_program_certificates.delay(self.student.username).get() assert mock_get_completed_programs.call_count == 3 @@ -391,7 +422,9 @@ class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiCo """ mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_get_certified_programs.return_value = [1] - mock_get_certified_programs.side_effect = self._make_side_effect([Exception('boom'), None]) + mock_get_certified_programs.side_effect = self._make_side_effect( + [Exception("boom"), None] + ) tasks.award_program_certificates.delay(self.student.username).get() assert mock_get_certified_programs.call_count == 2 assert mock_award_program_certificate.call_count == 1 @@ -464,59 +497,68 @@ class PostCourseCertificateTestCase(TestCase): """ def setUp(self): # lint-amnesty, pylint: disable=super-method-not-called - self.student = UserFactory.create(username='test-student') + self.student = UserFactory.create(username="test-student") self.course = CourseOverviewFactory.create( self_paced=True # Any option to allow the certificate to be viewable for the course ) self.certificate = GeneratedCertificateFactory( user=self.student, - mode='verified', + mode="verified", course_id=self.course.id, - status='downloadable' + status="downloadable", ) @httpretty.activate - @mock.patch('openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url') + @mock.patch("openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url") def test_post_course_certificate(self, mock_get_api_base_url): """ Ensure the correct API call gets made """ - mock_get_api_base_url.return_value = 'http://test-server/' + mock_get_api_base_url.return_value = "http://test-server/" test_client = requests.Session() - test_client.auth = SuppliedJwtAuth('test-token') + test_client.auth = SuppliedJwtAuth("test-token") httpretty.register_uri( httpretty.POST, - 'http://test-server/credentials/', + "http://test-server/credentials/", ) visible_date = datetime.now() - tasks.post_course_certificate(test_client, self.student.username, self.certificate, visible_date) + tasks.post_course_certificate( + test_client, self.student.username, self.certificate, visible_date + ) expected_body = { - 'username': self.student.username, - 'status': 'awarded', - 'credential': { - 'course_run_key': str(self.certificate.course_id), - 'mode': self.certificate.mode, - 'type': tasks.COURSE_CERTIFICATE, + "username": self.student.username, + "status": "awarded", + "credential": { + "course_run_key": str(self.certificate.course_id), + "mode": self.certificate.mode, + "type": tasks.COURSE_CERTIFICATE, }, - 'date_override': None, - 'attributes': [{ - 'name': 'visible_date', - 'value': visible_date.strftime('%Y-%m-%dT%H:%M:%SZ') # text representation of date - }] + "date_override": None, + "attributes": [ + { + "name": "visible_date", + "value": visible_date.strftime( + "%Y-%m-%dT%H:%M:%SZ" + ), # text representation of date + } + ], } - last_request_body = httpretty.last_request().body.decode('utf-8') + last_request_body = httpretty.last_request().body.decode("utf-8") assert json.loads(last_request_body) == expected_body @skip_unless_lms @ddt.ddt -@mock.patch("lms.djangoapps.certificates.api.auto_certificate_generation_enabled", mock.Mock(return_value=True)) -@mock.patch(TASKS_MODULE + '.post_course_certificate') -@override_settings(CREDENTIALS_SERVICE_USERNAME='test-service-username') +@mock.patch( + "lms.djangoapps.certificates.api.auto_certificate_generation_enabled", + mock.Mock(return_value=True), +) +@mock.patch(TASKS_MODULE + ".post_course_certificate") +@override_settings(CREDENTIALS_SERVICE_USERNAME="test-service-username") class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase): """ Test the award_course_certificate celery task @@ -529,21 +571,21 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase): self.course = CourseOverviewFactory.create( self_paced=True, # Any option to allow the certificate to be viewable for the course certificate_available_date=self.available_date, - certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE + certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE, ) - self.student = UserFactory.create(username='test-student') + self.student = UserFactory.create(username="test-student") # Instantiate the Certificate first so that the config doesn't execute issuance self.certificate = GeneratedCertificateFactory.create( user=self.student, - mode='verified', + mode="verified", course_id=self.course.id, - status='downloadable' + status="downloadable", ) self.create_credentials_config() self.site = SiteFactory() - ApplicationFactory.create(name='credentials') + ApplicationFactory.create(name="credentials") UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) def _add_certificate_date_override(self): @@ -552,12 +594,12 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase): """ self.certificate.date_override = CertificateDateOverrideFactory.create( generated_certificate=self.certificate, - overridden_by=UserFactory.create(username='test-admin'), + overridden_by=UserFactory.create(username="test-admin"), ) @ddt.data( - 'verified', - 'no-id-professional', + "verified", + "no-id-professional", ) def test_award_course_certificates(self, mode, mock_post_course_certificate): """ @@ -565,89 +607,119 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase): """ self.certificate.mode = mode self.certificate.save() - tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get() + tasks.award_course_certificate.delay( + self.student.username, str(self.course.id) + ).get() call_args, _ = mock_post_course_certificate.call_args assert call_args[1] == self.student.username assert call_args[2] == self.certificate assert call_args[3] == self.certificate.modified_date - def test_award_course_certificates_available_date(self, mock_post_course_certificate): + def test_award_course_certificates_available_date( + self, mock_post_course_certificate + ): """ Tests the API POST method is called with available date when the course is not self paced """ self.course.self_paced = False self.course.save() - tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get() + tasks.award_course_certificate.delay( + self.student.username, str(self.course.id) + ).get() call_args, _ = mock_post_course_certificate.call_args assert call_args[1] == self.student.username assert call_args[2] == self.certificate assert call_args[3] == self.available_date - def test_award_course_certificates_override_date(self, mock_post_course_certificate): + def test_award_course_certificates_override_date( + self, mock_post_course_certificate + ): """ Tests the API POST method is called with date override when present """ self._add_certificate_date_override() - tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get() + tasks.award_course_certificate.delay( + self.student.username, str(self.course.id) + ).get() call_args, _ = mock_post_course_certificate.call_args assert call_args[1] == self.student.username assert call_args[2] == self.certificate assert call_args[3] == self.certificate.modified_date assert call_args[4] == self.certificate.date_override.date - def test_award_course_cert_not_called_if_disabled(self, mock_post_course_certificate): + def test_award_course_cert_not_called_if_disabled( + self, mock_post_course_certificate + ): """ Test that the post method is never called if the config is disabled """ self.create_credentials_config(enabled=False) - with mock.patch(TASKS_MODULE + '.LOGGER.warning') as mock_warning: + with mock.patch(TASKS_MODULE + ".LOGGER.warning") as mock_warning: with pytest.raises(MaxRetriesExceededError): - tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get() + tasks.award_course_certificate.delay( + self.student.username, str(self.course.id) + ).get() assert mock_warning.called assert not mock_post_course_certificate.called - def test_award_course_cert_not_called_if_user_not_found(self, mock_post_course_certificate): + def test_award_course_cert_not_called_if_user_not_found( + self, mock_post_course_certificate + ): """ Test that the post method is never called if the user isn't found by username """ - with mock.patch(TASKS_MODULE + '.LOGGER.exception') as mock_exception: + with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception: # Use a random username here since this user won't be found in the DB - tasks.award_course_certificate.delay('random_username', str(self.course.id)).get() + tasks.award_course_certificate.delay( + "random_username", str(self.course.id) + ).get() assert mock_exception.called assert not mock_post_course_certificate.called - def test_award_course_cert_not_called_if_certificate_not_found(self, mock_post_course_certificate): + def test_award_course_cert_not_called_if_certificate_not_found( + self, mock_post_course_certificate + ): """ Test that the post method is never called if the certificate doesn't exist for the user and course """ self.certificate.delete() - with mock.patch(TASKS_MODULE + '.LOGGER.exception') as mock_exception: - tasks.award_course_certificate.delay(self.student.username, str(self.course.id)).get() + with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception: + tasks.award_course_certificate.delay( + self.student.username, str(self.course.id) + ).get() assert mock_exception.called assert not mock_post_course_certificate.called - def test_award_course_cert_not_called_if_course_overview_not_found(self, mock_post_course_certificate): + def test_award_course_cert_not_called_if_course_overview_not_found( + self, mock_post_course_certificate + ): """ Test that the post method is never called if the CourseOverview isn't found """ self.course.delete() - with mock.patch(TASKS_MODULE + '.LOGGER.exception') as mock_exception: + with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception: # Use the certificate course id here since the course will be deleted - tasks.award_course_certificate.delay(self.student.username, str(self.certificate.course_id)).get() + tasks.award_course_certificate.delay( + self.student.username, str(self.certificate.course_id) + ).get() assert mock_exception.called assert not mock_post_course_certificate.called - def test_award_course_cert_not_called_if_certificated_not_verified_mode(self, mock_post_course_certificate): + def test_award_course_cert_not_called_if_certificated_not_verified_mode( + self, mock_post_course_certificate + ): """ Test that the post method is never called if the GeneratedCertificate is an 'audit' cert """ # Temporarily disable the config so the signal isn't handled from .save self.create_credentials_config(enabled=False) - self.certificate.mode = 'audit' + self.certificate.mode = "audit" self.certificate.save() self.create_credentials_config() - tasks.award_course_certificate.delay(self.student.username, str(self.certificate.course_id)).get() + tasks.award_course_certificate.delay( + self.student.username, str(self.certificate.course_id) + ).get() assert not mock_post_course_certificate.called @@ -658,42 +730,44 @@ class RevokeProgramCertificateTestCase(TestCase): """ @httpretty.activate - @mock.patch('openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url') + @mock.patch("openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url") def test_revoke_program_certificate(self, mock_get_api_base_url): """ Ensure the correct API call gets made """ - mock_get_api_base_url.return_value = 'http://test-server/' - test_username = 'test-username' + mock_get_api_base_url.return_value = "http://test-server/" + test_username = "test-username" test_client = requests.Session() - test_client.auth = SuppliedJwtAuth('test-token') + test_client.auth = SuppliedJwtAuth("test-token") httpretty.register_uri( httpretty.POST, - 'http://test-server/credentials/', + "http://test-server/credentials/", ) tasks.revoke_program_certificate(test_client, test_username, 123) expected_body = { - 'username': test_username, - 'status': 'revoked', - 'credential': { - 'program_uuid': 123, - 'type': tasks.PROGRAM_CERTIFICATE, - } + "username": test_username, + "status": "revoked", + "credential": { + "program_uuid": 123, + "type": tasks.PROGRAM_CERTIFICATE, + }, } - last_request_body = httpretty.last_request().body.decode('utf-8') + last_request_body = httpretty.last_request().body.decode("utf-8") assert json.loads(last_request_body) == expected_body @skip_unless_lms @ddt.ddt -@mock.patch(TASKS_MODULE + '.revoke_program_certificate') -@mock.patch(TASKS_MODULE + '.get_certified_programs') -@mock.patch(TASKS_MODULE + '.get_inverted_programs') -@override_settings(CREDENTIALS_SERVICE_USERNAME='test-service-username') -class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase): +@mock.patch(TASKS_MODULE + ".revoke_program_certificate") +@mock.patch(TASKS_MODULE + ".get_certified_programs") +@mock.patch(TASKS_MODULE + ".get_inverted_programs") +@override_settings(CREDENTIALS_SERVICE_USERNAME="test-service-username") +class RevokeProgramCertificatesTestCase( + CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase +): """ Tests for the 'revoke_program_certificates' celery task. """ @@ -701,29 +775,24 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC def setUp(self): super().setUp() - self.student = UserFactory.create(username='test-student') - self.course_key = 'course-v1:testX+test101+2T2020' + self.student = UserFactory.create(username="test-student") + self.course_key = "course-v1:testX+test101+2T2020" self.site = SiteFactory() self.site_configuration = SiteConfigurationFactory(site=self.site) - ApplicationFactory.create(name='credentials') + ApplicationFactory.create(name="credentials") UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) self.create_credentials_config() - self.inverted_programs = {self.course_key: [{'uuid': 1}, {'uuid': 2}]} + self.inverted_programs = {self.course_key: [{"uuid": 1}, {"uuid": 2}]} - def _make_side_effect(self, side_effects): + def _make_side_effect(self, side_effects, *args, **kwargs): """ DRY helper. Returns a side effect function for use with mocks that will be called multiple times, permitting Exceptions to be raised (or not) in a specified order. - - See Also: - http://www.voidspace.org.uk/python/mock/examples.html#multiple-calls-with-different-effects - http://www.voidspace.org.uk/python/mock/mock.html#mock.Mock.side_effect - """ - def side_effect(*_a): + def side_effect(*args, **kwargs): if side_effects: exc = side_effects.pop(0) if exc: @@ -742,7 +811,9 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC Checks that the Programs API is used correctly to determine completed programs. """ - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() mock_get_inverted_programs.assert_any_call(self.student) def test_revoke_program_certificate( @@ -757,34 +828,37 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC """ expected_program_uuid = 1 mock_get_inverted_programs.return_value = { - self.course_key: [{'uuid': expected_program_uuid}] + self.course_key: [{"uuid": expected_program_uuid}] } mock_get_certified_programs.return_value = [expected_program_uuid] - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() call_args, _ = mock_revoke_program_certificate.call_args assert call_args[1] == self.student.username assert call_args[2] == expected_program_uuid @ddt.data( - ('credentials', 'enable_learner_issuance'), + ("credentials", "enable_learner_issuance"), ) @ddt.unpack def test_retry_if_config_disabled( - self, - disabled_config_type, - disabled_config_attribute, - *mock_helpers + self, disabled_config_type, disabled_config_attribute, *mock_helpers ): """ Checks that the task is aborted if any relevant api configs are disabled. """ - getattr(self, f'create_{disabled_config_type}_config')(**{disabled_config_attribute: False}) - with mock.patch(TASKS_MODULE + '.LOGGER.warning') as mock_warning: + getattr(self, f"create_{disabled_config_type}_config")( + **{disabled_config_attribute: False} + ) + with mock.patch(TASKS_MODULE + ".LOGGER.warning") as mock_warning: with pytest.raises(MaxRetriesExceededError): - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() assert mock_warning.called for mock_helper in mock_helpers: assert not mock_helper.called @@ -794,8 +868,10 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC Checks that the task will be aborted and not retried if the username passed was not found, and that an exception is logged. """ - with mock.patch(TASKS_MODULE + '.LOGGER.exception') as mock_exception: - tasks.revoke_program_certificates.delay('nonexistent-username', self.course_key).get() + with mock.patch(TASKS_MODULE + ".LOGGER.exception") as mock_exception: + tasks.revoke_program_certificates.delay( + "nonexistent-username", self.course_key + ).get() assert mock_exception.called for mock_helper in mock_helpers: assert not mock_helper.called @@ -811,7 +887,9 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC not part of any program. """ mock_get_inverted_programs.return_value = {} - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() assert mock_get_inverted_programs.called assert not mock_get_certified_programs.called assert not mock_revoke_program_certificate.called @@ -830,20 +908,29 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC """ mock_get_inverted_programs.return_value = self.inverted_programs mock_get_certified_programs.side_effect = [[1], [1, 2]] - mock_revoke_program_certificate.side_effect = self._make_side_effect([Exception('boom'), None]) + mock_revoke_program_certificate.side_effect = self._make_side_effect( + [Exception("boom"), None] + ) - with mock.patch(TASKS_MODULE + '.LOGGER.info') as mock_info, \ - mock.patch(TASKS_MODULE + '.LOGGER.warning') as mock_warning: - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + with mock.patch(TASKS_MODULE + ".LOGGER.info") as mock_info, mock.patch( + TASKS_MODULE + ".LOGGER.warning" + ) as mock_warning: + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() assert mock_revoke_program_certificate.call_count == 3 mock_warning.assert_called_once_with( - 'Failed to revoke certificate for program {uuid} of user {username}.'.format( - uuid=1, - username=self.student.username) + "Failed to revoke certificate for program {uuid} of user {username}.".format( + uuid=1, username=self.student.username + ) + ) + mock_info.assert_any_call( + f"Revoked certificate for program {1} for user {self.student.username}" + ) + mock_info.assert_any_call( + f"Revoked certificate for program {2} for user {self.student.username}" ) - mock_info.assert_any_call(f"Revoked certificate for program {1} for user {self.student.username}") - mock_info.assert_any_call(f"Revoked certificate for program {2} for user {self.student.username}") def test_retry_on_credentials_api_errors( self, @@ -859,8 +946,12 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC """ mock_get_inverted_programs.return_value = self.inverted_programs mock_get_certified_programs.return_value = [1] - mock_get_certified_programs.side_effect = self._make_side_effect([Exception('boom'), None]) - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + mock_get_certified_programs.side_effect = self._make_side_effect( + [Exception("boom"), None] + ) + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() assert mock_get_certified_programs.call_count == 2 assert mock_revoke_program_certificate.call_count == 1 @@ -881,7 +972,9 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC [exception, None] ) - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() assert mock_revoke_program_certificate.call_count == 3 @@ -902,7 +995,9 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC [exception, None] ) - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() assert mock_revoke_program_certificate.call_count == 2 @@ -923,7 +1018,9 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC [exception, None] ) - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() assert mock_revoke_program_certificate.call_count == 2 @@ -942,47 +1039,52 @@ class RevokeProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiC with mock.patch( TASKS_MODULE + ".get_credentials_api_client" ) as mock_get_api_client, mock.patch( - TASKS_MODULE + '.LOGGER.exception' + TASKS_MODULE + ".LOGGER.exception" ) as mock_exception: mock_get_api_client.side_effect = Exception("boom") with pytest.raises(MaxRetriesExceededError): - tasks.revoke_program_certificates.delay(self.student.username, self.course_key).get() + tasks.revoke_program_certificates.delay( + self.student.username, self.course_key + ).get() assert mock_exception.called assert mock_get_api_client.call_count == (tasks.MAX_RETRIES + 1) assert not mock_revoke_program_certificate.called @skip_unless_lms -@override_settings(CREDENTIALS_SERVICE_USERNAME='test-service-username') +@override_settings(CREDENTIALS_SERVICE_USERNAME="test-service-username") class UpdateCredentialsCourseCertificateConfigurationAvailableDateTestCase(TestCase): """ Tests for the update_credentials_course_certificate_configuration_available_date function """ + def setUp(self): super().setUp() self.course = CourseOverviewFactory.create( - certificate_available_date=datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + certificate_available_date=datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") ) - CourseModeFactory.create(course_id=self.course.id, mode_slug='verified') - CourseModeFactory.create(course_id=self.course.id, mode_slug='audit') + CourseModeFactory.create(course_id=self.course.id, mode_slug="verified") + CourseModeFactory.create(course_id=self.course.id, mode_slug="audit") self.available_date = self.course.certificate_available_date self.course_id = self.course.id - self.credentials_worker = UserFactory(username='test-service-username') + self.credentials_worker = UserFactory(username="test-service-username") def test_update_course_cert_available_date(self): - with mock.patch(TASKS_MODULE + '.post_course_certificate_configuration') as update_posted: + with mock.patch( + TASKS_MODULE + ".post_course_certificate_configuration" + ) as update_posted: tasks.update_credentials_course_certificate_configuration_available_date( - self.course_id, - self.available_date + self.course_id, self.available_date ) update_posted.assert_called_once() def test_course_with_two_paid_modes(self): - CourseModeFactory.create(course_id=self.course.id, mode_slug='professional') - with mock.patch(TASKS_MODULE + '.post_course_certificate_configuration') as update_posted: + CourseModeFactory.create(course_id=self.course.id, mode_slug="professional") + with mock.patch( + TASKS_MODULE + ".post_course_certificate_configuration" + ) as update_posted: tasks.update_credentials_course_certificate_configuration_available_date( - self.course_id, - self.available_date + self.course_id, self.available_date ) update_posted.assert_not_called() @@ -992,74 +1094,80 @@ class PostCourseCertificateConfigurationTestCase(TestCase): """ Test the post_course_certificate_configuration function """ + def setUp(self): super().setUp() self.certificate = { - 'mode': 'verified', - 'course_id': 'testCourse', + "mode": "verified", + "course_id": "testCourse", } @httpretty.activate - @mock.patch('openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url') + @mock.patch("openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url") def test_post_course_certificate_configuration(self, mock_get_api_base_url): """ Ensure the correct API call gets made """ - mock_get_api_base_url.return_value = 'http://test-server/' + mock_get_api_base_url.return_value = "http://test-server/" test_client = requests.Session() - test_client.auth = SuppliedJwtAuth('test-token') + test_client.auth = SuppliedJwtAuth("test-token") httpretty.register_uri( httpretty.POST, - 'http://test-server/course_certificates/', + "http://test-server/course_certificates/", ) - available_date = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + available_date = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") - tasks.post_course_certificate_configuration(test_client, self.certificate, available_date) + tasks.post_course_certificate_configuration( + test_client, self.certificate, available_date + ) expected_body = { - "course_id": 'testCourse', - "certificate_type": 'verified', + "course_id": "testCourse", + "certificate_type": "verified", "certificate_available_date": available_date, - "is_active": True + "is_active": True, } - last_request_body = httpretty.last_request().body.decode('utf-8') + last_request_body = httpretty.last_request().body.decode("utf-8") assert json.loads(last_request_body) == expected_body @skip_unless_lms -class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(CredentialsApiConfigMixin, TestCase): +class UpdateCertificateVisibleDatesOnCourseUpdateTestCase( + CredentialsApiConfigMixin, TestCase +): """ Tests for the `update_certificate_visible_date_on_course_update` task. """ + def setUp(self): super().setUp() self.credentials_api_config = self.create_credentials_config(enabled=False) # setup course self.course = CourseOverviewFactory.create() # setup users - self.student1 = UserFactory.create(username='test-student1') - self.student2 = UserFactory.create(username='test-student2') - self.student3 = UserFactory.create(username='test-student3') + self.student1 = UserFactory.create(username="test-student1") + self.student2 = UserFactory.create(username="test-student2") + self.student3 = UserFactory.create(username="test-student3") # award certificates to users in course we created self.certificate_student1 = GeneratedCertificateFactory.create( user=self.student1, - mode='verified', + mode="verified", course_id=self.course.id, - status='downloadable' + status="downloadable", ) self.certificate_student2 = GeneratedCertificateFactory.create( user=self.student2, - mode='verified', + mode="verified", course_id=self.course.id, - status='downloadable' + status="downloadable", ) self.certificate_student3 = GeneratedCertificateFactory.create( user=self.student3, - mode='verified', + mode="verified", course_id=self.course.id, - status='downloadable' + status="downloadable", ) def tearDown(self): @@ -1075,7 +1183,9 @@ class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(CredentialsApiConfigMi exception when the max number of retries has reached. """ with pytest.raises(MaxRetriesExceededError): - tasks.update_certificate_visible_date_on_course_update(self.course.id) # pylint: disable=no-value-for-parameter + tasks.update_certificate_visible_date_on_course_update( + self.course.id + ) # pylint: disable=no-value-for-parameter def test_update_visible_dates(self): """ @@ -1089,17 +1199,24 @@ class UpdateCertificateVisibleDatesOnCourseUpdateTestCase(CredentialsApiConfigMi self.credentials_api_config.enabled = True self.credentials_api_config.enable_learner_issuance = True - with mock.patch(f"{TASKS_MODULE}.award_course_certificate.delay") as award_course_cert: - tasks.update_certificate_visible_date_on_course_update(self.course.id) # pylint: disable=no-value-for-parameter + with mock.patch( + f"{TASKS_MODULE}.award_course_certificate.delay" + ) as award_course_cert: + tasks.update_certificate_visible_date_on_course_update( + self.course.id + ) # pylint: disable=no-value-for-parameter assert award_course_cert.call_count == 3 @skip_unless_lms -class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigMixin, TestCase): +class UpdateCertificateAvailableDateOnCourseUpdateTestCase( + CredentialsApiConfigMixin, TestCase +): """ Tests for the `update_certificate_available_date_on_course_update` task. """ + def setUp(self): super().setUp() self.credentials_api_config = self.create_credentials_config(enabled=False) @@ -1119,7 +1236,9 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM course = CourseOverviewFactory.create() with pytest.raises(MaxRetriesExceededError): - tasks.update_certificate_available_date_on_course_update(course.id) # pylint: disable=no-value-for-parameter + tasks.update_certificate_available_date_on_course_update( + course.id + ) # pylint: disable=no-value-for-parameter def test_update_certificate_available_date_with_self_paced_course(self): """ @@ -1133,16 +1252,19 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM self.credentials_api_config.enable_learner_issuance = True course = CourseOverviewFactory.create( - self_paced=True, - certificate_available_date=None + self_paced=True, certificate_available_date=None ) with mock.patch( f"{TASKS_MODULE}.update_credentials_course_certificate_configuration_available_date.delay" ) as update_credentials_course_cert_config: - tasks.update_certificate_available_date_on_course_update(course.id) # pylint: disable=no-value-for-parameter + tasks.update_certificate_available_date_on_course_update( + course.id + ) # pylint: disable=no-value-for-parameter - update_credentials_course_cert_config.assert_called_once_with(str(course.id), None) + update_credentials_course_cert_config.assert_called_once_with( + str(course.id), None + ) def test_update_certificate_available_date_with_instructor_paced_course(self): """ @@ -1160,15 +1282,19 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM course = CourseOverviewFactory.create( self_paced=False, certificate_available_date=available_date, - certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE + certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE, ) with mock.patch( f"{TASKS_MODULE}.update_credentials_course_certificate_configuration_available_date.delay" ) as update_credentials_course_cert_config: - tasks.update_certificate_available_date_on_course_update(course.id) # pylint: disable=no-value-for-parameter + tasks.update_certificate_available_date_on_course_update( + course.id + ) # pylint: disable=no-value-for-parameter - update_credentials_course_cert_config.assert_called_once_with(str(course.id), str(available_date)) + update_credentials_course_cert_config.assert_called_once_with( + str(course.id), str(available_date) + ) def test_update_certificate_available_date_with_expect_no_update(self): """ @@ -1183,7 +1309,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM course = CourseOverviewFactory.create( self_paced=False, certificate_available_date=available_date, - certificates_display_behavior=CertificatesDisplayBehaviors.EARLY_NO_INFO + certificates_display_behavior=CertificatesDisplayBehaviors.EARLY_NO_INFO, ) expected_message = ( @@ -1192,7 +1318,9 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM ) with LogCapture(level=logging.WARNING) as log_capture: - tasks.update_certificate_available_date_on_course_update(course.id) # pylint: disable=no-value-for-parameter + tasks.update_certificate_available_date_on_course_update( + course.id + ) # pylint: disable=no-value-for-parameter assert len(log_capture.records) == 1 assert log_capture.records[0].getMessage() == expected_message diff --git a/openedx/core/lib/tests/test_edx_api_utils.py b/openedx/core/lib/tests/test_edx_api_utils.py index 86aa17213c..68601caba0 100644 --- a/openedx/core/lib/tests/test_edx_api_utils.py +++ b/openedx/core/lib/tests/test_edx_api_utils.py @@ -7,7 +7,6 @@ from urllib.parse import urljoin import httpretty from django.core.cache import cache -from requests.exceptions import HTTPError from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.catalog.models import CatalogIntegration