diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 228376d30cd..2372e99f4a6 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -57,6 +57,17 @@ from slumber.exceptions import HttpClientError, HttpServerError from user_util import user_util +from openedx_events.learning.data import ( + CourseData, + CourseEnrollmentData, + UserData, + UserPersonalData, +) +from openedx_events.learning.signals import ( + COURSE_ENROLLMENT_CHANGED, + COURSE_ENROLLMENT_CREATED, + COURSE_UNENROLLMENT_COMPLETED, +) import openedx.core.djangoapps.django_comment_common.comment_client as cc from common.djangoapps.course_modes.models import CourseMode, get_cosmetic_verified_display_price from common.djangoapps.student.emails import send_proctoring_requirements_email @@ -1411,6 +1422,16 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False): self.mode = mode mode_changed = True + try: + course_data = CourseData( + course_key=self.course_id, + display_name=self.course.display_name, + ) + except CourseOverview.DoesNotExist: + course_data = CourseData( + course_key=self.course_id, + ) + if activation_changed or mode_changed: self.save() self._update_enrollment_in_request_cache( @@ -1419,6 +1440,24 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False): CourseEnrollmentState(self.mode, self.is_active), ) + COURSE_ENROLLMENT_CHANGED.send_event( + enrollment=CourseEnrollmentData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=course_data, + mode=self.mode, + is_active=self.is_active, + creation_date=self.created, + ) + ) + if activation_changed: if self.is_active: self.emit_event(EVENT_NAME_ENROLLMENT_ACTIVATED) @@ -1427,6 +1466,24 @@ def update_enrollment(self, mode=None, is_active=None, skip_refund=False): self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED) self.send_signal(EnrollStatusChange.unenroll) + COURSE_UNENROLLMENT_COMPLETED.send_event( + enrollment=CourseEnrollmentData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=course_data, + mode=self.mode, + is_active=self.is_active, + creation_date=self.created, + ) + ) + if mode_changed: if COURSEWARE_PROCTORING_IMPROVEMENTS.is_enabled(self.course_id): # If mode changed to one that requires proctoring, send proctoring requirements email @@ -1562,9 +1619,16 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal # All the server-side checks for whether a user is allowed to enroll. try: course = CourseOverview.get_from_id(course_key) + course_data = CourseData( + course_key=course.id, + display_name=course.display_name, + ) except CourseOverview.DoesNotExist: # This is here to preserve legacy behavior which allowed enrollment in courses # announced before the start of content creation. + course_data = CourseData( + course_key=course_key, + ) if check_access: log.warning("User %s failed to enroll in non-existent course %s", user.username, str(course_key)) raise NonExistentCourseError # lint-amnesty, pylint: disable=raise-missing-from @@ -1600,6 +1664,25 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal enrollment.update_enrollment(is_active=True, mode=mode) enrollment.send_signal(EnrollStatusChange.enroll) + # Announce user's enrollment + COURSE_ENROLLMENT_CREATED.send_event( + enrollment=CourseEnrollmentData( + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.profile.name, + ), + id=user.id, + is_active=user.is_active, + ), + course=course_data, + mode=enrollment.mode, + is_active=enrollment.is_active, + creation_date=enrollment.created, + ) + ) + return enrollment @classmethod diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index 0b34fd107de..f151f914ade 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -11,6 +11,7 @@ from django.conf import settings from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag +from openedx_events.tests.utils import OpenEdxEventsTestMixin from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory @@ -33,11 +34,13 @@ @override_waffle_flag(COURSEWARE_PROCTORING_IMPROVEMENTS, active=True) @patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True}) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): +class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase, OpenEdxEventsTestMixin): """ Test student enrollment, especially with different course modes. """ + ENABLED_OPENEDX_EVENTS = [] + USERNAME = "Bob" EMAIL = "bob@example.com" PASSWORD = "edx" @@ -45,7 +48,14 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): @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() cls.course = CourseFactory.create() cls.course_limited = CourseFactory.create() cls.proctored_course = CourseFactory( diff --git a/common/djangoapps/student/tests/test_events.py b/common/djangoapps/student/tests/test_events.py index 769aa352a20..4e83838342d 100644 --- a/common/djangoapps/student/tests/test_events.py +++ b/common/djangoapps/student/tests/test_events.py @@ -10,10 +10,27 @@ from django.test import TestCase from django_countries.fields import Country -from common.djangoapps.student.models import CourseEnrollmentAllowed -from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory +from common.djangoapps.student.models import CourseEnrollmentAllowed, CourseEnrollment +from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory, UserProfileFactory from common.djangoapps.student.tests.tests import UserSettingsEventTestMixin +from openedx_events.learning.data import ( + CourseData, + CourseEnrollmentData, + UserData, + UserPersonalData, +) +from openedx_events.learning.signals import ( + COURSE_ENROLLMENT_CHANGED, + COURSE_ENROLLMENT_CREATED, + COURSE_UNENROLLMENT_COMPLETED, +) +from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx.core.djangolib.testing.utils import skip_unless_lms + +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + class TestUserProfileEvents(UserSettingsEventTestMixin, TestCase): """ @@ -180,3 +197,179 @@ def test_enrolled_after_email_change(self): # CEAs shouldn't have been affected assert CourseEnrollmentAllowed.objects.count() == 1 assert CourseEnrollmentAllowed.objects.filter(email='test@edx.org').count() == 1 + + +@skip_unless_lms +class EnrollmentEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): + """ + Tests for the Open edX Events associated with the enrollment process through the enroll method. + + This class guarantees that the following events are sent during the user's enrollment, with + the exact Data Attributes as the event definition stated: + + - COURSE_ENROLLMENT_CREATED: sent after the user's enrollment. + - COURSE_ENROLLMENT_CHANGED: sent after the enrollment update. + - COURSE_UNENROLLMENT_COMPLETED: sent after the user's unenrollment. + """ + + ENABLED_OPENEDX_EVENTS = [ + "org.openedx.learning.course.enrollment.created.v1", + "org.openedx.learning.course.enrollment.changed.v1", + "org.openedx.learning.course.unenrollment.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.course = CourseFactory.create() + 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 = 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_enrollment_created_event_emitted(self): + """ + Test whether the student enrollment event is sent after the user's + enrollment process. + + Expected result: + - COURSE_ENROLLMENT_CREATED 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.Mock(side_effect=self._event_receiver_side_effect) + COURSE_ENROLLMENT_CREATED.connect(event_receiver) + + enrollment = CourseEnrollment.enroll(self.user, self.course.id) + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": COURSE_ENROLLMENT_CREATED, + "sender": None, + "enrollment": CourseEnrollmentData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course.id, + display_name=self.course.display_name, + ), + mode=enrollment.mode, + is_active=enrollment.is_active, + creation_date=enrollment.created, + ), + }, + event_receiver.call_args.kwargs + ) + + def test_enrollment_changed_event_emitted(self): + """ + Test whether the student enrollment changed event is sent after the enrollment + update process ends. + + Expected result: + - COURSE_ENROLLMENT_CHANGED 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. + """ + enrollment = CourseEnrollment.enroll(self.user, self.course.id) + event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect) + COURSE_ENROLLMENT_CHANGED.connect(event_receiver) + + enrollment.update_enrollment(mode="verified") + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": COURSE_ENROLLMENT_CHANGED, + "sender": None, + "enrollment": CourseEnrollmentData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course.id, + display_name=self.course.display_name, + ), + mode=enrollment.mode, + is_active=enrollment.is_active, + creation_date=enrollment.created, + ), + }, + event_receiver.call_args.kwargs + ) + + def test_unenrollment_completed_event_emitted(self): + """ + Test whether the student un-enrollment completed event is sent after the + user's unenrollment process. + + Expected result: + - COURSE_UNENROLLMENT_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. + """ + enrollment = CourseEnrollment.enroll(self.user, self.course.id) + event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect) + COURSE_UNENROLLMENT_COMPLETED.connect(event_receiver) + + CourseEnrollment.unenroll(self.user, self.course.id) + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": COURSE_UNENROLLMENT_COMPLETED, + "sender": None, + "enrollment": CourseEnrollmentData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course.id, + display_name=self.course.display_name, + ), + mode=enrollment.mode, + is_active=False, + creation_date=enrollment.created, + ), + }, + event_receiver.call_args.kwargs + ) diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 9fbe4ffa56e..36620ea4af7 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -33,6 +33,9 @@ from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED, COURSE_CERT_CHANGED, COURSE_CERT_REVOKED from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager +from openedx_events.learning.data import CourseData, UserData, UserPersonalData, CertificateData +from openedx_events.learning.signals import CERTIFICATE_CHANGED, CERTIFICATE_CREATED, CERTIFICATE_REVOKED + log = logging.getLogger(__name__) User = get_user_model() @@ -361,6 +364,28 @@ def invalidate(self): status=self.status, ) + CERTIFICATE_REVOKED.send_event( + certificate=CertificateData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course_id, + ), + mode=self.mode, + grade=self.grade, + current_status=self.status, + download_url=self.download_url, + name=self.name, + ) + ) + def mark_notpassing(self, grade): """ Invalidates a Generated Certificate by marking it as notpassing @@ -383,6 +408,28 @@ def mark_notpassing(self, grade): status=self.status, ) + CERTIFICATE_REVOKED.send_event( + certificate=CertificateData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course_id, + ), + mode=self.mode, + grade=self.grade, + current_status=self.status, + download_url=self.download_url, + name=self.name, + ) + ) + def is_valid(self): """ Return True if certificate is valid else return False. @@ -403,6 +450,29 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs mode=self.mode, status=self.status, ) + + CERTIFICATE_CHANGED.send_event( + certificate=CertificateData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course_id, + ), + mode=self.mode, + grade=self.grade, + current_status=self.status, + download_url=self.download_url, + name=self.name, + ) + ) + if CertificateStatuses.is_passing_status(self.status): COURSE_CERT_AWARDED.send_robust( sender=self.__class__, @@ -412,6 +482,28 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs status=self.status, ) + CERTIFICATE_CREATED.send_event( + certificate=CertificateData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course_id, + ), + mode=self.mode, + grade=self.grade, + current_status=self.status, + download_url=self.download_url, + name=self.name, + ) + ) + @python_2_unicode_compatible class CertificateGenerationHistory(TimeStampedModel): diff --git a/lms/djangoapps/certificates/tests/test_events.py b/lms/djangoapps/certificates/tests/test_events.py new file mode 100644 index 00000000000..0d2a4c22ebd --- /dev/null +++ b/lms/djangoapps/certificates/tests/test_events.py @@ -0,0 +1,227 @@ +""" +Test classes for the events sent in the certification process. + +Classes: + CertificateEventTest: Test event sent after creating, changing or deleting + certificates. +""" +from unittest.mock import Mock + +from openedx_events.learning.data import CertificateData, CourseData, UserData, UserPersonalData +from openedx_events.learning.signals import CERTIFICATE_CHANGED, CERTIFICATE_CREATED, CERTIFICATE_REVOKED +from openedx_events.tests.utils import OpenEdxEventsTestMixin + +from common.djangoapps.student.tests.factories import UserFactory +from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory +from lms.djangoapps.certificates.models import GeneratedCertificate, CertificateStatuses +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from openedx.core.djangolib.testing.utils import skip_unless_lms + +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase + + +@skip_unless_lms +class CertificateEventTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): + """ + Tests for the Open edX Events associated with the student's certification + process. + + This class guarantees that the following events are sent during the user's + certification process, with the exact Data Attributes as the event definition stated: + + - CERTIFICATE_CREATED: after the user's certificate generation has been + completed. + - CERTIFICATE_CHANGED: after the certificate update has been completed. + - CERTIFICATE_REVOKED: after the certificate revocation has been completed. + """ + + ENABLED_OPENEDX_EVENTS = [ + "org.openedx.learning.certificate.created.v1", + "org.openedx.learning.certificate.changed.v1", + "org.openedx.learning.certificate.revoked.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.course = CourseOverviewFactory() + self.user = UserFactory.create( + username="somestudent", + first_name="Student", + last_name="Person", + email="robot@robot.org", + is_active=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_certificate_created_event(self): + """ + Test whether the certificate created event is sent at the end of the + certificate creation process. + + Expected result: + - CERTIFICATE_CREATED 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) + CERTIFICATE_CREATED.connect(event_receiver) + + certificate = GeneratedCertificateFactory.create( + status=CertificateStatuses.downloadable, + user=self.user, + course_id=self.course.id, + mode=GeneratedCertificate.MODES.honor, + name="Certificate", + grade="100", + download_url="https://certificate.pdf" + ) + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": CERTIFICATE_CREATED, + "sender": None, + "certificate": CertificateData( + user=UserData( + pii=UserPersonalData( + username=certificate.user.username, + email=certificate.user.email, + name=certificate.user.profile.name, + ), + id=certificate.user.id, + is_active=certificate.user.is_active, + ), + course=CourseData( + course_key=certificate.course_id, + ), + mode=certificate.mode, + grade=certificate.grade, + current_status=certificate.status, + download_url=certificate.download_url, + name=certificate.name, + ), + }, + event_receiver.call_args.kwargs + ) + + def test_send_certificate_changed_event(self): + """ + Test whether the certificate changed event is sent at the end of the + certificate update process. + + Expected result: + - CERTIFICATE_CHANGED 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) + CERTIFICATE_CHANGED.connect(event_receiver) + certificate = GeneratedCertificateFactory.create( + status=CertificateStatuses.downloadable, + user=self.user, + course_id=self.course.id, + mode=GeneratedCertificate.MODES.honor, + name="Certificate", + grade="100", + download_url="https://certificate.pdf" + ) + + certificate.grade = "50" + certificate.save() + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": CERTIFICATE_CHANGED, + "sender": None, + "certificate": CertificateData( + user=UserData( + pii=UserPersonalData( + username=certificate.user.username, + email=certificate.user.email, + name=certificate.user.profile.name, + ), + id=certificate.user.id, + is_active=certificate.user.is_active, + ), + course=CourseData( + course_key=certificate.course_id, + ), + mode=certificate.mode, + grade=certificate.grade, + current_status=certificate.status, + download_url=certificate.download_url, + name=certificate.name, + ), + }, + event_receiver.call_args.kwargs + ) + + def test_send_certificate_revoked_event(self): + """ + Test whether the certificate revoked event is sent at the end of the + user certificate's revoking process. + + Expected result: + - CERTIFICATE_REVOKED 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) + CERTIFICATE_REVOKED.connect(event_receiver) + certificate = GeneratedCertificateFactory.create( + status=CertificateStatuses.downloadable, + user=self.user, + course_id=self.course.id, + mode=GeneratedCertificate.MODES.honor, + name="Certificate", + grade="100", + download_url="https://certificate.pdf" + ) + + certificate.invalidate() + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": CERTIFICATE_REVOKED, + "sender": None, + "certificate": CertificateData( + user=UserData( + pii=UserPersonalData( + username=certificate.user.username, + email=certificate.user.email, + name=certificate.user.profile.name, + ), + id=certificate.user.id, + is_active=certificate.user.is_active, + ), + course=CourseData( + course_key=certificate.course_id, + ), + mode=certificate.mode, + grade=certificate.grade, + current_status=certificate.status, + download_url=certificate.download_url, + name=certificate.name, + ), + }, + event_receiver.call_args.kwargs + ) diff --git a/lms/djangoapps/certificates/tests/test_models.py b/lms/djangoapps/certificates/tests/test_models.py index 3618fb027ab..841580ac124 100644 --- a/lms/djangoapps/certificates/tests/test_models.py +++ b/lms/djangoapps/certificates/tests/test_models.py @@ -12,6 +12,7 @@ from django.test import TestCase from django.test.utils import override_settings from opaque_keys.edx.locator import CourseKey, CourseLocator +from openedx_events.tests.utils import OpenEdxEventsTestMixin from path import Path as path from common.djangoapps.student.tests.factories import AdminFactory, UserFactory @@ -40,7 +41,7 @@ TEST_DATA_ROOT = PLATFORM_ROOT / TEST_DATA_DIR -class ExampleCertificateTest(TestCase): +class ExampleCertificateTest(TestCase, OpenEdxEventsTestMixin): """Tests for the ExampleCertificate model. """ COURSE_KEY = CourseLocator(org='test', course='test', run='test') @@ -50,6 +51,19 @@ class ExampleCertificateTest(TestCase): DOWNLOAD_URL = 'http://www.example.com' ERROR_REASON = 'Kaboom!' + ENABLED_OPENEDX_EVENTS = [] + + @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.cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY) @@ -97,10 +111,24 @@ def test_latest_status_is_course_specific(self): assert result is None -class CertificateHtmlViewConfigurationTest(TestCase): +class CertificateHtmlViewConfigurationTest(TestCase, OpenEdxEventsTestMixin): """ Test the CertificateHtmlViewConfiguration model. """ + + ENABLED_OPENEDX_EVENTS = [] + + @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.configuration_string = """{ @@ -190,12 +218,25 @@ def test_asset_file_saving_with_actual_name(self): assert certificate_template_asset.asset == 'certificate_template_assets/1/picture2.jpg' -class EligibleCertificateManagerTest(SharedModuleStoreTestCase): +class EligibleCertificateManagerTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): """ Test the GeneratedCertificate model's object manager for filtering out ineligible certs. """ + ENABLED_OPENEDX_EVENTS = [] + + @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.user = UserFactory() @@ -235,10 +276,24 @@ def test_filter_certificates_for_nonexistent_courses(self): @ddt.ddt -class TestCertificateGenerationHistory(TestCase): +class TestCertificateGenerationHistory(TestCase, OpenEdxEventsTestMixin): """ Test the CertificateGenerationHistory model's methods """ + + ENABLED_OPENEDX_EVENTS = [] + + @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.data( ({"student_set": "whitelisted_not_generated"}, "For exceptions", True), ({"student_set": "whitelisted_not_generated"}, "For exceptions", False), @@ -293,11 +348,24 @@ def test_get_task_name(self, is_regeneration, expected): assert certificate_generation_history.get_task_name() == expected -class CertificateInvalidationTest(SharedModuleStoreTestCase): +class CertificateInvalidationTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): """ Test for the Certificate Invalidation model. """ + ENABLED_OPENEDX_EVENTS = [] + + @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.course = CourseFactory() diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 4d5b37c91e3..34f32f64593 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -2021,7 +2021,7 @@ def test_certificate_generation_for_students(self): 'failed': 0, 'skipped': 2 } - with self.assertNumQueries(170): + with self.assertNumQueries(178): self.assertCertificatesGenerated(task_input, expected_results) expected_results = { diff --git a/openedx/core/djangoapps/course_groups/models.py b/openedx/core/djangoapps/course_groups/models.py index 58d394586a5..d3ef13c776d 100644 --- a/openedx/core/djangoapps/course_groups/models.py +++ b/openedx/core/djangoapps/course_groups/models.py @@ -16,6 +16,9 @@ from openedx.core.djangolib.model_mixins import DeletableByUserValue +from openedx_events.learning.data import CohortData, CourseData, UserData, UserPersonalData +from openedx_events.learning.signals import COHORT_MEMBERSHIP_CHANGED + log = logging.getLogger(__name__) @@ -130,6 +133,24 @@ def assign(cls, cohort, user): def save(self, force_insert=False, force_update=False, using=None, update_fields=None): self.full_clean(validate_unique=False) + COHORT_MEMBERSHIP_CHANGED.send_event( + cohort=CohortData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course_id, + ), + name=self.course_user_group.name, + ) + ) + log.info("Saving CohortMembership for user '%s' in '%s'", self.user.id, self.course_id) return super().save( force_insert=force_insert, diff --git a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py index 1b10177fe12..f3760cb9879 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py +++ b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py @@ -12,6 +12,7 @@ from django.test import TestCase from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import CourseLocator +from openedx_events.tests.utils import OpenEdxEventsTestMixin from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import UserFactory @@ -25,11 +26,24 @@ @patch("openedx.core.djangoapps.course_groups.cohorts.tracker", autospec=True) -class TestCohortSignals(TestCase): +class TestCohortSignals(TestCase, OpenEdxEventsTestMixin): """ Test cases to validate event emissions for various cohort-related workflows """ + ENABLED_OPENEDX_EVENTS = [] + + @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.course_key = CourseLocator("dummy", "dummy", "dummy") diff --git a/openedx/core/djangoapps/course_groups/tests/test_events.py b/openedx/core/djangoapps/course_groups/tests/test_events.py new file mode 100644 index 00000000000..2a1cc040470 --- /dev/null +++ b/openedx/core/djangoapps/course_groups/tests/test_events.py @@ -0,0 +1,108 @@ +""" +Test classes for the events sent in the cohort assignment process. + +Classes: + CohortEventTest: Test event sent after cohort membership changes. +""" +from openedx.core.djangoapps.course_groups.models import CohortMembership +from unittest.mock import Mock + +from openedx_events.learning.data import CohortData, CourseData, UserData, UserPersonalData +from openedx_events.learning.signals import COHORT_MEMBERSHIP_CHANGED +from openedx_events.tests.utils import OpenEdxEventsTestMixin + +from common.djangoapps.student.tests.factories import UserFactory +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from openedx.core.djangolib.testing.utils import skip_unless_lms + +from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory + +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase + + +@skip_unless_lms +class CohortEventTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): + """ + Tests for the Open edX Events associated with the cohort update process. + + This class guarantees that the following events are sent during the user's + certification process, with the exact Data Attributes as the event definition stated: + + - COHORT_MEMBERSHIP_CHANGED: when a cohort membership update ends. + """ + + ENABLED_OPENEDX_EVENTS = [ + "org.openedx.learning.cohort_membership.changed.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.course = CourseOverviewFactory() + self.user = UserFactory.create( + username="somestudent", + first_name="Student", + last_name="Person", + email="robot@robot.org", + is_active=True + ) + self.cohort = CohortFactory(course_id=self.course.id, name="FirstCohort") + 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_cohort_membership_changed_event(self): + """ + Test whether the COHORT_MEMBERSHIP_CHANGED event is sent when a cohort + membership update ends. + + Expected result: + - COHORT_MEMBERSHIP_CHANGED 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) + COHORT_MEMBERSHIP_CHANGED.connect(event_receiver) + + cohort_membership, _ = CohortMembership.assign( + cohort=self.cohort, + user=self.user, + ) + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": COHORT_MEMBERSHIP_CHANGED, + "sender": None, + "cohort": CohortData( + user=UserData( + pii=UserPersonalData( + username=cohort_membership.user.username, + email=cohort_membership.user.email, + name=cohort_membership.user.profile.name, + ), + id=cohort_membership.user.id, + is_active=cohort_membership.user.is_active, + ), + course=CourseData( + course_key=cohort_membership.course_id, + ), + name=cohort_membership.course_user_group.name, + ), + }, + event_receiver.call_args.kwargs + ) diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py index 9d47e2bc7ee..1e6af668ff1 100644 --- a/openedx/core/djangoapps/user_authn/views/login.py +++ b/openedx/core/djangoapps/user_authn/views/login.py @@ -28,6 +28,8 @@ 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 @@ -264,6 +266,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?") diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py index c97f973af8d..2670d512f10 100644 --- a/openedx/core/djangoapps/user_authn/views/register.py +++ b/openedx/core/djangoapps/user_authn/views/register.py @@ -23,6 +23,8 @@ 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 @@ -246,6 +248,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: diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_events.py b/openedx/core/djangoapps/user_authn/views/tests/test_events.py new file mode 100644 index 00000000000..2662462e59d --- /dev/null +++ b/openedx/core/djangoapps/user_authn/views/tests/test_events.py @@ -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('login_api') + 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 + ) diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_login.py b/openedx/core/djangoapps/user_authn/views/tests/test_login.py index ce376a7cd0f..b0ec6e732a3 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_login.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_login.py @@ -21,6 +21,7 @@ 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, @@ -43,11 +44,13 @@ @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' @@ -55,6 +58,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() @@ -948,13 +962,26 @@ def test_check_user_auth_flow_bad_email(self): @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") diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index ccc3b2dcc0b..a0d1f52cfee 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -18,6 +18,7 @@ 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 @@ -68,12 +69,16 @@ @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" @@ -87,6 +92,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") @@ -357,9 +373,13 @@ def test_register_duplicate_username_and_email_validation_errors(self): @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" @@ -420,6 +440,17 @@ class RegistrationViewTestV1(ThirdPartyAuthTestMixin, UserAPITestCase): ] link_template = "{link_label}" + @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") @@ -1746,6 +1777,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") @@ -1974,16 +2016,31 @@ def test_register_success_with_redirect(self, next_url, course_id, expected_redi @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') @@ -2140,11 +2197,25 @@ def test_expired_pipeline(self): @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, @@ -2158,21 +2229,48 @@ def test_social_auth_exception(self): @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() diff --git a/requirements/edunext/base.in b/requirements/edunext/base.in index 1f6bf4e9a76..992f71da588 100644 --- a/requirements/edunext/base.in +++ b/requirements/edunext/base.in @@ -29,6 +29,11 @@ ################### eox-tenant # Edunext multi-tenant plugin, allows a multi-tenant instance. +################### +# Libraries # +################### +openedx-events # Open edX Events from Hooks Extension Framework (OEP-50) + ##################### # eduNEXT Xblocks # ##################### diff --git a/requirements/edunext/base.txt b/requirements/edunext/base.txt index b4a8c43b00d..d3de34d8922 100644 --- a/requirements/edunext/base.txt +++ b/requirements/edunext/base.txt @@ -4,4 +4,14 @@ # # make edunext-upgrade # +attrs==20.3.0 # via -c requirements/edunext/../edx/base.txt, openedx-events +django==2.2.20 # via -c requirements/edunext/../edx/base.txt, edx-opaque-keys, openedx-events +edx-opaque-keys[django]==2.2.0 # via -c requirements/edunext/../edx/base.txt, openedx-events eox-tenant==5.0.1 # via -r requirements/edunext/base.in +openedx-events==0.6.0 # via -r requirements/edunext/base.in +pbr==5.5.1 # via -c requirements/edunext/../edx/base.txt, stevedore +pymongo==3.10.1 # via -c requirements/edunext/../edx/base.txt, edx-opaque-keys +pytz==2021.1 # via -c requirements/edunext/../edx/base.txt, django +six==1.15.0 # via -c requirements/edunext/../edx/base.txt, stevedore +sqlparse==0.4.1 # via -c requirements/edunext/../edx/base.txt, django +stevedore==1.32.0 # via -c requirements/edunext/../edx/base.txt, edx-opaque-keys