ENT-162 Create an enterprise enrollment during the enrollment flow

parent 78f235a5
......@@ -20,11 +20,13 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range
from django.test.utils import override_settings
import pytz
import httpretty
from course_modes.models import CourseMode
from enrollment.views import EnrollmentUserThrottle
from util.models import RateLimitConfiguration
from util.testing import UrlResetMixin
from util.tests.mixins.enterprise import EnterpriseServiceMockMixin
from enrollment import api
from enrollment.errors import CourseEnrollmentError
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
......@@ -53,6 +55,7 @@ class EnrollmentTestMixin(object):
enrollment_attributes=None,
min_mongo_calls=0,
max_mongo_calls=0,
enterprise_course_consent=None,
):
"""
Enroll in the course and verify the response's status code. If the expected status is 200, also validates
......@@ -79,6 +82,9 @@ class EnrollmentTestMixin(object):
if email_opt_in is not None:
data['email_opt_in'] = email_opt_in
if enterprise_course_consent is not None:
data['enterprise_course_consent'] = enterprise_course_consent
extra = {}
if as_server:
extra['HTTP_X_EDX_API_KEY'] = self.API_KEY
......@@ -130,7 +136,7 @@ class EnrollmentTestMixin(object):
@override_settings(EDX_API_KEY="i am a key")
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, EnterpriseServiceMockMixin):
"""
Test user enrollment, especially with different course modes.
"""
......@@ -924,6 +930,73 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG)
def test_enterprise_course_enrollment_invalid_consent(self):
"""Verify that the enterprise_course_consent must be a boolean. """
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=CourseMode.DEFAULT_MODE_SLUG,
mode_display_name=CourseMode.DEFAULT_MODE_SLUG,
)
self.assert_enrollment_status(
expected_status=status.HTTP_400_BAD_REQUEST,
enterprise_course_consent='invalid',
as_server=True,
)
@httpretty.activate
@override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker')
def test_enterprise_course_enrollment_api_error(self):
"""Verify that enterprise service errors are handled properly. """
UserFactory.create(
username='enterprise_worker',
email=self.EMAIL,
password=self.PASSWORD,
)
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=CourseMode.DEFAULT_MODE_SLUG,
mode_display_name=CourseMode.DEFAULT_MODE_SLUG,
)
self.mock_enterprise_course_enrollment_post_api_failure()
self.assert_enrollment_status(
expected_status=status.HTTP_400_BAD_REQUEST,
enterprise_course_consent=True,
as_server=True,
username='enterprise_worker'
)
self.assertEqual(
httpretty.last_request().path,
'/enterprise/api/v1/enterprise-course-enrollment/',
'No request was made to the mocked enterprise-course-enrollment API'
)
@httpretty.activate
@override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker')
def test_enterprise_course_enrollment_successful(self):
"""Verify that the enrollment completes when the EnterpriseCourseEnrollment creation succeeds. """
UserFactory.create(
username='enterprise_worker',
email=self.EMAIL,
password=self.PASSWORD,
)
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=CourseMode.DEFAULT_MODE_SLUG,
mode_display_name=CourseMode.DEFAULT_MODE_SLUG,
)
self.mock_enterprise_course_enrollment_post_api(username=self.user.username, course_id=unicode(self.course.id))
self.assert_enrollment_status(
expected_status=status.HTTP_200_OK,
enterprise_course_consent=True,
as_server=True,
username='enterprise_worker'
)
self.assertEqual(
httpretty.last_request().path,
'/enterprise/api/v1/enterprise-course-enrollment/',
'No request was made to the mocked enterprise-course-enrollment API'
)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentEmbargoTest(EnrollmentTestMixin, UrlResetMixin, ModuleStoreTestCase):
......
......@@ -16,8 +16,6 @@ from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView
from course_modes.models import CourseMode
from enrollment import api
from enrollment.errors import CourseEnrollmentError, CourseModeNotFoundError, CourseEnrollmentExistsError
from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain
from openedx.core.djangoapps.embargo import api as embargo_api
......@@ -28,6 +26,13 @@ from openedx.core.lib.api.authentication import (
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission, ApiKeyHeaderPermissionIsAuthenticated
from openedx.core.lib.exceptions import CourseNotFoundError
from openedx.core.lib.log_utils import audit_log
from util.enterprise_helpers import enterprise_enabled, EnterpriseApiClient, EnterpriseApiException
from enrollment import api
from enrollment.errors import (
CourseEnrollmentError,
CourseModeNotFoundError,
CourseEnrollmentExistsError
)
from student.auth import user_has_role
from student.models import User
from student.roles import CourseStaffRole, GlobalStaff
......@@ -362,6 +367,10 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
* user: Optional. The user ID of the currently logged in user. You
cannot use the command to enroll a different user.
* enterprise_course_consent: Optional. A Boolean value that
indicates the consent status for an EnterpriseCourseEnrollment
to be posted to the Enterprise service.
**GET Response Values**
If an unspecified error occurs when the user tries to obtain a
......@@ -574,6 +583,29 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
}
)
enterprise_course_consent = request.data.get('enterprise_course_consent')
# Check if the enterprise_course_enrollment is a boolean
if has_api_key_permissions and enterprise_enabled() and enterprise_course_consent is not None:
if not isinstance(enterprise_course_consent, bool):
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
'message': (u"'{value}' is an invalid enterprise course consent value.").format(
value=enterprise_course_consent
)
}
)
try:
EnterpriseApiClient().post_enterprise_course_enrollment(
username,
unicode(course_id),
enterprise_course_consent
)
except EnterpriseApiException as error:
log.exception("An unexpected error occurred while creating the new EnterpriseCourseEnrollment "
"for user [%s] in course run [%s]", username, course_id)
raise CourseEnrollmentError(error.message)
enrollment_attributes = request.data.get('enrollment_attributes')
enrollment = api.get_enrollment(username, unicode(course_id))
mode_changed = enrollment and mode is not None and enrollment['mode'] != mode
......
"""
Helpers to access the enterprise app
"""
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
import logging
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.utils.http import urlencode
from edx_rest_api_client.client import EdxRestApiClient
try:
from enterprise.models import EnterpriseCustomer
from enterprise import utils as enterprise_utils
from enterprise.tpa_pipeline import (
active_provider_requests_data_sharing,
active_provider_enforces_data_sharing,
get_enterprise_customer_for_request,
)
from enterprise.utils import consent_necessary_for_course
except ImportError:
pass
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.lib.token_utils import JwtBuilder
from slumber.exceptions import HttpClientError, HttpServerError
ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS = 'enterprise_customer_branding_override_details'
LOGGER = logging.getLogger("edx.enterprise_helpers")
class EnterpriseApiException(Exception):
"""
Exception for errors while communicating with the Enterprise service API.
"""
pass
class EnterpriseApiClient(object):
"""
Class for producing an Enterprise service API client.
"""
def __init__(self):
"""
Initialize an Enterprise service API client, authenticated using the Enterprise worker username.
"""
self.user = User.objects.get(username=settings.ENTERPRISE_SERVICE_WORKER_USERNAME)
jwt = JwtBuilder(self.user).build_token([])
self.client = EdxRestApiClient(
configuration_helpers.get_value('ENTERPRISE_API_URL', settings.ENTERPRISE_API_URL),
jwt=jwt
)
def post_enterprise_course_enrollment(self, username, course_id, consent_granted):
"""
Create an EnterpriseCourseEnrollment by using the corresponding serializer (for validation).
"""
data = {
'username': username,
'course_id': course_id,
'consent_granted': consent_granted,
}
endpoint = getattr(self.client, 'enterprise-course-enrollment') # pylint: disable=literal-used-as-attribute
try:
endpoint.post(data=data)
except (HttpClientError, HttpServerError):
message = (
"An error occured while posting EnterpriseCourseEnrollment for user {username} and "
"course run {course_id} (consent_granted value: {consent_granted})"
).format(
username=username,
course_id=course_id,
consent_granted=consent_granted,
)
LOGGER.exception(message)
raise EnterpriseApiException(message)
def enterprise_enabled():
"""
Determines whether the Enterprise app is installed
......
"""
Mixins for the EnterpriseApiClient.
"""
import json
import httpretty
from django.conf import settings
from django.core.cache import cache
class EnterpriseServiceMockMixin(object):
"""
Mocks for the Enterprise service responses.
"""
def setUp(self):
super(EnterpriseServiceMockMixin, self).setUp()
cache.clear()
@staticmethod
def get_enterprise_url(path):
"""Return a URL to the configured Enterprise API. """
return '{}{}/'.format(settings.ENTERPRISE_API_URL, path)
def mock_enterprise_course_enrollment_post_api( # pylint: disable=invalid-name
self,
username='test_user',
course_id='course-v1:edX+DemoX+Demo_Course',
consent_granted=True
):
"""
Helper method to register the enterprise course enrollment API POST endpoint.
"""
api_response = {
username: username,
course_id: course_id,
consent_granted: consent_granted,
}
api_response_json = json.dumps(api_response)
httpretty.register_uri(
method=httpretty.POST,
uri=self.get_enterprise_url('enterprise-course-enrollment'),
body=api_response_json,
content_type='application/json'
)
def mock_enterprise_course_enrollment_post_api_failure(self): # pylint: disable=invalid-name
"""
Helper method to register the enterprise course enrollment API endpoint for a failure.
"""
httpretty.register_uri(
method=httpretty.POST,
uri=self.get_enterprise_url('enterprise-course-enrollment'),
body='{}',
content_type='application/json',
status=500
)
......@@ -176,6 +176,7 @@ EDXMKTG_LOGGED_IN_COOKIE_NAME = ENV_TOKENS.get('EDXMKTG_LOGGED_IN_COOKIE_NAME',
EDXMKTG_USER_INFO_COOKIE_NAME = ENV_TOKENS.get('EDXMKTG_USER_INFO_COOKIE_NAME', EDXMKTG_USER_INFO_COOKIE_NAME)
LMS_ROOT_URL = ENV_TOKENS.get('LMS_ROOT_URL')
ENTERPRISE_API_URL = ENV_TOKENS.get('ENTERPRISE_API_URL', LMS_ROOT_URL + '/enterprise/api/v1/')
ENV_FEATURES = ENV_TOKENS.get('FEATURES', {})
for feature, value in ENV_FEATURES.items():
......
......@@ -61,6 +61,7 @@ DISCUSSION_SETTINGS = {
}
LMS_ROOT_URL = "http://localhost:8000"
ENTERPRISE_API_URL = LMS_ROOT_URL + '/enterprise/api/v1/'
# Features
FEATURES = {
......
......@@ -15,7 +15,7 @@ LMS_ROOT_URL = 'http://{}'.format(HOST)
ECOMMERCE_PUBLIC_URL_ROOT = 'http://localhost:18130'
ECOMMERCE_API_URL = 'http://edx.devstack.ecommerce:18130/api/v2'
ENTERPRISE_API_URL = 'http://enterprise.example.com/enterprise/api/v1/'
OAUTH_OIDC_ISSUER = '{}/oauth2'.format(LMS_ROOT_URL)
......
......@@ -590,3 +590,4 @@ COMPREHENSIVE_THEME_LOCALE_PATHS = [REPO_ROOT / "themes/conf/locale", ]
LMS_ROOT_URL = "http://localhost:8000"
ECOMMERCE_API_URL = 'https://ecommerce.example.com/api/v2/'
ENTERPRISE_API_URL = 'http://enterprise.example.com/enterprise/api/v1/'
......@@ -51,7 +51,7 @@ edx-lint==0.4.3
astroid==1.3.8
edx-django-oauth2-provider==1.1.4
edx-django-sites-extensions==2.1.1
edx-enterprise==0.22.0
edx-enterprise==0.23.0
edx-oauth2-provider==1.2.0
edx-opaque-keys==0.4.0
edx-organizations==0.4.3
......
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