Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 197 additions & 2 deletions common/djangoapps/student/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
from django.http import HttpResponse
from django.test import override_settings
from django.urls import reverse
from django.utils.http import urlencode
from openedx_filters import PipelineStep
from openedx_filters.learning.filters import DashboardRenderStarted, CourseEnrollmentStarted, CourseUnenrollmentStarted
from openedx_filters.learning.filters import CourseEnrollmentStarted, CourseUnenrollmentStarted, DashboardRenderStarted
from rest_framework import status
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory

from common.djangoapps.student.models import CourseEnrollment, EnrollmentNotAllowed, UnenrollmentNotAllowed
from common.djangoapps.student.models import CourseEnrollment, EnrollmentNotAllowed, Registration, UnenrollmentNotAllowed
from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory
from common.djangoapps.student.views.management import compose_activation_email
from openedx.core.djangolib.testing.utils import skip_unless_lms


Expand Down Expand Up @@ -111,6 +113,37 @@ def run_filter(self, context, template_name): # pylint: disable=arguments-diffe
)


class TestActivationEmailComposedPipelineStep(PipelineStep):
"""
Utility class used when getting steps for pipeline.
"""

def run_filter(self, user, message_context): # pylint: disable=arguments-differ
"""Pipeline step that stamps a marker onto the activation email context."""
message_context["is_enterprise_learner"] = True
return {"user": user, "message_context": message_context}


class TestActivationRedirectPipelineStep(PipelineStep):
"""
Utility class used when getting steps for pipeline.
"""

def run_filter(self, user, redirect_url): # pylint: disable=arguments-differ
"""Pipeline step that clears the post-activation redirect."""
return {"user": user, "redirect_url": ""}


class TestActivationRedirectOverridePipelineStep(PipelineStep):
"""
Utility class used when getting steps for pipeline.
"""

def run_filter(self, user, redirect_url): # pylint: disable=arguments-differ
"""Pipeline step that overrides the post-activation redirect to a custom URL."""
return {"user": user, "redirect_url": "https://custom-post-activation-page.com"}


@skip_unless_lms
class EnrollmentFiltersTest(ModuleStoreTestCase):
"""
Expand Down Expand Up @@ -464,3 +497,165 @@ def test_dashboard_render_without_filter_config(self):

self.assertContains(response, self.first_course.id)
self.assertContains(response, self.second_course.id)


@skip_unless_lms
class AccountActivationEmailFiltersTest(ModuleStoreTestCase):
"""
Tests for the Open edX Filters associated with composing the account activation email.

This class guarantees that the following filter is triggered when the activation email
is composed:
- AccountActivationEmailComposed
"""

def setUp(self): # pylint: disable=arguments-differ
super().setUp()
self.user = UserFactory()
self.registration = Registration()
self.registration.register(self.user)
self.registration.save()

@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.account.activation.email.compose.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestActivationEmailComposedPipelineStep",
],
"fail_silently": False,
},
},
)
def test_activation_email_composed_filter_executed(self):
"""
Test whether the activation email composed filter is triggered before the
activation email message context is finalized.

Expected result:
- AccountActivationEmailComposed is triggered and executes
TestActivationEmailComposedPipelineStep.
- The composed message's context contains the pipeline step's modification.
"""
message = compose_activation_email(self.user, self.registration)

self.assertTrue(message.context["is_enterprise_learner"])

@override_settings(OPEN_EDX_FILTERS_CONFIG={})
def test_activation_email_composed_without_filter_config(self):
"""
Test that compose_activation_email succeeds with no pipeline steps configured.

Expected result:
- AccountActivationEmailComposed executes a noop (empty pipeline).
- No 'is_enterprise_learner' key is injected into the message context.
"""
message = compose_activation_email(self.user, self.registration)

self.assertNotIn("is_enterprise_learner", message.context)


@skip_unless_lms
class AccountActivationCompletedFiltersTest(ModuleStoreTestCase):
"""
Tests for the Open edX Filters associated with the post-activation redirect.

This class guarantees that the following filter is triggered when a user's account
is activated:
- AccountActivationCompleted
"""

def setUp(self): # pylint: disable=arguments-differ
super().setUp()
self.password = self.TEST_PASSWORD
self.user = UserFactory(is_active=False, password=self.password)
self.registration = Registration()
self.registration.register(self.user)
self.registration.save()

def _login(self):
"""Activate, authenticate, then restore the original active state."""
is_active = self.user.is_active
self.user.is_active = True
self.user.save()
self.client.login(username=self.user.username, password=self.password)
self.user.is_active = is_active
self.user.save()

def _activation_url(self, redirect_url):
base = reverse('activate', args=[self.registration.activation_key])
return '{base}?{params}'.format(base=base, params=urlencode({'next': redirect_url}))

@override_settings(
LOGIN_REDIRECT_WHITELIST=['localhost:1991'],
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.account.activation.completed.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestActivationRedirectPipelineStep",
],
"fail_silently": False,
},
},
)
def test_activation_completed_filter_clears_redirect(self):
"""
Test that a pipeline step can clear the post-activation redirect.

Expected result:
- AccountActivationCompleted is triggered and executes
TestActivationRedirectPipelineStep.
- Despite a valid `next` URL being provided, the user is redirected to the
dashboard because the pipeline step cleared redirect_url.
"""
self._login()
redirect_url = 'http://localhost:1991/pied-piper/learn'

response = self.client.get(self._activation_url(redirect_url), HTTP_ACCEPT='*/*')

self.assertEqual(status.HTTP_302_FOUND, response.status_code)
self.assertTrue(response.url.endswith(reverse('dashboard')))

@override_settings(
LOGIN_REDIRECT_WHITELIST=['localhost:1991'],
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.account.activation.completed.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestActivationRedirectOverridePipelineStep",
],
"fail_silently": False,
},
},
)
def test_activation_completed_filter_overrides_redirect(self):
"""
Test that a pipeline step can override the post-activation redirect destination.

Expected result:
- AccountActivationCompleted is triggered and executes
TestActivationRedirectOverridePipelineStep.
- The user is redirected to the URL provided by the pipeline step, not the
original `next` URL.
"""
self._login()
redirect_url = 'http://localhost:1991/pied-piper/learn'

response = self.client.get(self._activation_url(redirect_url), HTTP_ACCEPT='*/*')

self.assertEqual(status.HTTP_302_FOUND, response.status_code)
self.assertEqual('https://custom-post-activation-page.com', response.url)

@override_settings(LOGIN_REDIRECT_WHITELIST=['localhost:1991'], OPEN_EDX_FILTERS_CONFIG={})
def test_activation_completed_without_filter_config(self):
"""
Test that activation redirect behaves as a noop with no pipeline steps configured.

Expected result:
- AccountActivationCompleted executes a noop (empty pipeline).
- The user is redirected to the originally requested `next` URL.
"""
self._login()
redirect_url = 'http://localhost:1991/pied-piper/learn'

response = self.client.get(self._activation_url(redirect_url), HTTP_ACCEPT='*/*')

self.assertEqual(status.HTTP_302_FOUND, response.status_code)
self.assertEqual(redirect_url, response.url)
15 changes: 12 additions & 3 deletions common/djangoapps/student/views/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
# Note that this lives in LMS, so this dependency should be refactored.
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx_filters.learning.filters import AccountActivationCompleted, AccountActivationEmailComposed
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated

Expand Down Expand Up @@ -98,7 +99,6 @@
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url
from openedx.features.discounts.applicability import FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG
from openedx.features.enterprise_support.utils import is_enterprise_learner
from common.djangoapps.util.db import outer_atomic
from common.djangoapps.util.json_request import JsonResponse
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -222,7 +222,6 @@ def compose_activation_email(
message_context = generate_activation_email_context(user, user_registration)
message_context.update({
'confirm_activation_link': _get_activation_confirmation_link(message_context['key'], redirect_url),
'is_enterprise_learner': is_enterprise_learner(user),
'is_first_purchase_discount_overridden': FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG.is_enabled(),
'route_enabled': route_enabled,
'routed_user': user.username,
Expand All @@ -231,6 +230,11 @@ def compose_activation_email(
'registration_flow': registration_flow,
'show_auto_generated_username': show_auto_generated_username(user.username),
})
# .. filter_implemented_name: AccountActivationEmailComposed
# .. filter_type: org.openedx.learning.account.activation.email.compose.v1
__, message_context = AccountActivationEmailComposed.run_filter(
user=user, message_context=message_context,
)

if route_enabled:
dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL']
Expand Down Expand Up @@ -695,7 +699,12 @@ def activate_account(request, key):
url_path = '/login?{}'.format(urllib.parse.urlencode(params))
return redirect(settings.AUTHN_MICROFRONTEND_URL + url_path)

response = redirect(redirect_url) if redirect_url and is_enterprise_learner(request.user) else redirect('dashboard')
# .. filter_implemented_name: AccountActivationCompleted
# .. filter_type: org.openedx.learning.account.activation.completed.v1
__, redirect_url = AccountActivationCompleted.run_filter(
user=request.user, redirect_url=redirect_url or "",
)
response = redirect(redirect_url) if redirect_url else redirect('dashboard')
if show_account_activation_popup:
response.delete_cookie(
settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME,
Expand Down
Loading