fix: update program enrollments to be case insensitive on external_user_key (#29646)

UTAsutin is an example partner who would use mixed casing on their external_user_key references for program enrollment upload and matriculation. Update the system to be case insensitive on external_user_key

Co-authored-by: Simon Chen <schen@edX-C02FW0GUML85.local>
This commit is contained in:
Simon Chen
2021-12-21 09:20:16 -05:00
committed by GitHub
parent a358012cc1
commit 313afd70ae
8 changed files with 220 additions and 43 deletions

View File

@@ -11,6 +11,7 @@ import logging
from django.contrib.auth import get_user_model
from django.db import IntegrityError, transaction
from requests.structures import CaseInsensitiveDict
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.api import get_access_role_by_role_name
@@ -158,10 +159,10 @@ def _get_program_enrollments_by_ext_key(program_uuid, external_user_keys):
).prefetch_related(
'program_course_enrollments'
).select_related('user')
return {
return CaseInsensitiveDict({
program_enrollment.external_user_key: program_enrollment
for program_enrollment in program_enrollments
}
})
def _get_lms_users(lms_usernames):

View File

@@ -5,6 +5,7 @@ Outside of this subpackage, import these functions
from `lms.djangoapps.program_enrollments.api`.
"""
import re
from functools import reduce
from operator import or_
@@ -62,7 +63,7 @@ def get_program_enrollment(
raise ValueError(_STUDENT_ARG_ERROR_MESSAGE)
filters = {
"user": user,
"external_user_key": external_user_key,
"external_user_key__iexact": external_user_key,
"curriculum_uuid": curriculum_uuid,
}
return ProgramEnrollment.objects.get(
@@ -100,7 +101,7 @@ def get_program_course_enrollment(
raise ValueError(_STUDENT_ARG_ERROR_MESSAGE)
filters = {
"program_enrollment__user": user,
"program_enrollment__external_user_key": external_user_key,
"program_enrollment__external_user_key__iexact": external_user_key,
"program_enrollment__curriculum_uuid": curriculum_uuid,
}
return ProgramCourseEnrollment.objects.get(
@@ -142,10 +143,13 @@ def fetch_program_enrollments(
raise ValueError(
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
)
external_user_key_regex = None
if external_user_keys:
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
filters = {
"curriculum_uuid__in": curriculum_uuids,
"user__in": users,
"external_user_key__in": external_user_keys,
"external_user_key__iregex": external_user_key_regex,
"status__in": program_enrollment_statuses,
}
if realized_only:
@@ -202,10 +206,13 @@ def fetch_program_course_enrollments(
raise ValueError(
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
)
external_user_key_regex = None
if external_user_keys:
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
filters = {
"program_enrollment__curriculum_uuid__in": curriculum_uuids,
"program_enrollment__user__in": users,
"program_enrollment__external_user_key__in": external_user_keys,
"program_enrollment__external_user_key__iregex": external_user_key_regex,
"program_enrollment__status__in": program_enrollment_statuses,
"program_enrollment__in": program_enrollments,
}
@@ -301,9 +308,12 @@ def fetch_program_enrollments_by_students(
raise ValueError(
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
)
external_user_key_regex = None
if external_user_keys:
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
filters = {
"user__in": users,
"external_user_key__in": external_user_keys,
"external_user_key__iregex": external_user_key_regex,
"status__in": program_enrollment_statuses,
}
if realized_only:
@@ -357,9 +367,12 @@ def fetch_program_course_enrollments_by_students(
raise ValueError(
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
)
external_user_key_regex = None
if external_user_keys:
external_user_key_regex = r'^(' + '|'.join([re.escape(b) for b in external_user_keys]) + ')$'
filters = {
"program_enrollment__user__in": users,
"program_enrollment__external_user_key__in": external_user_keys,
"program_enrollment__external_user_key__iregex": external_user_key_regex,
"program_enrollment__program_uuid__in": program_uuids,
"program_enrollment__curriculum_uuid__in": curriculum_uuids,
"course_key__in": course_keys,

View File

@@ -2,10 +2,10 @@
Tests for account linking Python API.
"""
from unittest.mock import patch
from uuid import uuid4
import ddt
from django.test import TestCase
from edx_django_utils.cache import RequestCache
from opaque_keys.edx.keys import CourseKey
@@ -98,7 +98,7 @@ class TestLinkProgramEnrollmentsMixin:
if refresh:
user.refresh_from_db()
enrollment = user.programenrollment_set.get(
program_uuid=program_uuid, external_user_key=external_user_key
program_uuid=program_uuid, external_user_key__iexact=external_user_key
)
assert enrollment is not None
@@ -128,6 +128,7 @@ class TestLinkProgramEnrollmentsMixin:
)
@ddt.ddt
class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
""" Tests for linking behavior """
@@ -153,6 +154,22 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
self._assert_no_user(another_program_enrollment)
def test_link_mixed_case_external_user_key(self):
"""
Test that when linking the program enrollment with same external user key,
but the casing on external_user_key is mixed, the linking is still successful
"""
program_enrollment = self._create_waiting_enrollment(self.program, 'student-43')
self._create_waiting_course_enrollment(program_enrollment, self.fruit_course)
self._create_waiting_course_enrollment(program_enrollment, self.animal_course)
link_program_enrollments(self.program, {'STUDEnt-43': self.user_1.username})
self._assert_program_enrollment(self.user_1, self.program, 'STUDEnt-43')
self._assert_user_enrolled_in_program_courses(
self.user_1, self.program, self.fruit_course, self.animal_course
)
def test_inactive_waiting_course_enrollment(self):
"""
Test that when a waiting program enrollment has waiting program course enrollments with a
@@ -272,14 +289,24 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
for course_enrollment in course_enrollments:
assert course_enrollment.mode == course_keys_to_mode[course_enrollment.course.id]
@ddt.data(
('001', '001'),
('learner-2', 'LEArneR-2'),
)
@ddt.unpack
@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):
def test_update_linking_enrollment_to_another_user(
self,
linked_external_key,
updated_external_key,
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')
program_enrollment = self._create_waiting_enrollment(self.program, linked_external_key)
self._create_waiting_course_enrollment(program_enrollment, self.fruit_course)
self._create_waiting_course_enrollment(program_enrollment, self.animal_course)
@@ -296,9 +323,9 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
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})
link_program_enrollments(self.program, {linked_external_key: self.user_1.username})
self._assert_program_enrollment(self.user_1, self.program, '0001', refresh=False)
self._assert_program_enrollment(self.user_1, self.program, linked_external_key, 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
@@ -310,12 +337,12 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
errors = link_program_enrollments(
self.program,
{
'0001': self.user_2.username,
updated_external_key: self.user_2.username,
}
)
assert not errors
self._assert_program_enrollment(self.user_2, self.program, '0001')
self._assert_program_enrollment(self.user_2, self.program, updated_external_key)
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
@@ -332,7 +359,7 @@ class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase):
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_program_enrollment(self.user_2, self.program, updated_external_key)
self._assert_user_enrolled_in_program_courses(self.user_2, self.program, self.fruit_course, self.animal_course)

View File

@@ -4,8 +4,9 @@ Tests for program enrollment reading Python API.
from uuid import UUID
import pytest
import ddt
import pytest
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.test import TestCase
@@ -157,6 +158,10 @@ class ProgramEnrollmentReadingTests(TestCase):
# enrollments in one curriculum, so it's not ambiguous).
(program_uuid_y, None, None, ext_6, 6),
(program_uuid_y, None, username_2, None, 10),
# use mixed casing for external_user_id
(program_uuid_x, curriculum_uuid_b, None, 'STUDENT-4', 4),
(program_uuid_x, curriculum_uuid_b, None, 'STUDent-5', 5),
(program_uuid_y, None, None, 'STudENT-6', 6),
)
@ddt.unpack
def test_get_program_enrollment(
@@ -193,6 +198,10 @@ class ProgramEnrollmentReadingTests(TestCase):
# because each user-course pairing can only have one
# program-course enrollment.
(program_uuid_y, curriculum_uuid_c, course_key_r, None, ext_6, 10),
# Use mixed casing for external_user_key
(program_uuid_x, None, course_key_p, username_3, 'stuDENT-3', 5),
(program_uuid_y, None, course_key_p, None, 'STudenT-4', 7),
(program_uuid_x, None, course_key_p, None, 'STUDENT-5', 6),
)
@ddt.unpack
def test_get_program_course_enrollment(
@@ -253,6 +262,15 @@ class ProgramEnrollmentReadingTests(TestCase):
{'program_uuid': program_uuid_x, 'waiting_only': True},
{5},
),
# Use mixed casing on external_user_key
(
{
'program_uuid': program_uuid_x,
'usernames': {username_1, username_2, username_3, username_4},
'external_user_keys': {'studeNT-3', 'STUdent-4', 'STudenT-5'}
},
{3, 4},
),
)
@ddt.unpack
def test_fetch_program_enrollments(self, kwargs, expected_enrollment_ids):
@@ -312,6 +330,16 @@ class ProgramEnrollmentReadingTests(TestCase):
},
{10},
),
# Use mixed casing on external_user_key
(
{
'program_uuid': program_uuid_x,
'course_key': course_key_p,
'usernames': {username_2, username_3},
'external_user_keys': {'STudENt-3', 'stuDENt-5'}
},
{5},
),
)
@ddt.unpack
def test_fetch_program_course_enrollments(self, kwargs, expected_enrollment_ids):
@@ -363,6 +391,11 @@ class ProgramEnrollmentReadingTests(TestCase):
{'external_user_key': ext_4, 'waiting_only': True},
{8},
),
# Use mixed casing on external_user_key
(
{'external_user_key': 'STudeNT-4', 'realized_only': True},
{4},
),
)
@ddt.unpack
def test_fetch_program_enrollments_by_student(self, kwargs, expected_enrollment_ids):
@@ -408,6 +441,11 @@ class ProgramEnrollmentReadingTests(TestCase):
{'external_user_keys': [ext_4], 'waiting_only': True},
{8},
),
# Use mixed casing on external_user_key
(
{'external_user_keys': ['STUdenT-4'], 'waiting_only': True},
{8},
),
)
@ddt.unpack
def test_fetch_program_enrollments_by_students(self, kwargs, expected_enrollment_ids):
@@ -461,6 +499,11 @@ class ProgramEnrollmentReadingTests(TestCase):
},
{8},
),
# Use mixed casing on external_user_key
(
{'external_user_keys': ['STUDENT-4'], 'realized_only': True},
set(),
),
)
@ddt.unpack
def test_fetch_program_course_enrollments_by_students(self, kwargs, expected_enrollment_ids):

View File

@@ -118,32 +118,44 @@ class EnrollmentTestMixin(CacheIsolationTestCase):
return self.create_program_course_enrollment(program_enrollment, course_status=course_status)
@ddt.ddt
class WritingProgramEnrollmentTest(EnrollmentTestMixin):
"""
Test cases for program enrollment writing functions.
"""
def test_write_program_enrollments_status_ended(self):
@ddt.data(
('learner-1', 'learner-1', PEStatuses.ENDED),
# Test mixing the external_user_key casing
('learner-1', 'LEARNER-1', PEStatuses.ENROLLED),
)
@ddt.unpack
def test_write_program_enrollments_status_ended(
self,
external_key_1,
external_key_2,
target_status
):
"""
Successfully updates program enrollment to status ended if requested.
Successfully updates program enrollment to status if requested.
This also validates history records are created on both create and update.
"""
assert ProgramEnrollment.objects.count() == 0
assert ProgramEnrollment.historical_records.count() == 0 # pylint: disable=no-member
write_program_enrollments(self.program_uuid, [{
'external_user_key': 'learner-1',
'external_user_key': external_key_1,
'status': PEStatuses.PENDING,
'curriculum_uuid': self.curriculum_uuid_a,
}], True, False)
assert ProgramEnrollment.objects.count() == 1
assert ProgramEnrollment.historical_records.count() == 1 # pylint: disable=no-member
write_program_enrollments(self.program_uuid, [{
'external_user_key': 'learner-1',
'status': PEStatuses.ENDED,
result = write_program_enrollments(self.program_uuid, [{
'external_user_key': external_key_2,
'status': target_status,
'curriculum_uuid': self.curriculum_uuid_a,
}], False, True)
assert ProgramEnrollment.objects.count() == 1
assert ProgramEnrollment.historical_records.count() == 2 # pylint: disable=no-member
assert ProgramEnrollment.objects.filter(status=PEStatuses.ENDED).exists()
assert ProgramEnrollment.objects.filter(status=target_status).exists()
@ddt.ddt
@@ -166,7 +178,7 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
and potentially that a CourseEnrollment also exists
"""
enrollment = ProgramCourseEnrollment.objects.get(
program_enrollment__external_user_key=external_user_key,
program_enrollment__external_user_key__iexact=external_user_key,
program_enrollment__program_uuid=self.program_uuid
)
assert expected_status == enrollment.status
@@ -306,6 +318,48 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
self.assert_program_course_enrollment('learner-5', CourseStatuses.ACTIVE, True)
self.assert_program_course_enrollment('learner-6', CourseStatuses.ACTIVE, False)
def test_create_or_update_with_mixed_cased_external_user_key(self):
"""
Test writing enrollments with both create and update flags true.
However, this time, the external_user_keys are mixed cased.
Existing enrollments should be updated. If no matching enrollment is found, create one.
"""
# learners 1-4 are already enrolled in courses, 5-6 only have a program enrollment
self.setup_change_test_data([
CourseStatuses.ACTIVE, CourseStatuses.ACTIVE,
CourseStatuses.ACTIVE, CourseStatuses.ACTIVE]
)
self.create_program_enrollment('LEArneR-5')
self.create_program_enrollment('leARnER-6', user=None)
course_enrollment_requests = [
self.course_enrollment_request('leaRNER-1', CourseStatuses.INACTIVE),
self.course_enrollment_request('LEarner-2', CourseStatuses.ACTIVE),
self.course_enrollment_request('learner-5', CourseStatuses.ACTIVE),
self.course_enrollment_request('learner-6', CourseStatuses.ACTIVE),
]
result = write_program_course_enrollments(
self.program_uuid,
self.course_id,
course_enrollment_requests,
True,
True,
)
self.assertDictEqual(
{
'leaRNER-1': CourseStatuses.INACTIVE,
'LEarner-2': CourseStatuses.ACTIVE,
'learner-5': CourseStatuses.ACTIVE,
'learner-6': CourseStatuses.ACTIVE,
},
result,
)
self.assert_program_course_enrollment('learner-1', CourseStatuses.INACTIVE, True)
self.assert_program_course_enrollment('learner-2', CourseStatuses.ACTIVE, True)
self.assert_program_course_enrollment('LEArneR-5', CourseStatuses.ACTIVE, True)
self.assert_program_course_enrollment('leARnER-6', CourseStatuses.ACTIVE, False)
def test_create_conflicting_enrollment(self):
"""
The program enrollments application already has a program_course_enrollment
@@ -322,6 +376,22 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
)
self.assertDictEqual({'learner-1': CourseStatuses.CONFLICT}, result)
def test_create_conflicting_enrollment_mixed_case_external_user_key(self):
"""
The program enrollments application already has a program_course_enrollment
record for this user with a mixed cased external_user_key and course
"""
self.create_program_and_course_enrollments('learner-1')
course_enrollment_requests = [self.course_enrollment_request('LeArnER-1')]
result = write_program_course_enrollments(
self.program_uuid,
self.course_id,
course_enrollment_requests,
True,
False,
)
self.assertDictEqual({'LeArnER-1': CourseStatuses.CONFLICT}, result)
def test_update_nonexistent_enrollment(self):
self.create_program_enrollment('learner-1')
result = write_program_course_enrollments(
@@ -374,7 +444,13 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
)
self.assertDictEqual({'learner-1': CourseStatuses.NOT_IN_PROGRAM}, result)
def test_create_enrollments_and_assign_staff(self):
@ddt.data(
'learner',
'LEARNer',
'learNER',
'LEARNER',
)
def test_create_enrollments_and_assign_staff(self, request_user_key_prefix):
"""
Successfully creates both waiting and linked program course enrollments with the course staff role.
"""
@@ -386,9 +462,15 @@ class WriteProgramCourseEnrollmentTest(EnrollmentTestMixin):
self.create_program_enrollment('learner-3', user=self.student_2)
course_enrollment_requests = [
self.course_enrollment_request('learner-1', CourseStatuses.ACTIVE, True),
self.course_enrollment_request('learner-2', CourseStatuses.ACTIVE, True),
self.course_enrollment_request('learner-3', CourseStatuses.ACTIVE, True),
self.course_enrollment_request(
'{}-1'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True
),
self.course_enrollment_request(
'{}-2'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True
),
self.course_enrollment_request(
'{}-3'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True
),
]
write_program_course_enrollments(
self.program_uuid,

View File

@@ -8,6 +8,7 @@ from `lms.djangoapps.program_enrollments.api`.
import logging
from requests.structures import CaseInsensitiveDict
from simple_history.utils import bulk_create_with_history
from common.djangoapps.course_modes.models import CourseMode
@@ -61,7 +62,7 @@ def write_program_enrollments(program_uuid, enrollment_requests, create, update)
existing_enrollments = fetch_program_enrollments(
program_uuid=program_uuid, external_user_keys=external_keys
)
existing_enrollments_by_key = {key: None for key in external_keys}
existing_enrollments_by_key = CaseInsensitiveDict({key: None for key in external_keys})
existing_enrollments_by_key.update({
enrollment.external_user_key: enrollment
for enrollment in existing_enrollments
@@ -210,9 +211,9 @@ def write_program_course_enrollments(
program_uuid=program_uuid,
external_user_keys=processable_external_keys,
).prefetch_related('program_course_enrollments')
program_enrollments_by_key = {
program_enrollments_by_key = CaseInsensitiveDict({
enrollment.external_user_key: enrollment for enrollment in program_enrollments
}
})
# Fetch enrollments regardless of anchored Program Enrollments
existing_course_enrollments = fetch_program_course_enrollments_by_students(
@@ -239,7 +240,9 @@ def write_program_course_enrollments(
program_enrollment__program_uuid=program_uuid
)
existing_course_enrollments_by_key = {key: None for key in processable_external_keys}
existing_course_enrollments_by_key = CaseInsensitiveDict({
key: None for key in processable_external_keys
})
existing_course_enrollments_by_key.update({
enrollment.program_enrollment.external_user_key: enrollment
for enrollment in existing_course_enrollments_of_program_enrollment
@@ -252,7 +255,7 @@ def write_program_course_enrollments(
enrollments_to_save = []
created_enrollments = []
updated_enrollments = []
staff_assignments_by_user_key = {}
staff_assignments_by_user_key = CaseInsensitiveDict()
for external_key, request in requests_by_key.items():
course_staff = request['course_staff']
status = request['status']
@@ -498,7 +501,7 @@ def _organize_requests_by_external_key(enrollment_requests):
where requests_by_key is dict[str: dict]
and duplicated_keys is set[str].
"""
requests_by_key = {}
requests_by_key = CaseInsensitiveDict()
duplicated_keys = set()
for request in enrollment_requests:
key = request['external_user_key']
@@ -534,11 +537,11 @@ def _get_conflicting_active_course_enrollments(
Returns:
results (dict) with detected conflict entry, or empty dict.
"""
conflicted_by_user_key = {}
conflicted_by_user_key = CaseInsensitiveDict()
requested_statuses_by_user_key = {
requested_statuses_by_user_key = CaseInsensitiveDict({
key: request.get('status') for key, request in requests_by_key.items()
}
})
for existing_enrollment in existing_course_enrollments:
external_user_key = existing_enrollment.program_enrollment.external_user_key

View File

@@ -5,6 +5,7 @@ Test signal handlers for program_enrollments
from unittest import mock
import ddt
import pytest
from django.core.cache import cache
from edx_django_utils.cache import RequestCache
@@ -12,6 +13,7 @@ from opaque_keys.edx.keys import CourseKey
from organizations.tests.factories import OrganizationFactory
from social_django.models import UserSocialAuth
from testfixtures import LogCapture
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollmentException
@@ -27,7 +29,6 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
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 # lint-amnesty, pylint: disable=wrong-import-order
class ProgramEnrollmentRetireSignalTests(ModuleStoreTestCase):
@@ -100,6 +101,7 @@ class ProgramEnrollmentRetireSignalTests(ModuleStoreTestCase):
self.assert_enrollment_and_history_retired(enrollment)
@ddt.ddt
class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase):
"""
Test post-save handler on UserSocialAuth
@@ -110,7 +112,7 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase):
def setUpClass(cls):
super().setUpClass()
cls.external_id = '0000'
cls.external_id = 'learner1a'
cls.provider_slug = 'uox'
cls.course_keys = [
CourseKey.from_string('course-v1:edX+DemoX+Test_Course'),
@@ -197,13 +199,18 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase):
for program_course_enrollment in program_course_enrollments:
self._assert_program_course_enrollment(program_course_enrollment)
def test_waiting_course_enrollments_completed(self):
@ddt.data(
'learner1a',
'LEarnER1A',
'LEARNER1A',
)
def test_waiting_course_enrollments_completed(self, auth_user_key):
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}'
uid=f'{self.provider_slug}:{auth_user_key}'
)
self._assert_program_enrollment_user(program_enrollment, self.user)

View File

@@ -4,6 +4,7 @@ Unit tests for program_course_enrollments tasks
from datetime import timedelta
import pytest
from django.db.models.base import ObjectDoesNotExist
from django.test import TestCase