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:
Felipe Montoya
2021-09-02 10:08:44 -05:00
committed by GitHub
12 changed files with 504 additions and 11 deletions

View File

@@ -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?")

View File

@@ -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:

View 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
)

View File

@@ -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'})

View File

@@ -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()