ENT-162 Create an enterprise enrollment during the enrollment flow

parent 78f235a5
...@@ -20,11 +20,13 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -20,11 +20,13 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range
from django.test.utils import override_settings from django.test.utils import override_settings
import pytz import pytz
import httpretty
from course_modes.models import CourseMode from course_modes.models import CourseMode
from enrollment.views import EnrollmentUserThrottle from enrollment.views import EnrollmentUserThrottle
from util.models import RateLimitConfiguration from util.models import RateLimitConfiguration
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from util.tests.mixins.enterprise import EnterpriseServiceMockMixin
from enrollment import api from enrollment import api
from enrollment.errors import CourseEnrollmentError from enrollment.errors import CourseEnrollmentError
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
...@@ -53,6 +55,7 @@ class EnrollmentTestMixin(object): ...@@ -53,6 +55,7 @@ class EnrollmentTestMixin(object):
enrollment_attributes=None, enrollment_attributes=None,
min_mongo_calls=0, min_mongo_calls=0,
max_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 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): ...@@ -79,6 +82,9 @@ class EnrollmentTestMixin(object):
if email_opt_in is not None: if email_opt_in is not None:
data['email_opt_in'] = email_opt_in data['email_opt_in'] = email_opt_in
if enterprise_course_consent is not None:
data['enterprise_course_consent'] = enterprise_course_consent
extra = {} extra = {}
if as_server: if as_server:
extra['HTTP_X_EDX_API_KEY'] = self.API_KEY extra['HTTP_X_EDX_API_KEY'] = self.API_KEY
...@@ -130,7 +136,7 @@ class EnrollmentTestMixin(object): ...@@ -130,7 +136,7 @@ class EnrollmentTestMixin(object):
@override_settings(EDX_API_KEY="i am a key") @override_settings(EDX_API_KEY="i am a key")
@ddt.ddt @ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @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. Test user enrollment, especially with different course modes.
""" """
...@@ -924,6 +930,73 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): ...@@ -924,6 +930,73 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
self.assertTrue(is_active) self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG) 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') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentEmbargoTest(EnrollmentTestMixin, UrlResetMixin, ModuleStoreTestCase): class EnrollmentEmbargoTest(EnrollmentTestMixin, UrlResetMixin, ModuleStoreTestCase):
......
...@@ -16,8 +16,6 @@ from rest_framework.throttling import UserRateThrottle ...@@ -16,8 +16,6 @@ from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView from rest_framework.views import APIView
from course_modes.models import CourseMode 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.authentication import SessionAuthenticationCrossDomainCsrf
from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain
from openedx.core.djangoapps.embargo import api as embargo_api from openedx.core.djangoapps.embargo import api as embargo_api
...@@ -28,6 +26,13 @@ from openedx.core.lib.api.authentication import ( ...@@ -28,6 +26,13 @@ from openedx.core.lib.api.authentication import (
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission, ApiKeyHeaderPermissionIsAuthenticated from openedx.core.lib.api.permissions import ApiKeyHeaderPermission, ApiKeyHeaderPermissionIsAuthenticated
from openedx.core.lib.exceptions import CourseNotFoundError from openedx.core.lib.exceptions import CourseNotFoundError
from openedx.core.lib.log_utils import audit_log 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.auth import user_has_role
from student.models import User from student.models import User
from student.roles import CourseStaffRole, GlobalStaff from student.roles import CourseStaffRole, GlobalStaff
...@@ -362,6 +367,10 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): ...@@ -362,6 +367,10 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
* user: Optional. The user ID of the currently logged in user. You * user: Optional. The user ID of the currently logged in user. You
cannot use the command to enroll a different user. 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** **GET Response Values**
If an unspecified error occurs when the user tries to obtain a If an unspecified error occurs when the user tries to obtain a
...@@ -574,6 +583,29 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): ...@@ -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_attributes = request.data.get('enrollment_attributes')
enrollment = api.get_enrollment(username, unicode(course_id)) enrollment = api.get_enrollment(username, unicode(course_id))
mode_changed = enrollment and mode is not None and enrollment['mode'] != mode mode_changed = enrollment and mode is not None and enrollment['mode'] != mode
......
""" """
Helpers to access the enterprise app 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 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 django.utils.http import urlencode
from edx_rest_api_client.client import EdxRestApiClient
try: try:
from enterprise.models import EnterpriseCustomer
from enterprise import utils as enterprise_utils 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 from enterprise.utils import consent_necessary_for_course
except ImportError: except ImportError:
pass pass
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers 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' ENTERPRISE_CUSTOMER_BRANDING_OVERRIDE_DETAILS = 'enterprise_customer_branding_override_details'
LOGGER = logging.getLogger("edx.enterprise_helpers") 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(): def enterprise_enabled():
""" """
Determines whether the Enterprise app is installed 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', ...@@ -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) 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') 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', {}) ENV_FEATURES = ENV_TOKENS.get('FEATURES', {})
for feature, value in ENV_FEATURES.items(): for feature, value in ENV_FEATURES.items():
......
...@@ -61,6 +61,7 @@ DISCUSSION_SETTINGS = { ...@@ -61,6 +61,7 @@ DISCUSSION_SETTINGS = {
} }
LMS_ROOT_URL = "http://localhost:8000" LMS_ROOT_URL = "http://localhost:8000"
ENTERPRISE_API_URL = LMS_ROOT_URL + '/enterprise/api/v1/'
# Features # Features
FEATURES = { FEATURES = {
......
...@@ -15,7 +15,7 @@ LMS_ROOT_URL = 'http://{}'.format(HOST) ...@@ -15,7 +15,7 @@ LMS_ROOT_URL = 'http://{}'.format(HOST)
ECOMMERCE_PUBLIC_URL_ROOT = 'http://localhost:18130' ECOMMERCE_PUBLIC_URL_ROOT = 'http://localhost:18130'
ECOMMERCE_API_URL = 'http://edx.devstack.ecommerce:18130/api/v2' 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) OAUTH_OIDC_ISSUER = '{}/oauth2'.format(LMS_ROOT_URL)
......
...@@ -590,3 +590,4 @@ COMPREHENSIVE_THEME_LOCALE_PATHS = [REPO_ROOT / "themes/conf/locale", ] ...@@ -590,3 +590,4 @@ COMPREHENSIVE_THEME_LOCALE_PATHS = [REPO_ROOT / "themes/conf/locale", ]
LMS_ROOT_URL = "http://localhost:8000" LMS_ROOT_URL = "http://localhost:8000"
ECOMMERCE_API_URL = 'https://ecommerce.example.com/api/v2/' 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 ...@@ -51,7 +51,7 @@ edx-lint==0.4.3
astroid==1.3.8 astroid==1.3.8
edx-django-oauth2-provider==1.1.4 edx-django-oauth2-provider==1.1.4
edx-django-sites-extensions==2.1.1 edx-django-sites-extensions==2.1.1
edx-enterprise==0.22.0 edx-enterprise==0.23.0
edx-oauth2-provider==1.2.0 edx-oauth2-provider==1.2.0
edx-opaque-keys==0.4.0 edx-opaque-keys==0.4.0
edx-organizations==0.4.3 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