Beispiel #1
0
    def get_catalog_options(self):
        """
        Retrieve a list of catalog ID and name pairs.

        Once retrieved, these name pairs can be used directly as a value
        for the `choices` argument to a ChoiceField.
        """
        # TODO: We will remove the discovery service catalog implementation
        # once we have fully migrated customer's to EnterpriseCustomerCatalogs.
        # For now, this code will prevent an admin from creating a new
        # EnterpriseCustomer with a discovery service catalog. They will have to first
        # save the EnterpriseCustomer admin form and then edit the EnterpriseCustomer
        # to add a discovery service catalog.
        if hasattr(self.instance, 'site'):
            catalog_api = CourseCatalogApiClient(self.user, self.instance.site)
        else:
            catalog_api = CourseCatalogApiClient(self.user)
        catalogs = catalog_api.get_all_catalogs()
        # order catalogs by name.
        catalogs = sorted(catalogs,
                          key=lambda catalog: catalog.get('name', '').lower())

        return BLANK_CHOICE_DASH + [(
            catalog['id'],
            catalog['name'],
        ) for catalog in catalogs]
Beispiel #2
0
    def clean_program(self):
        """
        Clean program.

        Try obtaining program treating form value as program UUID or title.

        Returns:
            dict: Program information if program found
        """
        program_id = self.cleaned_data[self.Fields.PROGRAM].strip()
        if not program_id:
            return None

        try:
            client = CourseCatalogApiClient(self._user, self._enterprise_customer.site)
            program = client.get_program_by_uuid(program_id) or client.get_program_by_title(program_id)
        except MultipleProgramMatchError as exc:
            raise ValidationError(ValidationMessages.MULTIPLE_PROGRAM_MATCH.format(program_count=exc.programs_matched))
        except (HttpClientError, HttpServerError):
            raise ValidationError(ValidationMessages.INVALID_PROGRAM_ID.format(program_id=program_id))

        if not program:
            raise ValidationError(ValidationMessages.INVALID_PROGRAM_ID.format(program_id=program_id))

        if program['status'] != ProgramStatuses.ACTIVE:
            raise ValidationError(
                ValidationMessages.PROGRAM_IS_INACTIVE.format(program_id=program_id, status=program['status'])
            )

        return program
Beispiel #3
0
    def _validate_program(self):
        """
        Verify that selected mode is available for program and all courses in the program
        """
        program = self.cleaned_data.get(self.Fields.PROGRAM)
        if not program:
            return

        course_runs = get_course_runs_from_program(program)
        try:
            client = CourseCatalogApiClient(self._user,
                                            self._enterprise_customer.site)
            available_modes = client.get_common_course_modes(course_runs)
            course_mode = self.cleaned_data.get(self.Fields.COURSE_MODE)
        except (HttpClientError, HttpServerError):
            raise ValidationError(
                ValidationMessages.FAILED_TO_OBTAIN_COURSE_MODES.format(
                    program_title=program.get("title")))

        if not course_mode:
            raise ValidationError(
                ValidationMessages.COURSE_WITHOUT_COURSE_MODE)
        if course_mode not in available_modes:
            raise ValidationError(
                ValidationMessages.COURSE_MODE_NOT_AVAILABLE.format(
                    mode=course_mode,
                    program_title=program.get("title"),
                    modes=", ".join(available_modes)))
Beispiel #4
0
    def courses(self, request, enterprise_customer, pk=None):  # pylint: disable=invalid-name
        """
        Retrieve the list of courses contained within this catalog.

        Only courses with active course runs are returned. A course run is considered active if it is currently
        open for enrollment, or will open in the future.
        """
        catalog_api = CourseCatalogApiClient(request.user,
                                             enterprise_customer.site)
        courses = catalog_api.get_paginated_catalog_courses(pk, request.GET)

        # If the API returned an empty response, that means pagination has ended.
        # An empty response can also mean that there was a problem fetching data from catalog API.
        self.ensure_data_exists(
            request,
            courses,
            error_message=
            ("Unable to fetch API response for catalog courses from endpoint '{endpoint}'. "
             "The resource you are looking for does not exist.".format(
                 endpoint=request.get_full_path())))
        serializer = serializers.EnterpriseCatalogCoursesReadOnlySerializer(
            courses)

        # Add enterprise related context for the courses.
        serializer.update_enterprise_courses(enterprise_customer,
                                             catalog_id=pk)
        return get_paginated_response(serializer.data, request)
    def setUp(self):
        super(TestCourseCatalogApi, self).setUp()
        self.user_mock = mock.Mock(spec=User)
        self.get_data_mock = self._make_patch(self._make_catalog_api_location("get_edx_api_data"))
        self.catalog_api_config_mock = self._make_patch(self._make_catalog_api_location("CatalogIntegration"))
        self.jwt_builder_mock = self._make_patch(self._make_catalog_api_location("JwtBuilder"))

        self.api = CourseCatalogApiClient(self.user_mock)
Beispiel #6
0
    def list(self, request):
        """
        DRF view to list all catalogs.

        Arguments:
            request (HttpRequest): Current request

        Returns:
            (Response): DRF response object containing course catalogs.
        """
        catalog_api = CourseCatalogApiClient(request.user)
        catalogs = catalog_api.get_paginated_catalogs(request.GET)
        self.ensure_data_exists(request, catalogs)
        serializer = serializers.ResponsePaginationSerializer(catalogs)
        return get_paginated_response(serializer.data, request)
 def test_is_course_in_catalog(self, catalog_id, course_id, api_resp,
                               expected, is_a_course_run,
                               mock_discovery_client_factory):
     """
     Test the API client that checks to determine if a given course ID is present
     in the given catalog.
     """
     discovery_client = mock_discovery_client_factory.return_value
     discovery_client.catalogs.return_value.contains.get.return_value = api_resp
     self.api = CourseCatalogApiClient(self.user_mock)
     assert self.api.is_course_in_catalog(catalog_id, course_id) == expected
     discovery_client.catalogs.assert_called_once_with(catalog_id)
     if is_a_course_run:
         discovery_client.catalogs.return_value.contains.get.assert_called_once_with(
             course_run_id=course_id)
     else:
         discovery_client.catalogs.return_value.contains.get.assert_called_once_with(
             course_id=course_id)
Beispiel #8
0
    def courses(self, request, pk=None):  # pylint: disable=invalid-name,unused-argument
        """
        Retrieve the list of courses contained within the catalog linked to this enterprise.

        Only courses with active course runs are returned. A course run is considered active if it is currently
        open for enrollment, or will open in the future.
        """
        enterprise_customer = self.get_object()
        self.check_object_permissions(request, enterprise_customer)
        self.ensure_data_exists(
            request,
            enterprise_customer.catalog,
            error_message=
            "No catalog is associated with Enterprise {enterprise_name} from endpoint '{path}'."
            .format(enterprise_name=enterprise_customer.name,
                    path=request.get_full_path()))

        # We have handled potential error cases and are now ready to call out to the Catalog API.
        catalog_api = CourseCatalogApiClient(request.user,
                                             enterprise_customer.site)
        courses = catalog_api.get_paginated_catalog_courses(
            enterprise_customer.catalog, request.GET)

        # An empty response means that there was a problem fetching data from Catalog API, since
        # a Catalog with no courses has a non empty response indicating that there are no courses.
        self.ensure_data_exists(
            request,
            courses,
            error_message=(
                "Unable to fetch API response for catalog courses for "
                "Enterprise {enterprise_name} from endpoint '{path}'.".format(
                    enterprise_name=enterprise_customer.name,
                    path=request.get_full_path())))

        serializer = serializers.EnterpriseCatalogCoursesReadOnlySerializer(
            courses)

        # Add enterprise related context for the courses.
        serializer.update_enterprise_courses(
            enterprise_customer, catalog_id=enterprise_customer.catalog)
        return get_paginated_response(serializer.data, request)
Beispiel #9
0
    def retrieve(self, request, pk=None):  # pylint: disable=invalid-name
        """
        DRF view to get catalog details.

        Arguments:
            request (HttpRequest): Current request
            pk (int): Course catalog identifier

        Returns:
            (Response): DRF response object containing course catalogs.
        """
        catalog_api = CourseCatalogApiClient(request.user)
        catalog = catalog_api.get_catalog(pk)
        self.ensure_data_exists(
            request,
            catalog,
            error_message=
            ("Unable to fetch API response for given catalog from endpoint '/catalog/{pk}/'. "
             "The resource you are looking for does not exist.".format(pk=pk)))
        serializer = self.serializer_class(catalog)
        return Response(serializer.data)
class TestCourseCatalogApi(CourseDiscoveryApiTestMixin, unittest.TestCase):
    """
    Test course catalog API methods.
    """

    EMPTY_RESPONSES = (None, {}, [], set(), "")

    def setUp(self):
        super(TestCourseCatalogApi, self).setUp()
        self.user_mock = mock.Mock(spec=User)
        self.get_data_mock = self._make_patch(self._make_catalog_api_location("get_edx_api_data"))
        self.catalog_api_config_mock = self._make_patch(self._make_catalog_api_location("CatalogIntegration"))
        self.jwt_builder_mock = self._make_patch(self._make_catalog_api_location("JwtBuilder"))

        self.api = CourseCatalogApiClient(self.user_mock)

    @staticmethod
    def _make_course_run(key, *seat_types):
        """
        Return course_run json representation expected by CourseCatalogAPI.
        """
        return {
            "key": key,
            "seats": [{"type": seat_type} for seat_type in seat_types]
        }

    _make_run = _make_course_run.__func__  # unwrapping to use within class definition

    def test_get_course_details(self):
        """
        Verify get_course_details of CourseCatalogApiClient works as expected.
        """
        course_key = 'edX+DemoX'
        expected_result = {"complex": "dict"}
        self.get_data_mock.return_value = expected_result

        actual_result = self.api.get_course_details(course_key)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.COURSES_ENDPOINT
        assert resource_id == 'edX+DemoX'
        assert actual_result == expected_result

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_course_details_empty_response(self, response):
        """
        Verify get_course_details of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_course_details(course_id='edX+DemoX') == {}

    @ddt.data(
        "course-v1:JediAcademy+AppliedTelekinesis+T1",
        "course-v1:TrantorAcademy+Psychohistory101+T1",
        "course-v1:StarfleetAcademy+WarpspeedNavigation+T2337",
        "course-v1:SinhonCompanionAcademy+Calligraphy+TermUnknown",
        "course-v1:CampArthurCurrie+HeavyWeapons+T2245_5",
    )
    def test_get_course_run(self, course_run_id):
        """
        Verify get_course_run of CourseCatalogApiClient works as expected.
        """
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_course_run(course_run_id)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.COURSE_RUNS_ENDPOINT
        assert resource_id is course_run_id
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_course_run_empty_response(self, response):
        """
        Verify get_course_run of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_course_run("any") == {}

    @ddt.data(
        "course-v1:JediAcademy+AppliedTelekinesis+T1",
        "course-v1:TrantorAcademy+Psychohistory101+T1",
    )
    def test_get_course_run_identifiers(self, course_run_id):

        self.get_data_mock.return_value = {}
        actual_result = self.api.get_course_run_identifiers(course_run_id)
        assert 'course_key' in actual_result
        assert actual_result['course_key'] is None
        assert 'course_uuid' in actual_result
        assert actual_result['course_uuid'] is None
        assert 'course_run_key' in actual_result
        assert actual_result['course_run_key'] is None
        assert 'course_run_uuid' in actual_result
        assert actual_result['course_run_uuid'] is None

        mock_response = {
            "key": "JediAcademy+AppliedTelekinesis",
            "uuid": "785b11f5-fad5-4ce1-9233-e1a3ed31aadb",
        }
        self.get_data_mock.return_value = mock_response
        actual_result = self.api.get_course_run_identifiers(course_run_id)
        assert 'course_key' in actual_result
        assert actual_result['course_key'] == 'JediAcademy+AppliedTelekinesis'
        assert 'course_uuid' in actual_result
        assert actual_result['course_uuid'] == '785b11f5-fad5-4ce1-9233-e1a3ed31aadb'
        assert 'course_run_key' in actual_result
        assert actual_result['course_run_key'] is None
        assert 'course_run_uuid' in actual_result
        assert actual_result['course_run_uuid'] is None

        mock_response = {
            "key": "JediAcademy+AppliedTelekinesis",
            "uuid": "785b11f5-fad5-4ce1-9233-e1a3ed31aadb",
            "course_runs": [],
        }
        self.get_data_mock.return_value = mock_response
        actual_result = self.api.get_course_run_identifiers(course_run_id)
        assert 'course_key' in actual_result
        assert actual_result['course_key'] == 'JediAcademy+AppliedTelekinesis'
        assert 'course_uuid' in actual_result
        assert actual_result['course_uuid'] == '785b11f5-fad5-4ce1-9233-e1a3ed31aadb'
        assert 'course_run_key' in actual_result
        assert actual_result['course_run_key'] is None
        assert 'course_run_uuid' in actual_result
        assert actual_result['course_run_uuid'] is None

        mock_response = {
            "key": "JediAcademy+AppliedTelekinesis",
            "uuid": "785b11f5-fad5-4ce1-9233-e1a3ed31aadb",
            "course_runs": [{
                "key": course_run_id,
                "uuid": "1234abcd-fad5-4ce1-9233-e1a3ed31aadb"
            }],
        }
        self.get_data_mock.return_value = mock_response
        actual_result = self.api.get_course_run_identifiers(course_run_id)
        assert 'course_key' in actual_result
        assert actual_result['course_key'] == 'JediAcademy+AppliedTelekinesis'
        assert 'course_uuid' in actual_result
        assert actual_result['course_uuid'] == '785b11f5-fad5-4ce1-9233-e1a3ed31aadb'
        assert 'course_run_key' in actual_result
        assert actual_result['course_run_key'] == course_run_id
        assert 'course_run_uuid' in actual_result
        assert actual_result['course_run_uuid'] == '1234abcd-fad5-4ce1-9233-e1a3ed31aadb'

    @ddt.data("Apollo", "Star Wars", "mk Ultra")
    def test_get_program_by_uuid(self, program_id):
        """
        Verify get_program_by_uuid of CourseCatalogApiClient works as expected.
        """
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_program_by_uuid(program_id)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.PROGRAMS_ENDPOINT
        assert resource_id is program_id
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_program_by_uuid_empty_response(self, response):
        """
        Verify get_program_by_uuid of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_program_by_uuid("any") is None

    @ddt.data("MicroMasters Certificate", "Professional Certificate", "XSeries Certificate")
    def test_get_program_type_by_slug(self, slug):
        """
        Verify get_program_type_by_slug of CourseCatalogApiClient works as expected.
        """
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_program_type_by_slug(slug)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.PROGRAM_TYPES_ENDPOINT
        assert resource_id is slug
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_program_type_by_slug_empty_response(self, response):
        """
        Verify get_program_type_by_slug of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_program_type_by_slug('slug') is None

    @ddt.data(
        (None, []),
        ({}, []),
        (
            {
                'courses': [
                    {'key': 'first+key'},
                    {'key': 'second+key'}
                ]
            },
            [
                'first+key',
                'second+key'
            ]
        )
    )
    @ddt.unpack
    def test_get_program_course_keys(self, response_body, expected_result):
        self.get_data_mock.return_value = response_body
        result = self.api.get_program_course_keys('fake-uuid')
        assert result == expected_result

    @ddt.data(
        (
            "course-v1:JediAcademy+AppliedTelekinesis+T1",
            {
                "course": "JediAcademy+AppliedTelekinesis"
            },
            {
                "course_runs": [{"key": "course-v1:JediAcademy+AppliedTelekinesis+T1"}]
            },
            "JediAcademy+AppliedTelekinesis",
            {"key": "course-v1:JediAcademy+AppliedTelekinesis+T1"}
        ),
        (
            "course-v1:JediAcademy+AppliedTelekinesis+T1",
            {},
            {},
            None,
            None
        ),
        (
            "course-v1:JediAcademy+AppliedTelekinesis+T1",
            {
                "course": "JediAcademy+AppliedTelekinesis"
            },
            {
                "course_runs": [
                    {"key": "course-v1:JediAcademy+AppliedTelekinesis+T222"},
                    {"key": "course-v1:JediAcademy+AppliedTelekinesis+T1"}
                ]
            },
            "JediAcademy+AppliedTelekinesis",
            {"key": "course-v1:JediAcademy+AppliedTelekinesis+T1"}
        ),
        (
            "course-v1:JediAcademy+AppliedTelekinesis+T1",
            {
                "course": "JediAcademy+AppliedTelekinesis"
            },
            {
                "course_runs": []
            },
            "JediAcademy+AppliedTelekinesis",
            None
        )
    )
    @ddt.unpack
    def test_get_course_and_course_run(
            self,
            course_run_id,
            course_runs_endpoint_response,
            course_endpoint_response,
            expected_resource_id,
            expected_course_run
    ):
        """
        Verify get_course_and_course_run of CourseCatalogApiClient works as expected.
        """
        self.get_data_mock.side_effect = [course_runs_endpoint_response, course_endpoint_response]
        expected_result = course_endpoint_response, expected_course_run

        actual_result = self.api.get_course_and_course_run(course_run_id)

        assert self.get_data_mock.call_count == 2
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.COURSES_ENDPOINT
        assert resource_id == expected_resource_id
        assert actual_result == expected_result

    @ddt.data(*EMPTY_RESPONSES)
    def test_load_data_with_exception(self, default):
        """
        ``_load_data`` returns a default value given an exception.
        """
        self.get_data_mock.side_effect = HttpClientError
        assert self.api._load_data('', default=default) == default  # pylint: disable=protected-access

    @responses.activate
    def test_get_catalog_results(self):
        """
        Verify `get_catalog_results` of CourseCatalogApiClient works as expected.
        """
        content_filter_query = {'content_type': 'course', 'aggregation_key': ['course:edX+DemoX']}
        response_dict = {
            'next': 'next',
            'previous': 'previous',
            'results': [{
                'enterprise_id': 'a9e8bb52-0c8d-4579-8496-1a8becb0a79c',
                'catalog_id': 1111,
                'uuid': '785b11f5-fad5-4ce1-9233-e1a3ed31aadb',
                'aggregation_key': 'course:edX+DemoX',
                'content_type': 'course',
                'title': 'edX Demonstration Course',
            }],
        }
        responses.add(
            responses.POST,
            url=urljoin(self.api.catalog_url, self.api.SEARCH_ALL_ENDPOINT),
            json=response_dict,
            status=200,
        )
        actual_result = self.api.get_catalog_results(
            content_filter_query=content_filter_query,
            query_params={'page': 2}
        )
        assert actual_result == response_dict

    @responses.activate
    @mock.patch.object(CourseCatalogApiClient, 'get_catalog_results_from_discovery', return_value={'result': 'dummy'})
    def test_get_catalog_results_cache(self, mocked_get_catalog_results_from_discovery):  # pylint: disable=invalid-name
        """
        Verify `get_catalog_results` of CourseCatalogApiClient works as expected.
        """
        content_filter_query = {'content_type': 'course', 'aggregation_key': ['course:edX+DemoX']}
        self.api.get_catalog_results(content_filter_query=content_filter_query)
        assert mocked_get_catalog_results_from_discovery.call_count == 1

        # searching same query should not hit discovery service again
        self.api.get_catalog_results(content_filter_query=content_filter_query)
        assert mocked_get_catalog_results_from_discovery.call_count == 1

        # getting catalog with different params should hit discovery
        content_filter_query.update({'partner': 'edx'})
        self.api.get_catalog_results(content_filter_query=content_filter_query)
        assert mocked_get_catalog_results_from_discovery.call_count == 2

    @responses.activate
    def test_get_catalog_results_with_traverse_pagination(self):
        """
        Verify `get_catalog_results` of CourseCatalogApiClient works as expected with traverse_pagination=True.
        """
        content_filter_query = {'content_type': 'course', 'aggregation_key': ['course:edX+DemoX']}
        response_dict = {
            'next': 'next',
            'previous': None,
            'results': [{
                'enterprise_id': 'a9e8bb52-0c8d-4579-8496-1a8becb0a79c',
                'catalog_id': 1111,
                'uuid': '785b11f5-fad5-4ce1-9233-e1a3ed31aadb',
                'aggregation_key': 'course:edX+DemoX',
                'content_type': 'course',
                'title': 'edX Demonstration Course',
            }],
        }

        def request_callback(request):
            """
            Mocked callback for POST request to search/all endpoint.
            """
            response = response_dict
            if 'page=2' in request.url:
                response = dict(response, next=None)
            return (200, {}, json.dumps(response))

        responses.add_callback(
            responses.POST,
            url=urljoin(self.api.catalog_url, self.api.SEARCH_ALL_ENDPOINT),
            callback=request_callback,
            content_type='application/json',
        )
        responses.add_callback(
            responses.POST,
            url='{}?{}'.format(urljoin(self.api.catalog_url, self.api.SEARCH_ALL_ENDPOINT), '?page=2&page_size=100'),
            callback=request_callback,
            content_type='application/json',
        )

        recieved_response = self.api.get_catalog_results(
            content_filter_query=content_filter_query,
            traverse_pagination=True
        )
        complete_response = {
            'next': None,
            'previous': None,
            'results': response_dict['results'] * 2
        }

        assert recieved_response == complete_response

    @responses.activate
    def test_get_catalog_results_with_exception(self):
        """
        Verify `get_catalog_results` of CourseCatalogApiClient works as expected in case of exception.
        """
        responses.add(
            responses.POST,
            url=urljoin(self.api.catalog_url, self.api.SEARCH_ALL_ENDPOINT),
            body=HttpClientError(content='boom'),
        )
        logger = logging.getLogger('enterprise.api_client.discovery')
        handler = MockLoggingHandler(level="ERROR")
        logger.addHandler(handler)
        with self.assertRaises(HttpClientError):
            self.api.get_catalog_results(
                content_filter_query='query',
                query_params={u'page': 2}
            )
        expected_message = ('Attempted to call course-discovery search/all/ endpoint with the following parameters: '
                            'content_filter_query: query, query_params: {}, traverse_pagination: False. '
                            'Failed to retrieve data from the catalog API. content -- [boom]').format({u'page': 2})
        assert handler.messages['error'][0] == expected_message
 def test_raise_error_missing_get_edx_api_data(self, *args):  # pylint: disable=unused-argument
     with self.assertRaises(NotConnectedToOpenEdX):
         CourseCatalogApiClient(mock.Mock(spec=User))
Beispiel #12
0
class TestCourseCatalogApi(CourseDiscoveryApiTestMixin, unittest.TestCase):
    """
    Test course catalog API methods.
    """

    EMPTY_RESPONSES = (None, {}, [], set(), "")

    def setUp(self):
        super(TestCourseCatalogApi, self).setUp()
        self.user_mock = mock.Mock(spec=User)
        self.get_data_mock = self._make_patch(self._make_catalog_api_location("get_edx_api_data"))
        self.catalog_api_config_mock = self._make_patch(self._make_catalog_api_location("CatalogIntegration"))
        self.jwt_builder_mock = self._make_patch(self._make_catalog_api_location("JwtBuilder"))

        self.api = CourseCatalogApiClient(self.user_mock)

    @staticmethod
    def _make_course_run(key, *seat_types):
        """
        Return course_run json representation expected by CourseCatalogAPI.
        """
        return {
            "key": key,
            "seats": [{"type": seat_type} for seat_type in seat_types]
        }

    _make_run = _make_course_run.__func__  # unwrapping to use within class definition

    def test_get_search_results(self):
        """
        Verify get_search_results of CourseCatalogApiClient works as expected.
        """
        querystring = 'very'
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict
        actual_result = self.api.get_search_results(querystring=querystring)
        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)
        assert resource == CourseCatalogApiClient.SEARCH_ALL_ENDPOINT
        assert resource_id is None
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_search_results_empty_response(self, response):
        """
        Verify get_search_results of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_search_results(querystring='querystring') == []

    def test_get_all_catalogs(self):
        """
        Verify get_all_catalogs of CourseCatalogApiClient works as expected.
        """
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_all_catalogs()

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.CATALOGS_ENDPOINT
        assert resource_id is None
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_all_catalogs_empty_response(self, response):
        """
        Verify get_all_catalogs of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_all_catalogs() == []

    def test_get_catalog_courses(self):
        """
        Verify get_catalog_courses of CourseCatalogApiClient works as expected.
        """
        catalog_id = 123
        expected_result = ['item1', 'item2', 'item3']
        self.get_data_mock.return_value = expected_result

        actual_result = self.api.get_catalog_courses(catalog_id)

        assert self.get_data_mock.call_count == 1
        resource, _ = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.CATALOGS_COURSES_ENDPOINT.format(catalog_id)
        assert actual_result == expected_result

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_catalog_courses_empty_response(self, response):
        """
        Verify get_catalog_courses of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_catalog_courses(catalog_id=1) == []

    def test_get_course_details(self):
        """
        Verify get_course_details of CourseCatalogApiClient works as expected.
        """
        course_key = 'edX+DemoX'
        expected_result = {"complex": "dict"}
        self.get_data_mock.return_value = expected_result

        actual_result = self.api.get_course_details(course_key)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.COURSES_ENDPOINT
        assert resource_id == 'edX+DemoX'
        assert actual_result == expected_result

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_course_details_empty_response(self, response):
        """
        Verify get_course_details of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_course_details(course_id='edX+DemoX') == {}

    def test_get_catalog(self):
        """
        Verify get_catalog of CourseCatalogApiClient works as expected.
        """
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_catalog(catalog_id=1)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.CATALOGS_ENDPOINT
        assert resource_id is 1
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_catalog_empty_response(self, response):
        """
        Verify get_catalog of CourseCatalogApiClient works as expected.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_catalog(catalog_id=1) == []

    def test_get_paginated_catalog_courses(self):
        """
        Verify get_paginated_catalog_courses of CourseCatalogApiClient works as expected.
        """
        catalog_id = 1
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_paginated_catalog_courses(catalog_id=catalog_id)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.CATALOGS_COURSES_ENDPOINT.format(catalog_id)
        assert resource_id is None
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_paginated_catalog_courses_empty_response(self, response):
        """
        Verify get_paginated_catalog_courses of CourseCatalogApiClient works as expected.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_paginated_catalog_courses(catalog_id=1) == []

    def test_get_paginated_catalogs(self):
        """
        Verify get_paginated_catalogs of CourseCatalogApiClient works as expected.
        """
        response_dict = {'very': 'complex', 'json': {'with': 'nested object'}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_paginated_catalogs()

        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.CATALOGS_ENDPOINT
        assert resource_id is None
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_paginated_catalogs_empty_response(self, response):
        """
        Verify get_paginated_catalogs of CourseCatalogApiClient works as expected for an empty response.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_paginated_catalog_courses(catalog_id=1) == []

    @ddt.data(
        "course-v1:JediAcademy+AppliedTelekinesis+T1",
        "course-v1:TrantorAcademy+Psychohistory101+T1",
        "course-v1:StarfleetAcademy+WarpspeedNavigation+T2337",
        "course-v1:SinhonCompanionAcademy+Calligraphy+TermUnknown",
        "course-v1:CampArthurCurrie+HeavyWeapons+T2245_5",
    )
    def test_get_course_run(self, course_run_id):
        """
        Verify get_course_run of CourseCatalogApiClient works as expected.
        """
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_course_run(course_run_id)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.COURSE_RUNS_ENDPOINT
        assert resource_id is course_run_id
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_course_run_empty_response(self, response):
        """
        Verify get_course_run of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_course_run("any") == {}

    @ddt.data("Apollo", "Star Wars", "mk Ultra")
    def test_get_program_by_uuid(self, program_id):
        """
        Verify get_program_by_uuid of CourseCatalogApiClient works as expected.
        """
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_program_by_uuid(program_id)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.PROGRAMS_ENDPOINT
        assert resource_id is program_id
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_program_by_uuid_empty_response(self, response):
        """
        Verify get_program_by_uuid of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_program_by_uuid("any") is None

    @ddt.unpack
    @ddt.data(
        ("mk Ultra", [{'title': "Star Wars"}], None),
        ("Star Wars", [{'title': "Star Wars"}], {'title': "Star Wars"}),
        (
            "Apollo",
            [{'title': "Star Wars"}, {'title': "Apollo", "uuid": "Apollo11"}],
            {'title': "Apollo", "uuid": "Apollo11"}
        ),
    )
    def test_get_program_by_title(self, program_title, response, expected_result):
        """
        Verify get_program_by_title of CourseCatalogApiClient works as expected.
        """
        self.get_data_mock.return_value = response

        actual_result = self.api.get_program_by_title(program_title)

        assert self.get_data_mock.call_count == 1
        resource, _ = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.PROGRAMS_ENDPOINT
        assert actual_result == expected_result

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_program_by_title_empty_response(self, response):
        """
        Verify get_program_by_title of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_program_by_title("any") is None

    def test_get_program_by_title_raise_multiple_match(self):
        """
        Verify get_program_by_title of CourseCatalogApiClient works as expected for when there are multiple matches.
        """
        self.get_data_mock.return_value = [
            {'title': "Apollo", "uuid": "Apollo11"},
            {'title': "Apollo", "uuid": "Apollo12"}
        ]
        with raises(CourseCatalogApiError):
            self.api.get_program_by_title("Apollo")

    @ddt.data("MicroMasters Certificate", "Professional Certificate", "XSeries Certificate")
    def test_get_program_type_by_slug(self, slug):
        """
        Verify get_program_type_by_slug of CourseCatalogApiClient works as expected.
        """
        response_dict = {"very": "complex", "json": {"with": " nested object"}}
        self.get_data_mock.return_value = response_dict

        actual_result = self.api.get_program_type_by_slug(slug)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.PROGRAM_TYPES_ENDPOINT
        assert resource_id is slug
        assert actual_result == response_dict

    @ddt.data(*EMPTY_RESPONSES)
    def test_get_program_type_by_slug_empty_response(self, response):
        """
        Verify get_program_type_by_slug of CourseCatalogApiClient works as expected for empty responses.
        """
        self.get_data_mock.return_value = response
        assert self.api.get_program_type_by_slug('slug') is None

    @ddt.unpack
    @ddt.data(
        # single run
        (("c1",), [_make_run("c1", "prof", "audit")], {"prof", "audit"}),
        # multiple runs - intersection
        (("c1", "c2"), [_make_run("c1", "prof", "audit"), _make_run("c2", "prof")], {"prof"}),
        # multiple runs, one of which is empty
        (("c1", "c2"), [_make_run("c1"), _make_run("c2", "prof")], set()),
        # multiple runs, one of which is empty - other way around
        (("c1", "c2"), [_make_run("c2", "prof"), _make_run("c1")], set()),
        # run(s) not found
        (("c1", "c3", "c4"), [_make_run("c1"), _make_run("c2", "prof")], set()),
    )
    def test_get_common_course_modes(self, course_runs, response, expected_result):
        """
        Verify get_common_course_modes of CourseCatalogApiClient works as expected.
        """
        def get_course_run(*args, **kwargs):  # pylint: disable=unused-argument
            """
            Return course run data from `reponse` argument by key.
            """
            resource_id = kwargs.get("resource_id")
            try:
                return next(item for item in response if item["key"] == resource_id)
            except StopIteration:
                return {}

        self.get_data_mock.side_effect = get_course_run

        actual_result = self.api.get_common_course_modes(course_runs)
        assert actual_result == expected_result

    @ddt.data(
        (23, 'course-v1:org+course+basic_course', {'courses': {}}, False, True),
        (
            45,
            'course-v1:org+course+fancy_course',
            {
                'courses': {
                    'course-v1:org+course+fancy_course': True
                }
            },
            True,
            True
        ),
        (
            93,
            'course-v1:org+course+my_course',
            {
                'courses': {
                    'course-v1:org+course+my_course': False
                }
            },
            False,
            True
        ),
        (23, 'basic_course', {'courses': {}}, False, False),
        (
            45,
            'fancy_course',
            {
                'courses': {
                    'fancy_course': True
                }
            },
            True,
            False
        ),
        (93, 'my_course', {'courses': {'my_course': False}}, False, False)
    )
    @ddt.unpack
    @mock.patch('enterprise.api_client.discovery.course_discovery_api_client')
    def test_is_course_in_catalog(
            self,
            catalog_id,
            course_id,
            api_resp,
            expected,
            is_a_course_run,
            mock_discovery_client_factory
    ):
        """
        Test the API client that checks to determine if a given course ID is present
        in the given catalog.
        """
        discovery_client = mock_discovery_client_factory.return_value
        discovery_client.catalogs.return_value.contains.get.return_value = api_resp
        self.api = CourseCatalogApiClient(self.user_mock)
        assert self.api.is_course_in_catalog(catalog_id, course_id) == expected
        discovery_client.catalogs.assert_called_once_with(catalog_id)
        if is_a_course_run:
            discovery_client.catalogs.return_value.contains.get.assert_called_once_with(course_run_id=course_id)
        else:
            discovery_client.catalogs.return_value.contains.get.assert_called_once_with(course_id=course_id)

    @ddt.data(
        (None, []),
        ({}, []),
        (
            {
                'courses': [
                    {'key': 'first+key'},
                    {'key': 'second+key'}
                ]
            },
            [
                'first+key',
                'second+key'
            ]
        )
    )
    @ddt.unpack
    def test_get_program_course_keys(self, response_body, expected_result):
        self.get_data_mock.return_value = response_body
        result = self.api.get_program_course_keys('fake-uuid')
        assert result == expected_result

    @ddt.data(
        (
            "course-v1:JediAcademy+AppliedTelekinesis+T1",
            {"course_runs": [{"key": "course-v1:JediAcademy+AppliedTelekinesis+T1"}]},
            "JediAcademy+AppliedTelekinesis",
            {"key": "course-v1:JediAcademy+AppliedTelekinesis+T1"}
        ),
        (
            "course-v1:JediAcademy+AppliedTelekinesis+T1",
            {},
            "JediAcademy+AppliedTelekinesis",
            None
        ),
        (
            "course-v1:JediAcademy+AppliedTelekinesis+T1",
            {"course_runs": [
                {"key": "course-v1:JediAcademy+AppliedTelekinesis+T222"},
                {"key": "course-v1:JediAcademy+AppliedTelekinesis+T1"}
            ]},
            "JediAcademy+AppliedTelekinesis",
            {"key": "course-v1:JediAcademy+AppliedTelekinesis+T1"}
        ),
        (
            "course-v1:JediAcademy+AppliedTelekinesis+T1",
            {"course_runs": []},
            "JediAcademy+AppliedTelekinesis",
            None
        )
    )
    @ddt.unpack
    def test_get_course_and_course_run(
            self,
            course_run_id,
            response_dict,
            expected_resource_id,
            expected_course_run
    ):
        """
        Verify get_course_and_course_run of CourseCatalogApiClient works as expected.
        """
        self.get_data_mock.return_value = response_dict
        expected_result = response_dict, expected_course_run

        actual_result = self.api.get_course_and_course_run(course_run_id)

        assert self.get_data_mock.call_count == 1
        resource, resource_id = self._get_important_parameters(self.get_data_mock)

        assert resource == CourseCatalogApiClient.COURSES_ENDPOINT
        assert resource_id == expected_resource_id
        assert actual_result == expected_result

    @ddt.data(*EMPTY_RESPONSES)
    def test_load_data_with_exception(self, default):
        """
        ``_load_data`` returns a default value given an exception.
        """
        self.get_data_mock.side_effect = HttpClientError
        assert self.api._load_data('', default=default) == default  # pylint: disable=protected-access