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)
Beispiel #2
0
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)
Beispiel #3
0
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
Beispiel #4
0
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)