class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase): def setUp(self): super(TestBinnedSchedulesBaseResolver, self).setUp() self.site = SiteFactory.create() self.site_config = SiteConfigurationFactory(site=self.site) self.schedule_config = ScheduleConfigFactory.create(site=self.site) self.resolver = BinnedSchedulesBaseResolver( async_send_task=Mock(name='async_send_task'), site=self.site, target_datetime=datetime.datetime.now(), day_offset=3, bin_num=2, ) @ddt.data( 'course1' ) def test_get_course_org_filter_equal(self, course_org_filter): self.site_config.values['course_org_filter'] = course_org_filter self.site_config.save() mock_query = Mock() result = self.resolver.filter_by_org(mock_query) self.assertEqual(result, mock_query.filter.return_value) mock_query.filter.assert_called_once_with(enrollment__course__org=course_org_filter) @ddt.unpack @ddt.data( (['course1', 'course2'], ['course1', 'course2']) ) def test_get_course_org_filter_include__in(self, course_org_filter, expected_org_list): self.site_config.values['course_org_filter'] = course_org_filter self.site_config.save() mock_query = Mock() result = self.resolver.filter_by_org(mock_query) self.assertEqual(result, mock_query.filter.return_value) mock_query.filter.assert_called_once_with(enrollment__course__org__in=expected_org_list) @ddt.unpack @ddt.data( (None, set([])), ('course1', set([u'course1'])), (['course1', 'course2'], set([u'course1', u'course2'])) ) def test_get_course_org_filter_exclude__in(self, course_org_filter, expected_org_list): SiteConfigurationFactory.create( values={'course_org_filter': course_org_filter}, ) mock_query = Mock() result = self.resolver.filter_by_org(mock_query) mock_query.exclude.assert_called_once_with(enrollment__course__org__in=expected_org_list) self.assertEqual(result, mock_query.exclude.return_value)
class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase): def setUp(self): super(TestBinnedSchedulesBaseResolver, self).setUp() self.site = SiteFactory.create() self.site_config = SiteConfigurationFactory(site=self.site) self.schedule_config = ScheduleConfigFactory.create(site=self.site) self.resolver = BinnedSchedulesBaseResolver( async_send_task=Mock(name='async_send_task'), site=self.site, target_datetime=datetime.datetime.now(), day_offset=3, bin_num=2, ) @ddt.data('course1') def test_get_course_org_filter_equal(self, course_org_filter): self.site_config.values['course_org_filter'] = course_org_filter self.site_config.save() mock_query = Mock() result = self.resolver.filter_by_org(mock_query) self.assertEqual(result, mock_query.filter.return_value) mock_query.filter.assert_called_once_with( enrollment__course__org=course_org_filter) @ddt.unpack @ddt.data((['course1', 'course2'], ['course1', 'course2'])) def test_get_course_org_filter_include__in(self, course_org_filter, expected_org_list): self.site_config.values['course_org_filter'] = course_org_filter self.site_config.save() mock_query = Mock() result = self.resolver.filter_by_org(mock_query) self.assertEqual(result, mock_query.filter.return_value) mock_query.filter.assert_called_once_with( enrollment__course__org__in=expected_org_list) @ddt.unpack @ddt.data((None, set([])), ('course1', set([u'course1'])), (['course1', 'course2'], set([u'course1', u'course2']))) def test_get_course_org_filter_exclude__in(self, course_org_filter, expected_org_list): SiteConfigurationFactory.create( values={'course_org_filter': course_org_filter}, ) mock_query = Mock() result = self.resolver.filter_by_org(mock_query) mock_query.exclude.assert_called_once_with( enrollment__course__org__in=expected_org_list) self.assertEqual(result, mock_query.exclude.return_value)
class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase): """ Tests for the 'award_program_certificates' celery task. """ def setUp(self): super(AwardProgramCertificatesTestCase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.create_credentials_config() self.student = UserFactory.create(username='******') self.site = SiteFactory() self.site_configuration = SiteConfigurationFactory(site=self.site) self.catalog_integration = self.create_catalog_integration() ApplicationFactory.create(name='credentials') UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) def test_completion_check( self, mock_get_completed_programs, mock_get_certified_programs, # pylint: disable=unused-argument mock_award_program_certificate, # pylint: disable=unused-argument ): """ Checks that the Programs API is used correctly to determine completed programs. """ tasks.award_program_certificates.delay(self.student.username).get() mock_get_completed_programs.assert_any_call(self.site, self.student) @ddt.data( ([1], [2, 3]), ([], [1, 2, 3]), ([1, 2, 3], []), ) @ddt.unpack def test_awarding_certs( self, already_awarded_program_uuids, expected_awarded_program_uuids, mock_get_completed_programs, mock_get_certified_programs, mock_award_program_certificate, ): """ Checks that the Credentials API is used to award certificates for the proper programs. """ mock_get_completed_programs.return_value = {1: 1, 2: 2, 3: 3} mock_get_certified_programs.return_value = already_awarded_program_uuids 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 ] assert actual_program_uuids == expected_awarded_program_uuids 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' ) 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, ): """ Checks that the Credentials API is used to award certificates for the proper programs and those program will be skipped which are provided by 'programs_without_certificates' list in site configuration. """ # all completed programs mock_get_completed_programs.return_value = {1: 1, 2: 2, 3: 3, 4: 4} # already awarded programs mock_get_certified_programs.return_value = [1] # programs to be skipped self.site_configuration.site_values = { "programs_without_certificates": [2] } self.site_configuration.save() mocked_get_current_site_configuration.return_value = self.site_configuration # programs which are expected to be awarded. # (completed_programs - (already_awarded+programs + to_be_skipped_programs) 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 ] assert actual_program_uuids == expected_awarded_program_uuids 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'), ) @ddt.unpack def test_retry_if_config_disabled(self, disabled_config_type, disabled_config_attribute, *mock_helpers): """ Checks that the task is aborted if any relevant api configs are disabled. """ getattr(self, 'create_{}_config'.format(disabled_config_type))( **{ 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 for mock_helper in mock_helpers: assert not mock_helper.called def test_abort_if_invalid_username(self, *mock_helpers): """ 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() assert mock_exception.called for mock_helper in mock_helpers: assert not mock_helper.called def test_abort_if_no_completed_programs( self, mock_get_completed_programs, mock_get_certified_programs, mock_award_program_certificate, ): """ Checks that the task will be aborted without further action if there are no programs for which to award a certificate. """ mock_get_completed_programs.return_value = {} tasks.award_program_certificates.delay(self.student.username).get() assert mock_get_completed_programs.called assert not mock_get_certified_programs.called assert not mock_award_program_certificate.called @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): """ Checks that the task will be aborted without further action if there exists a list programs_without_certificates with ["ALL"] value in site configuration. """ mock_get_value.return_value = ["ALL"] mock_get_completed_programs.return_value = {1: 1, 2: 2} tasks.award_program_certificates.delay(self.student.username).get() assert not mock_get_completed_programs.called assert not mock_get_certified_programs.called assert not mock_award_program_certificate.called @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): """ 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_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 pytest.raises(MaxRetriesExceededError): tasks.award_program_certificates.delay( self.student.username).get() assert mock_exception.called assert mock_get_api_client.call_count == (tasks.MAX_RETRIES + 1) assert not mock_award_program_certificate.called def _make_side_effect(self, side_effects): """ 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): # pylint: disable=missing-docstring if side_effects: exc = side_effects.pop(0) if exc: raise exc return mock.DEFAULT return side_effect def test_continue_awarding_certs_if_error( self, mock_get_completed_programs, mock_get_certified_programs, mock_award_program_certificate, ): """ Checks that a single failure to award one of several certificates does not cause the entire task to fail. Also ensures that successfully awarded certs are logged as INFO and warning is logged for failed requests if there are retries available. """ 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]) 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( u'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}" ) def test_retry_on_programs_api_errors(self, mock_get_completed_programs, *_mock_helpers): """ Ensures that any otherwise-unhandled errors that arise while trying to get completed programs (e.g. network issues or other 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]) tasks.award_program_certificates.delay(self.student.username).get() assert mock_get_completed_programs.call_count == 3 def test_retry_on_credentials_api_errors( self, mock_get_completed_programs, mock_get_certified_programs, mock_award_program_certificate, ): """ Ensures that any otherwise-unhandled errors that arise while trying to get existing program credentials (e.g. network issues or other transient API errors) will cause the task to be failed and queued for retry. """ 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]) 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 def test_retry_on_credentials_api_429_error( self, mock_get_completed_programs, mock_get_certified_programs, # pylint: disable=unused-argument mock_award_program_certificate, ): """ Verify that a 429 error causes the task to fail and then retry. """ exception = exceptions.HttpClientError() exception.response = mock.Mock(status_code=429) mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_award_program_certificate.side_effect = self._make_side_effect( [exception, None]) tasks.award_program_certificates.delay(self.student.username).get() assert mock_award_program_certificate.call_count == 3 def test_no_retry_on_credentials_api_404_error( self, mock_get_completed_programs, mock_get_certified_programs, # pylint: disable=unused-argument mock_award_program_certificate, ): """ Verify that a 404 error causes the task to fail but there is no retry. """ exception = exceptions.HttpNotFoundError() exception.response = mock.Mock(status_code=404) mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_award_program_certificate.side_effect = self._make_side_effect( [exception, None]) tasks.award_program_certificates.delay(self.student.username).get() assert mock_award_program_certificate.call_count == 2 def test_no_retry_on_credentials_api_4XX_error( self, mock_get_completed_programs, mock_get_certified_programs, # pylint: disable=unused-argument mock_award_program_certificate, ): """ Verify that other 4XX errors cause task to fail but there is no retry. """ exception = exceptions.HttpClientError() exception.response = mock.Mock(status_code=418) mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_award_program_certificate.side_effect = self._make_side_effect( [exception, None]) tasks.award_program_certificates.delay(self.student.username).get() assert mock_award_program_certificate.call_count == 2
class AwardProgramCertificatesTestCase(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase): """ Tests for the 'award_program_certificates' celery task. """ def setUp(self): super(AwardProgramCertificatesTestCase, self).setUp() self.create_credentials_config() self.student = UserFactory.create(username='******') self.site = SiteFactory() self.site_configuration = SiteConfigurationFactory(site=self.site) self.catalog_integration = self.create_catalog_integration() ClientFactory.create(name='credentials') UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) def test_completion_check( self, mock_get_completed_programs, mock_get_certified_programs, # pylint: disable=unused-argument mock_award_program_certificate, # pylint: disable=unused-argument ): """ Checks that the Programs API is used correctly to determine completed programs. """ tasks.award_program_certificates.delay(self.student.username).get() mock_get_completed_programs.assert_called(self.site, self.student) @ddt.data( ([1], [2, 3]), ([], [1, 2, 3]), ([1, 2, 3], []), ) @ddt.unpack def test_awarding_certs( self, already_awarded_program_uuids, expected_awarded_program_uuids, mock_get_completed_programs, mock_get_certified_programs, mock_award_program_certificate, ): """ Checks that the Credentials API is used to award certificates for the proper programs. """ mock_get_completed_programs.return_value = {1: 1, 2: 2, 3: 3} mock_get_certified_programs.return_value = already_awarded_program_uuids 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] self.assertEqual(actual_program_uuids, expected_awarded_program_uuids) actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list] self.assertEqual(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') 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, ): """ Checks that the Credentials API is used to award certificates for the proper programs and those program will be skipped which are provided by 'programs_without_certificates' list in site configuration. """ # all completed programs mock_get_completed_programs.return_value = {1: 1, 2: 2, 3: 3, 4: 4} # already awarded programs mock_get_certified_programs.return_value = [1] # programs to be skipped self.site_configuration.values = { "programs_without_certificates": [2] } self.site_configuration.save() mocked_get_current_site_configuration.return_value = self.site_configuration # programs which are expected to be awarded. # (completed_programs - (already_awarded+programs + to_be_skipped_programs) 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] self.assertEqual(actual_program_uuids, expected_awarded_program_uuids) actual_visible_dates = [call[0][3] for call in mock_award_program_certificate.call_args_list] self.assertEqual(actual_visible_dates, expected_awarded_program_uuids) # program uuids are same as mock dates @ddt.data( ('credentials', 'enable_learner_issuance'), ) @ddt.unpack def test_retry_if_config_disabled( self, disabled_config_type, disabled_config_attribute, *mock_helpers ): """ Checks that the task is aborted if any relevant api configs are disabled. """ getattr(self, 'create_{}_config'.format(disabled_config_type))(**{disabled_config_attribute: False}) with mock.patch(TASKS_MODULE + '.LOGGER.warning') as mock_warning: with self.assertRaises(MaxRetriesExceededError): tasks.award_program_certificates.delay(self.student.username).get() self.assertTrue(mock_warning.called) for mock_helper in mock_helpers: self.assertFalse(mock_helper.called) def test_abort_if_invalid_username(self, *mock_helpers): """ 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() self.assertTrue(mock_exception.called) for mock_helper in mock_helpers: self.assertFalse(mock_helper.called) def test_abort_if_no_completed_programs( self, mock_get_completed_programs, mock_get_certified_programs, mock_award_program_certificate, ): """ Checks that the task will be aborted without further action if there are no programs for which to award a certificate. """ mock_get_completed_programs.return_value = {} tasks.award_program_certificates.delay(self.student.username).get() self.assertTrue(mock_get_completed_programs.called) self.assertFalse(mock_get_certified_programs.called) self.assertFalse(mock_award_program_certificate.called) @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 ): """ Checks that the task will be aborted without further action if there exists a list programs_without_certificates with ["ALL"] value in site configuration. """ mock_get_value.return_value = ["ALL"] mock_get_completed_programs.return_value = {1: 1, 2: 2} tasks.award_program_certificates.delay(self.student.username).get() self.assertFalse(mock_get_completed_programs.called) self.assertFalse(mock_get_certified_programs.called) self.assertFalse(mock_award_program_certificate.called) def _make_side_effect(self, side_effects): """ 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): # pylint: disable=missing-docstring if side_effects: exc = side_effects.pop(0) if exc: raise exc return mock.DEFAULT return side_effect def test_continue_awarding_certs_if_error( self, mock_get_completed_programs, mock_get_certified_programs, mock_award_program_certificate, ): """ Checks that a single failure to award one of several certificates does not cause the entire task to fail. Also ensures that successfully awarded certs are logged as INFO and warning is logged for failed requests if there are retries available. """ 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]) with mock.patch(TASKS_MODULE + '.LOGGER.info') as mock_info, \ mock.patch(TASKS_MODULE + '.LOGGER.warning') as mock_warning: tasks.award_program_certificates.delay(self.student.username).get() self.assertEqual(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) ) mock_info.assert_any_call(mock.ANY, 1, self.student.username) mock_info.assert_any_call(mock.ANY, 2, self.student.username) def test_retry_on_programs_api_errors( self, mock_get_completed_programs, *_mock_helpers ): """ Ensures that any otherwise-unhandled errors that arise while trying to get completed programs (e.g. network issues or other 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]) tasks.award_program_certificates.delay(self.student.username).get() self.assertEqual(mock_get_completed_programs.call_count, 3) def test_retry_on_credentials_api_errors( self, mock_get_completed_programs, mock_get_certified_programs, mock_award_program_certificate, ): """ Ensures that any otherwise-unhandled errors that arise while trying to get existing program credentials (e.g. network issues or other transient API errors) will cause the task to be failed and queued for retry. """ 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]) tasks.award_program_certificates.delay(self.student.username).get() self.assertEqual(mock_get_certified_programs.call_count, 2) self.assertEqual(mock_award_program_certificate.call_count, 1) def test_retry_on_credentials_api_429_error( self, mock_get_completed_programs, mock_get_certified_programs, # pylint: disable=unused-argument mock_award_program_certificate, ): """ Verify that a 429 error causes the task to fail and then retry. """ exception = exceptions.HttpClientError() exception.response = mock.Mock(status_code=429) mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_award_program_certificate.side_effect = self._make_side_effect( [exception, None] ) tasks.award_program_certificates.delay(self.student.username).get() self.assertEqual(mock_award_program_certificate.call_count, 3) def test_no_retry_on_credentials_api_404_error( self, mock_get_completed_programs, mock_get_certified_programs, # pylint: disable=unused-argument mock_award_program_certificate, ): """ Verify that a 404 error causes the task to fail but there is no retry. """ exception = exceptions.HttpNotFoundError() exception.response = mock.Mock(status_code=404) mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_award_program_certificate.side_effect = self._make_side_effect( [exception, None] ) tasks.award_program_certificates.delay(self.student.username).get() self.assertEqual(mock_award_program_certificate.call_count, 2) def test_no_retry_on_credentials_api_4XX_error( self, mock_get_completed_programs, mock_get_certified_programs, # pylint: disable=unused-argument mock_award_program_certificate, ): """ Verify that other 4XX errors cause task to fail but there is no retry. """ exception = exceptions.HttpClientError() exception.response = mock.Mock(status_code=418) mock_get_completed_programs.return_value = {1: 1, 2: 2} mock_award_program_certificate.side_effect = self._make_side_effect( [exception, None] ) tasks.award_program_certificates.delay(self.student.username).get() self.assertEqual(mock_award_program_certificate.call_count, 2)