Commit 3844fd40 by Marko Jevtic

[SOL-1772] Add course query preview endpoint

parent b1c8889f
import json
import mock
import ddt
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import RequestFactory
from edx_rest_api_client.client import EdxRestApiClient
import httpretty
from requests.exceptions import ConnectionError, Timeout
from oscar.core.loading import get_model
from slumber.exceptions import SlumberBaseException
from ecommerce.extensions.api.serializers import ProductSerializer
from ecommerce.extensions.api.v2.views.catalog import CatalogViewSet
from ecommerce.extensions.api.v2.tests.views.mixins import CatalogMixin
from ecommerce.tests.mixins import ApiMockMixin, CatalogPreviewMockMixin
from ecommerce.tests.testcases import TestCase
......@@ -12,7 +22,9 @@ Catalog = get_model('catalogue', 'Catalog')
StockRecord = get_model('partner', 'StockRecord')
class CatalogViewSetTest(CatalogMixin, TestCase):
@httpretty.activate
@ddt.ddt
class CatalogViewSetTest(CatalogMixin, CatalogPreviewMockMixin, ApiMockMixin, TestCase):
"""Test the Catalog and related products APIs."""
catalog_list_path = reverse('api:v2:catalog-list')
......@@ -22,6 +34,12 @@ class CatalogViewSetTest(CatalogMixin, TestCase):
self.client.login(username=self.user.username, password=self.password)
def prepare_request(self, url):
factory = RequestFactory()
request = factory.get(url)
request.site = self.site
return request
def test_staff_authorization_required(self):
"""Verify that only users with staff permissions can access the API. """
response = self.client.get(self.catalog_list_path)
......@@ -83,6 +101,47 @@ class CatalogViewSetTest(CatalogMixin, TestCase):
expected_data = ProductSerializer(self.stock_record.product, context={'request': response.wsgi_request}).data
self.assertListEqual(response_data['results'], [expected_data])
@ddt.data(
('/api/v2/coupons/preview/', 400),
('/api/v2/coupons/preview/?query=', 400),
('/api/v2/coupons/preview/?wrong=parameter', 400),
('/api/v2/coupons/preview/?query=id:course*', 200)
)
@ddt.unpack
@mock.patch(
'ecommerce.extensions.api.v2.views.catalog.get_course_catalog_api_client',
mock.Mock(return_value=EdxRestApiClient(
settings.COURSE_CATALOG_API_URL,
jwt='auth-token'
))
)
def test_preview_catalog_query_results(self, url, status_code):
"""Test catalog query preview."""
self.mock_course_runs_contains_api_response()
request = self.prepare_request(url)
response = CatalogViewSet().preview(request)
self.assertEqual(response.status_code, status_code)
@ddt.data(ConnectionError, SlumberBaseException, Timeout)
@mock.patch(
'ecommerce.extensions.api.v2.views.catalog.get_course_catalog_api_client',
mock.Mock(return_value=EdxRestApiClient(
settings.COURSE_CATALOG_API_URL,
jwt='auth-token'
))
)
def test_preview_catalog_course_discovery_service_not_available(self, error):
"""Test catalog query preview when course discovery is not available."""
url = '/api/v2/coupons/preview/?query=id:course*'
request = self.prepare_request(url)
self.mock_api_error(error=error, url='{}course_runs/?q=id:course*'.format(settings.COURSE_CATALOG_API_URL))
response = CatalogViewSet().preview(request)
self.assertEqual(response.status_code, 400)
class PartnerCatalogViewSetTest(CatalogMixin, TestCase):
......
......@@ -8,6 +8,7 @@ import ddt
from django.core.urlresolvers import reverse
from django.db.utils import IntegrityError
from django.test import RequestFactory
import httpretty
from oscar.apps.catalogue.categories import create_from_breadcrumbs
from oscar.core.loading import get_class, get_model
from oscar.test import factories
......@@ -39,6 +40,7 @@ Voucher = get_model('voucher', 'Voucher')
COUPONS_LINK = reverse('api:v2:coupons-list')
@httpretty.activate
@ddt.ddt
class CouponViewSetTest(CouponMixin, CourseCatalogTestMixin, TestCase):
"""Unit tests for creating coupon order."""
......
import logging
from oscar.core.loading import get_model
from requests.exceptions import ConnectionError, Timeout
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework_extensions.decorators import action
from rest_framework_extensions.mixins import NestedViewSetMixin
from slumber.exceptions import SlumberBaseException
from ecommerce.extensions.api import serializers
from ecommerce.core.url_utils import get_course_catalog_api_client
Catalog = get_model('catalogue', 'Catalog')
logger = logging.getLogger(__name__)
class CatalogViewSet(NestedViewSetMixin, ReadOnlyModelViewSet):
queryset = Catalog.objects.all()
serializer_class = serializers.CatalogSerializer
permission_classes = (IsAuthenticated, IsAdminUser,)
@action(is_for_list=True, methods=['get'])
def preview(self, request):
"""
Preview the results of the catalog query.
A list of course runs, indicating a course run presence within the catalog, will be returned.
---
parameters:
- name: query
description: Elasticsearch querystring query
required: true
type: string
paramType: query
multiple: false
"""
query = request.GET.get('query', '')
if query:
try:
api = get_course_catalog_api_client(request.site)
response = api.course_runs.get(q=query)
return Response(response)
except (ConnectionError, SlumberBaseException, Timeout):
logger.error('Unable to connect to Course Catalog service.')
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_400_BAD_REQUEST)
......@@ -41,11 +41,7 @@ Voucher = get_model('voucher', 'Voucher')
class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
"""Endpoint for creating coupons.
Creates a new coupon product, adds it to a basket and creates a
new order from that basket.
"""
""" Coupon resource. """
queryset = Product.objects.filter(product_class__name='Coupon')
permission_classes = (IsAuthenticated, IsAdminUser)
filter_backends = (filters.DjangoFilterBackend, )
......
......@@ -27,7 +27,7 @@ from ecommerce.extensions.offer.utils import format_benefit_value
from ecommerce.extensions.payment.tests.processors import DummyProcessor
from ecommerce.extensions.test.factories import prepare_voucher
from ecommerce.tests.factories import StockRecordFactory
from ecommerce.tests.mixins import CouponMixin, LmsApiMockMixin
from ecommerce.tests.mixins import ApiMockMixin, CouponMixin, LmsApiMockMixin
from ecommerce.tests.testcases import TestCase
Applicator = get_class('offer.utils', 'Applicator')
......@@ -175,7 +175,7 @@ class BasketSingleItemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockM
@httpretty.activate
@ddt.ddt
@override_settings(PAYMENT_PROCESSORS=['ecommerce.extensions.payment.tests.processors.DummyProcessor'])
class BasketSummaryViewTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
class BasketSummaryViewTests(CourseCatalogTestMixin, LmsApiMockMixin, ApiMockMixin, TestCase):
""" BasketSummaryView basket view tests. """
path = reverse('basket:summary')
......@@ -199,12 +199,6 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
toggle_switch(settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + DummyProcessor.NAME, True)
def mock_course_api_error(self, error):
def callback(request, uri, headers): # pylint: disable=unused-argument
raise error
course_url = get_lms_url('api/courses/v1/courses/{}/'.format(self.course.id))
httpretty.register_uri(httpretty.GET, course_url, body=callback, content_type='application/json')
def create_basket_and_add_product(self, product):
basket = factories.BasketFactory(owner=self.user, site=self.site)
basket.add_product(product, 1)
......@@ -227,7 +221,10 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
self.assertEqual(basket.lines.count(), 1)
logger_name = 'ecommerce.extensions.basket.views'
self.mock_course_api_error(error)
self.mock_api_error(
error=error,
url=get_lms_url('api/courses/v1/courses/{}/'.format(self.course.id))
)
with LogCapture(logger_name) as l:
response = self.client.get(self.path)
......
......@@ -279,6 +279,18 @@ class TestServerUrlMixin(object):
return 'http://{domain}{path}'.format(domain=site.domain, path=path)
class ApiMockMixin(object):
""" Common Mocks for the API responses. """
def setUp(self):
super(ApiMockMixin, self).setUp()
def mock_api_error(self, error, url):
def callback(request, uri, headers): # pylint: disable=unused-argument
raise error
httpretty.register_uri(httpretty.GET, url, body=callback, content_type='application/json')
class LmsApiMockMixin(object):
""" Mocks for the LMS API reponses. """
......@@ -303,6 +315,36 @@ class LmsApiMockMixin(object):
httpretty.register_uri(httpretty.GET, course_url, body=course_info_json, content_type='application/json')
class CatalogPreviewMockMixin(object):
""" Mocks for the Course Discovery responses. """
def setUp(self):
super(CatalogPreviewMockMixin, self).setUp()
def mock_course_runs_contains_api_response(self, course_run=None, query=None):
""" Helper function to register an API endpoint for the course run information. """
course_run_info = {
'count': 1,
'results': [{
'key': course_run.id,
'title': course_run.name,
}] if course_run else [{
'key': 'course-v1:test+test+test',
'title': 'Test course',
}],
}
course_run_info_json = json.dumps(course_run_info)
course_run_url = '{}course_runs/?query={}'.format(
settings.COURSE_CATALOG_API_URL,
query if query else 'id:course*'
)
httpretty.register_uri(
httpretty.GET, course_run_url,
body=course_run_info_json,
content_type='application/json'
)
class CouponMixin(object):
""" Mixin for preparing data for coupons and creating coupons. """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment