From 82533f2756f93ab9d1e261a9bde386209effb28f Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Wed, 24 Jun 2026 04:40:33 +0000 Subject: [PATCH 1/2] feat: remove enterprise enrollment imports and adds CourseEnrollmentViewStarted filter --- .../enrollments/tests/test_views.py | 39 +--------------- openedx/core/djangoapps/enrollments/views.py | 45 +++++++------------ requirements/constraints.txt | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 7 files changed, 21 insertions(+), 73 deletions(-) diff --git a/openedx/core/djangoapps/enrollments/tests/test_views.py b/openedx/core/djangoapps/enrollments/tests/test_views.py index a6b34cbfc60b..137c3a600295 100644 --- a/openedx/core/djangoapps/enrollments/tests/test_views.py +++ b/openedx/core/djangoapps/enrollments/tests/test_views.py @@ -10,7 +10,6 @@ from urllib.parse import quote import ddt -import httpretty import pytest import pytz from django.conf import settings @@ -45,8 +44,6 @@ from openedx.core.djangoapps.user_api.models import RetirementState, UserOrgTag, UserRetirementStatus from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.lib.django_test_client_utils import get_absolute_url -from openedx.features.enterprise_support.tests import FAKE_ENTERPRISE_CUSTOMER -from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseServiceMockMixin from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range @@ -159,7 +156,7 @@ def _get_enrollments(self): @override_waffle_flag(ENABLE_NOTIFICATIONS, True) @ddt.ddt @skip_unless_lms -class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase, EnterpriseServiceMockMixin): +class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): """ Test user enrollment, especially with different course modes. """ @@ -1243,40 +1240,6 @@ def test_enrollment_with_global_staff_permissions(self, using_global_staff_user, assert course_mode == CourseMode.VERIFIED self.client.logout() - @httpretty.activate - @override_settings(ENTERPRISE_SERVICE_WORKER_USERNAME='enterprise_worker', - FEATURES=dict(ENABLE_ENTERPRISE_INTEGRATION=True)) - @patch('openedx.features.enterprise_support.api.enterprise_customer_from_api') - def test_enterprise_course_enrollment_with_ec_uuid(self, mock_enterprise_customer_from_api): - """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': str(self.course.id), - 'ec_uuid': 'this-is-a-real-uuid' - } - mock_enterprise_customer_from_api.return_value = FAKE_ENTERPRISE_CUSTOMER - self.mock_enterprise_course_enrollment_post_api() - 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', - ) - assert httpretty.last_request().path == '/consent/api/v1/data_sharing_consent' # pylint: disable=no-member - assert 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. diff --git a/openedx/core/djangoapps/enrollments/views.py b/openedx/core/djangoapps/enrollments/views.py index dc3423245e9b..b95c22823500 100644 --- a/openedx/core/djangoapps/enrollments/views.py +++ b/openedx/core/djangoapps/enrollments/views.py @@ -13,12 +13,12 @@ from django.db import IntegrityError # lint-amnesty, pylint: disable=wrong-import-order from django.db.models import Q # lint-amnesty, pylint: disable=wrong-import-order from django.utils.decorators import method_decorator # lint-amnesty, pylint: disable=wrong-import-order -from edx_rest_framework_extensions.auth.jwt.authentication import ( +from edx_rest_framework_extensions.auth.jwt.authentication import ( # lint-amnesty, pylint: disable=wrong-import-order JwtAuthentication, -) # lint-amnesty, pylint: disable=wrong-import-order -from edx_rest_framework_extensions.auth.session.authentication import ( +) +from edx_rest_framework_extensions.auth.session.authentication import ( # lint-amnesty, pylint: disable=wrong-import-order SessionAuthenticationAllowInactiveUser, -) # lint-amnesty, pylint: disable=wrong-import-order +) from opaque_keys import InvalidKeyError # lint-amnesty, pylint: disable=wrong-import-order from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=wrong-import-order from rest_framework import permissions, status # lint-amnesty, pylint: disable=wrong-import-order @@ -57,12 +57,7 @@ from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin from openedx.core.lib.exceptions import CourseNotFoundError from openedx.core.lib.log_utils import audit_log -from openedx.features.enterprise_support.api import ( - ConsentApiServiceClient, - EnterpriseApiException, - EnterpriseApiServiceClient, - enterprise_enabled, -) +from openedx_filters.learning.filters import CourseEnrollmentViewStarted log = logging.getLogger(__name__) REQUIRED_ATTRIBUTES = { @@ -774,26 +769,16 @@ def post(self, request): data={"message": ("'{value}' is an invalid enrollment activation status.").format(value=is_active)}, ) - explicit_linked_enterprise = request.data.get("linked_enterprise_customer") - if explicit_linked_enterprise and has_api_key_permissions and enterprise_enabled(): - enterprise_api_client = EnterpriseApiServiceClient() - consent_client = ConsentApiServiceClient() - try: - enterprise_api_client.post_enterprise_course_enrollment(username, str(course_id)) - 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(str(error)) # lint-amnesty, pylint: disable=raise-missing-from - kwargs = { - "username": username, - "course_id": str(course_id), - "enterprise_customer_uuid": explicit_linked_enterprise, - } - consent_client.provide_consent(**kwargs) + # Filter that executes enterprise-specific functions only if the enrollment is associated + # with an enterprise customer + try: + user, course_id, has_api_key_permissions = CourseEnrollmentViewStarted.run_filter( + user=user, + course_key=course_id, + has_api_key_permissions=has_api_key_permissions, + ) + except CourseEnrollmentViewStarted.PreventEnrollment as exc: + raise EnrollmentNotAllowed(str(exc)) from exc enrollment_attributes = request.data.get("enrollment_attributes") force_enrollment = request.data.get("force_enrollment") diff --git a/requirements/constraints.txt b/requirements/constraints.txt index b9605da356d3..f9888235807d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -42,7 +42,7 @@ django-stubs<6 # The team that owns this package will manually bump this package rather than having it pulled in automatically. # This is to allow them to better control its deployment and to do it in a process that works better # for them. -edx-enterprise==8.0.18 +edx-enterprise==8.1.3 # Date: 2023-07-26 # Our legacy Sass code is incompatible with anything except this ancient libsass version. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index b40c11e89013..26a46f9393dd 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -473,7 +473,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.0.18 +edx-enterprise==8.1.3 # via # -c requirements/constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 9aebd8f03eaa..796ddf61ca74 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -747,7 +747,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.0.18 +edx-enterprise==8.1.3 # via # -c requirements/constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index beba6b61871c..124256317209 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -557,7 +557,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.0.18 +edx-enterprise==8.1.3 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 574c10d9d68c..bf6bec70e94d 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -578,7 +578,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.0.18 +edx-enterprise==8.1.3 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt From 61da77e71a5c1dca16194ffe687ebacfb207e34b Mon Sep 17 00:00:00 2001 From: Kira Miller <31229189+kiram15@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:03:32 +0000 Subject: [PATCH 2/2] fix: version bump and add test --- .../enrollments/tests/test_filters.py | 157 ++++++++++++++++++ openedx/core/djangoapps/enrollments/views.py | 2 +- requirements/constraints.txt | 2 +- requirements/edx/base.txt | 4 +- requirements/edx/development.txt | 4 +- requirements/edx/doc.txt | 4 +- requirements/edx/testing.txt | 4 +- 7 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 openedx/core/djangoapps/enrollments/tests/test_filters.py diff --git a/openedx/core/djangoapps/enrollments/tests/test_filters.py b/openedx/core/djangoapps/enrollments/tests/test_filters.py new file mode 100644 index 000000000000..45a1eca6e838 --- /dev/null +++ b/openedx/core/djangoapps/enrollments/tests/test_filters.py @@ -0,0 +1,157 @@ +""" +Test for CourseEnrollmentViewStarted filter in enrollment views. +""" +import json +from django.test import override_settings +from rest_framework import status +from rest_framework.test import APITestCase + +from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory +from common.djangoapps.course_modes.tests.factories import CourseModeFactory +from openedx_filters import PipelineStep +from openedx_filters.learning.filters import CourseEnrollmentViewStarted +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory +from django.urls import reverse +from openedx.core.djangolib.testing.utils import skip_unless_lms + + +class TestCourseEnrollmentViewStartedPipelineStep(PipelineStep): + """ + Utility pipeline step that prevents enrollment via CourseEnrollmentViewStarted filter. + """ + + def run_filter(self, user, course_key, requester_is_backend_service): # pylint: disable=arguments-differ + """Pipeline step that prevents enrollment for specific users or conditions.""" + if user.username == "blocked_user": + raise CourseEnrollmentViewStarted.PreventEnrollment( + "User is not allowed to enroll in this course." + ) + return { + "user": user, + "course_key": course_key, + "requester_is_backend_service": requester_is_backend_service, + } + + +@skip_unless_lms +class CourseEnrollmentViewStartedFilterTest(APITestCase, ModuleStoreTestCase): + """ + Tests for the CourseEnrollmentViewStarted filter in the enrollment view. + + This class guarantees that the following filters are triggered when attempting + to enroll a user through the enrollment API endpoint: + + - CourseEnrollmentViewStarted + """ + + def setUp(self): # pylint: disable=arguments-differ + super().setUp() + self.course = CourseFactory.create(emit_signals=True) + self.user = UserFactory.create( + username="test_user", + email="test@example.com", + password="password", + ) + self.blocked_user = UserFactory.create( + username="blocked_user", + email="blocked@example.com", + password="password", + ) + UserProfileFactory.create(user=self.user, name="Test User") + UserProfileFactory.create(user=self.blocked_user, name="Blocked User") + + # Create course modes + CourseModeFactory.create( + course_id=self.course.id, + mode_slug='audit', + mode_display_name='Audit' + ) + + @override_settings( + OPEN_EDX_FILTERS_CONFIG={ + "org.openedx.learning.course.enrollment.view.started.v1": { + "pipeline": [ + "openedx.core.djangoapps.enrollments.tests.test_filters.TestCourseEnrollmentViewStartedPipelineStep", + ], + "fail_silently": False, + }, + }, + ) + def test_course_enrollment_view_started_filter_executed(self): + """ + Test that the CourseEnrollmentViewStarted filter is executed when enrolling through the view. + + Expected result: + - CourseEnrollmentViewStarted is triggered and executes the pipeline step. + - The enrollment is created successfully if the filter allows it. + """ + self.client.login(username=self.user.username, password="password") + + response = self.client.post( + reverse('courseenrollments'), + { + "course_details": {"course_id": str(self.course.id)}, + "mode": "audit", + }, + format="json", + ) + + self.assertEqual(status.HTTP_200_OK, response.status_code) + + @override_settings( + OPEN_EDX_FILTERS_CONFIG={ + "org.openedx.learning.course.enrollment.view.started.v1": { + "pipeline": [ + "openedx.core.djangoapps.enrollments.tests.test_filters.TestCourseEnrollmentViewStartedPipelineStep", + ], + "fail_silently": False, + }, + }, + ) + def test_course_enrollment_view_started_filter_prevent_enrollment(self): + """ + Test that enrollment is prevented when filter raises PreventEnrollment. + + Expected result: + - CourseEnrollmentViewStarted is triggered and executes the pipeline step. + - The pipeline step raises PreventEnrollment for blocked_user. + - The endpoint returns HTTP 403 Forbidden. + """ + self.client.login(username=self.blocked_user.username, password="password") + + response = self.client.post( + reverse('courseenrollments'), + { + "course_details": {"course_id": str(self.course.id)}, + "mode": "audit", + }, + format="json", + ) + + self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) + response_data = json.loads(response.content.decode('utf-8')) + self.assertIn("message", response_data) + self.assertIn("User is not allowed to enroll", response_data["message"]) + + @override_settings(OPEN_EDX_FILTERS_CONFIG={}) + def test_course_enrollment_view_started_without_filter_configuration(self): + """ + Test enrollment without filter configuration (no filter interference). + + Expected result: + - CourseEnrollmentViewStarted does not have any effect. + - The enrollment process succeeds without filter intervention. + """ + self.client.login(username=self.user.username, password="password") + + response = self.client.post( + reverse('courseenrollments'), + { + "course_details": {"course_id": str(self.course.id)}, + "mode": "audit", + }, + format="json", + ) + + self.assertEqual(status.HTTP_200_OK, response.status_code) diff --git a/openedx/core/djangoapps/enrollments/views.py b/openedx/core/djangoapps/enrollments/views.py index b95c22823500..6d88db1c8efb 100644 --- a/openedx/core/djangoapps/enrollments/views.py +++ b/openedx/core/djangoapps/enrollments/views.py @@ -775,7 +775,7 @@ def post(self, request): user, course_id, has_api_key_permissions = CourseEnrollmentViewStarted.run_filter( user=user, course_key=course_id, - has_api_key_permissions=has_api_key_permissions, + requester_is_backend_service=has_api_key_permissions, ) except CourseEnrollmentViewStarted.PreventEnrollment as exc: raise EnrollmentNotAllowed(str(exc)) from exc diff --git a/requirements/constraints.txt b/requirements/constraints.txt index f9888235807d..8e63c78219a1 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -42,7 +42,7 @@ django-stubs<6 # The team that owns this package will manually bump this package rather than having it pulled in automatically. # This is to allow them to better control its deployment and to do it in a process that works better # for them. -edx-enterprise==8.1.3 +edx-enterprise==8.4.0 # Date: 2023-07-26 # Our legacy Sass code is incompatible with anything except this ancient libsass version. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 26a46f9393dd..7de9bd48b01e 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -473,7 +473,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.1.3 +edx-enterprise==8.4.0 # via # -c requirements/constraints.txt # -r requirements/edx/kernel.in @@ -829,7 +829,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.3.0 +openedx-filters==3.6.0 # via # -r requirements/edx/kernel.in # edx-enterprise diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 796ddf61ca74..d8b39aff5f59 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -747,7 +747,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.1.3 +edx-enterprise==8.4.0 # via # -c requirements/constraints.txt # -r requirements/edx/doc.txt @@ -1375,7 +1375,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.3.0 +openedx-filters==3.6.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 124256317209..44ef1efae416 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -557,7 +557,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.1.3 +edx-enterprise==8.4.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt @@ -1001,7 +1001,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.3.0 +openedx-filters==3.6.0 # via # -r requirements/edx/base.txt # edx-enterprise diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index bf6bec70e94d..2c26402d6f72 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -578,7 +578,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.1.3 +edx-enterprise==8.4.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt @@ -1046,7 +1046,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.3.0 +openedx-filters==3.6.0 # via # -r requirements/edx/base.txt # edx-enterprise