Files
edx-platform/lms/djangoapps/program_enrollments/api/tests/test_linking.py
2021-05-03 14:55:37 +05:00

473 lines
20 KiB
Python

"""
Tests for account linking Python API.
"""
from unittest.mock import patch
from uuid import uuid4
from django.test import TestCase
from edx_django_utils.cache import RequestCache
from opaque_keys.edx.keys import CourseKey
from testfixtures import LogCapture
from common.djangoapps.student.api import get_course_access_role
from common.djangoapps.student.roles import CourseStaffRole
from common.djangoapps.student.tests.factories import CourseAccessRoleFactory, UserFactory
from lms.djangoapps.program_enrollments.tests.factories import (
CourseAccessRoleAssignmentFactory,
ProgramCourseEnrollmentFactory,
ProgramEnrollmentFactory
)
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from ..linking import (
NO_LMS_USER_TEMPLATE,
NO_PROGRAM_ENROLLMENT_TEMPLATE,
_user_already_linked_message,
link_program_enrollments
)
LOG_PATH = 'lms.djangoapps.program_enrollments.api.linking'
class TestLinkProgramEnrollmentsMixin:
""" Utility methods and test data for testing linking """
@classmethod
def setUpTestData(cls): # pylint: disable=missing-function-docstring
cls.program = uuid4()
cls.curriculum = uuid4()
cls.other_program = uuid4()
cls.fruit_course = CourseKey.from_string('course-v1:edX+Oranges+Apples')
cls.animal_course = CourseKey.from_string('course-v1:edX+Cats+Dogs')
CourseOverviewFactory.create(id=cls.fruit_course)
CourseOverviewFactory.create(id=cls.animal_course)
def setUp(self):
self.user_1 = UserFactory.create()
self.user_2 = UserFactory.create()
def tearDown(self):
RequestCache.clear_all_namespaces()
def _create_waiting_enrollment(self, program_uuid, external_user_key):
"""
Create a waiting program enrollment for the given program and external user key.
"""
return ProgramEnrollmentFactory.create(
user=None,
program_uuid=program_uuid,
curriculum_uuid=self.curriculum,
external_user_key=external_user_key,
)
def _create_waiting_course_enrollment(self, program_enrollment, course_key, status='active'):
"""
Create a waiting program course enrollment for the given program enrollment,
course key, and optionally status.
"""
return ProgramCourseEnrollmentFactory.create(
program_enrollment=program_enrollment,
course_key=course_key,
course_enrollment=None,
status=status,
)
def _assert_no_user(self, program_enrollment, refresh=True):
"""
Assert that the given program enrollment has no LMS user associated with it
"""
if refresh:
program_enrollment.refresh_from_db()
assert program_enrollment.user is None
def _assert_no_program_enrollment(self, user, program_uuid, refresh=True):
"""
Assert that the given user is not enrolled in the given program
"""
if refresh:
user.refresh_from_db()
assert not user.programenrollment_set.filter(program_uuid=program_uuid).exists()
def _assert_program_enrollment(self, user, program_uuid, external_user_key, refresh=True):
"""
Assert that the given user is enrolled in the given program with the
given external user key.
"""
if refresh:
user.refresh_from_db()
enrollment = user.programenrollment_set.get(
program_uuid=program_uuid, external_user_key=external_user_key
)
assert enrollment is not None
def _assert_user_enrolled_in_program_courses(self, user, program_uuid, *course_keys):
"""
Assert that the given user has active enrollments in the given courses
through the given program.
"""
user.refresh_from_db()
program_enrollment = user.programenrollment_set.get(
user=user, program_uuid=program_uuid
)
all_course_enrollments = program_enrollment.program_course_enrollments
program_course_enrollments = all_course_enrollments.select_related(
'course_enrollment__course'
).filter(
course_enrollment__isnull=False
)
course_enrollments = [
program_course_enrollment.course_enrollment
for program_course_enrollment in program_course_enrollments
]
assert all(course_enrollment.is_active for course_enrollment in course_enrollments)
self.assertCountEqual(
course_keys,
[course_enrollment.course.id for course_enrollment in course_enrollments]
)
class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
""" Tests for linking behavior """
def test_link_only_specified_program(self):
"""
Test that when there are two waiting program enrollments with the same external user key,
only the specified program's program enrollment will be linked
"""
program_enrollment = self._create_waiting_enrollment(self.program, '0001')
self._create_waiting_course_enrollment(program_enrollment, self.fruit_course)
self._create_waiting_course_enrollment(program_enrollment, self.animal_course)
another_program_enrollment = self._create_waiting_enrollment(self.other_program, '0001')
self._create_waiting_course_enrollment(another_program_enrollment, self.fruit_course)
self._create_waiting_course_enrollment(another_program_enrollment, self.animal_course)
link_program_enrollments(self.program, {'0001': self.user_1.username})
self._assert_program_enrollment(self.user_1, self.program, '0001')
self._assert_user_enrolled_in_program_courses(
self.user_1, self.program, self.fruit_course, self.animal_course
)
self._assert_no_user(another_program_enrollment)
def test_inactive_waiting_course_enrollment(self):
"""
Test that when a waiting program enrollment has waiting program course enrollments with a
status of 'inactive' the course enrollment created after calling link_program_enrollments
will be inactive.
"""
program_enrollment = self._create_waiting_enrollment(self.program, '0001')
active_enrollment = self._create_waiting_course_enrollment(
program_enrollment,
self.fruit_course
)
inactive_enrollment = self._create_waiting_course_enrollment(
program_enrollment,
self.animal_course,
status='inactive'
)
link_program_enrollments(self.program, {'0001': self.user_1.username})
self._assert_program_enrollment(self.user_1, self.program, '0001')
active_enrollment.refresh_from_db()
assert active_enrollment.course_enrollment is not None
assert active_enrollment.course_enrollment.course.id == self.fruit_course
assert active_enrollment.course_enrollment.is_active
inactive_enrollment.refresh_from_db()
assert inactive_enrollment.course_enrollment is not None
assert inactive_enrollment.course_enrollment.course.id == self.animal_course
assert not inactive_enrollment.course_enrollment.is_active
def test_realize_course_access_roles(self):
program_enrollment = self._create_waiting_enrollment(self.program, '0001')
active_enrollment_1 = self._create_waiting_course_enrollment(
program_enrollment,
self.fruit_course,
status='active'
)
active_enrollment_2 = self._create_waiting_course_enrollment(
program_enrollment,
self.animal_course,
status='active'
)
CourseAccessRoleAssignmentFactory(enrollment=active_enrollment_1)
CourseAccessRoleAssignmentFactory(enrollment=active_enrollment_2)
link_program_enrollments(self.program, {'0001': self.user_1.username})
# assert that staff CourseAccessRoles are created for the user in the courses
fruit_course_staff_role = get_course_access_role(
self.user_1,
self.fruit_course.org,
self.fruit_course,
CourseStaffRole.ROLE
)
assert fruit_course_staff_role is not None
animal_course_staff_role = get_course_access_role(
self.user_1,
self.animal_course.org,
self.animal_course,
CourseStaffRole.ROLE
)
assert animal_course_staff_role is not None
# assert that all CourseAccessRoleAssignment objects are deleted
assert not active_enrollment_1.courseaccessroleassignment_set.all().exists()
assert not active_enrollment_2.courseaccessroleassignment_set.all().exists()
def test_realize_course_access_roles_user_with_existing_course_access_role(self):
"""
This test asserts that, given a user that already has a staff CourseAccessRole in a course,
if that user has a CourseAccessRoleAssignment that describes a staff role in that same course,
that we do not mistakenly violate the unique_together constraint on the CourseAccessRole model by
creating a duplicate. As of now, this is handled by the CourseStaffRole code itself, which silently
ignores such duplicates, but this test is to ensure we do not regress.
"""
program_enrollment = self._create_waiting_enrollment(self.program, '0001')
active_enrollment_1 = self._create_waiting_course_enrollment(
program_enrollment,
self.fruit_course,
status='active'
)
# create an CourseAccessRole for the user
CourseAccessRoleFactory(user=self.user_1, course_id=self.fruit_course, role=CourseStaffRole.ROLE)
# create a corresponding CourseAccessRoleAssignmentFactory that would, theoretically, cause a
# duplicate object to be created, violating the CourseAccessRole integrity constraints
CourseAccessRoleAssignmentFactory(enrollment=active_enrollment_1)
link_program_enrollments(self.program, {'0001': self.user_1.username})
# assert that staff CourseAccessRoles remains
fruit_course_staff_role = get_course_access_role(
self.user_1,
self.fruit_course.org,
self.fruit_course,
CourseStaffRole.ROLE
)
assert fruit_course_staff_role is not None
# assert that all CourseAccessRoleAssignment objects are deleted
assert not active_enrollment_1.courseaccessroleassignment_set.all().exists()
@staticmethod
def _assert_course_enrollments_in_mode(course_enrollments, course_keys_to_mode):
"""
Assert that all program course enrollments are in the correct modes as
described by course_keys_to_mode.
Arguments:
user: the user whose course enrollments we are checking
program_uuid: the UUID of the program in which the user is enrolled
course_keys_to_mode: a mapping from course keys to the the mode
slug the user's enrollment should be in
"""
assert len(course_enrollments) == len(course_keys_to_mode)
for course_enrollment in course_enrollments:
assert course_enrollment.mode == course_keys_to_mode[course_enrollment.course.id]
@patch('lms.djangoapps.program_enrollments.api.linking.CourseMode.modes_for_course_dict')
def test_update_linking_enrollment_to_another_user(self, mock_modes_for_course_dict):
"""
Test that when link_program_enrollments is called with a program and an external_user_key,
user pair and that program is already linked to a different user with the same external_user_key
that the original user's link is removed and replaced by a link with the new user.
"""
program_enrollment = self._create_waiting_enrollment(self.program, '0001')
self._create_waiting_course_enrollment(program_enrollment, self.fruit_course)
self._create_waiting_course_enrollment(program_enrollment, self.animal_course)
# in order to test what happens to a learner's enrollment in a course without an audit mode
# (e.g. Master's only course), we need to mock out the course modes that exist for our courses;
# doing it this way helps to avoid needing to use the modulestore when using the CourseModeFactory
def mocked_modes_for_course_dict(course_key):
if course_key == self.animal_course:
return {'masters': 'masters'}
else:
return {'audit': 'audit'}
mock_modes_for_course_dict.side_effect = mocked_modes_for_course_dict
# do the initial link of user_1 to the program enrollment
link_program_enrollments(self.program, {'0001': self.user_1.username})
self._assert_program_enrollment(self.user_1, self.program, '0001', refresh=False)
self._assert_no_program_enrollment(self.user_2, self.program, refresh=False)
# grab the user's original course enrollment before the link between the program
# and the course enrollments is severed
course_enrollments_for_user_1 = [pce.course_enrollment
for pce
in program_enrollment.program_course_enrollments.all()]
errors = link_program_enrollments(
self.program,
{
'0001': self.user_2.username,
}
)
assert errors == {}
self._assert_program_enrollment(self.user_2, self.program, '0001')
self._assert_no_program_enrollment(self.user_1, self.program)
# assert that all of user_1's course enrollments as part of the program
# are inactive
for course_enrollment in course_enrollments_for_user_1:
course_enrollment.refresh_from_db()
assert not course_enrollment.is_active
# assert that user_1's course enrollments are in the expected mode
# after unlinking
course_keys_to_mode = {
self.fruit_course: 'audit',
self.animal_course: 'masters',
}
self._assert_course_enrollments_in_mode(course_enrollments_for_user_1, course_keys_to_mode)
# assert that user_2 has been successfully linked to the program
self._assert_program_enrollment(self.user_2, self.program, '0001')
self._assert_user_enrolled_in_program_courses(self.user_2, self.program, self.fruit_course, self.animal_course)
class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase):
""" Tests for linking error behavior """
def test_program_enrollment_not_found__nonexistant(self):
self._create_waiting_enrollment(self.program, '0001')
self._program_enrollment_not_found()
def test_program_enrollment_not_found__different_program(self):
self._create_waiting_enrollment(self.program, '0001')
self._create_waiting_enrollment(self.other_program, '0002')
self._program_enrollment_not_found()
def _program_enrollment_not_found(self):
"""
Helper for test_program_not_found_* tests.
tries to link user_1 to '0001' and user_2 to '0002' in program
asserts that user_2 was not linked because the enrollment was not found
"""
with LogCapture() as logger:
errors = link_program_enrollments(
self.program,
{
'0001': self.user_1.username,
'0002': self.user_2.username,
}
)
expected_error_msg = NO_PROGRAM_ENROLLMENT_TEMPLATE.format(
program_uuid=self.program,
external_user_key='0002'
)
logger.check_present((LOG_PATH, 'WARNING', expected_error_msg))
self.assertDictEqual(errors, {'0002': expected_error_msg})
self._assert_program_enrollment(self.user_1, self.program, '0001')
self._assert_no_program_enrollment(self.user_2, self.program)
def test_user_not_found(self):
self._create_waiting_enrollment(self.program, '0001')
enrollment_2 = self._create_waiting_enrollment(self.program, '0002')
with LogCapture() as logger:
errors = link_program_enrollments(
self.program,
{
'0001': self.user_1.username,
'0002': 'nonexistant-user',
}
)
expected_error_msg = NO_LMS_USER_TEMPLATE.format('nonexistant-user')
logger.check_present((LOG_PATH, 'WARNING', expected_error_msg))
self.assertDictEqual(errors, {'0002': expected_error_msg})
self._assert_program_enrollment(self.user_1, self.program, '0001')
self._assert_no_user(enrollment_2)
def test_enrollment_already_linked_to_target_user(self):
self._create_waiting_enrollment(self.program, '0001')
program_enrollment = ProgramEnrollmentFactory.create(
user=self.user_2,
program_uuid=self.program,
external_user_key='0002',
)
self._assert_no_program_enrollment(self.user_1, self.program, refresh=False)
self._assert_program_enrollment(self.user_2, self.program, '0002', refresh=False)
with LogCapture() as logger:
errors = link_program_enrollments(
self.program,
{
'0001': self.user_1.username,
'0002': self.user_2.username
}
)
expected_error_msg = _user_already_linked_message(program_enrollment, self.user_2)
logger.check_present((LOG_PATH, 'WARNING', expected_error_msg))
self.assertDictEqual(errors, {'0002': expected_error_msg})
self._assert_program_enrollment(self.user_1, self.program, '0001')
self._assert_program_enrollment(self.user_2, self.program, '0002')
def test_error_enrolling_in_course(self):
nonexistant_course = CourseKey.from_string('course-v1:edX+Zilch+Bupkis')
program_enrollment_1 = self._create_waiting_enrollment(self.program, '0001')
course_enrollment_1 = self._create_waiting_course_enrollment(
program_enrollment_1, nonexistant_course
)
course_enrollment_2 = self._create_waiting_course_enrollment(
program_enrollment_1, self.animal_course
)
program_enrollment_2 = self._create_waiting_enrollment(self.program, '0002')
self._create_waiting_course_enrollment(program_enrollment_2, self.fruit_course)
self._create_waiting_course_enrollment(program_enrollment_2, self.animal_course)
errors = link_program_enrollments(
self.program,
{
'0001': self.user_1.username,
'0002': self.user_2.username
}
)
assert errors['0001'] in 'NonExistentCourseError: '
self._assert_no_program_enrollment(self.user_1, self.program)
self._assert_no_user(program_enrollment_1)
course_enrollment_1.refresh_from_db()
assert course_enrollment_1.course_enrollment is None
course_enrollment_2.refresh_from_db()
assert course_enrollment_2.course_enrollment is None
self._assert_user_enrolled_in_program_courses(
self.user_2, self.program, self.animal_course, self.fruit_course
)
def test_integrity_error(self):
existing_program_enrollment = self._create_waiting_enrollment(self.program, 'learner-0')
existing_program_enrollment.user = self.user_1
existing_program_enrollment.save()
program_enrollment_1 = self._create_waiting_enrollment(self.program, '0001')
self._create_waiting_enrollment(self.program, '0002')
errors = link_program_enrollments(
self.program,
{
'0001': self.user_1.username,
'0002': self.user_2.username,
}
)
assert len(errors) == 1
assert 'UNIQUE constraint failed' in errors['0001']
self._assert_no_user(program_enrollment_1)
self._assert_program_enrollment(self.user_2, self.program, '0002')