Commit e56966c1 by Ayub khan Committed by GitHub

Merge pull request #15896 from edx/LEARNER-2294

Remove basket creation calls for audit orders from commerce and remove waffle flag used to stop those calls
parents daaf31fd 32679b04
......@@ -5,7 +5,6 @@ from datetime import datetime, timedelta
from uuid import uuid4
import ddt
import httpretty
import mock
import pytz
from django.conf import settings
......@@ -15,15 +14,13 @@ from django.test.utils import override_settings
from edx_rest_api_client import exceptions
from nose.plugins.attrib import attr
from commerce.api.v0.views import SAILTHRU_CAMPAIGN_COOKIE, STOP_BASKET_CREATION_FLAG
from commerce.api.v0.views import SAILTHRU_CAMPAIGN_COOKIE
from commerce.constants import Messages
from commerce.tests import TEST_BASKET_ID, TEST_ORDER_NUMBER, TEST_PAYMENT_DATA
from commerce.tests.mocks import mock_basket_order, mock_create_basket
from commerce.tests.mocks import mock_basket_order
from commerce.tests.test_views import UserMixin
from course_modes.models import CourseMode
from enrollment.api import get_enrollment
from openedx.core.djangoapps.embargo.test_utils import restrict_course
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.core.lib.django_test_client_utils import get_absolute_url
from student.models import CourseEnrollment
from course_modes.tests.factories import CourseModeFactory
......@@ -42,7 +39,7 @@ UTM_COOKIE_CONTENTS = {
@ddt.ddt
class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase):
"""
Tests for the commerce orders view.
Tests for the commerce Baskets view.
"""
def _post_to_view(self, course_id=None, marketing_email_opt_in=False, include_utm_cookie=False):
"""
......@@ -69,22 +66,6 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
actual = json.loads(response.content)['detail']
self.assertEqual(actual, expected_msg)
def assertResponsePaymentData(self, response):
""" Asserts correctness of a JSON body containing payment information. """
actual_response = json.loads(response.content)
self.assertEqual(actual_response, TEST_PAYMENT_DATA)
def assertValidEcommerceInternalRequestErrorResponse(self, response):
""" Asserts the response is a valid response sent when the E-Commerce API is unavailable. """
self.assertEqual(response.status_code, 500)
actual = json.loads(response.content)['detail']
self.assertIn('Call to E-Commerce API failed', actual)
def assertUserNotEnrolled(self):
""" Asserts that the user is NOT enrolled in the course, and that an enrollment event was NOT fired. """
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
self.assert_no_events_were_emitted()
def setUp(self):
super(BasketsViewTests, self).setUp()
self.url = reverse('commerce_api:v0:baskets:create')
......@@ -147,102 +128,15 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
self.assertEqual(406, self.client.post(self.url, {}).status_code)
self.assertEqual(406, self.client.post(self.url, {'not_course_id': ''}).status_code)
def test_ecommerce_api_timeout(self):
"""
If the call to the E-Commerce API times out, the view should log an error and return an HTTP 503 status.
"""
with mock_create_basket(exception=exceptions.Timeout):
response = self._post_to_view()
self.assertValidEcommerceInternalRequestErrorResponse(response)
self.assertUserNotEnrolled()
def test_ecommerce_api_error(self):
"""
If the E-Commerce API raises an error, the view should return an HTTP 503 status.
"""
with mock_create_basket(exception=exceptions.SlumberBaseException):
response = self._post_to_view()
self.assertValidEcommerceInternalRequestErrorResponse(response)
self.assertUserNotEnrolled()
def _test_successful_ecommerce_api_call(self, is_completed=True, utm_tracking_present=False):
"""
Verifies that the view contacts the E-Commerce API with the correct data and headers.
"""
with mock.patch('commerce.api.v0.views.audit_log') as mock_audit_log:
response = self._post_to_view(include_utm_cookie=utm_tracking_present)
# Verify that an audit message was logged
self.assertTrue(mock_audit_log.called)
# Validate the response content
if is_completed:
msg = Messages.ORDER_COMPLETED.format(order_number=TEST_ORDER_NUMBER)
self.assertResponseMessage(response, msg)
else:
self.assertResponsePaymentData(response)
# Make sure ecommerce API call forwards Sailthru cookie
self.assertIn('{}=sailthru id'.format(SAILTHRU_CAMPAIGN_COOKIE), httpretty.last_request().headers['cookie'])
# Check that UTM tracking cookie is passed along in request to ecommerce for attribution
if utm_tracking_present:
cookie_string = '{cookie_name}={cookie_contents}'.format(
cookie_name=UTM_COOKIE_NAME, cookie_contents=json.dumps(UTM_COOKIE_CONTENTS))
self.assertIn(cookie_string, httpretty.last_request().headers['cookie'])
@override_waffle_flag(STOP_BASKET_CREATION_FLAG, active=False)
@ddt.data(True, False)
def test_course_with_honor_seat_sku(self, user_is_active):
"""
If the course has a SKU, the view should get authorization from the E-Commerce API before enrolling
the user in the course. If authorization is approved, the user should be redirected to the user dashboard.
"""
# Set user's active flag
self.user.is_active = user_is_active
self.user.save() # pylint: disable=no-member
return_value = {'id': TEST_BASKET_ID, 'payment_data': None, 'order': {'number': TEST_ORDER_NUMBER}}
with mock_create_basket(response=return_value):
# Test that call without utm tracking works
self._test_successful_ecommerce_api_call()
with mock.patch('student.models.RegistrationCookieConfiguration.current') as config:
instance = config.return_value
instance.utm_cookie_name = UTM_COOKIE_NAME
# Test that call with cookie passes cookie along
self._test_successful_ecommerce_api_call(utm_tracking_present=True)
@override_waffle_flag(STOP_BASKET_CREATION_FLAG, active=False)
@ddt.data(True, False)
def test_course_with_paid_seat_sku(self, user_is_active):
"""
If the course has a SKU, the view should return data that the client
will use to redirect the user to an external payment processor.
"""
# Set user's active flag
self.user.is_active = user_is_active
self.user.save() # pylint: disable=no-member
return_value = {'id': TEST_BASKET_ID, 'payment_data': TEST_PAYMENT_DATA, 'order': None}
with mock_create_basket(response=return_value):
self._test_successful_ecommerce_api_call(is_completed=False)
@override_waffle_flag(STOP_BASKET_CREATION_FLAG, active=True)
@ddt.data(True, False)
def test_course_without_creating_order(self, user_is_active):
def test_course_for_active_and_inactive_user(self, user_is_active):
"""
If the course has a SKU, and the STOP_BASKET_CREATION waffle flag is on,
the enrollment should happen without contacting ecommerce api
Test course enrollment for active and inactive user.
"""
# Set user's active flag
self.user.is_active = user_is_active
self.user.save() # pylint: disable=no-member
with mock_create_basket(expect_called=False):
response = self._post_to_view()
response = self._post_to_view()
# Validate the response content
self.assertEqual(response.status_code, 200)
......@@ -254,11 +148,9 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
def _test_course_without_sku(self, enrollment_mode=CourseMode.DEFAULT_MODE_SLUG):
"""
Validates the view bypasses the E-Commerce API when the course has no CourseModes with SKUs.
Validates the view when course has no CourseModes with SKUs.
"""
# Place an order
with mock_create_basket(expect_called=False):
response = self._post_to_view()
response = self._post_to_view()
# Validate the response content
self.assertEqual(response.status_code, 200)
......@@ -299,22 +191,6 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
# We should be enrolled in honor mode
self._test_course_without_sku(enrollment_mode=CourseMode.HONOR)
@override_settings(ECOMMERCE_API_URL=None)
def test_ecommerce_service_not_configured(self):
"""
If the E-Commerce Service is not configured, the view should enroll the user.
"""
with mock_create_basket(expect_called=False):
response = self._post_to_view()
# Validate the response
self.assertEqual(response.status_code, 200)
msg = Messages.NO_ECOM_API.format(username=self.user.username, course_id=self.course.id)
self.assertResponseMessage(response, msg)
# Ensure that the user is not enrolled and that no calls were made to the E-Commerce API
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
def assertProfessionalModeBypassed(self):
""" Verifies that the view returns HTTP 406 when a course with no honor or audit mode is encountered. """
......@@ -323,9 +199,7 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
sku_string = uuid4().hex.decode('ascii')
CourseModeFactory.create(course_id=self.course.id, mode_slug=mode, mode_display_name=mode,
sku=sku_string, bulk_sku='BULK-{}'.format(sku_string))
with mock_create_basket(expect_called=False):
response = self._post_to_view()
response = self._post_to_view()
# The view should return an error status code
self.assertEqual(response.status_code, 406)
......@@ -376,9 +250,6 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
self.assertIsNotNone(get_enrollment(self.user.username, unicode(self.course.id)))
with mock_create_basket():
self._test_successful_ecommerce_api_call(is_completed=False)
@mock.patch('commerce.api.v0.views.update_email_opt_in')
@ddt.data(*itertools.product((False, True), (False, True), (False, True)))
@ddt.unpack
......@@ -395,21 +266,17 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
if is_exception:
mock_update.side_effect = Exception("boink")
return_value = {'id': TEST_BASKET_ID, 'payment_data': None, 'order': {'number': TEST_ORDER_NUMBER}}
with mock_create_basket(response=return_value, expect_called=has_sku):
response = self._post_to_view(marketing_email_opt_in=is_opt_in)
response = self._post_to_view(marketing_email_opt_in=is_opt_in)
self.assertEqual(mock_update.called, is_opt_in)
self.assertEqual(response.status_code, 200)
def test_closed_course(self):
"""
Ensure that the view does not attempt to create a basket for closed
courses.
Verifies that the view returns HTTP 406 when a course is closed.
"""
self.course.enrollment_end = datetime.now(pytz.UTC) - timedelta(days=1)
modulestore().update_item(self.course, self.user.id) # pylint:disable=no-member
with mock_create_basket(expect_called=False):
self.assertEqual(self._post_to_view().status_code, 406)
self.assertEqual(self._post_to_view().status_code, 406)
@attr(shard=1)
......
""" API v0 views. """
import logging
import requests
from edx_rest_api_client import exceptions
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
......@@ -11,9 +10,7 @@ from rest_framework.status import HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT
from rest_framework.views import APIView
from commerce.constants import Messages
from commerce.exceptions import InvalidResponseError
from commerce.http import DetailResponse, InternalRequestErrorResponse
from commerce.utils import COMMERCE_API_WAFFLE_FLAG_NAMESPACE
from commerce.http import DetailResponse
from course_modes.models import CourseMode
from courseware import courses
from enrollment.api import add_enrollment
......@@ -21,15 +18,12 @@ from enrollment.views import EnrollmentCrossDomainSessionAuth
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
from openedx.core.djangoapps.embargo import api as embargo_api
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
from openedx.core.djangoapps.waffle_utils import WaffleFlag
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from openedx.core.lib.log_utils import audit_log
from student.models import CourseEnrollment, RegistrationCookieConfiguration
from student.models import CourseEnrollment
from util.json_request import JsonResponse
log = logging.getLogger(__name__)
SAILTHRU_CAMPAIGN_COOKIE = 'sailthru_bid'
STOP_BASKET_CREATION_FLAG = WaffleFlag(COMMERCE_API_WAFFLE_FLAG_NAMESPACE, 'stop_basket_creation')
class BasketsView(APIView):
......@@ -85,7 +79,7 @@ class BasketsView(APIView):
def post(self, request, *args, **kwargs):
"""
Attempt to enroll the user, and if needed, create the basket.
Attempt to enroll the user.
"""
user = request.user
valid, course_key, error = self._is_data_valid(request)
......@@ -120,11 +114,7 @@ class BasketsView(APIView):
# Accept either honor or audit as an enrollment mode to
# maintain backwards compatibility with existing courses
default_enrollment_mode = audit_mode or honor_mode
if not default_enrollment_mode:
msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format(course_id=course_id)
return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
elif not default_enrollment_mode.sku or STOP_BASKET_CREATION_FLAG.is_enabled():
if default_enrollment_mode:
msg = Messages.ENROLL_DIRECTLY.format(
username=user.username,
course_id=course_id
......@@ -141,81 +131,8 @@ class BasketsView(APIView):
self._handle_marketing_opt_in(request, course_key, user)
return DetailResponse(msg)
else:
return self._create_basket_to_order(request, user, course_key, default_enrollment_mode)
def _add_request_cookie_to_api_session(self, server_session, request, cookie_name):
""" Add cookie from user request into server session """
user_cookie = None
if cookie_name:
user_cookie = request.COOKIES.get(cookie_name)
if user_cookie:
server_cookie = {cookie_name: user_cookie}
if server_session.cookies:
requests.utils.add_dict_to_cookiejar(server_session.cookies, server_cookie)
else:
server_session.cookies = requests.utils.cookiejar_from_dict(server_cookie)
def _create_basket_to_order(self, request, user, course_key, default_enrollment_mode):
"""
Connect to the ecommerce service to create the basket and the order to do the enrollment
"""
# Setup the API
course_id = unicode(course_key)
try:
api_session = requests.Session()
api = ecommerce_api_client(user, session=api_session)
except ValueError:
self._enroll(course_key, user)
msg = Messages.NO_ECOM_API.format(username=user.username, course_id=course_id)
log.debug(msg)
return DetailResponse(msg)
response = None
# Make the API call
try:
# Pass along Sailthru campaign id
self._add_request_cookie_to_api_session(api_session, request, SAILTHRU_CAMPAIGN_COOKIE)
# Pass along UTM tracking info
utm_cookie_name = RegistrationCookieConfiguration.current().utm_cookie_name
self._add_request_cookie_to_api_session(api_session, request, utm_cookie_name)
response_data = api.baskets.post({
'products': [{'sku': default_enrollment_mode.sku}],
'checkout': True,
})
payment_data = response_data["payment_data"]
if payment_data:
# Pass data to the client to begin the payment flow.
response = JsonResponse(payment_data)
elif response_data['order']:
# The order was completed immediately because there is no charge.
msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number'])
log.debug(msg)
response = DetailResponse(msg)
else:
msg = u'Unexpected response from basket endpoint.'
log.error(
msg + u' Could not enroll user %(username)s in course %(course_id)s.',
{'username': user.id, 'course_id': course_id},
)
raise InvalidResponseError(msg)
except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
log.exception(ex.message)
return InternalRequestErrorResponse(ex.message)
finally:
audit_log(
'checkout_requested',
course_id=course_id,
mode=default_enrollment_mode.slug,
processor_name=None,
user_id=user.id
)
self._handle_marketing_opt_in(request, course_key, user)
return response
msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format(course_id=course_id)
return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
class BasketOrderView(APIView):
......
......@@ -80,24 +80,6 @@ class mock_ecommerce_api_endpoint(object):
httpretty.reset()
class mock_create_basket(mock_ecommerce_api_endpoint):
""" Mocks calls to E-Commerce API client basket creation method. """
default_response = {
'id': 7,
'order': {'number': '100004'}, # never both None.
'payment_data': {
'payment_processor_name': 'test-processor',
'payment_form_data': {},
'payment_page_url': 'http://example.com/pay',
},
}
method = httpretty.POST
def get_path(self):
return '/baskets/'
class mock_basket_order(mock_ecommerce_api_endpoint):
""" Mocks calls to E-Commerce API client basket order method. """
......
......@@ -7,9 +7,6 @@ from django.conf import settings
from commerce.models import CommerceConfiguration
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace
COMMERCE_API_WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='commerce_api')
def is_account_activation_requirement_disabled():
......
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