Move to new consent API
This commit is contained in:
@@ -355,6 +355,7 @@ AUTHENTICATION_BACKENDS = (
|
||||
LMS_BASE = None
|
||||
LMS_ROOT_URL = "http://localhost:8000"
|
||||
ENTERPRISE_API_URL = LMS_ROOT_URL + '/enterprise/api/v1/'
|
||||
ENTERPRISE_CONSENT_API_URL = LMS_ROOT_URL + '/consent/api/v1/'
|
||||
|
||||
# These are standard regexes for pulling out info like course_ids, usage_ids, etc.
|
||||
# They are used so that URLs with deprecated-format strings still work.
|
||||
@@ -1177,6 +1178,7 @@ OPTIONAL_APPS = (
|
||||
|
||||
# Enterprise App (http://github.com/edx/edx-enterprise)
|
||||
('enterprise', None),
|
||||
('consent', None),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ from lms.djangoapps.commerce.tests import test_utils as ecomm_test_utils
|
||||
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
|
||||
from openedx.core.djangoapps.embargo.test_utils import restrict_course
|
||||
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
|
||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseServiceMockMixin
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from util import organizations_helpers as organizations_api
|
||||
@@ -35,7 +34,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@attr(shard=3)
|
||||
@ddt.ddt
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMockMixin, CourseCatalogServiceMockMixin):
|
||||
class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTestCase, CourseCatalogServiceMockMixin):
|
||||
"""
|
||||
Course Mode View tests
|
||||
"""
|
||||
@@ -48,13 +47,6 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
|
||||
self.client.login(username=self.user.username, password="edx")
|
||||
|
||||
# Create a service user, because the track selection page depends on it
|
||||
UserFactory.create(
|
||||
username='enterprise_worker',
|
||||
email="enterprise_worker@example.com",
|
||||
password="edx",
|
||||
)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@httpretty.activate
|
||||
@ddt.data(
|
||||
@@ -82,8 +74,6 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
user=self.user
|
||||
)
|
||||
|
||||
self.mock_enterprise_learner_api()
|
||||
|
||||
# Configure whether we're upgrading or not
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
@@ -133,109 +123,6 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
self.assertRedirects(response, 'http://testserver/basket/add/?sku=TEST', fetch_redirect_response=False)
|
||||
ecomm_test_utils.update_commerce_config(enabled=False)
|
||||
|
||||
def _generate_enterprise_learner_context(self, enable_audit_enrollment=False):
|
||||
"""
|
||||
Internal helper to support common pieces of test case variations
|
||||
"""
|
||||
# Create the course modes
|
||||
for mode in ('audit', 'honor', 'verified'):
|
||||
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
|
||||
|
||||
catalog_integration = self.create_catalog_integration()
|
||||
UserFactory(username=catalog_integration.service_username)
|
||||
|
||||
self.mock_course_discovery_api_for_catalog_contains(
|
||||
catalog_id=1, course_run_ids=[str(self.course.id)]
|
||||
)
|
||||
self.mock_enterprise_learner_api(enable_audit_enrollment=enable_audit_enrollment)
|
||||
|
||||
return reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
|
||||
@httpretty.activate
|
||||
def test_no_enrollment(self):
|
||||
url = self._generate_enterprise_learner_context()
|
||||
response = self.client.get(url)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
@httpretty.activate
|
||||
@waffle.testutils.override_switch("populate-multitenant-programs", True)
|
||||
def test_enterprise_learner_context(self):
|
||||
"""
|
||||
Test: Track selection page should show the enterprise context message if user belongs to the Enterprise.
|
||||
"""
|
||||
url = self._generate_enterprise_learner_context()
|
||||
|
||||
# User visits the track selection page directly without ever enrolling
|
||||
response = self.client.get(url)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertContains(
|
||||
response,
|
||||
'Welcome, {username}! You are about to enroll in {course_name}, from {partner_names}, '
|
||||
'sponsored by TestShib. Please select your enrollment information below.'.format(
|
||||
username=self.user.username,
|
||||
course_name=self.course.display_name_with_default_escaped,
|
||||
partner_names=self.course.org
|
||||
)
|
||||
)
|
||||
|
||||
@httpretty.activate
|
||||
@waffle.testutils.override_switch("populate-multitenant-programs", True)
|
||||
def test_enterprise_learner_context_with_multiple_organizations(self):
|
||||
"""
|
||||
Test: Track selection page should show the enterprise context message with multiple organization names
|
||||
if user belongs to the Enterprise.
|
||||
"""
|
||||
url = self._generate_enterprise_learner_context()
|
||||
|
||||
# Creating organization
|
||||
for i in xrange(2):
|
||||
test_organization_data = {
|
||||
'name': 'test organization ' + str(i),
|
||||
'short_name': 'test_organization_' + str(i),
|
||||
'description': 'Test Organization Description',
|
||||
'active': True,
|
||||
'logo': '/logo_test1.png/'
|
||||
}
|
||||
test_org = organizations_api.add_organization(organization_data=test_organization_data)
|
||||
organizations_api.add_organization_course(organization_data=test_org, course_id=unicode(self.course.id))
|
||||
|
||||
# User visits the track selection page directly without ever enrolling
|
||||
response = self.client.get(url)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertContains(
|
||||
response,
|
||||
'Welcome, {username}! You are about to enroll in {course_name}, from test organization 0 and '
|
||||
'test organization 1, sponsored by TestShib. Please select your enrollment information below.'.format(
|
||||
username=self.user.username,
|
||||
course_name=self.course.display_name_with_default_escaped
|
||||
)
|
||||
)
|
||||
|
||||
@httpretty.activate
|
||||
@waffle.testutils.override_switch("populate-multitenant-programs", True)
|
||||
def test_enterprise_learner_context_audit_disabled(self):
|
||||
"""
|
||||
Track selection page should hide the audit choice by default in an Enterprise Customer/Learner context
|
||||
"""
|
||||
|
||||
# User visits the track selection page directly without ever enrolling, sees only Verified track choice
|
||||
url = self._generate_enterprise_learner_context()
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, 'Pursue a Verified Certificate')
|
||||
self.assertNotContains(response, 'Audit This Course')
|
||||
|
||||
@httpretty.activate
|
||||
def test_enterprise_learner_context_audit_enabled(self):
|
||||
"""
|
||||
Track selection page should display Audit choice when specified for an Enterprise Customer
|
||||
"""
|
||||
|
||||
# User visits the track selection page directly without ever enrolling, sees both Verified and Audit choices
|
||||
url = self._generate_enterprise_learner_context(enable_audit_enrollment=True)
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, 'Pursue a Verified Certificate')
|
||||
self.assertContains(response, 'Audit This Course')
|
||||
|
||||
@httpretty.activate
|
||||
@ddt.data(
|
||||
'',
|
||||
@@ -263,8 +150,6 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
user=self.user
|
||||
)
|
||||
|
||||
self.mock_enterprise_learner_api()
|
||||
|
||||
# Verify that the prices render correctly
|
||||
response = self.client.get(
|
||||
reverse('course_modes_choose', args=[unicode(self.course.id)]),
|
||||
@@ -286,8 +171,6 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
for mode in available_modes:
|
||||
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
|
||||
|
||||
self.mock_enterprise_learner_api()
|
||||
|
||||
# Check whether credit upsell is shown on the page
|
||||
# This should *only* be shown when a credit mode is available
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
@@ -530,8 +413,6 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
for mode in ["honor", "verified"]:
|
||||
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
|
||||
|
||||
self.mock_enterprise_learner_api()
|
||||
|
||||
# Load the track selection page
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
@@ -558,7 +439,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseServiceMockMixin):
|
||||
class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
"""Test embargo restrictions on the track selection page. """
|
||||
|
||||
URLCONF_MODULES = ['openedx.core.djangoapps.embargo']
|
||||
@@ -576,13 +457,6 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseSe
|
||||
self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
|
||||
self.client.login(username=self.user.username, password="edx")
|
||||
|
||||
# Create a service user
|
||||
UserFactory.create(
|
||||
username='enterprise_worker',
|
||||
email="enterprise_worker@example.com",
|
||||
password="edx",
|
||||
)
|
||||
|
||||
# Construct the URL for the track selection page
|
||||
self.url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
|
||||
@@ -595,6 +469,5 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase, EnterpriseSe
|
||||
@httpretty.activate
|
||||
def test_embargo_allow(self):
|
||||
|
||||
self.mock_enterprise_learner_api()
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -24,7 +24,6 @@ from edxmako.shortcuts import render_to_response
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
|
||||
from openedx.core.djangoapps.embargo import api as embargo_api
|
||||
from openedx.features.enterprise_support import api as enterprise_api
|
||||
from student.models import CourseEnrollment
|
||||
from third_party_auth.decorators import tpa_hint_ends_existing_session
|
||||
from util import organizations_helpers as organization_api
|
||||
@@ -162,36 +161,6 @@ class ChooseModeView(View):
|
||||
title_content = _("Congratulations! You are now enrolled in {course_name}").format(
|
||||
course_name=course.display_name_with_default_escaped
|
||||
)
|
||||
enterprise_learner_data = enterprise_api.get_enterprise_learner_data(site=request.site, user=request.user)
|
||||
if enterprise_learner_data:
|
||||
enterprise_learner = enterprise_learner_data[0]
|
||||
is_course_in_enterprise_catalog = enterprise_api.is_course_in_enterprise_catalog(
|
||||
site=request.site,
|
||||
course_id=course_id,
|
||||
enterprise_catalog_id=enterprise_learner['enterprise_customer']['catalog']
|
||||
)
|
||||
|
||||
if is_course_in_enterprise_catalog:
|
||||
partner_names = partner_name = course.display_organization \
|
||||
if course.display_organization else course.org
|
||||
enterprise_name = enterprise_learner['enterprise_customer']['name']
|
||||
organizations = organization_api.get_course_organizations(course_id=course.id)
|
||||
if organizations:
|
||||
partner_names = ' and '.join([org.get('name', partner_name) for org in organizations])
|
||||
|
||||
title_content = _("Welcome, {username}! You are about to enroll in {course_name},"
|
||||
" from {partner_names}, sponsored by {enterprise_name}. Please select your enrollment"
|
||||
" information below.").format(
|
||||
username=request.user.username,
|
||||
course_name=course.display_name_with_default_escaped,
|
||||
partner_names=partner_names,
|
||||
enterprise_name=enterprise_name
|
||||
)
|
||||
|
||||
# Hide the audit modes for this enterprise customer, if necessary
|
||||
if not enterprise_learner['enterprise_customer'].get('enable_audit_enrollment'):
|
||||
for audit_mode in CourseMode.AUDIT_MODES:
|
||||
modes.pop(audit_mode, None)
|
||||
|
||||
context["title_content"] = title_content
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ class EnrollmentTestMixin(object):
|
||||
min_mongo_calls=0,
|
||||
max_mongo_calls=0,
|
||||
enterprise_course_consent=None,
|
||||
linked_enterprise_customer=None,
|
||||
):
|
||||
"""
|
||||
Enroll in the course and verify the response's status code. If the expected status is 200, also validates
|
||||
@@ -85,6 +86,9 @@ class EnrollmentTestMixin(object):
|
||||
if enterprise_course_consent is not None:
|
||||
data['enterprise_course_consent'] = enterprise_course_consent
|
||||
|
||||
if linked_enterprise_customer is not None:
|
||||
data['linked_enterprise_customer'] = linked_enterprise_customer
|
||||
|
||||
extra = {}
|
||||
if as_server:
|
||||
extra['HTTP_X_EDX_API_KEY'] = self.API_KEY
|
||||
@@ -961,6 +965,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
self.assertTrue(is_active)
|
||||
self.assertEqual(course_mode, updated_mode)
|
||||
|
||||
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=True)
|
||||
def test_enterprise_course_enrollment_invalid_consent(self):
|
||||
"""Verify that the enterprise_course_consent must be a boolean. """
|
||||
CourseModeFactory.create(
|
||||
@@ -976,6 +981,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
|
||||
@httpretty.activate
|
||||
@override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker')
|
||||
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=True)
|
||||
def test_enterprise_course_enrollment_api_error(self):
|
||||
"""Verify that enterprise service errors are handled properly. """
|
||||
UserFactory.create(
|
||||
@@ -1003,6 +1009,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
|
||||
@httpretty.activate
|
||||
@override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker')
|
||||
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=True)
|
||||
def test_enterprise_course_enrollment_successful(self):
|
||||
"""Verify that the enrollment completes when the EnterpriseCourseEnrollment creation succeeds. """
|
||||
UserFactory.create(
|
||||
@@ -1028,6 +1035,43 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, Ente
|
||||
'No request was made to the mocked enterprise-course-enrollment API'
|
||||
)
|
||||
|
||||
@httpretty.activate
|
||||
@override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker')
|
||||
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=True)
|
||||
def test_enterprise_course_enrollment_with_ec_uuid(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,
|
||||
)
|
||||
consent_kwargs = {
|
||||
'username': self.user.username,
|
||||
'course_id': unicode(self.course.id),
|
||||
'ec_uuid': 'this-is-a-real-uuid'
|
||||
}
|
||||
self.mock_consent_missing(**consent_kwargs)
|
||||
self.mock_consent_post(**consent_kwargs)
|
||||
self.assert_enrollment_status(
|
||||
expected_status=status.HTTP_200_OK,
|
||||
as_server=True,
|
||||
username='enterprise_worker',
|
||||
linked_enterprise_customer='this-is-a-real-uuid',
|
||||
)
|
||||
self.assertEqual(
|
||||
httpretty.last_request().path,
|
||||
'/consent/api/v1/data_sharing_consent',
|
||||
)
|
||||
self.assertEqual(
|
||||
httpretty.last_request().method,
|
||||
httpretty.POST
|
||||
)
|
||||
|
||||
def test_enrollment_attributes_always_written(self):
|
||||
""" Enrollment attributes should always be written, regardless of whether
|
||||
the enrollment is being created or updated.
|
||||
|
||||
@@ -29,7 +29,12 @@ 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 openedx.features.enterprise_support.api import EnterpriseApiClient, EnterpriseApiException, enterprise_enabled
|
||||
from openedx.features.enterprise_support.api import (
|
||||
ConsentApiClient,
|
||||
EnterpriseApiClient,
|
||||
EnterpriseApiException,
|
||||
enterprise_enabled
|
||||
)
|
||||
from student.auth import user_has_role
|
||||
from student.models import User
|
||||
from student.roles import CourseStaffRole, GlobalStaff
|
||||
@@ -591,27 +596,42 @@ 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)
|
||||
explicit_linked_enterprise = request.data.get('linked_enterprise_customer')
|
||||
if has_api_key_permissions and enterprise_enabled():
|
||||
# We received an explicitly-linked EnterpriseCustomer for the enrollment
|
||||
if explicit_linked_enterprise is not None:
|
||||
kwargs = {
|
||||
'username': username,
|
||||
'course_id': unicode(course_id),
|
||||
'enterprise_customer_uuid': explicit_linked_enterprise,
|
||||
}
|
||||
consent_client = ConsentApiClient()
|
||||
consent_client.provide_consent(**kwargs)
|
||||
|
||||
# We received an implicit "consent granted" parameter from ecommerce
|
||||
# TODO: Once ecommerce has been deployed with explicit enterprise support, remove this
|
||||
# entire chunk of logic, related tests, and any supporting methods no longer required.
|
||||
elif enterprise_course_consent is not None:
|
||||
# Check if the enterprise_course_enrollment is a boolean
|
||||
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))
|
||||
|
||||
@@ -12,6 +12,7 @@ from lms.djangoapps.ccx.tests.factories import CcxFactory
|
||||
from nose.plugins.attrib import attr
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
|
||||
from pyquery import PyQuery as pq
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import AdminFactory
|
||||
@@ -32,7 +33,7 @@ QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
||||
class CourseInfoTestCase(EnterpriseTestConsentRequired, LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the Course Info page
|
||||
"""
|
||||
@@ -61,8 +62,7 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
||||
self.assertNotIn("You are not currently enrolled in this course", resp.content)
|
||||
|
||||
# TODO: LEARNER-611: If this is only tested under Course Info, does this need to move?
|
||||
@mock.patch('openedx.features.enterprise_support.api.get_enterprise_consent_url')
|
||||
def test_redirection_missing_enterprise_consent(self, mock_get_url):
|
||||
def test_redirection_missing_enterprise_consent(self):
|
||||
"""
|
||||
Verify that users viewing the course info who are enrolled, but have not provided
|
||||
data sharing consent, are first redirected to a consent page, and then, once they've
|
||||
@@ -70,19 +70,10 @@ class CourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
|
||||
"""
|
||||
self.setup_user()
|
||||
self.enroll(self.course)
|
||||
mock_get_url.return_value = reverse('dashboard')
|
||||
|
||||
url = reverse('info', args=[self.course.id.to_deprecated_string()])
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse('dashboard')
|
||||
)
|
||||
mock_get_url.assert_called_once()
|
||||
mock_get_url.return_value = None
|
||||
response = self.client.get(url)
|
||||
self.assertNotIn("You are not currently enrolled in this course", response.content)
|
||||
self.verify_consent_required(self.client, url)
|
||||
|
||||
def test_anonymous_user(self):
|
||||
url = reverse('info', args=[self.course.id.to_deprecated_string()])
|
||||
|
||||
@@ -15,6 +15,7 @@ from courseware.tests.factories import (
|
||||
StaffFactory
|
||||
)
|
||||
from courseware.tests.helpers import CourseAccessTestMixin, LoginEnrollmentTestCase
|
||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -22,7 +23,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
class TestViewAuth(EnterpriseTestConsentRequired, ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Check that view authentication works properly.
|
||||
"""
|
||||
@@ -201,28 +202,18 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
)
|
||||
)
|
||||
|
||||
@patch('openedx.features.enterprise_support.api.get_enterprise_consent_url')
|
||||
def test_redirection_missing_enterprise_consent(self, mock_get_url):
|
||||
def test_redirection_missing_enterprise_consent(self):
|
||||
"""
|
||||
Verify that enrolled students are redirected to the Enterprise consent
|
||||
URL if a linked Enterprise Customer requires data sharing consent
|
||||
and it has not yet been provided.
|
||||
"""
|
||||
mock_get_url.return_value = reverse('dashboard')
|
||||
self.login(self.enrolled_user)
|
||||
url = reverse(
|
||||
'courseware',
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()}
|
||||
)
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse('dashboard')
|
||||
)
|
||||
mock_get_url.assert_called_once()
|
||||
mock_get_url.return_value = None
|
||||
response = self.client.get(url)
|
||||
self.assertNotIn("You are not currently enrolled in this course", response.content)
|
||||
self.verify_consent_required(self.client, url, status_code=302)
|
||||
|
||||
def test_instructor_page_access_nonstaff(self):
|
||||
"""
|
||||
|
||||
@@ -212,8 +212,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
NUM_PROBLEMS = 20
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 10, 146),
|
||||
(ModuleStoreEnum.Type.split, 4, 146),
|
||||
(ModuleStoreEnum.Type.mongo, 10, 145),
|
||||
(ModuleStoreEnum.Type.split, 4, 145),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
|
||||
|
||||
@@ -472,34 +472,24 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
|
||||
|
||||
@mock.patch('student_account.views.enterprise_customer_for_request')
|
||||
@ddt.data(
|
||||
('signin_user', False, None, None, None),
|
||||
('register_user', False, None, None, None),
|
||||
('signin_user', True, 'Fake EC', 'http://logo.com/logo.jpg', u'{enterprise_name} - {platform_name}'),
|
||||
('register_user', True, 'Fake EC', 'http://logo.com/logo.jpg', u'{enterprise_name} - {platform_name}'),
|
||||
('signin_user', True, 'Fake EC', None, u'{enterprise_name} - {platform_name}'),
|
||||
('register_user', True, 'Fake EC', None, u'{enterprise_name} - {platform_name}'),
|
||||
('signin_user', True, 'Fake EC', 'http://logo.com/logo.jpg', None),
|
||||
('register_user', True, 'Fake EC', 'http://logo.com/logo.jpg', None),
|
||||
('signin_user', True, 'Fake EC', None, None),
|
||||
('register_user', True, 'Fake EC', None, None),
|
||||
('signin_user', False, None, None),
|
||||
('register_user', False, None, None),
|
||||
('signin_user', True, 'Fake EC', 'http://logo.com/logo.jpg'),
|
||||
('register_user', True, 'Fake EC', 'http://logo.com/logo.jpg'),
|
||||
('signin_user', True, 'Fake EC', None),
|
||||
('register_user', True, 'Fake EC', None),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_enterprise_register(self, url_name, ec_present, ec_name, logo_url, welcome_message, mock_get_ec):
|
||||
def test_enterprise_register(self, url_name, ec_present, ec_name, logo_url, mock_get_ec):
|
||||
"""
|
||||
Verify that when an EnterpriseCustomer is received on the login and register views,
|
||||
the appropriate sidebar is rendered.
|
||||
"""
|
||||
if ec_present:
|
||||
mock_ec = mock_get_ec.return_value
|
||||
mock_ec.name = ec_name
|
||||
if logo_url:
|
||||
mock_ec.branding_configuration.logo.url = logo_url
|
||||
else:
|
||||
mock_ec.branding_configuration.logo = None
|
||||
if welcome_message:
|
||||
mock_ec.branding_configuration.welcome_message = welcome_message
|
||||
else:
|
||||
del mock_ec.branding_configuration.welcome_message
|
||||
mock_get_ec.return_value = {
|
||||
'name': ec_name,
|
||||
'branding_configuration': {'logo': logo_url}
|
||||
}
|
||||
else:
|
||||
mock_get_ec.return_value = None
|
||||
|
||||
@@ -511,8 +501,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
|
||||
self.assertNotContains(response, text=enterprise_sidebar_div_id)
|
||||
else:
|
||||
self.assertContains(response, text=enterprise_sidebar_div_id)
|
||||
if not welcome_message:
|
||||
welcome_message = settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE
|
||||
welcome_message = settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE
|
||||
expected_message = welcome_message.format(
|
||||
start_bold=u'<b>',
|
||||
end_bold=u'</b>',
|
||||
|
||||
@@ -263,23 +263,17 @@ def enterprise_sidebar_context(request):
|
||||
|
||||
platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)
|
||||
|
||||
if enterprise_customer.branding_configuration.logo:
|
||||
enterprise_logo_url = enterprise_customer.branding_configuration.logo.url
|
||||
else:
|
||||
enterprise_logo_url = ''
|
||||
logo_url = enterprise_customer.get('branding_configuration', {}).get('logo', '')
|
||||
|
||||
if getattr(enterprise_customer.branding_configuration, 'welcome_message', None):
|
||||
branded_welcome_template = enterprise_customer.branding_configuration.welcome_message
|
||||
else:
|
||||
branded_welcome_template = configuration_helpers.get_value(
|
||||
'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE',
|
||||
settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE
|
||||
)
|
||||
branded_welcome_template = configuration_helpers.get_value(
|
||||
'ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE',
|
||||
settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE
|
||||
)
|
||||
|
||||
branded_welcome_string = branded_welcome_template.format(
|
||||
start_bold=u'<b>',
|
||||
end_bold=u'</b>',
|
||||
enterprise_name=enterprise_customer.name,
|
||||
enterprise_name=enterprise_customer['name'],
|
||||
platform_name=platform_name
|
||||
)
|
||||
|
||||
@@ -290,8 +284,8 @@ def enterprise_sidebar_context(request):
|
||||
platform_welcome_string = platform_welcome_template.format(platform_name=platform_name)
|
||||
|
||||
context = {
|
||||
'enterprise_name': enterprise_customer.name,
|
||||
'enterprise_logo_url': enterprise_logo_url,
|
||||
'enterprise_name': enterprise_customer['name'],
|
||||
'enterprise_logo_url': logo_url,
|
||||
'enterprise_branded_welcome_string': branded_welcome_string,
|
||||
'platform_welcome_string': platform_welcome_string,
|
||||
}
|
||||
|
||||
@@ -969,6 +969,11 @@ if LMS_ROOT_URL is not None:
|
||||
DEFAULT_ENTERPRISE_API_URL = LMS_ROOT_URL + '/enterprise/api/v1/'
|
||||
ENTERPRISE_API_URL = ENV_TOKENS.get('ENTERPRISE_API_URL', DEFAULT_ENTERPRISE_API_URL)
|
||||
|
||||
DEFAULT_ENTERPRISE_CONSENT_API_URL = None
|
||||
if LMS_ROOT_URL is not None:
|
||||
DEFAULT_ENTERPRISE_CONSENT_API_URL = LMS_ROOT_URL + '/consent/api/v1/'
|
||||
ENTERPRISE_CONSENT_API_URL = ENV_TOKENS.get('ENTERPRISE_CONSENT_API_URL', DEFAULT_ENTERPRISE_CONSENT_API_URL)
|
||||
|
||||
ENTERPRISE_SERVICE_WORKER_USERNAME = ENV_TOKENS.get(
|
||||
'ENTERPRISE_SERVICE_WORKER_USERNAME',
|
||||
ENTERPRISE_SERVICE_WORKER_USERNAME
|
||||
|
||||
@@ -3216,6 +3216,7 @@ ENTERPRISE_COURSE_ENROLLMENT_AUDIT_MODES = ['audit', 'honor']
|
||||
# and are overridden by the configuration parameter accessors defined in aws.py
|
||||
|
||||
ENTERPRISE_API_URL = LMS_ROOT_URL + '/enterprise/api/v1/'
|
||||
ENTERPRISE_CONSENT_API_URL = LMS_ROOT_URL + '/consent/api/v1/'
|
||||
ENTERPRISE_SERVICE_WORKER_USERNAME = 'enterprise_worker'
|
||||
ENTERPRISE_API_CACHE_TIMEOUT = 3600 # Value is in seconds
|
||||
ENTERPRISE_CUSTOMER_LOGO_IMAGE_SIZE = 512 # Enterprise logo image size limit in KB's
|
||||
|
||||
@@ -605,7 +605,9 @@ COMPREHENSIVE_THEME_LOCALE_PATHS = [REPO_ROOT / "themes/conf/locale", ]
|
||||
|
||||
LMS_ROOT_URL = "http://localhost:8000"
|
||||
|
||||
ENABLE_ENTERPRISE_INTEGRATION = False
|
||||
ECOMMERCE_API_URL = 'https://ecommerce.example.com/api/v2/'
|
||||
ENTERPRISE_API_URL = 'http://enterprise.example.com/enterprise/api/v1/'
|
||||
ENTERPRISE_CONSENT_API_URL = 'http://enterprise.example.com/consent/api/v1/'
|
||||
|
||||
ACTIVATION_EMAIL_FROM_ADDRESS = 'test_activate@edx.org'
|
||||
|
||||
@@ -160,7 +160,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
course_home_url(self.course)
|
||||
|
||||
# Fetch the view and verify the query counts
|
||||
with self.assertNumQueries(42, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(41, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
@@ -127,7 +127,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
|
||||
course_updates_url(self.course)
|
||||
|
||||
# Fetch the view and verify that the query counts haven't changed
|
||||
with self.assertNumQueries(33, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(32, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_updates_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
@@ -16,7 +16,7 @@ from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext as _
|
||||
from edx_rest_api_client.client import EdxRestApiClient
|
||||
from requests.exceptions import ConnectionError, Timeout
|
||||
from slumber.exceptions import HttpClientError, HttpServerError, SlumberBaseException
|
||||
from slumber.exceptions import HttpClientError, HttpNotFoundError, HttpServerError, SlumberBaseException
|
||||
|
||||
from openedx.core.djangoapps.catalog.models import CatalogIntegration
|
||||
from openedx.core.djangoapps.catalog.utils import create_catalog_api_client
|
||||
@@ -26,9 +26,7 @@ from third_party_auth.pipeline import get as get_partial_pipeline
|
||||
from third_party_auth.provider import Registry
|
||||
|
||||
try:
|
||||
from enterprise import utils as enterprise_utils
|
||||
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomer
|
||||
from enterprise.utils import consent_necessary_for_course
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -43,6 +41,62 @@ class EnterpriseApiException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConsentApiClient(object):
|
||||
"""
|
||||
Class for producing an Enterprise Consent service API client
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize a consent 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([])
|
||||
url = configuration_helpers.get_value('ENTERPRISE_CONSENT_API_URL', settings.ENTERPRISE_CONSENT_API_URL)
|
||||
self.client = EdxRestApiClient(
|
||||
url,
|
||||
jwt=jwt,
|
||||
append_slash=False,
|
||||
)
|
||||
self.consent_endpoint = self.client.data_sharing_consent
|
||||
|
||||
def revoke_consent(self, **kwargs):
|
||||
"""
|
||||
Revoke consent from any existing records that have it at the given scope.
|
||||
|
||||
This endpoint takes any given kwargs, which are understood as filtering the
|
||||
conceptual scope of the consent involved in the request.
|
||||
"""
|
||||
return self.consent_endpoint.delete(**kwargs)
|
||||
|
||||
def provide_consent(self, **kwargs):
|
||||
"""
|
||||
Provide consent at the given scope.
|
||||
|
||||
This endpoint takes any given kwargs, which are understood as filtering the
|
||||
conceptual scope of the consent involved in the request.
|
||||
"""
|
||||
return self.consent_endpoint.post(kwargs)
|
||||
|
||||
def consent_required(self, enrollment_exists=False, **kwargs):
|
||||
"""
|
||||
Determine if consent is required at the given scope.
|
||||
|
||||
This endpoint takes any given kwargs, which are understood as filtering the
|
||||
conceptual scope of the consent involved in the request.
|
||||
"""
|
||||
|
||||
# Call the endpoint with the given kwargs, and check the value that it provides.
|
||||
response = self.consent_endpoint.get(**kwargs)
|
||||
|
||||
# No Enterprise record exists, but we're already enrolled in a course. So, go ahead and proceed.
|
||||
if enrollment_exists and not response.get('exists', False):
|
||||
return False
|
||||
|
||||
# In all other cases, just trust the Consent API.
|
||||
return response['consent_required']
|
||||
|
||||
|
||||
class EnterpriseApiClient(object):
|
||||
"""
|
||||
Class for producing an Enterprise service API client.
|
||||
@@ -59,6 +113,10 @@ class EnterpriseApiClient(object):
|
||||
jwt=jwt
|
||||
)
|
||||
|
||||
def get_enterprise_customer(self, uuid):
|
||||
endpoint = getattr(self.client, 'enterprise-customer')
|
||||
return endpoint(uuid).get()
|
||||
|
||||
def post_enterprise_course_enrollment(self, username, course_id, consent_granted):
|
||||
"""
|
||||
Create an EnterpriseCourseEnrollment by using the corresponding serializer (for validation).
|
||||
@@ -166,25 +224,16 @@ class EnterpriseApiClient(object):
|
||||
|
||||
api_resource_name = 'enterprise-learner'
|
||||
|
||||
cache_key = get_cache_key(
|
||||
site_domain=site.domain,
|
||||
resource=api_resource_name,
|
||||
username=user.username
|
||||
)
|
||||
|
||||
response = cache.get(cache_key)
|
||||
if not response:
|
||||
try:
|
||||
endpoint = getattr(self.client, api_resource_name)
|
||||
querystring = {'username': user.username}
|
||||
response = endpoint().get(**querystring)
|
||||
cache.set(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT)
|
||||
except (HttpClientError, HttpServerError):
|
||||
message = ("An error occurred while getting EnterpriseLearner data for user {username}".format(
|
||||
username=user.username
|
||||
))
|
||||
LOGGER.exception(message)
|
||||
return None
|
||||
try:
|
||||
endpoint = getattr(self.client, api_resource_name)
|
||||
querystring = {'username': user.username}
|
||||
response = endpoint().get(**querystring)
|
||||
except (HttpClientError, HttpServerError):
|
||||
message = ("An error occurred while getting EnterpriseLearner data for user {username}".format(
|
||||
username=user.username
|
||||
))
|
||||
LOGGER.exception(message)
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
@@ -210,7 +259,7 @@ def data_sharing_consent_required(view_func):
|
||||
Otherwise, just call the wrapped view function.
|
||||
"""
|
||||
# Redirect to the consent URL, if consent is required.
|
||||
consent_url = get_enterprise_consent_url(request, course_id)
|
||||
consent_url = get_enterprise_consent_url(request, course_id, enrollment_exists=True)
|
||||
if consent_url:
|
||||
real_user = getattr(request.user, 'real_user', request.user)
|
||||
LOGGER.warning(
|
||||
@@ -233,52 +282,98 @@ def enterprise_enabled():
|
||||
return 'enterprise' in settings.INSTALLED_APPS and getattr(settings, 'ENABLE_ENTERPRISE_INTEGRATION', True)
|
||||
|
||||
|
||||
def enterprise_customer_for_request(request, tpa_hint=None):
|
||||
def enterprise_customer_for_request(request):
|
||||
"""
|
||||
Check all the context clues of the request to determine if
|
||||
the request being made is tied to a particular EnterpriseCustomer.
|
||||
"""
|
||||
|
||||
if not enterprise_enabled():
|
||||
return None
|
||||
|
||||
ec = None
|
||||
sso_provider_id = request.GET.get('tpa_hint')
|
||||
|
||||
running_pipeline = get_partial_pipeline(request)
|
||||
if running_pipeline:
|
||||
# Determine if the user is in the middle of a third-party auth pipeline,
|
||||
# and set the tpa_hint parameter to match if so.
|
||||
tpa_hint = Registry.get_from_pipeline(running_pipeline).provider_id
|
||||
# and set the sso_provider_id parameter to match if so.
|
||||
sso_provider_id = Registry.get_from_pipeline(running_pipeline).provider_id
|
||||
|
||||
if tpa_hint:
|
||||
if sso_provider_id:
|
||||
# If we have a third-party auth provider, get the linked enterprise customer.
|
||||
try:
|
||||
ec = EnterpriseCustomer.objects.get(enterprise_customer_identity_provider__provider_id=tpa_hint)
|
||||
# FIXME: Implement an Enterprise API endpoint where we can get the EC
|
||||
# directly via the linked SSO provider
|
||||
# Check if there's an Enterprise Customer such that the linked SSO provider
|
||||
# has an ID equal to the ID we got from the running pipeline or from the
|
||||
# request tpa_hint URL parameter.
|
||||
ec_uuid = EnterpriseCustomer.objects.get(
|
||||
enterprise_customer_identity_provider__provider_id=sso_provider_id
|
||||
).uuid
|
||||
except EnterpriseCustomer.DoesNotExist:
|
||||
pass
|
||||
# If there is not an EnterpriseCustomer linked to this SSO provider, set
|
||||
# the UUID variable to be null.
|
||||
ec_uuid = None
|
||||
else:
|
||||
# Check if we got an Enterprise UUID passed directly as either a query
|
||||
# parameter, or as a value in the Enterprise cookie.
|
||||
ec_uuid = request.GET.get('enterprise_customer') or request.COOKIES.get(settings.ENTERPRISE_CUSTOMER_COOKIE_NAME)
|
||||
|
||||
ec_uuid = request.GET.get('enterprise_customer') or request.COOKIES.get(settings.ENTERPRISE_CUSTOMER_COOKIE_NAME)
|
||||
# If we haven't obtained an EnterpriseCustomer through the other methods, check the
|
||||
# session cookies and URL parameters for an explicitly-passed EnterpriseCustomer.
|
||||
if not ec and ec_uuid:
|
||||
if not ec_uuid and request.user.is_authenticated():
|
||||
# If there's no way to get an Enterprise UUID for the request, check to see
|
||||
# if there's already an Enterprise attached to the requesting user on the backend.
|
||||
learner_data = get_enterprise_learner_data(request.site, request.user)
|
||||
if learner_data:
|
||||
ec_uuid = learner_data[0]['enterprise_customer']['uuid']
|
||||
if ec_uuid:
|
||||
# If we were able to obtain an EnterpriseCustomer UUID, go ahead
|
||||
# and use it to attempt to retrieve EnterpriseCustomer details
|
||||
# from the EnterpriseCustomer API.
|
||||
try:
|
||||
ec = EnterpriseCustomer.objects.get(uuid=ec_uuid)
|
||||
except (EnterpriseCustomer.DoesNotExist, ValueError):
|
||||
ec = EnterpriseApiClient().get_enterprise_customer(ec_uuid)
|
||||
except HttpNotFoundError:
|
||||
ec = None
|
||||
|
||||
return ec
|
||||
|
||||
|
||||
def consent_needed_for_course(user, course_id):
|
||||
def consent_needed_for_course(request, user, course_id, enrollment_exists=False):
|
||||
"""
|
||||
Wrap the enterprise app check to determine if the user needs to grant
|
||||
data sharing permissions before accessing a course.
|
||||
"""
|
||||
if not enterprise_enabled():
|
||||
return False
|
||||
return consent_necessary_for_course(user, course_id)
|
||||
|
||||
consent_key = ('data_sharing_consent_needed', course_id)
|
||||
|
||||
if request.session.get(consent_key) is False:
|
||||
return False
|
||||
|
||||
enterprise_learner_details = get_enterprise_learner_data(request.site, user)
|
||||
if not enterprise_learner_details:
|
||||
consent_needed = False
|
||||
else:
|
||||
client = ConsentApiClient()
|
||||
consent_needed = any(
|
||||
client.consent_required(
|
||||
username=user.username,
|
||||
course_id=course_id,
|
||||
enterprise_customer_uuid=learner['enterprise_customer']['uuid'],
|
||||
enrollment_exists=enrollment_exists,
|
||||
)
|
||||
for learner in enterprise_learner_details
|
||||
)
|
||||
if not consent_needed:
|
||||
# Set an ephemeral item in the user's session to prevent us from needing
|
||||
# to make a Consent API request every time this function is called.
|
||||
request.session[consent_key] = False
|
||||
|
||||
return consent_needed
|
||||
|
||||
|
||||
def get_enterprise_consent_url(request, course_id, user=None, return_to=None):
|
||||
def get_enterprise_consent_url(request, course_id, user=None, return_to=None, enrollment_exists=False):
|
||||
"""
|
||||
Build a URL to redirect the user to the Enterprise app to provide data sharing
|
||||
consent for a specific course ID.
|
||||
@@ -290,10 +385,13 @@ def get_enterprise_consent_url(request, course_id, user=None, return_to=None):
|
||||
* return_to: url name label for the page to return to after consent is granted.
|
||||
If None, return to request.path instead.
|
||||
"""
|
||||
if not enterprise_enabled():
|
||||
return ''
|
||||
|
||||
if user is None:
|
||||
user = request.user
|
||||
|
||||
if not consent_needed_for_course(user, course_id):
|
||||
if not consent_needed_for_course(request, user, course_id, enrollment_exists=enrollment_exists):
|
||||
return None
|
||||
|
||||
if return_to is None:
|
||||
@@ -318,30 +416,6 @@ def get_enterprise_consent_url(request, course_id, user=None, return_to=None):
|
||||
return full_url
|
||||
|
||||
|
||||
def get_cache_key(**kwargs):
|
||||
"""
|
||||
Get MD5 encoded cache key for given arguments.
|
||||
|
||||
Here is the format of key before MD5 encryption.
|
||||
key1:value1__key2:value2 ...
|
||||
|
||||
Example:
|
||||
>>> get_cache_key(site_domain="example.com", resource="enterprise-learner")
|
||||
# Here is key format for above call
|
||||
# "site_domain:example.com__resource:enterprise-learner"
|
||||
a54349175618ff1659dee0978e3149ca
|
||||
|
||||
Arguments:
|
||||
**kwargs: Key word arguments that need to be present in cache key.
|
||||
|
||||
Returns:
|
||||
An MD5 encoded key uniquely identified by the key word arguments.
|
||||
"""
|
||||
key = '__'.join(['{}:{}'.format(item, value) for item, value in six.iteritems(kwargs)])
|
||||
|
||||
return hashlib.md5(key).hexdigest()
|
||||
|
||||
|
||||
def get_enterprise_learner_data(site, user):
|
||||
"""
|
||||
Client API operation adapter/wrapper
|
||||
@@ -366,42 +440,40 @@ def get_dashboard_consent_notification(request, user, course_enrollments):
|
||||
Returns:
|
||||
str: Either an empty string, or a string containing the HTML code for the notification banner.
|
||||
"""
|
||||
if not enterprise_enabled():
|
||||
return ''
|
||||
|
||||
enrollment = None
|
||||
enterprise_enrollment = None
|
||||
consent_needed = False
|
||||
course_id = request.GET.get(CONSENT_FAILED_PARAMETER)
|
||||
|
||||
if course_id:
|
||||
|
||||
enterprise_customer = enterprise_customer_for_request(request)
|
||||
if not enterprise_customer:
|
||||
return ''
|
||||
|
||||
for course_enrollment in course_enrollments:
|
||||
if str(course_enrollment.course_id) == course_id:
|
||||
enrollment = course_enrollment
|
||||
break
|
||||
|
||||
try:
|
||||
enterprise_enrollment = EnterpriseCourseEnrollment.objects.get(
|
||||
course_id=course_id,
|
||||
enterprise_customer_user__user_id=user.id,
|
||||
)
|
||||
except EnterpriseCourseEnrollment.DoesNotExist:
|
||||
pass
|
||||
client = ConsentApiClient()
|
||||
consent_needed = client.consent_required(
|
||||
enterprise_customer_uuid=enterprise_customer['uuid'],
|
||||
username=user.username,
|
||||
course_id=course_id,
|
||||
)
|
||||
|
||||
if enterprise_enrollment and enrollment:
|
||||
enterprise_customer = enterprise_enrollment.enterprise_customer_user.enterprise_customer
|
||||
contact_info = getattr(enterprise_customer, 'contact_email', None)
|
||||
if consent_needed and enrollment:
|
||||
|
||||
if contact_info is None:
|
||||
message_template = _(
|
||||
'If you have concerns about sharing your data, please contact your administrator '
|
||||
'at {enterprise_customer_name}.'
|
||||
)
|
||||
else:
|
||||
message_template = _(
|
||||
'If you have concerns about sharing your data, please contact your administrator '
|
||||
'at {enterprise_customer_name} at {contact_info}.'
|
||||
)
|
||||
message_template = _(
|
||||
'If you have concerns about sharing your data, please contact your administrator '
|
||||
'at {enterprise_customer_name}.'
|
||||
)
|
||||
|
||||
message = message_template.format(
|
||||
enterprise_customer_name=enterprise_customer.name,
|
||||
contact_info=contact_info,
|
||||
enterprise_customer_name=enterprise_customer['name'],
|
||||
)
|
||||
title = _(
|
||||
'Enrollment in {course_name} was not complete.'
|
||||
@@ -417,52 +489,3 @@ def get_dashboard_consent_notification(request, user, course_enrollments):
|
||||
}
|
||||
)
|
||||
return ''
|
||||
|
||||
|
||||
def is_course_in_enterprise_catalog(site, course_id, enterprise_catalog_id):
|
||||
"""
|
||||
Verify that the provided course id exists in the site base list of course
|
||||
run keys from the provided enterprise course catalog.
|
||||
|
||||
Arguments:
|
||||
course_id (str): The course ID.
|
||||
site: (django.contrib.sites.Site) site instance
|
||||
enterprise_catalog_id (Int): Course catalog id of enterprise
|
||||
|
||||
Returns:
|
||||
Boolean
|
||||
|
||||
"""
|
||||
cache_key = get_cache_key(
|
||||
site_domain=site.domain,
|
||||
resource='catalogs.contains',
|
||||
course_id=course_id,
|
||||
catalog_id=enterprise_catalog_id
|
||||
)
|
||||
response = cache.get(cache_key)
|
||||
if not response:
|
||||
catalog_integration = CatalogIntegration.current()
|
||||
if not catalog_integration.enabled:
|
||||
LOGGER.error("Catalog integration is not enabled.")
|
||||
return False
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=catalog_integration.service_username)
|
||||
except User.DoesNotExist:
|
||||
LOGGER.exception("Catalog service user '%s' does not exist.", catalog_integration.service_username)
|
||||
return False
|
||||
|
||||
try:
|
||||
# GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids}
|
||||
response = create_catalog_api_client(user=user).catalogs(enterprise_catalog_id).contains.get(
|
||||
course_run_id=course_id
|
||||
)
|
||||
cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
|
||||
except (ConnectionError, SlumberBaseException, Timeout):
|
||||
LOGGER.exception('Unable to connect to Course Catalog service for catalog contains endpoint.')
|
||||
return False
|
||||
|
||||
try:
|
||||
return response['courses'][course_id]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
@@ -7,6 +7,8 @@ import mock
|
||||
import httpretty
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
|
||||
class EnterpriseServiceMockMixin(object):
|
||||
@@ -14,6 +16,8 @@ class EnterpriseServiceMockMixin(object):
|
||||
Mocks for the Enterprise service responses.
|
||||
"""
|
||||
|
||||
consent_url = '{}{}'.format(settings.ENTERPRISE_CONSENT_API_URL, 'data_sharing_consent')
|
||||
|
||||
def setUp(self):
|
||||
super(EnterpriseServiceMockMixin, self).setUp()
|
||||
cache.clear()
|
||||
@@ -23,6 +27,19 @@ class EnterpriseServiceMockMixin(object):
|
||||
"""Return a URL to the configured Enterprise API. """
|
||||
return '{}{}/'.format(settings.ENTERPRISE_API_URL, path)
|
||||
|
||||
def mock_get_enterprise_customer(self, uuid, response, status):
|
||||
"""
|
||||
Helper to mock the HTTP call to the /enterprise-customer/uuid endpoint
|
||||
"""
|
||||
body = json.dumps(response)
|
||||
httpretty.register_uri(
|
||||
method=httpretty.GET,
|
||||
uri=(self.get_enterprise_url('enterprise-customer') + uuid + '/'),
|
||||
body=body,
|
||||
content_type='application/json',
|
||||
status=status,
|
||||
)
|
||||
|
||||
def mock_enterprise_course_enrollment_post_api( # pylint: disable=invalid-name
|
||||
self,
|
||||
username='test_user',
|
||||
@@ -57,6 +74,70 @@ class EnterpriseServiceMockMixin(object):
|
||||
status=500
|
||||
)
|
||||
|
||||
def mock_consent_response(
|
||||
self,
|
||||
username,
|
||||
course_id,
|
||||
ec_uuid,
|
||||
method=httpretty.GET,
|
||||
granted=True,
|
||||
required=False,
|
||||
exists=True,
|
||||
response_code=None
|
||||
):
|
||||
response_body = {
|
||||
'username': username,
|
||||
'course_id': course_id,
|
||||
'enterprise_customer_uuid': ec_uuid,
|
||||
'consent_provided': granted,
|
||||
'consent_required': required,
|
||||
'exists': exists,
|
||||
}
|
||||
httpretty.register_uri(
|
||||
method=method,
|
||||
uri=self.consent_url,
|
||||
content_type='application/json',
|
||||
body=json.dumps(response_body),
|
||||
status=response_code or 200,
|
||||
)
|
||||
|
||||
def mock_consent_post(self, username, course_id, ec_uuid):
|
||||
self.mock_consent_response(
|
||||
username,
|
||||
course_id,
|
||||
ec_uuid,
|
||||
method=httpretty.POST,
|
||||
granted=True,
|
||||
exists=True,
|
||||
)
|
||||
|
||||
def mock_consent_get(self, username, course_id, ec_uuid):
|
||||
self.mock_consent_response(
|
||||
username,
|
||||
course_id,
|
||||
ec_uuid
|
||||
)
|
||||
|
||||
def mock_consent_missing(self, username, course_id, ec_uuid):
|
||||
self.mock_consent_response(
|
||||
username,
|
||||
course_id,
|
||||
ec_uuid,
|
||||
exists=False,
|
||||
granted=False,
|
||||
required=True,
|
||||
)
|
||||
|
||||
def mock_consent_not_required(self, username, course_id, ec_uuid):
|
||||
self.mock_consent_response(
|
||||
username,
|
||||
course_id,
|
||||
ec_uuid,
|
||||
exists=False,
|
||||
granted=False,
|
||||
required=False,
|
||||
)
|
||||
|
||||
def mock_enterprise_learner_api(
|
||||
self,
|
||||
catalog_id=1,
|
||||
@@ -134,7 +215,7 @@ class EnterpriseServiceMockMixin(object):
|
||||
)
|
||||
|
||||
|
||||
class EnterpriseTestConsentRequired(object):
|
||||
class EnterpriseTestConsentRequired(SimpleTestCase):
|
||||
"""
|
||||
Mixin to help test the data_sharing_consent_required decorator.
|
||||
"""
|
||||
@@ -149,20 +230,30 @@ class EnterpriseTestConsentRequired(object):
|
||||
* url: URL to test
|
||||
* status_code: expected status code of URL when no data sharing consent is required.
|
||||
"""
|
||||
with mock.patch('openedx.features.enterprise_support.api.enterprise_enabled', return_value=True):
|
||||
with mock.patch('openedx.features.enterprise_support.api.consent_necessary_for_course') as mock_consent_necessary: # pylint: disable=line-too-long
|
||||
# Ensure that when consent is necessary, the user is redirected to the consent page.
|
||||
mock_consent_necessary.return_value = True
|
||||
response = client.get(url)
|
||||
assert response.status_code == 302
|
||||
assert 'grant_data_sharing_permissions' in response.url # pylint: disable=no-member
|
||||
def mock_consent_reverse(*args, **kwargs):
|
||||
if args[0] == 'grant_data_sharing_permissions':
|
||||
return '/enterprise/grant_data_sharing_permissions'
|
||||
return reverse(*args, **kwargs)
|
||||
|
||||
# Ensure that when consent is not necessary, the user continues through to the requested page.
|
||||
mock_consent_necessary.return_value = False
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
with mock.patch('openedx.features.enterprise_support.api.reverse', side_effect=mock_consent_reverse):
|
||||
with mock.patch('openedx.features.enterprise_support.api.enterprise_enabled', return_value=True):
|
||||
with mock.patch(
|
||||
'openedx.features.enterprise_support.api.consent_needed_for_course'
|
||||
) as mock_consent_necessary:
|
||||
# Ensure that when consent is necessary, the user is redirected to the consent page.
|
||||
mock_consent_necessary.return_value = True
|
||||
response = client.get(url)
|
||||
while(response.status_code == 302 and 'grant_data_sharing_permissions' not in response.url):
|
||||
response = client.get(response.url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('grant_data_sharing_permissions', response.url) # pylint: disable=no-member
|
||||
|
||||
# If we were expecting a redirect, ensure it's not to the data sharing permission page
|
||||
if status_code == 302:
|
||||
assert 'grant_data_sharing_permissions' not in response.url # pylint: disable=no-member
|
||||
return response
|
||||
# Ensure that when consent is not necessary, the user continues through to the requested page.
|
||||
mock_consent_necessary.return_value = False
|
||||
response = client.get(url)
|
||||
self.assertEqual(response.status_code, status_code)
|
||||
|
||||
# If we were expecting a redirect, ensure it's not to the data sharing permission page
|
||||
if status_code == 302:
|
||||
self.assertNotIn('grant_data_sharing_permissions', response.url) # pylint: disable=no-member
|
||||
return response
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
"""
|
||||
Test the enterprise support APIs.
|
||||
"""
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import ddt
|
||||
import httpretty
|
||||
import mock
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from openedx.features.enterprise_support.api import (
|
||||
consent_needed_for_course,
|
||||
data_sharing_consent_required,
|
||||
enterprise_customer_for_request,
|
||||
enterprise_enabled,
|
||||
@@ -16,80 +22,105 @@ from openedx.features.enterprise_support.api import (
|
||||
get_enterprise_consent_url,
|
||||
)
|
||||
|
||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseServiceMockMixin
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=True)
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestEnterpriseApi(unittest.TestCase):
|
||||
class TestEnterpriseApi(EnterpriseServiceMockMixin, SimpleTestCase):
|
||||
"""
|
||||
Test enterprise support APIs.
|
||||
"""
|
||||
|
||||
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=True)
|
||||
@mock.patch('openedx.features.enterprise_support.api.EnterpriseCustomer')
|
||||
@mock.patch('openedx.features.enterprise_support.api.get_partial_pipeline')
|
||||
def test_enterprise_customer_for_request(self, pipeline_mock, ec_class_mock):
|
||||
"""
|
||||
Test that the correct EnterpriseCustomer, if any, is returned.
|
||||
"""
|
||||
def get_ec_mock(**kwargs):
|
||||
by_provider_id_kw = 'enterprise_customer_identity_provider__provider_id'
|
||||
provider_id = kwargs.get(by_provider_id_kw, '')
|
||||
uuid = kwargs.get('uuid', '')
|
||||
if uuid == 'real-uuid' or provider_id == 'real-provider-id':
|
||||
return 'this-is-actually-an-enterprise-customer'
|
||||
elif uuid == 'not-a-uuid':
|
||||
raise ValueError
|
||||
else:
|
||||
raise Exception
|
||||
|
||||
ec_class_mock.DoesNotExist = Exception
|
||||
ec_class_mock.objects.get.side_effect = get_ec_mock
|
||||
|
||||
pipeline_mock.return_value = None
|
||||
|
||||
request = mock.MagicMock()
|
||||
request.GET.get.return_value = 'real-uuid'
|
||||
self.assertEqual(enterprise_customer_for_request(request), 'this-is-actually-an-enterprise-customer')
|
||||
request.GET.get.return_value = 'not-a-uuid'
|
||||
self.assertEqual(enterprise_customer_for_request(request), None)
|
||||
request.GET.get.return_value = 'fake-uuid'
|
||||
self.assertEqual(enterprise_customer_for_request(request), None)
|
||||
request.GET.get.return_value = None
|
||||
self.assertEqual(
|
||||
enterprise_customer_for_request(request, tpa_hint='real-provider-id'),
|
||||
'this-is-actually-an-enterprise-customer'
|
||||
def setUp(self):
|
||||
UserFactory.create(
|
||||
username='enterprise_worker',
|
||||
email='ent_worker@example.com',
|
||||
password='password123',
|
||||
)
|
||||
self.assertEqual(enterprise_customer_for_request(request, tpa_hint='fake-provider-id'), None)
|
||||
self.assertEqual(enterprise_customer_for_request(request, tpa_hint=None), None)
|
||||
super(TestEnterpriseApi, self).setUp()
|
||||
|
||||
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=True)
|
||||
@httpretty.activate
|
||||
@override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker')
|
||||
def test_consent_needed_for_course(self):
|
||||
user = mock.MagicMock(
|
||||
username='janedoe',
|
||||
is_authenticated=lambda: True,
|
||||
)
|
||||
request = mock.MagicMock(session={})
|
||||
self.mock_enterprise_learner_api()
|
||||
self.mock_consent_missing(user.username, 'fake-course', 'cf246b88-d5f6-4908-a522-fc307e0b0c59')
|
||||
self.assertTrue(consent_needed_for_course(request, user, 'fake-course'))
|
||||
self.mock_consent_get(user.username, 'fake-course', 'cf246b88-d5f6-4908-a522-fc307e0b0c59')
|
||||
self.assertFalse(consent_needed_for_course(request, user, 'fake-course'))
|
||||
# Test that the result is cached when false (remove the HTTP mock so if the result
|
||||
# isn't cached, we'll fail spectacularly.)
|
||||
httpretty.reset()
|
||||
self.assertFalse(consent_needed_for_course(request, user, 'fake-course'))
|
||||
|
||||
@httpretty.activate
|
||||
@mock.patch('openedx.features.enterprise_support.api.get_enterprise_learner_data')
|
||||
@mock.patch('openedx.features.enterprise_support.api.EnterpriseCustomer')
|
||||
@mock.patch('openedx.features.enterprise_support.api.Registry')
|
||||
@mock.patch('openedx.features.enterprise_support.api.get_partial_pipeline')
|
||||
def test_get_enterprise_customer_for_request_from_pipeline(self, pipeline_mock, registry_mock, ec_class_mock):
|
||||
"""
|
||||
Test that the correct EnterpriseCustomer, if any, is returned when
|
||||
the user is in the middle of a third-party auth pipeline.
|
||||
"""
|
||||
def get_ec_mock(**kwargs):
|
||||
by_provider_id_kw = 'enterprise_customer_identity_provider__provider_id'
|
||||
provider_id = kwargs.get(by_provider_id_kw, '')
|
||||
uuid = kwargs.get('uuid', '')
|
||||
if uuid == 'real-uuid' or provider_id == 'real-provider-id':
|
||||
# Only return the good value if we get the parameter we expect.
|
||||
return 'this-is-actually-an-enterprise-customer'
|
||||
@mock.patch('openedx.features.enterprise_support.api.Registry')
|
||||
@override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker')
|
||||
def test_enterprise_customer_for_request(
|
||||
self,
|
||||
mock_registry,
|
||||
mock_partial,
|
||||
mock_ec_model,
|
||||
mock_get_el_data
|
||||
):
|
||||
def mock_get_ec(**kwargs):
|
||||
uuid = kwargs.get('enterprise_customer_identity_provider__provider_id')
|
||||
if uuid:
|
||||
return mock.MagicMock(uuid=uuid)
|
||||
raise Exception
|
||||
|
||||
ec_class_mock.DoesNotExist = Exception
|
||||
ec_class_mock.objects.get.side_effect = get_ec_mock
|
||||
mock_ec_model.objects.get.side_effect = mock_get_ec
|
||||
mock_ec_model.DoesNotExist = Exception
|
||||
|
||||
# Truthy value from the pipeline getter to imitate a running pipeline
|
||||
pipeline_mock.return_value = {"fake_pipeline": "sofake"}
|
||||
mock_partial.return_value = True
|
||||
mock_registry.get_from_pipeline.return_value.provider_id = 'real-ent-uuid'
|
||||
|
||||
provider_mock = registry_mock.get_from_pipeline.return_value
|
||||
provider_mock.provider_id = 'real-provider-id'
|
||||
self.mock_get_enterprise_customer('real-ent-uuid', {"real": "enterprisecustomer"}, 200)
|
||||
|
||||
request = mock.MagicMock()
|
||||
ec = enterprise_customer_for_request(mock.MagicMock())
|
||||
|
||||
self.assertEqual(enterprise_customer_for_request(request), 'this-is-actually-an-enterprise-customer')
|
||||
self.assertEqual(ec, {"real": "enterprisecustomer"})
|
||||
|
||||
httpretty.reset()
|
||||
|
||||
self.mock_get_enterprise_customer('real-ent-uuid', {"detail": "Not found."}, 404)
|
||||
|
||||
ec = enterprise_customer_for_request(mock.MagicMock())
|
||||
|
||||
self.assertIsNone(ec)
|
||||
|
||||
mock_registry.get_from_pipeline.return_value.provider_id = None
|
||||
|
||||
httpretty.reset()
|
||||
|
||||
self.mock_get_enterprise_customer('real-ent-uuid', {"real": "enterprisecustomer"}, 200)
|
||||
|
||||
ec = enterprise_customer_for_request(mock.MagicMock(GET={"enterprise_customer": 'real-ent-uuid'}))
|
||||
|
||||
self.assertEqual(ec, {"real": "enterprisecustomer"})
|
||||
|
||||
ec = enterprise_customer_for_request(
|
||||
mock.MagicMock(GET={}, COOKIES={settings.ENTERPRISE_CUSTOMER_COOKIE_NAME: 'real-ent-uuid'})
|
||||
)
|
||||
|
||||
self.assertEqual(ec, {"real": "enterprisecustomer"})
|
||||
|
||||
mock_get_el_data.return_value = [{'enterprise_customer': {'uuid': 'real-ent-uuid'}}]
|
||||
|
||||
ec = enterprise_customer_for_request(
|
||||
mock.MagicMock(GET={}, COOKIES={}, user=mock.MagicMock(is_authenticated=lambda: True), site=1)
|
||||
)
|
||||
|
||||
self.assertEqual(ec, {"real": "enterprisecustomer"})
|
||||
|
||||
def check_data_sharing_consent(self, consent_required=False, consent_url=None):
|
||||
"""
|
||||
@@ -120,7 +151,7 @@ class TestEnterpriseApi(unittest.TestCase):
|
||||
self.assertEqual(response, (args, kwargs))
|
||||
|
||||
@mock.patch('openedx.features.enterprise_support.api.enterprise_enabled')
|
||||
@mock.patch('openedx.features.enterprise_support.api.consent_necessary_for_course')
|
||||
@mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course')
|
||||
def test_data_consent_required_enterprise_disabled(self,
|
||||
mock_consent_necessary,
|
||||
mock_enterprise_enabled):
|
||||
@@ -136,7 +167,7 @@ class TestEnterpriseApi(unittest.TestCase):
|
||||
mock_consent_necessary.assert_not_called()
|
||||
|
||||
@mock.patch('openedx.features.enterprise_support.api.enterprise_enabled')
|
||||
@mock.patch('openedx.features.enterprise_support.api.consent_necessary_for_course')
|
||||
@mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course')
|
||||
def test_no_course_data_consent_required(self,
|
||||
mock_consent_necessary,
|
||||
mock_enterprise_enabled):
|
||||
@@ -154,7 +185,7 @@ class TestEnterpriseApi(unittest.TestCase):
|
||||
mock_consent_necessary.assert_called_once()
|
||||
|
||||
@mock.patch('openedx.features.enterprise_support.api.enterprise_enabled')
|
||||
@mock.patch('openedx.features.enterprise_support.api.consent_necessary_for_course')
|
||||
@mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course')
|
||||
@mock.patch('openedx.features.enterprise_support.api.get_enterprise_consent_url')
|
||||
def test_data_consent_required(self, mock_get_consent_url, mock_consent_necessary, mock_enterprise_enabled):
|
||||
"""
|
||||
@@ -172,11 +203,18 @@ class TestEnterpriseApi(unittest.TestCase):
|
||||
mock_enterprise_enabled.assert_called_once()
|
||||
mock_consent_necessary.assert_called_once()
|
||||
|
||||
@mock.patch('openedx.features.enterprise_support.api.reverse')
|
||||
@mock.patch('openedx.features.enterprise_support.api.consent_needed_for_course')
|
||||
def test_get_enterprise_consent_url(self, needed_for_course_mock):
|
||||
def test_get_enterprise_consent_url(self, needed_for_course_mock, reverse_mock):
|
||||
"""
|
||||
Verify that get_enterprise_consent_url correctly builds URLs.
|
||||
"""
|
||||
def fake_reverse(*args, **kwargs):
|
||||
if args[0] == 'grant_data_sharing_permissions':
|
||||
return '/enterprise/grant_data_sharing_permissions'
|
||||
return reverse(*args, **kwargs)
|
||||
|
||||
reverse_mock.side_effect = fake_reverse
|
||||
needed_for_course_mock.return_value = True
|
||||
|
||||
request_mock = mock.MagicMock(
|
||||
@@ -196,119 +234,60 @@ class TestEnterpriseApi(unittest.TestCase):
|
||||
actual_url = get_enterprise_consent_url(request_mock, course_id, return_to=return_to)
|
||||
self.assertEqual(actual_url, expected_url)
|
||||
|
||||
def test_get_dashboard_consent_notification_no_param(self):
|
||||
"""
|
||||
Test that the output of the consent notification renderer meets expectations.
|
||||
"""
|
||||
request = mock.MagicMock(
|
||||
GET={}
|
||||
)
|
||||
notification_string = get_dashboard_consent_notification(
|
||||
request, None, None
|
||||
)
|
||||
self.assertEqual(notification_string, '')
|
||||
|
||||
def test_get_dashboard_consent_notification_no_enrollments(self):
|
||||
request = mock.MagicMock(
|
||||
GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'}
|
||||
)
|
||||
enrollments = []
|
||||
user = mock.MagicMock(id=1)
|
||||
notification_string = get_dashboard_consent_notification(
|
||||
request, user, enrollments,
|
||||
)
|
||||
self.assertEqual(notification_string, '')
|
||||
|
||||
def test_get_dashboard_consent_notification_no_matching_enrollments(self):
|
||||
request = mock.MagicMock(
|
||||
GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'}
|
||||
)
|
||||
enrollments = [mock.MagicMock(course_id='other_course_id')]
|
||||
user = mock.MagicMock(id=1)
|
||||
notification_string = get_dashboard_consent_notification(
|
||||
request, user, enrollments,
|
||||
)
|
||||
self.assertEqual(notification_string, '')
|
||||
|
||||
def test_get_dashboard_consent_notification_no_matching_ece(self):
|
||||
request = mock.MagicMock(
|
||||
GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'}
|
||||
)
|
||||
enrollments = [mock.MagicMock(course_id='course-v1:edX+DemoX+Demo_Course')]
|
||||
user = mock.MagicMock(id=1)
|
||||
notification_string = get_dashboard_consent_notification(
|
||||
request, user, enrollments,
|
||||
)
|
||||
self.assertEqual(notification_string, '')
|
||||
|
||||
@mock.patch('openedx.features.enterprise_support.api.EnterpriseCourseEnrollment')
|
||||
def test_get_dashboard_consent_notification_no_contact_info(self, ece_mock):
|
||||
mock_get_ece = ece_mock.objects.get
|
||||
ece_mock.DoesNotExist = Exception
|
||||
mock_ece = mock_get_ece.return_value
|
||||
mock_ece.enterprise_customer_user = mock.MagicMock(
|
||||
enterprise_customer=mock.MagicMock(
|
||||
contact_email=None
|
||||
)
|
||||
)
|
||||
mock_ec = mock_ece.enterprise_customer_user.enterprise_customer
|
||||
mock_ec.name = 'Veridian Dynamics'
|
||||
|
||||
request = mock.MagicMock(
|
||||
GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'}
|
||||
)
|
||||
enrollments = [
|
||||
mock.MagicMock(
|
||||
course_id='course-v1:edX+DemoX+Demo_Course',
|
||||
course_overview=mock.MagicMock(
|
||||
display_name='edX Demo Course',
|
||||
@ddt.data(
|
||||
(False, {'real': 'enterprise', 'uuid': ''}, 'course', [], []),
|
||||
(True, {}, 'course', [], []),
|
||||
(True, {'real': 'enterprise'}, None, [], []),
|
||||
(True, {'name': 'GriffCo', 'uuid': ''}, 'real-course', [], []),
|
||||
(True, {'name': 'GriffCo', 'uuid': ''}, 'real-course', [mock.MagicMock(course_id='other-id')], []),
|
||||
(
|
||||
True,
|
||||
{'name': 'GriffCo', 'uuid': 'real-uuid'},
|
||||
'real-course',
|
||||
[
|
||||
mock.MagicMock(
|
||||
course_id='real-course',
|
||||
course_overview=mock.MagicMock(
|
||||
display_name='My Cool Course'
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
user = mock.MagicMock(id=1)
|
||||
notification_string = get_dashboard_consent_notification(
|
||||
request, user, enrollments,
|
||||
)
|
||||
expected_message = (
|
||||
'If you have concerns about sharing your data, please contact your '
|
||||
'administrator at Veridian Dynamics.'
|
||||
)
|
||||
self.assertIn(expected_message, notification_string)
|
||||
expected_header = 'Enrollment in edX Demo Course was not complete.'
|
||||
self.assertIn(expected_header, notification_string)
|
||||
|
||||
@mock.patch('openedx.features.enterprise_support.api.EnterpriseCourseEnrollment')
|
||||
def test_get_dashboard_consent_notification_contact_info(self, ece_mock):
|
||||
mock_get_ece = ece_mock.objects.get
|
||||
ece_mock.DoesNotExist = Exception
|
||||
mock_ece = mock_get_ece.return_value
|
||||
mock_ece.enterprise_customer_user = mock.MagicMock(
|
||||
enterprise_customer=mock.MagicMock(
|
||||
contact_email='v.palmer@veridiandynamics.com'
|
||||
)
|
||||
)
|
||||
mock_ec = mock_ece.enterprise_customer_user.enterprise_customer
|
||||
mock_ec.name = 'Veridian Dynamics'
|
||||
],
|
||||
[
|
||||
'If you have concerns about sharing your data, please contact your administrator at GriffCo.',
|
||||
'Enrollment in My Cool Course was not complete.'
|
||||
]
|
||||
),
|
||||
|
||||
)
|
||||
@ddt.unpack
|
||||
@mock.patch('openedx.features.enterprise_support.api.ConsentApiClient')
|
||||
@mock.patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
|
||||
def test_get_dashboard_consent_notification(
|
||||
self,
|
||||
consent_return_value,
|
||||
enterprise_customer,
|
||||
course_id,
|
||||
enrollments,
|
||||
expected_substrings,
|
||||
ec_for_request,
|
||||
consent_client_class
|
||||
):
|
||||
request = mock.MagicMock(
|
||||
GET={'consent_failed': 'course-v1:edX+DemoX+Demo_Course'}
|
||||
GET={'consent_failed': course_id}
|
||||
)
|
||||
enrollments = [
|
||||
mock.MagicMock(
|
||||
course_id='course-v1:edX+DemoX+Demo_Course',
|
||||
course_overview=mock.MagicMock(
|
||||
display_name='edX Demo Course',
|
||||
)
|
||||
),
|
||||
]
|
||||
user = mock.MagicMock(id=1)
|
||||
consent_client = consent_client_class.return_value
|
||||
consent_client.consent_required.return_value = consent_return_value
|
||||
|
||||
ec_for_request.return_value = enterprise_customer
|
||||
|
||||
user = mock.MagicMock()
|
||||
|
||||
notification_string = get_dashboard_consent_notification(
|
||||
request, user, enrollments,
|
||||
)
|
||||
expected_message = (
|
||||
'If you have concerns about sharing your data, please contact your '
|
||||
'administrator at Veridian Dynamics at v.palmer@veridiandynamics.com.'
|
||||
)
|
||||
self.assertIn(expected_message, notification_string)
|
||||
expected_header = 'Enrollment in edX Demo Course was not complete.'
|
||||
self.assertIn(expected_header, notification_string)
|
||||
|
||||
if expected_substrings:
|
||||
for substr in expected_substrings:
|
||||
self.assertIn(substr, notification_string)
|
||||
else:
|
||||
self.assertEqual(notification_string, '')
|
||||
|
||||
@@ -48,7 +48,7 @@ edx-lint==0.4.3
|
||||
astroid==1.3.8
|
||||
edx-django-oauth2-provider==1.1.4
|
||||
edx-django-sites-extensions==2.3.0
|
||||
edx-enterprise==0.40.1
|
||||
edx-enterprise==0.40.2
|
||||
edx-oauth2-provider==1.2.0
|
||||
edx-opaque-keys==0.4.0
|
||||
edx-organizations==0.4.5
|
||||
|
||||
Reference in New Issue
Block a user