Merge pull request #14542 from open-craft/bdero/ent-162
ENT-162 Create an EnterpriseCourseEnrollment during the enrollment flow
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,31 +1,75 @@
|
||||
"""
|
||||
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
|
||||
|
||||
0
common/djangoapps/util/tests/mixins/__init__.py
Normal file
0
common/djangoapps/util/tests/mixins/__init__.py
Normal file
57
common/djangoapps/util/tests/mixins/enterprise.py
Normal file
57
common/djangoapps/util/tests/mixins/enterprise.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user