Files
edx-platform/lms/djangoapps/program_enrollments/tests/test_signals.py
2021-02-22 12:58:16 +05:00

405 lines
16 KiB
Python

"""
Test signal handlers for program_enrollments
"""
from unittest import mock
import pytest
from django.core.cache import cache
from edx_django_utils.cache import RequestCache
from opaque_keys.edx.keys import CourseKey
from organizations.tests.factories import OrganizationFactory
from social_django.models import UserSocialAuth
from testfixtures import LogCapture
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollmentException
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.third_party_auth.models import SAMLProviderConfig
from common.djangoapps.third_party_auth.tests.factories import SAMLProviderConfigFactory
from lms.djangoapps.program_enrollments.signals import _listen_for_lms_retire, logger
from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL
from openedx.core.djangoapps.catalog.tests.factories import OrganizationFactory as CatalogOrganizationFactory
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import fake_completed_retirement
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class ProgramEnrollmentRetireSignalTests(ModuleStoreTestCase):
"""
Test the _listen_for_lms_retire signal
"""
def create_enrollment_and_history(self, user=None, external_user_key='defaultExternalKey'):
"""
Create ProgramEnrollment and several History entries
"""
if user:
enrollment = ProgramEnrollmentFactory(user=user, external_user_key=external_user_key)
else:
enrollment = ProgramEnrollmentFactory(external_user_key=external_user_key)
for status in ['pending', 'suspended', 'canceled', 'enrolled']:
enrollment.status = status
enrollment.save()
return enrollment
def assert_enrollment_and_history_retired(self, enrollment):
"""
Assert that for the enrollment and all histories, external key is None
"""
enrollment.refresh_from_db()
assert enrollment.external_user_key is not None
assert enrollment.external_user_key.startswith('retired_external_key')
for history_record in enrollment.historical_records.all():
assert history_record.external_user_key is not None
assert history_record.external_user_key.startswith('retired_external_key')
def test_retire_enrollment(self):
"""
Test basic retirement of program enrollment
"""
enrollment = self.create_enrollment_and_history()
_listen_for_lms_retire(sender=self.__class__, user=enrollment.user)
self.assert_enrollment_and_history_retired(enrollment)
def test_retire_enrollment_multiple(self):
"""
Test basic retirement of user with multiple program enrollments
"""
enrollment = self.create_enrollment_and_history()
enrollment2 = self.create_enrollment_and_history(user=enrollment.user)
enrollment3 = self.create_enrollment_and_history(user=enrollment.user)
_listen_for_lms_retire(sender=self.__class__, user=enrollment.user)
self.assert_enrollment_and_history_retired(enrollment)
self.assert_enrollment_and_history_retired(enrollment2)
self.assert_enrollment_and_history_retired(enrollment3)
def test_success_no_enrollment(self):
"""
Basic success path for users who have no enrollments, should simply not error
"""
user = UserFactory()
_listen_for_lms_retire(sender=self.__class__, user=user)
def test_idempotent(self):
"""
Tests that running a retirement multiple times does not throw an error
"""
enrollment = self.create_enrollment_and_history()
# Run twice to make sure no errors are raised
_listen_for_lms_retire(sender=self.__class__, user=enrollment.user)
fake_completed_retirement(enrollment.user)
_listen_for_lms_retire(sender=self.__class__, user=enrollment.user)
self.assert_enrollment_and_history_retired(enrollment)
class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase):
"""
Test post-save handler on UserSocialAuth
"""
ENABLED_CACHES = ['default']
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.external_id = '0000'
cls.provider_slug = 'uox'
cls.course_keys = [
CourseKey.from_string('course-v1:edX+DemoX+Test_Course'),
CourseKey.from_string('course-v1:edX+DemoX+Another_Test_Course'),
]
cls.organization = OrganizationFactory.create(
short_name='UoX'
)
cls.user = UserFactory.create()
for course_key in cls.course_keys:
CourseOverviewFactory(id=course_key)
cls.provider_config = SAMLProviderConfigFactory.create(
organization=cls.organization, slug=cls.provider_slug
)
def setUp(self):
super().setUp()
RequestCache.clear_all_namespaces()
catalog_org = CatalogOrganizationFactory.create(key=self.organization.short_name)
self.program_uuid = self._create_catalog_program(catalog_org)['uuid']
def _create_catalog_program(self, catalog_org):
""" helper method to create a cached catalog program """
program = ProgramFactory.create(
authoring_organizations=[catalog_org]
)
cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']), program, None)
return program
def _create_waiting_program_enrollment(self):
""" helper method to create a waiting program enrollment """
return ProgramEnrollmentFactory.create(
user=None,
external_user_key=self.external_id,
program_uuid=self.program_uuid,
)
def _create_waiting_course_enrollments(self, program_enrollment):
""" helper method to create waiting course enrollments """
return [
ProgramCourseEnrollmentFactory(
program_enrollment=program_enrollment,
course_enrollment=None,
course_key=course_key,
)
for course_key in self.course_keys
]
def _assert_program_enrollment_user(self, program_enrollment, user):
""" validate program enrollment has a user """
program_enrollment.refresh_from_db()
assert program_enrollment.user == user
def _assert_program_course_enrollment(self, program_course_enrollment, mode=CourseMode.MASTERS):
""" validate program course enrollment has a valid course enrollment """
program_course_enrollment.refresh_from_db()
student_course_enrollment = program_course_enrollment.course_enrollment
assert student_course_enrollment.user == self.user
assert student_course_enrollment.course.id == program_course_enrollment.course_key
assert student_course_enrollment.mode == mode
def test_update_social_auth(self):
"""
Makes sure we can update a social_auth row to trigger the same program enrollments
"""
program_enrollment = self._create_waiting_program_enrollment()
program_course_enrollments = self._create_waiting_course_enrollments(program_enrollment)
user_social_auth = UserSocialAuth.objects.create(
user=self.user,
uid='{}:{}'.format(self.provider_slug, 'gobbledegook')
)
# Not yet a thing, didn't match
program_enrollment.refresh_from_db()
assert program_enrollment.user is None
user_social_auth.uid = f'{self.provider_slug}:{self.external_id}'
user_social_auth.save()
# now we see the enrollments realized
self._assert_program_enrollment_user(program_enrollment, self.user)
for program_course_enrollment in program_course_enrollments:
self._assert_program_course_enrollment(program_course_enrollment)
def test_waiting_course_enrollments_completed(self):
program_enrollment = self._create_waiting_program_enrollment()
program_course_enrollments = self._create_waiting_course_enrollments(program_enrollment)
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}'
)
self._assert_program_enrollment_user(program_enrollment, self.user)
for program_course_enrollment in program_course_enrollments:
self._assert_program_course_enrollment(program_course_enrollment)
def test_same_user_key_in_multiple_organizations(self):
uox_program_enrollment = self._create_waiting_program_enrollment()
second_organization = OrganizationFactory.create()
SAMLProviderConfigFactory.create(organization=second_organization, slug='aiu')
catalog_org = CatalogOrganizationFactory.create(key=second_organization.short_name)
program_uuid = self._create_catalog_program(catalog_org)['uuid']
# aiu enrollment with the same student key as our uox user
aiu_program_enrollment = ProgramEnrollmentFactory.create(
user=None,
external_user_key=self.external_id,
program_uuid=program_uuid
)
UserSocialAuth.objects.create(
user=UserFactory.create(),
uid='{}:{}'.format('not_used', self.external_id),
)
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}',
)
self._assert_program_enrollment_user(uox_program_enrollment, self.user)
aiu_user = UserFactory.create()
UserSocialAuth.objects.create(
user=aiu_user,
uid='{}:{}'.format('aiu', self.external_id),
)
self._assert_program_enrollment_user(aiu_program_enrollment, aiu_user)
def test_only_active_saml_config_used(self):
""" makes sure only the active row in SAMLProvider config is used """
program_enrollment = self._create_waiting_program_enrollment()
# update will create a second record
self.provider_config.organization = None
self.provider_config.save()
assert len(SAMLProviderConfig.objects.all()) == 2
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}'
)
program_enrollment.refresh_from_db()
assert program_enrollment.user is None
def test_learner_already_enrolled_in_course(self):
course_key = self.course_keys[0]
course = CourseOverview.objects.get(id=course_key)
CourseEnrollmentFactory(user=self.user, course=course, mode=CourseMode.VERIFIED)
program_enrollment = self._create_waiting_program_enrollment()
program_course_enrollments = self._create_waiting_course_enrollments(program_enrollment)
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}'
)
self._assert_program_enrollment_user(program_enrollment, self.user)
duplicate_program_course_enrollment = program_course_enrollments[0]
self._assert_program_course_enrollment(
duplicate_program_course_enrollment, CourseMode.VERIFIED
)
program_course_enrollment = program_course_enrollments[1]
self._assert_program_course_enrollment(program_course_enrollment)
def test_enrolled_with_no_course_enrollments(self):
program_enrollment = self._create_waiting_program_enrollment()
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}'
)
self._assert_program_enrollment_user(program_enrollment, self.user)
def test_create_social_auth_with_no_waiting_enrollments(self):
"""
No exceptions should be raised if there are no enrollments to update
"""
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}'
)
def test_create_social_auth_provider_has_no_organization(self):
"""
No exceptions should be raised if provider is not linked to an organization
"""
provider = SAMLProviderConfigFactory.create()
UserSocialAuth.objects.create(
user=self.user,
uid=f'{provider.slug}:{self.external_id}'
)
def test_create_social_auth_non_saml_provider(self):
"""
No exceptions should be raised for a non-SAML uid
"""
UserSocialAuth.objects.create(
user=self.user,
uid='google-oauth-user@gmail.com'
)
UserSocialAuth.objects.create(
user=self.user,
uid='123:123:123'
)
def test_saml_provider_not_found(self):
"""
An error should be logged for incoming social auth entries with a saml id but
no matching saml configuration exists
"""
with LogCapture(logger.name) as log:
UserSocialAuth.objects.create(
user=self.user,
uid='abc:123456'
)
log.check_present(
(
logger.name,
'WARNING',
'Got incoming social auth for provider={} but no such provider exists'.format('abc')
)
)
def test_cannot_find_catalog_program(self):
"""
An error should be logged if a program enrollment exists but a matching catalog
program cannot be found
"""
self.program_uuid = self._create_catalog_program(None)['uuid']
self._create_waiting_program_enrollment()
with LogCapture(logger.name) as log:
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}'
)
error_template = (
'Failed to complete waiting enrollments for organization={}.'
' No catalog programs with matching authoring_organization exist.'
)
log.check_present(
(
logger.name,
'WARNING',
error_template.format('UoX')
)
)
def test_exception_on_enrollment_failure(self):
program_enrollment = self._create_waiting_program_enrollment()
self._create_waiting_course_enrollments(program_enrollment)
with mock.patch('common.djangoapps.student.models.CourseEnrollment.enroll') as enrollMock:
enrollMock.side_effect = CourseEnrollmentException('something has gone wrong')
with pytest.raises(CourseEnrollmentException):
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}'
)
def test_log_on_unexpected_exception(self):
"""
unexpected errors as part of the account linking process should be logged and re-raised
"""
program_enrollment = self._create_waiting_program_enrollment()
self._create_waiting_course_enrollments(program_enrollment)
with mock.patch('lms.djangoapps.program_enrollments.api.linking.enroll_in_masters_track') as enrollMock:
enrollMock.side_effect = Exception('unexpected error')
with LogCapture(logger.name) as log:
with self.assertRaisesRegex(Exception, 'unexpected error'):
UserSocialAuth.objects.create(
user=self.user,
uid=f'{self.provider_slug}:{self.external_id}',
)
error_template = 'Unable to link waiting enrollments for user {}, social auth creation failed: {}'
log.check_present(
(
logger.name,
'WARNING',
error_template.format(self.user.id, 'unexpected error')
)
)