Commit 3844fd40 by Marko Jevtic

[SOL-1772] Add course query preview endpoint

parent b1c8889f
import json import json
import mock
import ddt
from django.conf import settings
from django.core.urlresolvers import reverse 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 oscar.core.loading import get_model
from slumber.exceptions import SlumberBaseException
from ecommerce.extensions.api.serializers import ProductSerializer 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.extensions.api.v2.tests.views.mixins import CatalogMixin
from ecommerce.tests.mixins import ApiMockMixin, CatalogPreviewMockMixin
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -12,7 +22,9 @@ Catalog = get_model('catalogue', 'Catalog') ...@@ -12,7 +22,9 @@ Catalog = get_model('catalogue', 'Catalog')
StockRecord = get_model('partner', 'StockRecord') 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.""" """Test the Catalog and related products APIs."""
catalog_list_path = reverse('api:v2:catalog-list') catalog_list_path = reverse('api:v2:catalog-list')
...@@ -22,6 +34,12 @@ class CatalogViewSetTest(CatalogMixin, TestCase): ...@@ -22,6 +34,12 @@ class CatalogViewSetTest(CatalogMixin, TestCase):
self.client.login(username=self.user.username, password=self.password) 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): def test_staff_authorization_required(self):
"""Verify that only users with staff permissions can access the API. """ """Verify that only users with staff permissions can access the API. """
response = self.client.get(self.catalog_list_path) response = self.client.get(self.catalog_list_path)
...@@ -83,6 +101,47 @@ class CatalogViewSetTest(CatalogMixin, TestCase): ...@@ -83,6 +101,47 @@ class CatalogViewSetTest(CatalogMixin, TestCase):
expected_data = ProductSerializer(self.stock_record.product, context={'request': response.wsgi_request}).data expected_data = ProductSerializer(self.stock_record.product, context={'request': response.wsgi_request}).data
self.assertListEqual(response_data['results'], [expected_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): class PartnerCatalogViewSetTest(CatalogMixin, TestCase):
......
...@@ -8,6 +8,7 @@ import ddt ...@@ -8,6 +8,7 @@ import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.test import RequestFactory from django.test import RequestFactory
import httpretty
from oscar.apps.catalogue.categories import create_from_breadcrumbs from oscar.apps.catalogue.categories import create_from_breadcrumbs
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from oscar.test import factories from oscar.test import factories
...@@ -39,6 +40,7 @@ Voucher = get_model('voucher', 'Voucher') ...@@ -39,6 +40,7 @@ Voucher = get_model('voucher', 'Voucher')
COUPONS_LINK = reverse('api:v2:coupons-list') COUPONS_LINK = reverse('api:v2:coupons-list')
@httpretty.activate
@ddt.ddt @ddt.ddt
class CouponViewSetTest(CouponMixin, CourseCatalogTestMixin, TestCase): class CouponViewSetTest(CouponMixin, CourseCatalogTestMixin, TestCase):
"""Unit tests for creating coupon order.""" """Unit tests for creating coupon order."""
......
import logging
from oscar.core.loading import get_model 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.permissions import IsAuthenticated, IsAdminUser
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework_extensions.decorators import action
from rest_framework_extensions.mixins import NestedViewSetMixin from rest_framework_extensions.mixins import NestedViewSetMixin
from slumber.exceptions import SlumberBaseException
from ecommerce.extensions.api import serializers from ecommerce.extensions.api import serializers
from ecommerce.core.url_utils import get_course_catalog_api_client
Catalog = get_model('catalogue', 'Catalog') Catalog = get_model('catalogue', 'Catalog')
logger = logging.getLogger(__name__)
class CatalogViewSet(NestedViewSetMixin, ReadOnlyModelViewSet): class CatalogViewSet(NestedViewSetMixin, ReadOnlyModelViewSet):
queryset = Catalog.objects.all() queryset = Catalog.objects.all()
serializer_class = serializers.CatalogSerializer serializer_class = serializers.CatalogSerializer
permission_classes = (IsAuthenticated, IsAdminUser,) 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') ...@@ -41,11 +41,7 @@ Voucher = get_model('voucher', 'Voucher')
class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
"""Endpoint for creating coupons. """ Coupon resource. """
Creates a new coupon product, adds it to a basket and creates a
new order from that basket.
"""
queryset = Product.objects.filter(product_class__name='Coupon') queryset = Product.objects.filter(product_class__name='Coupon')
permission_classes = (IsAuthenticated, IsAdminUser) permission_classes = (IsAuthenticated, IsAdminUser)
filter_backends = (filters.DjangoFilterBackend, ) filter_backends = (filters.DjangoFilterBackend, )
......
...@@ -27,7 +27,7 @@ from ecommerce.extensions.offer.utils import format_benefit_value ...@@ -27,7 +27,7 @@ from ecommerce.extensions.offer.utils import format_benefit_value
from ecommerce.extensions.payment.tests.processors import DummyProcessor from ecommerce.extensions.payment.tests.processors import DummyProcessor
from ecommerce.extensions.test.factories import prepare_voucher from ecommerce.extensions.test.factories import prepare_voucher
from ecommerce.tests.factories import StockRecordFactory 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 from ecommerce.tests.testcases import TestCase
Applicator = get_class('offer.utils', 'Applicator') Applicator = get_class('offer.utils', 'Applicator')
...@@ -175,7 +175,7 @@ class BasketSingleItemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockM ...@@ -175,7 +175,7 @@ class BasketSingleItemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockM
@httpretty.activate @httpretty.activate
@ddt.ddt @ddt.ddt
@override_settings(PAYMENT_PROCESSORS=['ecommerce.extensions.payment.tests.processors.DummyProcessor']) @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. """ """ BasketSummaryView basket view tests. """
path = reverse('basket:summary') path = reverse('basket:summary')
...@@ -199,12 +199,6 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase): ...@@ -199,12 +199,6 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
toggle_switch(settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + DummyProcessor.NAME, True) 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): def create_basket_and_add_product(self, product):
basket = factories.BasketFactory(owner=self.user, site=self.site) basket = factories.BasketFactory(owner=self.user, site=self.site)
basket.add_product(product, 1) basket.add_product(product, 1)
...@@ -227,7 +221,10 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase): ...@@ -227,7 +221,10 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
self.assertEqual(basket.lines.count(), 1) self.assertEqual(basket.lines.count(), 1)
logger_name = 'ecommerce.extensions.basket.views' 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: with LogCapture(logger_name) as l:
response = self.client.get(self.path) response = self.client.get(self.path)
......
...@@ -279,6 +279,18 @@ class TestServerUrlMixin(object): ...@@ -279,6 +279,18 @@ class TestServerUrlMixin(object):
return 'http://{domain}{path}'.format(domain=site.domain, path=path) 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): class LmsApiMockMixin(object):
""" Mocks for the LMS API reponses. """ """ Mocks for the LMS API reponses. """
...@@ -303,6 +315,36 @@ class LmsApiMockMixin(object): ...@@ -303,6 +315,36 @@ class LmsApiMockMixin(object):
httpretty.register_uri(httpretty.GET, course_url, body=course_info_json, content_type='application/json') 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): class CouponMixin(object):
""" Mixin for preparing data for coupons and creating coupons. """ """ 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