Merge pull request #28266 from eduNEXT/MJG/1st_batch_openedx_events
[BD-32] feat: 1st batch of Open edX Events
This commit is contained in:
@@ -29,6 +29,8 @@ from edx_django_utils.monitoring import set_custom_attribute
|
||||
from ratelimit.decorators import ratelimit
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from openedx_events.learning.data import UserData, UserPersonalData
|
||||
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
|
||||
from common.djangoapps.edxmako.shortcuts import render_to_response
|
||||
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
@@ -298,6 +300,19 @@ def _handle_successful_authentication_and_login(user, request):
|
||||
django_login(request, user)
|
||||
request.session.set_expiry(604800 * 4)
|
||||
log.debug("Setting user session expiry to 4 weeks")
|
||||
|
||||
# Announce user's login
|
||||
SESSION_LOGIN_COMPLETED.send_event(
|
||||
user=UserData(
|
||||
pii=UserPersonalData(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
name=user.profile.name,
|
||||
),
|
||||
id=user.id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
AUDIT_LOG.critical("Login failed - Could not create session. Is memcached running?")
|
||||
log.critical("Login failed - Could not create session. Is memcached running?")
|
||||
|
||||
@@ -23,6 +23,8 @@ from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from edx_django_utils.monitoring import set_custom_attribute
|
||||
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
|
||||
from openedx_events.learning.data import UserData, UserPersonalData
|
||||
from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED
|
||||
from pytz import UTC
|
||||
from ratelimit.decorators import ratelimit
|
||||
from requests import HTTPError
|
||||
@@ -252,6 +254,18 @@ def create_account_with_params(request, params):
|
||||
# Announce registration
|
||||
REGISTER_USER.send(sender=None, user=user, registration=registration)
|
||||
|
||||
STUDENT_REGISTRATION_COMPLETED.send_event(
|
||||
user=UserData(
|
||||
pii=UserPersonalData(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
name=user.profile.name,
|
||||
),
|
||||
id=user.id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
)
|
||||
|
||||
create_comments_service_user(user)
|
||||
|
||||
try:
|
||||
|
||||
183
openedx/core/djangoapps/user_authn/views/tests/test_events.py
Normal file
183
openedx/core/djangoapps/user_authn/views/tests/test_events.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Test classes for the events sent in the registration process.
|
||||
|
||||
Classes:
|
||||
RegistrationEventTest: Test event sent after registering a user through the
|
||||
user API.
|
||||
LoginSessionEventTest: Test event sent after creating the user's login session
|
||||
user through the user API.
|
||||
"""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.urls import reverse
|
||||
from openedx_events.learning.data import UserData, UserPersonalData
|
||||
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED, STUDENT_REGISTRATION_COMPLETED
|
||||
from openedx_events.tests.utils import OpenEdxEventsTestMixin
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory
|
||||
from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
class RegistrationEventTest(UserAPITestCase, OpenEdxEventsTestMixin):
|
||||
"""
|
||||
Tests for the Open edX Events associated with the registration process through
|
||||
the registration view.
|
||||
|
||||
This class guarantees that the following events are sent after registering
|
||||
a user, with the exact Data Attributes as the event definition stated:
|
||||
|
||||
- STUDENT_REGISTRATION_COMPLETED: after the user's registration has been
|
||||
completed.
|
||||
"""
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = ["org.openedx.learning.student.registration.completed.v1"]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
So the Open edX Events Isolation starts, the setUpClass must be explicitly
|
||||
called with the method that executes the isolation. We do this to avoid
|
||||
MRO resolution conflicts with other sibling classes while ensuring the
|
||||
isolation process begins.
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super().setUp()
|
||||
self.url = reverse("user_api_registration")
|
||||
self.user_info = {
|
||||
"email": "user@example.com",
|
||||
"name": "Test User",
|
||||
"username": "test",
|
||||
"password": "password",
|
||||
"honor_code": "true",
|
||||
}
|
||||
self.receiver_called = False
|
||||
|
||||
def _event_receiver_side_effect(self, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Used show that the Open edX Event was called by the Django signal handler.
|
||||
"""
|
||||
self.receiver_called = True
|
||||
|
||||
def test_send_registration_event(self):
|
||||
"""
|
||||
Test whether the student registration event is sent during the user's
|
||||
registration process.
|
||||
|
||||
Expected result:
|
||||
- STUDENT_REGISTRATION_COMPLETED is sent and received by the mocked receiver.
|
||||
- The arguments that the receiver gets are the arguments sent by the event
|
||||
except the metadata generated on the fly.
|
||||
"""
|
||||
event_receiver = Mock(side_effect=self._event_receiver_side_effect)
|
||||
STUDENT_REGISTRATION_COMPLETED.connect(event_receiver)
|
||||
|
||||
self.client.post(self.url, self.user_info)
|
||||
|
||||
user = User.objects.get(username=self.user_info.get("username"))
|
||||
self.assertTrue(self.receiver_called)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": STUDENT_REGISTRATION_COMPLETED,
|
||||
"sender": None,
|
||||
"user": UserData(
|
||||
pii=UserPersonalData(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
name=user.profile.name,
|
||||
),
|
||||
id=user.id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
class LoginSessionEventTest(UserAPITestCase, OpenEdxEventsTestMixin):
|
||||
"""
|
||||
Tests for the Open edX Events associated with the login process through the
|
||||
login_user view.
|
||||
|
||||
This class guarantees that the following events are sent after the user's
|
||||
session creation, with the exact Data Attributes as the event definition
|
||||
stated:
|
||||
|
||||
- SESSION_LOGIN_COMPLETED: after login has been completed.
|
||||
"""
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = ["org.openedx.learning.auth.session.login.completed.v1"]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super().setUp()
|
||||
self.url = reverse("user_api_login_session", kwargs={"api_version": "v1"})
|
||||
self.user = UserFactory.create(
|
||||
username="test",
|
||||
email="test@example.com",
|
||||
password="password",
|
||||
)
|
||||
self.user_profile = UserProfileFactory.create(user=self.user, name="Test Example")
|
||||
self.receiver_called = True
|
||||
|
||||
def _event_receiver_side_effect(self, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Used show that the Open edX Event was called by the Django signal handler.
|
||||
"""
|
||||
self.receiver_called = True
|
||||
|
||||
def test_send_login_event(self):
|
||||
"""
|
||||
Test whether the student login event is sent after the user's
|
||||
login process.
|
||||
|
||||
Expected result:
|
||||
- SESSION_LOGIN_COMPLETED is sent and received by the mocked receiver.
|
||||
- The arguments that the receiver gets are the arguments sent by the event
|
||||
except the metadata generated on the fly.
|
||||
"""
|
||||
event_receiver = Mock(side_effect=self._event_receiver_side_effect)
|
||||
SESSION_LOGIN_COMPLETED.connect(event_receiver)
|
||||
data = {
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
}
|
||||
|
||||
self.client.post(self.url, data)
|
||||
|
||||
user = User.objects.get(username=self.user.username)
|
||||
self.assertTrue(self.receiver_called)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": SESSION_LOGIN_COMPLETED,
|
||||
"sender": None,
|
||||
"user": UserData(
|
||||
pii=UserPersonalData(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
name=user.profile.name,
|
||||
),
|
||||
id=user.id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
@@ -21,6 +21,7 @@ from django.urls import NoReverseMatch, reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
|
||||
from freezegun import freeze_time
|
||||
from common.djangoapps.student.tests.factories import RegistrationFactory, UserFactory, UserProfileFactory
|
||||
from openedx_events.tests.utils import OpenEdxEventsTestMixin
|
||||
|
||||
from openedx.core.djangoapps.password_policy.compliance import (
|
||||
NonCompliantPasswordException,
|
||||
@@ -44,11 +45,13 @@ from common.djangoapps.util.password_policy_validators import DEFAULT_MAX_PASSWO
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class LoginTest(SiteMixin, CacheIsolationTestCase):
|
||||
class LoginTest(SiteMixin, CacheIsolationTestCase, OpenEdxEventsTestMixin):
|
||||
"""
|
||||
Test login_user() view
|
||||
"""
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = []
|
||||
|
||||
ENABLED_CACHES = ['default']
|
||||
LOGIN_FAILED_WARNING = 'Email or password is incorrect'
|
||||
ACTIVATE_ACCOUNT_WARNING = 'In order to sign in, you need to activate your account'
|
||||
@@ -56,6 +59,17 @@ class LoginTest(SiteMixin, CacheIsolationTestCase):
|
||||
user_email = 'test@edx.org'
|
||||
password = 'test_password'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self):
|
||||
"""Setup a test user along with its registration and profile"""
|
||||
super().setUp()
|
||||
@@ -954,13 +968,26 @@ class LoginTest(SiteMixin, CacheIsolationTestCase):
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
class LoginSessionViewTest(ApiTestCase):
|
||||
class LoginSessionViewTest(ApiTestCase, OpenEdxEventsTestMixin):
|
||||
"""Tests for the login end-points of the user API. """
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = []
|
||||
|
||||
USERNAME = "bob"
|
||||
EMAIL = "bob@example.com"
|
||||
PASSWORD = "password"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url = reverse("user_api_login_session", kwargs={'api_version': 'v1'})
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from pytz import UTC
|
||||
from social_django.models import Partial, UserSocialAuth
|
||||
from openedx_events.tests.utils import OpenEdxEventsTestMixin
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from openedx.core.djangoapps.site_configuration.helpers import get_value
|
||||
@@ -69,12 +70,16 @@ from common.djangoapps.util.password_policy_validators import (
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
class RegistrationViewValidationErrorTest(ThirdPartyAuthTestMixin, UserAPITestCase, RetirementTestCase):
|
||||
class RegistrationViewValidationErrorTest(
|
||||
ThirdPartyAuthTestMixin, UserAPITestCase, RetirementTestCase, OpenEdxEventsTestMixin
|
||||
):
|
||||
"""
|
||||
Tests for catching duplicate email and username validation errors within
|
||||
the registration end-points of the User API.
|
||||
"""
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = []
|
||||
|
||||
maxDiff = None
|
||||
|
||||
USERNAME = "bob"
|
||||
@@ -88,6 +93,17 @@ class RegistrationViewValidationErrorTest(ThirdPartyAuthTestMixin, UserAPITestCa
|
||||
COUNTRY = "us"
|
||||
GOALS = "Learn all the things!"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super().setUp()
|
||||
self.url = reverse("user_api_registration")
|
||||
@@ -423,9 +439,13 @@ class RegistrationViewValidationErrorTest(ThirdPartyAuthTestMixin, UserAPITestCa
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
class RegistrationViewTestV1(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
class RegistrationViewTestV1(
|
||||
ThirdPartyAuthTestMixin, UserAPITestCase, OpenEdxEventsTestMixin
|
||||
):
|
||||
"""Tests for the registration end-points of the User API. """
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = []
|
||||
|
||||
maxDiff = None
|
||||
|
||||
USERNAME = "bob"
|
||||
@@ -486,6 +506,17 @@ class RegistrationViewTestV1(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
]
|
||||
link_template = "<a href='/honor' rel='noopener' target='_blank'>{link_label}</a>"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super().setUp()
|
||||
self.url = reverse("user_api_registration")
|
||||
@@ -1815,6 +1846,17 @@ class RegistrationViewTestV2(RegistrationViewTestV1):
|
||||
|
||||
# pylint: disable=test-inherits-tests
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(RegistrationViewTestV1, self).setUp() # lint-amnesty, pylint: disable=bad-super-call
|
||||
self.url = reverse("user_api_registration_v2")
|
||||
@@ -2043,16 +2085,31 @@ class RegistrationViewTestV2(RegistrationViewTestV1):
|
||||
|
||||
@httpretty.activate
|
||||
@ddt.ddt
|
||||
class ThirdPartyRegistrationTestMixin(ThirdPartyOAuthTestMixin, CacheIsolationTestCase):
|
||||
class ThirdPartyRegistrationTestMixin(
|
||||
ThirdPartyOAuthTestMixin, CacheIsolationTestCase, OpenEdxEventsTestMixin
|
||||
):
|
||||
"""
|
||||
Tests for the User API registration endpoint with 3rd party authentication.
|
||||
"""
|
||||
CREATE_USER = False
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = []
|
||||
|
||||
ENABLED_CACHES = ['default']
|
||||
|
||||
__test__ = False
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url = reverse('user_api_registration')
|
||||
@@ -2209,11 +2266,25 @@ class ThirdPartyRegistrationTestMixin(ThirdPartyOAuthTestMixin, CacheIsolationTe
|
||||
|
||||
@skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
class TestFacebookRegistrationView(
|
||||
ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase
|
||||
ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase, OpenEdxEventsTestMixin
|
||||
):
|
||||
"""Tests the User API registration endpoint with Facebook authentication."""
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = []
|
||||
|
||||
__test__ = True
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def test_social_auth_exception(self):
|
||||
"""
|
||||
According to the do_auth method in social_core.backends.facebook.py,
|
||||
@@ -2227,21 +2298,48 @@ class TestFacebookRegistrationView(
|
||||
|
||||
@skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
|
||||
class TestGoogleRegistrationView(
|
||||
ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase
|
||||
ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase, OpenEdxEventsTestMixin
|
||||
):
|
||||
"""Tests the User API registration endpoint with Google authentication."""
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = []
|
||||
|
||||
__test__ = True
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
class RegistrationValidationViewTests(test_utils.ApiTestCase, OpenEdxEventsTestMixin):
|
||||
"""
|
||||
Tests for validity of user data in registration forms.
|
||||
"""
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = []
|
||||
|
||||
endpoint_name = 'registration_validation'
|
||||
path = reverse(endpoint_name)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
cache.clear()
|
||||
|
||||
Reference in New Issue
Block a user