Commit 91140126 by Renzo Lucioni

Merge pull request #88 from edx/renzo/kill-v1

Delete API v1 package
parents 08257edb ab58284f
...@@ -29,18 +29,13 @@ class JwtAuthentication(JSONWebTokenAuthentication): ...@@ -29,18 +29,13 @@ class JwtAuthentication(JSONWebTokenAuthentication):
Example: Example:
Access an endpoint protected by JWT authentication as follows. Access an endpoint protected by JWT authentication as follows.
>>> url = 'http://localhost:8002/api/v1/orders/' # Protected by JwtAuthentication >>> url = 'http://localhost:8002/api/v2/baskets/' # Protected by JwtAuthentication
>>> data = {'sku': 'SEAT-HONOR-EDX-DEMOX-DEMO-COURSE'} >>> token = jwt.encode({'username': 'Saul', 'email': 'saul@bettercallsaul.com'}, 'insecure-secret-key')
>>> payload = {
'username': 'Saul',
'email': 'saul@bettercallsaul.com',
'tracking_context': {'lms_user_id': '123', 'lms_client_id': 'xyz'},
}
>>> token = jwt.encode(payload, 'insecure-secret-key')
>>> headers = { >>> headers = {
'content-type': 'application/json', 'content-type': 'application/json',
'Authorization': 'JWT ' + token 'Authorization': 'JWT ' + token
} }
>>> data = {'products': [{'sku': 'PAID-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'}
>>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response = requests.post(url, data=json.dumps(data), headers=headers)
>>> response.status_code >>> response.status_code
200 200
......
...@@ -3,6 +3,5 @@ from django.conf.urls import patterns, url, include ...@@ -3,6 +3,5 @@ from django.conf.urls import patterns, url, include
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^v1/', include('ecommerce.extensions.api.v1.urls', namespace='v1')),
url(r'^v2/', include('ecommerce.extensions.api.v2.urls', namespace='v2')), url(r'^v2/', include('ecommerce.extensions.api.v2.urls', namespace='v2')),
) )
# -*- coding: utf-8 -*-
"""Integration tests of the orders endpoint and fulfillment."""
import logging
from decimal import Decimal as D
import jwt
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from oscar.test import factories
from oscar.core.loading import get_model
from rest_framework import status
from ecommerce.extensions.api.serializers import OrderSerializer
Order = get_model('order', 'Order')
ShippingEventType = get_model('order', 'ShippingEventType')
class OrdersIntegrationTests(TestCase):
USER_DATA = {
'username': 'sgoodman',
'email': 'saul@bettercallsaul.com',
}
FREE_TRIAL_SKU = u'ṖÜḄḶЇĊ⸚ḊЁḞЁṄḊЁṚ'
JWT_SECRET_KEY = getattr(settings, 'JWT_AUTH')['JWT_SECRET_KEY']
def setUp(self):
# Override all loggers, suppressing logging calls of severity CRITICAL and below
logging.disable(logging.CRITICAL)
self.product_class = factories.ProductClassFactory(
name=u'𝕿𝖗𝖎𝖆𝖑',
requires_shipping=False,
track_stock=False
)
self.courthouse = factories.ProductFactory(
structure='parent',
title=u'𝑩𝒆𝒓𝒏𝒂𝒍𝒊𝒍𝒍𝒐 𝑪𝒐𝒖𝒏𝒕𝒚 𝑨𝒏𝒏𝒆𝒙',
product_class=self.product_class,
stockrecords=None,
)
self.free_trial = factories.ProductFactory(
structure='child',
parent=self.courthouse,
title=u'𝕋𝕣𝕚𝕒𝕝 𝕨𝕚𝕥𝕙 ℙ𝕦𝕓𝕝𝕚𝕔 𝔻𝕖𝕗𝕖𝕟𝕕𝕖𝕣',
product_class=self.product_class,
stockrecords__partner_sku=self.FREE_TRIAL_SKU,
stockrecords__price_excl_tax=D('0.00'),
)
# Ideally, we'd use Oscar's ShippingEventTypeFactory here, but it's not exposed/public.
shipped_event = ShippingEventType(code='shipped', name='Shipped')
shipped_event.save()
# Remove logger override
self.addCleanup(logging.disable, logging.NOTSET)
@override_settings(FULFILLMENT_MODULES=['ecommerce.extensions.fulfillment.tests.modules.FakeFulfillmentModule'])
def test_order_free_product(self):
"""Test that a free product can be ordered and fulfilled successfully."""
self._create_and_verify_order(self.FREE_TRIAL_SKU)
def _order(self, sku):
data = {'sku': sku}
token = jwt.encode(self.USER_DATA, self.JWT_SECRET_KEY)
response = self.client.post(reverse('api:v1:orders:create_list'), data, HTTP_AUTHORIZATION='JWT ' + token)
return response
def _create_and_verify_order(self, sku):
response = self._order(sku=sku)
# Verify that the orders endpoint has successfully created the order
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Verify that the response data is valid
self.assertDictContainsSubset(OrderSerializer(Order.objects.get()).data, response.data)
from django.conf.urls import patterns, url, include
from ecommerce.extensions.api.v1 import views
ORDER_NUMBER_PATTERN = r'(?P<number>[-\w]+)'
ORDER_URLS = patterns(
'',
url(r'^$', views.OrderListCreateAPIView.as_view(), name='create_list'),
url(
r'^{number}/$'.format(number=ORDER_NUMBER_PATTERN),
views.RetrieveOrderView.as_view(),
name='retrieve'
),
url(
r'^{number}/fulfill/$'.format(number=ORDER_NUMBER_PATTERN),
views.OrderFulfillView.as_view(),
name='fulfill'
),
)
urlpatterns = patterns(
'',
url(r'^orders/', include(ORDER_URLS, namespace='orders'))
)
...@@ -7,6 +7,7 @@ import logging ...@@ -7,6 +7,7 @@ import logging
import ddt import ddt
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.cache import cache from django.core.cache import cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
...@@ -22,6 +23,8 @@ from ecommerce.extensions.api import exceptions as api_exceptions ...@@ -22,6 +23,8 @@ from ecommerce.extensions.api import exceptions as api_exceptions
from ecommerce.extensions.api.constants import APIConstants as AC from ecommerce.extensions.api.constants import APIConstants as AC
from ecommerce.extensions.api.serializers import OrderSerializer from ecommerce.extensions.api.serializers import OrderSerializer
from ecommerce.extensions.api.tests.test_authentication import AccessTokenMixin, OAUTH2_PROVIDER_URL from ecommerce.extensions.api.tests.test_authentication import AccessTokenMixin, OAUTH2_PROVIDER_URL
from ecommerce.extensions.fulfillment.mixins import FulfillmentMixin
from ecommerce.extensions.fulfillment.status import LINE, ORDER
from ecommerce.extensions.payment import exceptions as payment_exceptions from ecommerce.extensions.payment import exceptions as payment_exceptions
from ecommerce.extensions.payment.processors import Cybersource from ecommerce.extensions.payment.processors import Cybersource
from ecommerce.extensions.payment.tests.processors import DummyProcessor, AnotherDummyProcessor from ecommerce.extensions.payment.tests.processors import DummyProcessor, AnotherDummyProcessor
...@@ -29,6 +32,8 @@ from ecommerce.tests.mixins import UserMixin, ThrottlingMixin, BasketCreationMix ...@@ -29,6 +32,8 @@ from ecommerce.tests.mixins import UserMixin, ThrottlingMixin, BasketCreationMix
Basket = get_model('basket', 'Basket') Basket = get_model('basket', 'Basket')
Order = get_model('order', 'Order')
ShippingEventType = get_model('order', 'ShippingEventType')
@ddt.ddt @ddt.ddt
...@@ -338,6 +343,93 @@ class OrderListViewTests(AccessTokenMixin, ThrottlingMixin, UserMixin, TestCase) ...@@ -338,6 +343,93 @@ class OrderListViewTests(AccessTokenMixin, ThrottlingMixin, UserMixin, TestCase)
self.assertEqual(content['results'][0]['number'], unicode(order.number)) self.assertEqual(content['results'][0]['number'], unicode(order.number))
@ddt.ddt
class OrderFulfillViewTests(UserMixin, TestCase):
def setUp(self):
super(OrderFulfillViewTests, self).setUp()
ShippingEventType.objects.create(name=FulfillmentMixin.SHIPPING_EVENT_NAME)
self.user = self.create_user(is_superuser=True)
self.client.login(username=self.user.username, password=self.password)
self.order = factories.create_order()
self.order.status = ORDER.FULFILLMENT_ERROR
self.order.save()
self.order.lines.all().update(status=LINE.FULFILLMENT_CONFIGURATION_ERROR)
self.url = reverse('api:v2:orders:fulfill', kwargs={'number': self.order.number})
def _put_to_view(self):
"""
PUT to the view being tested.
Returns:
Response
"""
return self.client.put(self.url)
@ddt.data('delete', 'get', 'post')
def test_post_required(self, method):
""" Verify that the view only responds to PUT and PATCH operations. """
response = getattr(self.client, method)(self.url)
self.assertEqual(405, response.status_code)
def test_login_required(self):
""" The view should return HTTP 401 status if the user is not logged in. """
self.client.logout()
self.assertEqual(401, self._put_to_view().status_code)
def test_change_permissions_required(self):
"""
The view requires the user to have change permissions for Order objects. If the user does not have permission,
the view should return HTTP 403 status.
"""
self.user.is_superuser = False
self.user.save()
self.assertEqual(403, self._put_to_view().status_code)
permission = Permission.objects.get(codename='change_order')
self.user.user_permissions.add(permission)
self.assertNotEqual(403, self._put_to_view().status_code)
@ddt.data(ORDER.OPEN, ORDER.COMPLETE)
def test_order_fulfillment_error_state_required(self, order_status):
""" If the order is not in the Fulfillment Error state, the view must return an HTTP 406. """
self.order.status = order_status
self.order.save()
self.assertEqual(406, self._put_to_view().status_code)
def test_ideal_conditions(self):
"""
If the user is authenticated/authorized, and the order is in the Fulfillment Error state, the view should
attempt to fulfill the order. The view should return HTTP 200.
"""
self.assertEqual(ORDER.FULFILLMENT_ERROR, self.order.status)
with mock.patch('ecommerce.extensions.order.processing.EventHandler.handle_shipping_event') as mocked:
def handle_shipping_event(order, _event_type, _lines, _line_quantities, **_kwargs):
order.status = ORDER.COMPLETE
order.save()
return order
mocked.side_effect = handle_shipping_event
response = self._put_to_view()
self.assertTrue(mocked.called)
self.assertEqual(200, response.status_code)
# Reload the order from the DB and check its status
self.order = Order.objects.get(number=self.order.number)
self.assertEqual(unicode(self.order.number), response.data['number'])
self.assertEqual(self.order.status, response.data['status'])
def test_fulfillment_failed(self):
""" If fulfillment fails, the view should return HTTP 500. """
self.assertEqual(ORDER.FULFILLMENT_ERROR, self.order.status)
response = self._put_to_view()
self.assertEqual(500, response.status_code)
class PaymentProcessorListViewTests(TestCase, UserMixin): class PaymentProcessorListViewTests(TestCase, UserMixin):
""" Ensures correct behavior of the payment processors list view.""" """ Ensures correct behavior of the payment processors list view."""
......
...@@ -4,15 +4,15 @@ import logging ...@@ -4,15 +4,15 @@ import logging
from django.conf import settings from django.conf import settings
from oscar.core.loading import get_model from oscar.core.loading import get_model
from rest_framework import status from rest_framework import status
from rest_framework.generics import CreateAPIView, RetrieveAPIView, ListAPIView from rest_framework.generics import CreateAPIView, RetrieveAPIView, ListAPIView, UpdateAPIView
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
from rest_framework.response import Response from rest_framework.response import Response
from ecommerce.extensions.api import data, exceptions as api_exceptions, serializers from ecommerce.extensions.api import data, exceptions as api_exceptions, serializers
from ecommerce.extensions.api.constants import APIConstants as AC from ecommerce.extensions.api.constants import APIConstants as AC
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from ecommerce.extensions.api.v1.views import OrderFulfillView # pylint: disable=unused-import
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.fulfillment.mixins import FulfillmentMixin
from ecommerce.extensions.payment import exceptions as payment_exceptions from ecommerce.extensions.payment import exceptions as payment_exceptions
from ecommerce.extensions.payment.helpers import (get_processor_class, get_default_processor_class, from ecommerce.extensions.payment.helpers import (get_processor_class, get_default_processor_class,
get_processor_class_by_name) get_processor_class_by_name)
...@@ -83,7 +83,7 @@ class BasketCreateView(EdxOrderPlacementMixin, CreateAPIView): ...@@ -83,7 +83,7 @@ class BasketCreateView(EdxOrderPlacementMixin, CreateAPIView):
>>> data = {'products': [{'sku': 'SOME-SEAT'}, {'sku': 'SOME-OTHER-SEAT'}], 'checkout': False} >>> data = {'products': [{'sku': 'SOME-SEAT'}, {'sku': 'SOME-OTHER-SEAT'}], 'checkout': False}
>>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response = requests.post(url, data=json.dumps(data), headers=headers)
>>> json.loads(response.content) >>> response.json()
{ {
u'id': 7, u'id': 7,
u'order': None, u'order': None,
...@@ -94,7 +94,7 @@ class BasketCreateView(EdxOrderPlacementMixin, CreateAPIView): ...@@ -94,7 +94,7 @@ class BasketCreateView(EdxOrderPlacementMixin, CreateAPIView):
>>> data = {'products': [{'sku': 'FREE-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'} >>> data = {'products': [{'sku': 'FREE-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'}
>>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response = requests.post(url, data=json.dumps(data), headers=headers)
>>> json.loads(response.content) >>> response.json()
{ {
u'id': 7, u'id': 7,
u'order': {u'number': u'OSCR-100007'}, u'order': {u'number': u'OSCR-100007'},
...@@ -105,7 +105,7 @@ class BasketCreateView(EdxOrderPlacementMixin, CreateAPIView): ...@@ -105,7 +105,7 @@ class BasketCreateView(EdxOrderPlacementMixin, CreateAPIView):
>>> data = {'products': [{'sku': 'PAID-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'} >>> data = {'products': [{'sku': 'PAID-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'}
>>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response = requests.post(url, data=json.dumps(data), headers=headers)
>>> json.loads(response.content) >>> response.json()
{ {
u'id': 7, u'id': 7,
u'order': None, u'order': None,
...@@ -344,6 +344,29 @@ class OrderByBasketRetrieveView(OrderRetrieveView): ...@@ -344,6 +344,29 @@ class OrderByBasketRetrieveView(OrderRetrieveView):
lookup_field = 'basket_id' lookup_field = 'basket_id'
class OrderFulfillView(FulfillmentMixin, UpdateAPIView):
permission_classes = (IsAuthenticated, DjangoModelPermissions,)
lookup_field = 'number'
queryset = Order.objects.all()
serializer_class = serializers.OrderSerializer
def update(self, request, *args, **kwargs):
order = self.get_object()
if not order.can_retry_fulfillment:
return Response(status=status.HTTP_406_NOT_ACCEPTABLE)
logger.info('Retrying fulfillment of order [%s]...', order.number)
order = self.fulfill_order(order)
if order.can_retry_fulfillment:
logger.warning('Fulfillment of order [%s] failed!', order.number)
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
serializer = self.get_serializer(order)
return Response(serializer.data)
class PaymentProcessorListView(ListAPIView): class PaymentProcessorListView(ListAPIView):
"""View that lists the available payment processors.""" """View that lists the available payment processors."""
pagination_class = None pagination_class = None
......
...@@ -7,6 +7,7 @@ ShippingEventType = get_model('order', 'ShippingEventType') ...@@ -7,6 +7,7 @@ ShippingEventType = get_model('order', 'ShippingEventType')
EventHandler = get_class('order.processing', 'EventHandler') EventHandler = get_class('order.processing', 'EventHandler')
# TODO (RFL): Retire this mixin in favor of the `post_checkout_callback`.
class FulfillmentMixin(object): class FulfillmentMixin(object):
"""A mixin that provides the ability to fulfill orders.""" """A mixin that provides the ability to fulfill orders."""
SHIPPING_EVENT_NAME = 'Shipped' SHIPPING_EVENT_NAME = 'Shipped'
......
...@@ -18,7 +18,7 @@ $(document).ready(function () { ...@@ -18,7 +18,7 @@ $(document).ready(function () {
// Make AJAX call and update status // Make AJAX call and update status
$.ajax({ $.ajax({
url: '/api/v1/orders/' + order_number + '/fulfill/', url: '/api/v2/orders/' + order_number + '/fulfill/',
method: 'PUT', method: 'PUT',
headers: {'X-CSRFToken': $.cookie('csrftoken')} headers: {'X-CSRFToken': $.cookie('csrftoken')}
}).success(function (data) { }).success(function (data) {
......
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