Files
edx-platform/lms/djangoapps/support/tests/test_views.py
Usama Sadiq 87a5611375 chore: update edx-proctoring package version (#36256)
* fix: fix tests with new version of edx-proctoring

* feat: Upgrade Python dependency edx-proctoring (#36255)

chore: update edx-proctoring version

Commit generated by workflow `openedx/edx-platform/.github/workflows/upgrade-one-python-dependency.yml@refs/heads/master`

Co-authored-by: UsamaSadiq <41958659+UsamaSadiq@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-17 14:37:09 +05:00

2485 lines
96 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Tests for support views.
"""
import itertools
import json
import re
from datetime import datetime, timedelta
from unittest.mock import patch
from urllib.parse import quote
from uuid import UUID, uuid4
import ddt
from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.db.models import signals
from django.http import HttpResponse
from django.test.utils import override_settings
from django.urls import reverse
from django.utils import timezone
from edx_proctoring.api import create_exam_attempt, update_attempt_status
from edx_proctoring.models import ProctoredExam
from edx_proctoring.runtime import set_runtime_service
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus
from edx_proctoring.tests.test_services import MockLearningSequencesService, MockScheduleItemData
from edx_proctoring.tests.test_utils.utils import ProctoredExamTestCase
from oauth2_provider.models import AccessToken, RefreshToken
from opaque_keys.edx.locator import BlockUsageLocator
from organizations.tests.factories import OrganizationFactory
from pytz import UTC
from rest_framework import status
from social_django.models import UserSocialAuth
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase, SharedModuleStoreTestCase,
)
from xmodule.modulestore.tests.factories import CourseFactory
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory
from common.djangoapps.student.models import (
ENROLLED_TO_ENROLLED,
UNENROLLED_TO_ENROLLED,
CourseEnrollment,
CourseEnrollmentAttribute,
ManualEnrollmentAudit
)
from common.djangoapps.student.roles import GlobalStaff, SupportStaffRole
from common.djangoapps.student.tests.factories import (
CourseEnrollmentFactory,
CourseEnrollmentAttributeFactory,
UserFactory,
)
from common.djangoapps.third_party_auth.tests.factories import SAMLProviderConfigFactory
from common.test.utils import disable_signal
from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
from lms.djangoapps.support.models import CourseResetAudit
from lms.djangoapps.support.serializers import ProgramEnrollmentSerializer
from lms.djangoapps.support.tests.factories import CourseResetCourseOptInFactory, CourseResetAuditFactory
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.tests.factories import SSOVerificationFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.oauth_dispatch.tests import factories
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
from openedx.features.enterprise_support.api import enterprise_is_enabled
from openedx.features.enterprise_support.tests.factories import (
EnterpriseCourseEnrollmentFactory,
EnterpriseCustomerUserFactory
)
try:
from consent.models import DataSharingConsent
except ImportError: # pragma: no cover
pass
class SupportViewTestCase(ModuleStoreTestCase):
"""
Base class for support view tests.
"""
USERNAME = "support"
EMAIL = "support@example.com"
PASSWORD = "support"
def setUp(self):
"""Create a user and log in. """
super().setUp()
self.user = UserFactory(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.course = CourseFactory.create()
success = self.client.login(username=self.USERNAME, password=self.PASSWORD)
assert success, 'Could not log in'
class SupportViewManageUserTests(SupportViewTestCase):
"""
Base class for support view tests.
"""
ZENDESK_URL = 'http://zendesk.example.com/'
def setUp(self):
"""Make the user support staff"""
super().setUp()
SupportStaffRole().add_users(self.user)
@override_settings(ZENDESK_URL=ZENDESK_URL)
def test_get_contact_us(self):
"""
Tests Support View contact us Page
"""
url = reverse('support:contact_us')
response = self.client.get(url)
assert response.status_code == 200
def test_get_contact_us_redirect_if_undefined_zendesk_url(self):
"""
Tests the Support contact us Page redirects if ZENDESK_URL setting is not defined
"""
url = reverse('support:contact_us')
response = self.client.get(url)
assert response.status_code == 302
def test_get_password_assistance(self):
"""
Tests password assistance
"""
# Ensure that user is not logged in if they need
# password assistance.
self.client.logout()
url = '/password_assistance'
response = self.client.get(url)
assert response.status_code == 200
def test_get_support_form(self):
"""
Tests Support View to return Manage User Form
"""
url = reverse('support:manage_user')
response = self.client.get(url)
assert response.status_code == 200
def test_get_form_with_user_info(self):
"""
Tests Support View to return Manage User Form
with user info
"""
url = reverse('support:manage_user_detail') + self.user.username
response = self.client.get(url)
data = json.loads(response.content.decode('utf-8'))
assert data['username'] == self.user.username
def test_disable_user_account(self):
"""
Tests Support View to disable the user account
"""
test_user = UserFactory(
username='foobar', email='foobar@foobar.com', password='foobar'
)
application = factories.ApplicationFactory(user=test_user)
access_token = factories.AccessTokenFactory(user=test_user, application=application)
factories.RefreshTokenFactory(
user=test_user, application=application, access_token=access_token
)
assert 0 != AccessToken.objects.filter(user=test_user).count()
assert 0 != RefreshToken.objects.filter(user=test_user).count()
url = reverse('support:manage_user_detail') + test_user.username
response = self.client.post(url, data={
'username_or_email': test_user.username,
'comment': 'Test comment'
})
data = json.loads(response.content.decode('utf-8'))
assert data['success_msg'] == 'User Disabled Successfully'
test_user = User.objects.get(username=test_user.username, email=test_user.email)
assert test_user.has_usable_password() is False
assert 0 == AccessToken.objects.filter(user=test_user).count()
assert 0 == RefreshToken.objects.filter(user=test_user).count()
@ddt.ddt
class SupportViewAccessTests(SupportViewTestCase):
"""
Tests for access control of support views.
"""
@ddt.data(*(
(url_name, role, has_access)
for (url_name, (role, has_access))
in itertools.product((
'support:index',
'support:certificates',
'support:enrollment',
'support:enrollment_list',
'support:manage_user',
'support:manage_user_detail',
'support:link_program_enrollments',
), (
(GlobalStaff, True),
(SupportStaffRole, True),
(None, False)
))
))
@ddt.unpack
def test_access(self, url_name, role, has_access):
if role is not None:
role().add_users(self.user)
url = reverse(url_name)
response = self.client.get(url)
if has_access:
assert response.status_code == 200
else:
assert response.status_code == 403
@ddt.data(
"support:index",
"support:certificates",
"support:enrollment",
"support:enrollment_list",
"support:manage_user",
"support:manage_user_detail",
"support:link_program_enrollments",
)
def test_require_login(self, url_name):
url = reverse(url_name)
# Log out then try to retrieve the page
self.client.logout()
response = self.client.get(url)
# Expect a redirect to the login page
redirect_url = "{login_url}?next={original_url}".format(
login_url=reverse("signin_user"),
original_url=quote(url),
)
self.assertRedirects(response, redirect_url)
class SupportViewIndexTests(SupportViewTestCase):
"""
Tests for the support index view.
"""
EXPECTED_URL_NAMES = [
"support:certificates",
"support:link_program_enrollments",
]
def setUp(self):
"""Make the user support staff. """
super().setUp()
SupportStaffRole().add_users(self.user)
def test_index(self):
response = self.client.get(reverse("support:index"))
self.assertContains(response, "Support")
# Check that all the expected links appear on the index page.
for url_name in self.EXPECTED_URL_NAMES:
self.assertContains(response, reverse(url_name))
class SupportViewCertificatesTests(SupportViewTestCase):
"""
Tests for the certificates support view.
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
def setUp(self):
"""Make the user support staff. """
super().setUp()
SupportStaffRole().add_users(self.user)
def test_certificates_no_filter(self):
# Check that an empty initial filter is passed to the JavaScript client correctly.
response = self.client.get(reverse("support:certificates"))
self.assertContains(response, "userFilter: ''")
def test_certificates_with_user_filter(self):
# Check that an initial filter is passed to the JavaScript client.
url = reverse("support:certificates") + "?user=student@example.com"
response = self.client.get(url)
self.assertContains(response, "userFilter: 'student@example.com'")
def test_certificates_along_with_course_filter(self):
# Check that an initial filter is passed to the JavaScript client.
url = reverse("support:certificates") + "?user=student@example.com&course_id=" + quote(str(self.course.id))
response = self.client.get(url)
self.assertContains(response, "userFilter: 'student@example.com'")
# use replase due to escaping course id:
# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#escapejs
self.assertContains(response, "courseFilter: '" + str(self.course.id).replace('-', '\\u002D') + "'")
@ddt.ddt
class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase):
"""Tests for the enrollment support view."""
def setUp(self):
super().setUp()
SupportStaffRole().add_users(self.user)
self.course = CourseFactory(display_name='te')
self.student = UserFactory.create(username='student', email='test@example.com', password='test')
for mode in (
CourseMode.AUDIT, CourseMode.PROFESSIONAL, CourseMode.CREDIT_MODE,
CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.VERIFIED, CourseMode.HONOR
):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
self.verification_deadline = VerificationDeadline(
course_key=self.course.id,
deadline=datetime.now(UTC) + timedelta(days=365)
)
self.verification_deadline.save()
self.enrollment = CourseEnrollmentFactory.create(
mode=CourseMode.AUDIT, user=self.student, course_id=self.course.id
)
self.url = reverse('support:enrollment_list', kwargs={'username_or_email': self.student.username})
def assert_enrollment(self, mode):
"""
Assert that the student's enrollment has the correct mode.
"""
enrollment = CourseEnrollment.get_enrollment(self.student, self.course.id)
assert enrollment.mode == mode
@ddt.data('username', 'email')
def test_get_enrollments(self, search_string_type):
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.get(url)
assert response.status_code == 200
data = json.loads(response.content.decode('utf-8'))
assert len(data) == 1
self.assertDictContainsSubset({
'mode': CourseMode.AUDIT,
'manual_enrollment': {},
'user': self.student.username,
'course_id': str(self.course.id),
'is_active': True,
'verified_upgrade_deadline': None,
}, data[0])
assert {CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.HONOR, CourseMode.NO_ID_PROFESSIONAL_MODE,
CourseMode.PROFESSIONAL, CourseMode.CREDIT_MODE} == {mode['slug'] for mode in data[0]['course_modes']}
assert 'enterprise_course_enrollments' not in data[0]
assert data[0]['order_number'] == ''
assert data[0]['source_system'] == ''
@ddt.data(*itertools.product(['username', 'email'], [(3, 'ORD-003'), (1, 'ORD-001')]))
@ddt.unpack
def test_order_number_information(self, search_string_type, order_details):
for count in range(order_details[0]):
CourseEnrollmentAttributeFactory(
enrollment=self.enrollment,
namespace='order',
name='order_number',
value='ORD-00{}'.format(count + 1)
)
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.get(url)
assert response.status_code == 200
data = json.loads(response.content.decode('utf-8'))
assert len(data) == 1
assert data[0]['order_number'] == order_details[1]
def test_order_source_system_information(self):
CourseEnrollmentAttributeFactory(
enrollment=self.enrollment,
namespace='order',
name='source_system',
value='commercetools'
)
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': self.student.username}
)
response = self.client.get(url)
assert response.status_code == 200
data = json.loads(response.content.decode('utf-8'))
assert len(data) == 1
assert data[0]['source_system'] == 'commercetools'
@override_settings(FEATURES=dict(ENABLE_ENTERPRISE_INTEGRATION=True))
@enterprise_is_enabled()
def test_get_enrollments_enterprise_enabled(self):
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': self.student.username}
)
enterprise_customer_user = EnterpriseCustomerUserFactory(
user_id=self.student.id
)
enterprise_course_enrollment = EnterpriseCourseEnrollmentFactory(
course_id=self.course.id,
enterprise_customer_user=enterprise_customer_user
)
data_sharing_consent = DataSharingConsent(
course_id=self.course.id,
enterprise_customer=enterprise_customer_user.enterprise_customer,
username=self.student.username,
granted=True
)
data_sharing_consent.save()
response = self.client.get(url)
assert response.status_code == 200
data = json.loads(response.content.decode('utf-8'))
assert len(data) == 1
enterprise_course_enrollments_data = data[0]['enterprise_course_enrollments']
assert len(enterprise_course_enrollments_data) == 1
expected = {
'course_id': str(enterprise_course_enrollment.course_id),
'enterprise_customer_name': enterprise_customer_user.enterprise_customer.name,
'enterprise_customer_user_id': enterprise_customer_user.id,
'license': None,
'saved_for_later': enterprise_course_enrollment.saved_for_later,
'data_sharing_consent': {
'username': self.student.username,
'enterprise_customer_uuid': str(enterprise_customer_user.enterprise_customer_id),
'exists': data_sharing_consent.exists,
'consent_provided': data_sharing_consent.granted,
'consent_required': data_sharing_consent.consent_required(),
'course_id': str(enterprise_course_enrollment.course_id),
}
}
assert enterprise_course_enrollments_data[0] == expected
@ddt.data(
(True, 'Self Paced'),
(False, 'Instructor Paced')
)
@ddt.unpack
def test_pacing_type(self, is_self_paced, pacing_type):
"""
Test correct pacing type is returned in the enrollment.
"""
# Course enrollment is made against course overview. Therefore, the self_paced
# attr of course overview needs to be updated before getting the enrollment data.
course_overview = CourseOverview.get_from_id(self.course.id)
course_overview.self_paced = is_self_paced
course_overview.save()
response = self.client.get(self.url)
assert response.status_code == 200
data = json.loads(response.content.decode('utf-8'))
assert len(data) == 1
self.assertEqual(data[0]['pacing_type'], pacing_type)
def test_get_manual_enrollment_history(self):
ManualEnrollmentAudit.create_manual_enrollment_audit(
self.user,
self.student.email,
ENROLLED_TO_ENROLLED,
'Financial Assistance',
CourseEnrollment.objects.get(course_id=self.course.id, user=self.student)
)
response = self.client.get(self.url)
assert response.status_code == 200
self.assertDictContainsSubset({
'enrolled_by': self.user.email,
'reason': 'Financial Assistance',
}, json.loads(response.content.decode('utf-8'))[0]['manual_enrollment'])
@disable_signal(signals, 'post_save')
@ddt.data('username', 'email')
def test_create_new_enrollment(self, search_string_type):
"""
Assert that a new enrollment is created through post request endpoint.
"""
test_user = UserFactory.create(username='newStudent', email='test2@example.com', password='test')
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(test_user.email) is None
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(test_user, search_string_type)}
)
response = self.client.post(url, data={
'course_id': str(self.course.id),
'mode': CourseMode.AUDIT,
'reason': 'Financial Assistance'
})
assert response.status_code == 200
manual_enrollment = ManualEnrollmentAudit.get_manual_enrollment_by_email(test_user.email)
assert manual_enrollment is not None
assert manual_enrollment.reason == response.json()['reason']
assert manual_enrollment.enrolled_email == 'test2@example.com'
assert manual_enrollment.state_transition == UNENROLLED_TO_ENROLLED
@disable_signal(signals, 'post_save')
@ddt.data('username', 'email')
def test_create_new_enrollment_invalid_mode(self, search_string_type):
"""
Assert that a new enrollment is not created with a vulnerable/invalid enrollment mode.
"""
test_user = UserFactory.create(username='newStudent', email='test2@example.com', password='test')
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(test_user.email) is None
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(test_user, search_string_type)}
)
response = self.client.post(url, data={
'course_id': str(self.course.id),
'mode': '<script>alert("xss")</script>',
'reason': 'Financial Assistance'
})
test_key_error = b'&lt;script&gt;alert(&#34;xss&#34;)&lt;/script&gt; is not a valid mode for course-v1:org'
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert test_key_error in response.content
@disable_signal(signals, 'post_save')
@ddt.data('username', 'email')
def test_create_existing_enrollment(self, search_string_type):
"""
Assert that a new enrollment is not created when an enrollment already exist for that course.
"""
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.post(url, data={
'course_id': str(self.course.id),
'mode': CourseMode.AUDIT,
'reason': 'Financial Assistance'
})
assert response.status_code == 400
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None
@disable_signal(signals, 'post_save')
@ddt.data('username', 'email')
def test_change_enrollment(self, search_string_type):
"""
Assert changing mode for an enrollment.
"""
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.patch(url, data={
'course_id': str(self.course.id),
'old_mode': CourseMode.AUDIT,
'new_mode': CourseMode.VERIFIED,
'reason': 'Financial Assistance'
}, content_type='application/json')
assert response.status_code == 200
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is not None
self.assert_enrollment(CourseMode.VERIFIED)
@disable_signal(signals, 'post_save')
@ddt.data('username', 'email')
def test_change_enrollment_invalid_old_mode(self, search_string_type):
"""
Assert changing mode fails for an enrollment having a vulnerable/invalid old mode.
"""
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.patch(url, data={
'course_id': str(self.course.id),
'old_mode': '<script>alert("xss")</script>',
'new_mode': CourseMode.VERIFIED,
'reason': 'Financial Assistance'
}, content_type='application/json')
test_key_error = b'is not enrolled with mode &lt;script&gt;alert(&#34;xss&#34;)&lt;/script&gt;'
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert test_key_error in response.content
@disable_signal(signals, 'post_save')
@ddt.data('username', 'email')
@patch("common.djangoapps.entitlements.models.get_course_uuid_for_course")
def test_change_enrollment_mode_fullfills_entitlement(self, search_string_type, mock_get_course_uuid):
"""
Assert that changing student's enrollment fulfills it's respective entitlement if it exists.
"""
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None
enrollment = CourseEnrollment.get_enrollment(self.student, self.course.id)
entitlement = CourseEntitlementFactory.create(
user=self.user,
mode=CourseMode.VERIFIED,
enrollment_course_run=enrollment
)
mock_get_course_uuid.return_value = entitlement.course_uuid
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.patch(url, data={
'course_id': str(self.course.id),
'old_mode': CourseMode.AUDIT,
'new_mode': CourseMode.VERIFIED,
'reason': 'Financial Assistance'
}, content_type='application/json')
entitlement.refresh_from_db()
assert response.status_code == 200
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is not None
assert entitlement.enrollment_course_run is not None
assert entitlement.is_entitlement_redeemable() is False
self.assert_enrollment(CourseMode.VERIFIED)
@ddt.data(
({}, r"The field \w+ is required."),
({'course_id': 'bad course key'}, 'Could not parse course key.'),
({
'course_id': 'course-v1:TestX+T101+2015',
'old_mode': CourseMode.AUDIT,
'new_mode': CourseMode.VERIFIED,
'reason': ''
}, 'Could not find enrollment for user'),
({
'course_id': None,
'old_mode': CourseMode.HONOR,
'new_mode': CourseMode.VERIFIED,
'reason': ''
}, r'User \w+ is not enrolled with mode ' + CourseMode.HONOR),
({
'course_id': 'course-v1:TestX+T101+2015',
'old_mode': CourseMode.AUDIT,
'new_mode': CourseMode.CREDIT_MODE,
'reason': 'Enrollment cannot be changed to credit mode'
}, '')
)
@ddt.unpack
def test_change_enrollment_bad_data(self, data, error_message):
# `self` isn't available from within the DDT declaration, so
# assign the course ID here
if 'course_id' in data and data['course_id'] is None:
data['course_id'] = str(self.course.id)
response = self.client.patch(self.url, data, content_type='application/json')
assert response.status_code == 400
assert re.match(error_message, response.content.decode('utf-8').replace("'", '').replace('"', '')) is not None
self.assert_enrollment(CourseMode.AUDIT)
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None
@disable_signal(signals, 'post_save')
@ddt.data('honor', 'audit', 'verified', 'professional', 'no-id-professional', 'credit')
def test_update_enrollment_for_all_modes(self, new_mode):
""" Verify support can changed the enrollment to all available modes"""
self.assert_update_enrollment('username', new_mode)
@disable_signal(signals, 'post_save')
@ddt.data('honor', 'audit', 'verified', 'professional', 'no-id-professional')
def test_update_enrollment_for_ended_course(self, new_mode):
""" Verify support can changed the enrollment of archived course. """
self.set_course_end_date_and_expiry()
self.assert_update_enrollment('username', new_mode)
@ddt.data('username', 'email')
def test_get_enrollments_with_expired_mode(self, search_string_type):
""" Verify that page can get the all modes with archived course. """
self.set_course_end_date_and_expiry()
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.get(url)
self._assert_generated_modes(response)
@disable_signal(signals, 'post_save')
@ddt.data('username', 'email')
def test_update_enrollments_with_expired_mode(self, search_string_type):
""" Verify that enrollment can be updated to verified mode. """
self.set_course_end_date_and_expiry()
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None
self.assert_update_enrollment(search_string_type, CourseMode.VERIFIED)
def _assert_generated_modes(self, response):
"""Dry method to generate course modes dict and test with response data."""
modes = CourseMode.modes_for_course(self.course.id, include_expired=True, only_selectable=False)
modes_data = []
for mode in modes:
expiry = mode.expiration_datetime.strftime('%Y-%m-%dT%H:%M:%SZ') if mode.expiration_datetime else None
modes_data.append({
'sku': mode.sku,
'expiration_datetime': expiry,
'name': mode.name,
'currency': mode.currency,
'bulk_sku': mode.bulk_sku,
'min_price': mode.min_price,
'suggested_prices': mode.suggested_prices,
'slug': mode.slug,
'description': mode.description
})
assert response.status_code == 200
data = json.loads(response.content.decode('utf-8'))
assert len(data) == 1
assert modes_data == data[0]['course_modes']
assert {CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.PROFESSIONAL,
CourseMode.HONOR, CourseMode.CREDIT_MODE} == {mode['slug'] for mode in data[0]['course_modes']}
def assert_update_enrollment(self, search_string_type, new_mode):
""" Dry method to update the enrollment and assert response."""
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
with patch('lms.djangoapps.support.views.enrollments.get_credit_provider_attribute_values') as mock_method:
credit_provider = (
['Arizona State University'], 'You are now eligible for credit from Arizona State University'
)
mock_method.return_value = credit_provider
response = self.client.patch(url, data={
'course_id': str(self.course.id),
'old_mode': CourseMode.AUDIT,
'new_mode': new_mode,
'reason': 'Financial Assistance'
}, content_type='application/json')
assert response.status_code == 200
assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is not None
self.assert_enrollment(new_mode)
if new_mode == 'credit':
enrollment_attr = CourseEnrollmentAttribute.objects.first()
assert enrollment_attr.value == str(credit_provider[0])
def set_course_end_date_and_expiry(self):
""" Set the course-end date and expire its verified mode."""
self.course.start = datetime(year=1970, month=1, day=1, tzinfo=UTC)
self.course.end = datetime(year=1970, month=1, day=10, tzinfo=UTC)
# change verified mode expiry.
verified_mode = CourseMode.objects.get(
course_id=self.course.id,
mode_slug=CourseMode.VERIFIED
)
verified_mode.expiration_datetime = datetime(year=1970, month=1, day=9, tzinfo=UTC)
verified_mode.save()
@ddt.ddt
class SupportViewLinkProgramEnrollmentsTests(SupportViewTestCase):
"""
Tests for the link_program_enrollments support view.
"""
patch_render = patch(
'lms.djangoapps.support.views.program_enrollments.render_to_response',
return_value=HttpResponse(),
autospec=True,
)
def setUp(self):
"""Make the user support staff. """
super().setUp()
self.url = reverse("support:link_program_enrollments")
SupportStaffRole().add_users(self.user)
self.program_uuid = str(uuid4())
self.text = '0001,user-0001\n0002,user-02'
@patch_render
def test_get(self, mocked_render):
self.client.get(self.url)
render_call_dict = mocked_render.call_args[0][1]
assert render_call_dict == {
'successes': [],
'errors': [],
'program_uuid': '',
'text': ''
}
def test_rendering(self):
"""
Test the view without mocking out the rendering like the rest of the tests.
"""
response = self.client.get(self.url)
content = str(response.content, encoding='utf-8')
assert '"programUUID": ""' in content
assert '"text": ""' in content
@patch_render
def test_invalid_uuid(self, mocked_render):
self.client.post(self.url, data={
'program_uuid': 'notauuid',
'text': self.text,
})
msg = "Supplied program UUID 'notauuid' is not a valid UUID."
render_call_dict = mocked_render.call_args[0][1]
assert render_call_dict['errors'] == [msg]
@patch_render
@ddt.data(
('program_uuid', ''),
('', 'text'),
('', ''),
)
@ddt.unpack
def test_missing_parameter(self, program_uuid, text, mocked_render):
error = (
"You must provide both a program uuid "
"and a series of lines with the format "
"'external_user_key,lms_username'."
)
self.client.post(self.url, data={
'program_uuid': program_uuid,
'text': text,
})
render_call_dict = mocked_render.call_args[0][1]
assert render_call_dict['errors'] == [error]
@ddt.data(
'0001,learner-01\n0002,learner-02', # normal
'0001,learner-01,apple,orange\n0002,learner-02,purple', # extra fields
'\t0001 , \t learner-01 \n 0002 , learner-02 ', # whitespace
)
@patch('lms.djangoapps.support.views.utils.link_program_enrollments')
def test_text(self, text, mocked_link):
self.client.post(self.url, data={
'program_uuid': self.program_uuid,
'text': text,
})
mocked_link.assert_called_once()
mocked_link.assert_called_with(
UUID(self.program_uuid),
{
'0001': 'learner-01',
'0002': 'learner-02',
}
)
@patch_render
def test_junk_text(self, mocked_render):
text = 'alsdjflajsdflakjs'
self.client.post(self.url, data={
'program_uuid': self.program_uuid,
'text': text,
})
msg = "All linking lines must be in the format 'external_user_key,lms_username'"
render_call_dict = mocked_render.call_args[0][1]
assert render_call_dict['errors'] == [msg]
def _setup_user_from_username(self, username):
"""
Setup a user from the passed in username.
If username passed in is falsy, return None
"""
created_user = None
if username:
created_user = UserFactory(username=username, password=self.PASSWORD)
return created_user
def _setup_enrollments(self, external_user_key, linked_user=None):
"""
Create enrollments for testing linking.
The enrollments can be create with already linked edX user.
"""
program_enrollment = ProgramEnrollmentFactory.create(
external_user_key=external_user_key,
program_uuid=self.program_uuid,
user=linked_user
)
course_enrollment = None
if linked_user:
course_enrollment = CourseEnrollmentFactory.create(
course_id=self.course.id,
user=linked_user,
mode=CourseMode.MASTERS,
is_active=True
)
program_course_enrollment = ProgramCourseEnrollmentFactory.create(
program_enrollment=program_enrollment,
course_key=self.course.id,
course_enrollment=course_enrollment,
status='active'
)
return program_enrollment, program_course_enrollment
@ddt.data(
('', None),
('linked_user', None),
('linked_user', 'original_user')
)
@ddt.unpack
@patch_render
def test_linking_program_enrollment(self, username, original_username, mocked_render):
external_user_key = '0001'
linked_user = self._setup_user_from_username(username)
original_user = self._setup_user_from_username(original_username)
program_enrollment, program_course_enrollment = self._setup_enrollments(
external_user_key,
original_user
)
self.client.post(self.url, data={
'program_uuid': self.program_uuid,
'text': external_user_key + ',' + username
})
render_call_dict = mocked_render.call_args[0][1]
if username:
expected_success = f"('{external_user_key}', '{username}')"
assert render_call_dict['successes'] == [expected_success]
program_enrollment.refresh_from_db()
assert program_enrollment.user == linked_user
program_course_enrollment.refresh_from_db()
assert program_course_enrollment.course_enrollment.user == linked_user
else:
error = "All linking lines must be in the format 'external_user_key,lms_username'"
assert render_call_dict['errors'] == [error]
@ddt.ddt
class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
"""
View tests for Program Enrollments Inspector
"""
patch_render = patch(
'lms.djangoapps.support.views.program_enrollments.render_to_response',
return_value=HttpResponse(),
autospec=True,
)
def setUp(self):
super().setUp()
self.url = reverse("support:program_enrollments_inspector")
SupportStaffRole().add_users(self.user)
self.program_uuid = str(uuid4())
self.external_user_key = 'abcaaa'
# Setup three orgs and their SAML providers
self.org_key_list = ['test_org', 'donut_org', 'tri_org']
for org_key in self.org_key_list:
lms_org = OrganizationFactory(
short_name=org_key
)
SAMLProviderConfigFactory(
organization=lms_org,
slug=org_key,
enabled=True,
)
self.no_saml_org_key = 'no_saml_org'
self.no_saml_lms_org = OrganizationFactory(
short_name=self.no_saml_org_key
)
def _serialize_datetime(self, dt):
return dt.strftime('%Y-%m-%dT%H:%M:%S')
def test_initial_rendering(self):
response = self.client.get(self.url)
content = str(response.content, encoding='utf-8')
expected_organization_serialized = '"orgKeys": {}'.format(
json.dumps(sorted(self.org_key_list))
)
assert response.status_code == 200
assert expected_organization_serialized in content
assert '"learnerInfo": {}' in content
def _construct_user(self, username, org_key=None, external_user_key=None):
"""
Provided the username, create an edx account user. If the org_key is provided,
SSO link the user with the IdP associated with org_key. Return the created user and
expected user info object from the view
"""
user = UserFactory(username=username)
user_info = {
'username': user.username,
'email': user.email
}
if org_key and external_user_key:
user_social_auth = UserSocialAuth.objects.create(
user=user,
uid=f'{org_key}:{external_user_key}',
provider='tpa-saml'
)
user_info['sso_list'] = [{
'uid': user_social_auth.uid
}]
return user, user_info
def _construct_enrollments(self, program_uuids, course_ids, external_user_key, edx_user=None):
"""
A helper function to setup the program enrollments for a given learner.
If the edx user is provided, it will try to SSO the user with the enrollments
Return the expected info object that should be created based on the model setup
"""
program_enrollments = []
for program_uuid in program_uuids:
course_enrollment = None
program_enrollment = ProgramEnrollmentFactory.create(
external_user_key=external_user_key,
program_uuid=program_uuid,
user=edx_user
)
for course_id in course_ids:
if edx_user:
course_enrollment = CourseEnrollmentFactory.create(
course_id=course_id,
user=edx_user,
mode=CourseMode.MASTERS,
is_active=True
)
program_course_enrollment = ProgramCourseEnrollmentFactory.create(
# lint-amnesty, pylint: disable=unused-variable
program_enrollment=program_enrollment,
course_key=course_id,
course_enrollment=course_enrollment,
status='active',
)
program_enrollments.append(program_enrollment)
serialized = ProgramEnrollmentSerializer(program_enrollments, many=True)
return serialized.data
def _construct_id_verification(self, user):
"""
Helper function to create the SSO verified record for the user
so that the user is ID Verified
"""
SSOVerificationFactory(
identity_provider_slug=self.org_key_list[0],
user=user,
)
return IDVerificationService.user_status(user)
@patch_render
def test_search_username_well_connected_user(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'test_user_connected',
self.org_key_list[0],
self.external_user_key
)
id_verified = self._construct_id_verification(created_user)
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
self.external_user_key,
created_user
)
self.client.get(self.url, data={
'edx_user': created_user.username,
'org_key': self.org_key_list[0]
})
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments,
'id_verification': id_verified
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_username_user_not_connected(self, mocked_render):
created_user, expected_user_info = self._construct_user('user_not_connected')
self.client.get(self.url, data={
'edx_user': created_user.email,
'org_key': self.org_key_list[0]
})
expected_info = {
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user)
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_username_user_no_enrollment(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'user_connected',
self.org_key_list[0],
self.external_user_key
)
self.client.get(self.url, data={
'edx_user': created_user.email,
'org_key': self.org_key_list[0]
})
expected_info = {
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user),
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_username_user_no_course_enrollment(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'user_connected',
self.org_key_list[0],
self.external_user_key
)
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[],
self.external_user_key,
created_user,
)
self.client.get(self.url, data={
'edx_user': created_user.email,
'org_key': self.org_key_list[0]
})
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments,
'id_verification': IDVerificationService.user_status(created_user),
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_username_user_not_connected_with_enrollments(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'user_not_connected'
)
self._construct_enrollments(
[self.program_uuid],
[],
self.external_user_key,
)
self.client.get(self.url, data={
'edx_user': created_user.email,
'org_key': self.org_key_list[0]
})
expected_info = {
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user),
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_username_user_id_verified(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'user_not_connected'
)
id_verified = self._construct_id_verification(created_user)
expected_info = {
'user': expected_user_info,
'id_verification': id_verified
}
self.client.get(self.url, data={
'edx_user': created_user.email,
'org_key': self.org_key_list[0]
})
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_external_key_well_connected(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'test_user_connected',
self.org_key_list[0],
self.external_user_key
)
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
self.external_user_key,
created_user
)
id_verified = self._construct_id_verification(created_user)
self.client.get(self.url, data={
'external_user_key': self.external_user_key,
'org_key': self.org_key_list[0]
})
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments,
'id_verification': id_verified,
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@ddt.data(
('', 'test_org'),
('bad_key', '')
)
@ddt.unpack
@patch_render
def test_search_no_external_user_key(self, user_key, org_key, mocked_render):
self.client.get(self.url, data={
'external_user_key': user_key,
'org_key': org_key,
})
expected_error = (
"To perform a search, you must provide either the student's "
"(a) edX username, "
"(b) email address associated with their edX account, or "
"(c) Identity-providing institution and external key!"
)
render_call_dict = mocked_render.call_args[0][1]
assert {} == render_call_dict['learner_program_enrollments']
assert expected_error == render_call_dict['error']
@patch_render
def test_search_external_user_not_connected(self, mocked_render):
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
self.external_user_key,
)
self.client.get(self.url, data={
'external_user_key': self.external_user_key,
'org_key': self.org_key_list[0]
})
expected_info = {
'user': {
'external_user_key': self.external_user_key,
},
'enrollments': expected_enrollments
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_external_user_not_in_system(self, mocked_render):
external_user_key = 'not_in_system'
self.client.get(self.url, data={
'external_user_key': external_user_key,
'org_key': self.org_key_list[0],
})
expected_error = 'No user found for external key {} for institution {}'.format(
external_user_key, self.org_key_list[0]
)
render_call_dict = mocked_render.call_args[0][1]
assert expected_error == render_call_dict['error']
@patch_render
def test_search_external_user_case_insensitive(self, mocked_render):
external_user_key = 'AbCdEf123'
requested_external_user_key = 'aBcDeF123'
created_user, expected_user_info = self._construct_user(
'test_user_connected',
self.org_key_list[0],
external_user_key
)
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
external_user_key,
created_user
)
id_verified = self._construct_id_verification(created_user)
self.client.get(self.url, data={
'external_user_key': requested_external_user_key,
'org_key': self.org_key_list[0]
})
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments,
'id_verification': id_verified,
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@ddt.ddt
class ProgramEnrollmentsInspectorAPIViewTests(SupportViewTestCase):
"""
View tests for Program Enrollments Inspector API
"""
_url = reverse("support:program_enrollments_inspector_details")
def setUp(self):
super().setUp()
SupportStaffRole().add_users(self.user)
self.program_uuid = str(uuid4())
self.external_user_key = 'abcaaa'
# Setup three orgs and their SAML providers
self.org_key_list = ['test_org', 'donut_org', 'tri_org']
for org_key in self.org_key_list:
lms_org = OrganizationFactory(
short_name=org_key
)
SAMLProviderConfigFactory(
organization=lms_org,
slug=org_key,
enabled=True,
)
self.no_saml_org_key = 'no_saml_org'
self.no_saml_lms_org = OrganizationFactory(
short_name=self.no_saml_org_key
)
def _serialize_datetime(self, dt):
return dt.strftime('%Y-%m-%dT%H:%M:%S')
def test_default_response(self):
response = self.client.get(self._url)
content = json.loads(response.content.decode('utf-8'))
assert response.status_code == 200
assert '' == content['org_keys']
def _construct_user(self, username, org_key=None, external_user_key=None):
"""
Provided the username, create an edx account user. If the org_key is provided,
SSO link the user with the IdP associated with org_key. Return the created user and
expected user info object from the view
"""
user = UserFactory(username=username)
user_info = {
'username': user.username,
'email': user.email
}
if org_key and external_user_key:
user_social_auth = UserSocialAuth.objects.create(
user=user,
uid=f'{org_key}:{external_user_key}',
provider='tpa-saml'
)
user_info['sso_list'] = [{
'uid': user_social_auth.uid
}]
return user, user_info
def _construct_enrollments(self, program_uuids, course_ids, external_user_key, edx_user=None):
"""
A helper function to setup the program enrollments for a given learner.
If the edx user is provided, it will try to SSO the user with the enrollments
Return the expected info object that should be created based on the model setup
"""
program_enrollments = []
for program_uuid in program_uuids:
course_enrollment = None
program_enrollment = ProgramEnrollmentFactory.create(
external_user_key=external_user_key,
program_uuid=program_uuid,
user=edx_user
)
for course_id in course_ids:
if edx_user:
course_enrollment = CourseEnrollmentFactory.create(
course_id=course_id,
user=edx_user,
mode=CourseMode.MASTERS,
is_active=True
)
program_course_enrollment = ProgramCourseEnrollmentFactory.create(
# lint-amnesty, pylint: disable=unused-variable
program_enrollment=program_enrollment,
course_key=course_id,
course_enrollment=course_enrollment,
status='active',
)
program_enrollments.append(program_enrollment)
serialized = ProgramEnrollmentSerializer(program_enrollments, many=True)
return serialized.data
def _construct_id_verification(self, user):
"""
Helper function to create the SSO verified record for the user
so that the user is ID Verified
"""
SSOVerificationFactory(
identity_provider_slug=self.org_key_list[0],
user=user,
)
return IDVerificationService.user_status(user)
def test_search_username_well_connected_user(self):
created_user, expected_user_info = self._construct_user(
'test_user_connected',
self.org_key_list[0],
self.external_user_key
)
id_verified = self._construct_id_verification(created_user)
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
self.external_user_key,
created_user
)
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
response = json.loads(response.content.decode('utf-8'))
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments,
'id_verification': id_verified
}
assert expected_info == response['learner_program_enrollments']
def test_search_username_user_not_connected(self):
created_user, expected_user_info = self._construct_user('user_not_connected')
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
response = json.loads(response.content.decode('utf-8'))
expected_info = {
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user)
}
assert expected_info == response['learner_program_enrollments']
def test_search_username_user_no_enrollment(self):
created_user, expected_user_info = self._construct_user(
'user_connected',
self.org_key_list[0],
self.external_user_key
)
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
response = json.loads(response.content.decode('utf-8'))
expected_info = {
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user),
}
assert expected_info == response['learner_program_enrollments']
def test_search_username_user_no_course_enrollment(self):
created_user, expected_user_info = self._construct_user(
'user_connected',
self.org_key_list[0],
self.external_user_key
)
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[],
self.external_user_key,
created_user,
)
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
response = json.loads(response.content.decode('utf-8'))
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments,
'id_verification': IDVerificationService.user_status(created_user),
}
assert expected_info == response['learner_program_enrollments']
def test_search_username_user_not_connected_with_enrollments(self):
created_user, expected_user_info = self._construct_user(
'user_not_connected'
)
self._construct_enrollments(
[self.program_uuid],
[],
self.external_user_key,
)
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
response = json.loads(response.content.decode('utf-8'))
expected_info = {
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user),
}
assert expected_info == response['learner_program_enrollments']
def test_search_username_user_id_verified(self):
created_user, expected_user_info = self._construct_user(
'user_not_connected'
)
id_verified = self._construct_id_verification(created_user)
expected_info = {
'user': expected_user_info,
'id_verification': id_verified
}
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
response = json.loads(response.content.decode('utf-8'))
assert expected_info == response['learner_program_enrollments']
@ddt.data(
('', 'test_org'),
('bad_key', '')
)
@ddt.unpack
def test_search_no_external_user_key(self, user_key, org_key):
response = self.client.get(self._url + f'?external_user_key={user_key}&org_key={org_key}')
response = json.loads(response.content.decode('utf-8'))
expected_error = (
"To perform a search, you must provide either the student's "
"(a) edX username, "
"(b) email address associated with their edX account, or "
"(c) Identity-providing institution and external key!"
)
assert {} == response['learner_program_enrollments']
assert expected_error == response['error']
def test_search_external_user_not_connected(self):
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
self.external_user_key,
)
response = self.client.get(
self._url + f'?external_user_key={self.external_user_key}&org_key={self.org_key_list[0]}'
)
response = json.loads(response.content.decode('utf-8'))
expected_info = {
'user': {
'external_user_key': self.external_user_key,
},
'enrollments': expected_enrollments
}
assert expected_info == response['learner_program_enrollments']
def test_search_external_user_not_in_system(self):
external_user_key = 'not_in_system'
response = self.client.get(
self._url + f'?external_user_key={external_user_key}&org_key={self.org_key_list[0]}'
)
response = json.loads(response.content.decode('utf-8'))
expected_error = 'No user found for external key {} for institution {}'.format(
external_user_key, self.org_key_list[0]
)
assert expected_error == response['error']
def test_search_external_user_case_insensitive(self):
external_user_key = 'AbCdEf123'
requested_external_user_key = 'aBcDeF123'
created_user, expected_user_info = self._construct_user(
'test_user_connected',
self.org_key_list[0],
external_user_key
)
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
external_user_key,
created_user
)
id_verified = self._construct_id_verification(created_user)
response = self.client.get(
self._url + f'?external_user_key={requested_external_user_key}&org_key={self.org_key_list[0]}'
)
response = json.loads(response.content.decode('utf-8'))
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments,
'id_verification': id_verified,
}
assert expected_info == response['learner_program_enrollments']
class SsoRecordsTests(SupportViewTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
def setUp(self):
"""Make the user support staff"""
super().setUp()
SupportStaffRole().add_users(self.user)
self.student = UserFactory.create(username='student', email='test@example.com', password='test')
self.url = reverse("support:sso_records", kwargs={'username_or_email': self.student.username})
self.org_key_list = ['test_org']
for org_key in self.org_key_list:
lms_org = OrganizationFactory(
short_name=org_key
)
SAMLProviderConfigFactory(
organization=lms_org,
slug=org_key,
enabled=True,
)
def test_empty_response(self):
response = self.client.get(self.url)
data = json.loads(response.content.decode('utf-8'))
assert response.status_code == 200
assert len(data) == 0
def test_user_does_not_exist(self):
response = self.client.get(reverse("support:sso_records", kwargs={'username_or_email': 'wrong_username'}))
data = json.loads(response.content.decode('utf-8'))
assert response.status_code == 200
assert len(data) == 0
def test_response(self):
user_social_auth = UserSocialAuth.objects.create( # lint-amnesty, pylint: disable=unused-variable
user=self.student,
uid=self.student.email,
provider='tpa-saml'
)
response = self.client.get(self.url)
data = json.loads(response.content.decode('utf-8'))
assert response.status_code == 200
assert len(data) == 1
self.assertContains(response, '"uid": "test@example.com"')
def test_history_response(self):
'''Tests changes in SSO history for a user'''
user_social_auth = UserSocialAuth.objects.create( # lint-amnesty, pylint: disable=unused-variable
user=self.student,
uid=self.student.email,
provider='tpa-saml'
)
sso = UserSocialAuth.objects.get(user=self.student)
sso.uid = self.student.email + ':' + sso.provider
sso.save()
response = self.client.get(self.url)
data = json.loads(response.content.decode('utf-8'))
assert response.status_code == 200
assert len(data) == 1
assert len(data[0].get('history')) == 2
assert data[0].get('history')[0].get('uid') == "test@example.com:tpa-saml"
assert data[0].get('history')[1].get('uid') == "test@example.com"
class FeatureBasedEnrollmentSupportApiViewTests(SupportViewTestCase):
"""
Test suite for FBE Support API view.
"""
def setUp(self):
super().setUp()
SupportStaffRole().add_users(self.user)
def test_fbe_enabled_response(self):
"""
Test the response for the api view when the gating and duration configs
are enabled.
"""
for course_mode in [CourseMode.AUDIT, CourseMode.VERIFIED]:
CourseModeFactory.create(mode_slug=course_mode, course_id=self.course.id)
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
response = self.client.get(
reverse("support:feature_based_enrollment_details", kwargs={'course_id': str(self.course.id)})
)
data = json.loads(response.content.decode('utf-8'))
gating_config = data['gating_config']
duration_config = data['duration_config']
assert str(self.course.id) == data['course_id']
assert gating_config['enabled']
assert gating_config['enabled_as_of'] == '2018-01-01 00:00:00+00:00'
assert duration_config['enabled']
assert duration_config['enabled_as_of'] == '2018-01-01 00:00:00+00:00'
def test_fbe_disabled_response(self):
"""
Test the FBE support api view response to be empty when no gating and duration
config is present.
"""
response = self.client.get(
reverse("support:feature_based_enrollment_details", kwargs={'course_id': str(self.course.id)})
)
data = json.loads(response.content.decode('utf-8'))
assert data == {}
@ddt.ddt
class LinkProgramEnrollmentSupportAPIViewTests(SupportViewTestCase):
"""
Tests for the link_program_enrollments support view.
"""
_url = reverse("support:link_program_enrollments_details")
def setUp(self):
"""
Make the user support staff.
"""
super().setUp()
SupportStaffRole().add_users(self.user)
self.program_uuid = str(uuid4())
self.username_pair_text = '0001,user-0001\n0002,user-02'
def _setup_user_from_username(self, username):
"""
Setup a user from the passed in username.
If username passed in is falsy, return None
"""
created_user = None
if username:
created_user = UserFactory(username=username, password=self.PASSWORD)
return created_user
def _setup_enrollments(self, external_user_key, linked_user=None):
"""
Create enrollments for testing linking.
The enrollments can be created with already linked edX user.
"""
program_enrollment = ProgramEnrollmentFactory.create(
external_user_key=external_user_key,
program_uuid=self.program_uuid,
user=linked_user
)
course_enrollment = None
if linked_user:
course_enrollment = CourseEnrollmentFactory.create(
course_id=self.course.id,
user=linked_user,
mode=CourseMode.MASTERS,
is_active=True
)
program_course_enrollment = ProgramCourseEnrollmentFactory.create(
program_enrollment=program_enrollment,
course_key=self.course.id,
course_enrollment=course_enrollment,
status='active'
)
return program_enrollment, program_course_enrollment
def test_invalid_uuid(self):
"""
Tests if enrollment linkages are refused for an invalid uuid
"""
response = self.client.post(self._url, data={
'program_uuid': 'notauuid',
'username_pair_text': self.username_pair_text,
})
msg = "Supplied program UUID 'notauuid' is not a valid UUID."
data = json.loads(response.content.decode('utf-8'))
assert data['errors'] == [msg]
@ddt.data(
('program_uuid', ''),
('', 'username_pair_text'),
('', '')
)
@ddt.unpack
def test_missing_parameter(self, program_uuid, username_pair_text):
"""
Tests if enrollment linkages are refused for missing parameters
"""
error = (
"You must provide both a program uuid "
"and a series of lines with the format "
"'external_user_key,lms_username'."
)
response = self.client.post(self._url, data={
'program_uuid': program_uuid,
'username_pair_text': username_pair_text
})
response_data = json.loads(response.content.decode('utf-8'))
assert response_data['errors'] == [error]
@ddt.data(
'0001,learner-01\n0002,learner-02', # normal
'0001,learner-01,apple,orange\n0002,learner-02,purple', # extra fields
'\t0001 , \t learner-01 \n 0002 , learner-02 ', # whitespace
)
@patch('lms.djangoapps.support.views.utils.link_program_enrollments')
def test_username_pair_text(self, username_pair_text, mocked_link):
"""
Tests if enrollment linkages are created for different types of
username_pair_text format
"""
response = self.client.post(self._url, data={
'program_uuid': self.program_uuid,
'username_pair_text': username_pair_text,
})
response_data = json.loads(response.content.decode('utf-8'))
mocked_link.assert_called_once()
mocked_link.assert_called_with(
UUID(self.program_uuid),
{
'0001': 'learner-01',
'0002': 'learner-02',
}
)
success = ["('0001', 'learner-01')", "('0002', 'learner-02')"]
assert response_data['successes'] == success
mocked_link.reset_mock()
def test_invalid_username_pair_text(self):
"""
Tests if enrollment linkages are refused for invalid types of
username_pair_text format
"""
username_pair_text = 'garbage_text'
response = self.client.post(self._url, data={
'program_uuid': self.program_uuid,
'username_pair_text': username_pair_text,
})
msg = "All linking lines must be in the format 'external_user_key,lms_username'"
response_data = json.loads(response.content.decode('utf-8'))
assert response_data['errors'] == [msg]
@ddt.data(
('linked_user', None),
('linked_user', 'original_user')
)
@ddt.unpack
def test_linking_program_enrollment_with_username(self, username, original_username):
"""
Tests if enrollment linkages are created for valid usernames
"""
external_user_key = '0001'
linked_user = self._setup_user_from_username(username)
original_user = self._setup_user_from_username(original_username)
program_enrollment, program_course_enrollment = self._setup_enrollments(
external_user_key,
original_user
)
response = self.client.post(self._url, data={
'program_uuid': self.program_uuid,
'username_pair_text': external_user_key + ',' + username
})
response_data = json.loads(response.content.decode('utf-8'))
expected_success = f"('{external_user_key}', '{username}')"
assert response_data['successes'] == [expected_success]
program_enrollment.refresh_from_db()
assert program_enrollment.user == linked_user
program_course_enrollment.refresh_from_db()
assert program_course_enrollment.course_enrollment.user == linked_user
@ddt.data(
('', None),
)
@ddt.unpack
def test_linking_program_enrollment_without_username(self, username, original_username):
"""
Tests if enrollment linkages are refused for invalid usernames
"""
external_user_key = '0001'
linked_user = self._setup_user_from_username(username)
original_user = self._setup_user_from_username(original_username)
program_enrollment, program_course_enrollment = self._setup_enrollments(
external_user_key,
original_user
)
response = self.client.post(self._url, data={
'program_uuid': self.program_uuid,
'username_pair_text': external_user_key + ',' + username
})
response_data = json.loads(response.content.decode('utf-8'))
error = "All linking lines must be in the format 'external_user_key,lms_username'"
assert response_data['errors'] == [error]
class SAMLProvidersWithOrgTests(SupportViewTestCase):
"""
Tests for the get_saml_providers API View
"""
_url = reverse("support:get_saml_providers")
def setUp(self):
"""
Make the user support staff.
"""
super().setUp()
SupportStaffRole().add_users(self.user)
self.org_key_list = ['test_org', 'donut_org', 'tri_org']
for org_key in self.org_key_list:
lms_org = OrganizationFactory(
short_name=org_key
)
SAMLProviderConfigFactory(
organization=lms_org,
slug=org_key,
enabled=True,
)
def test_returning_saml_providers(self):
response = self.client.get(self._url)
response_data = json.loads(response.content.decode('utf-8'))
assert response_data == self.org_key_list
class TestOnboardingView(SupportViewTestCase, ProctoredExamTestCase):
"""
Tests for OnboardingView
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
def setUp(self):
super().setUp()
SupportStaffRole().add_users(self.user)
# update default course key
self.course_id = 'course-v1:a+b+c'
self.proctored_exam_id = self._create_proctored_exam()
self.onboarding_exam_id = self._create_onboarding_exam()
self.other_user = User.objects.create(username='otheruser', password='test')
self.other_course_content = 'block-v1:test+course+2+type@sequential+block@other_onboard'
self.other_course = CourseFactory.create(
org='x',
course='y',
run='z',
enable_proctored_exams=True,
proctoring_provider=settings.PROCTORING_BACKENDS['DEFAULT'],
)
yesterday = timezone.now() - timezone.timedelta(days=1)
self.course_scheduled_sections = {
BlockUsageLocator.from_string(self.content_id_onboarding): MockScheduleItemData(yesterday),
BlockUsageLocator.from_string(self.other_course_content): MockScheduleItemData(yesterday),
}
set_runtime_service('learning_sequences', MockLearningSequencesService(
list(self.course_scheduled_sections.keys()),
self.course_scheduled_sections,
))
self.onboarding_exam = ProctoredExam.objects.get(id=self.onboarding_exam_id)
def tearDown(self): # lint-amnesty, pylint: disable=super-method-not-called
"""
Override deafult implementation to prevent `default` key deletion from TRACKERS in
an inherited tearDown() method of ProctoredExamTestCase
"""
return
def _url(self, username):
return reverse("support:onboarding_status", kwargs={'username_or_email': username})
def _create_enrollment(self):
""" Create enrollment in default course """
# updated course key = 'course-v1:a+b+c'
self.course = CourseFactory.create(
org='a',
course='b',
run='c',
enable_proctored_exams=True,
proctoring_provider=settings.PROCTORING_BACKENDS['DEFAULT'],
)
CourseEnrollmentFactory(
is_active=True,
mode='verified',
course_id=self.course.id,
user=self.user
)
def test_wrong_username(self):
"""
Test that a request with a username which does not exits returns 404
"""
response = self.client.get(self._url(username='does_not_exist'))
self.assertEqual(response.status_code, 404)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(response_data['verified_in'], None)
self.assertEqual(response_data['current_status'], None)
def test_no_record(self):
"""
Test that a request with a username which do not have any onboarding exam returns empty data
"""
response = self.client.get(self._url(username=self.other_user.username))
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(response_data['verified_in'], None)
self.assertEqual(response_data['current_status'], None)
def test_no_verified_attempts(self):
"""
Test that if there are no verified attempts, the most recent status is returned
"""
self._create_enrollment()
# create first attempt
attempt_id = create_exam_attempt(self.onboarding_exam_id, self.user.id, True)
update_attempt_status(attempt_id, ProctoredExamStudentAttemptStatus.submitted)
response = self.client.get(self._url(username=self.user.username))
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(response_data['verified_in'], None)
self.assertEqual(
response_data['current_status']['onboarding_status'],
ProctoredExamStudentAttemptStatus.submitted
)
# Create second attempt and assert that most recent attempt is returned
create_exam_attempt(self.onboarding_exam_id, self.user.id, True)
response = self.client.get(self._url(username=self.user.username))
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(response_data['verified_in'], None)
self.assertEqual(
response_data['current_status']['onboarding_status'],
ProctoredExamStudentAttemptStatus.created
)
def test_get_verified_attempt(self):
"""
Test that if there is at least one verified attempt, the status returned is always verified
"""
self._create_enrollment()
# Create first attempt
attempt_id = create_exam_attempt(self.onboarding_exam_id, self.user.id, True)
update_attempt_status(attempt_id, ProctoredExamStudentAttemptStatus.verified)
response = self.client.get(self._url(username=self.user.username))
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(
response_data['verified_in']['onboarding_status'],
ProctoredExamStudentAttemptStatus.verified
)
self.assertEqual(
response_data['current_status']['onboarding_status'],
ProctoredExamStudentAttemptStatus.verified
)
# Create second attempt and assert that verified attempt is still returned
create_exam_attempt(self.onboarding_exam_id, self.user.id, True)
response = self.client.get(self._url(username=self.user.username))
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(
response_data['verified_in']['onboarding_status'],
ProctoredExamStudentAttemptStatus.verified
)
self.assertEqual(
response_data['current_status']['onboarding_status'],
ProctoredExamStudentAttemptStatus.verified
)
def test_verified_in_another_course(self):
"""
Test that, if there is at least one verified attempt in any course for a given user,
the current status will return `other_course_approved`
"""
# Create a submitted attempt in the current course
attempt_id = create_exam_attempt(self.onboarding_exam_id, self.user.id, True)
update_attempt_status(attempt_id, ProctoredExamStudentAttemptStatus.submitted)
# Create an attempt in the other course that has been verified
other_course_id = 'course-v1:x+y+z'
other_course_onboarding_exam = ProctoredExam.objects.create(
course_id=other_course_id,
content_id=self.other_course_content,
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90,
is_active=True,
is_proctored=True,
is_practice_exam=True,
backend='test'
)
self.user_id = self.user.id
self._create_exam_attempt(other_course_onboarding_exam.id, ProctoredExamStudentAttemptStatus.verified, True)
# professional enrollment
CourseEnrollmentFactory(
is_active=True,
mode='professional',
course_id=self.other_course.id,
user=self.user
)
# default enrollment afterwards with submitted status
self._create_enrollment()
response = self.client.get(self._url(username=self.user.username))
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
# assert that originally verified enrollment is reflected correctly
self.assertEqual(response_data['verified_in']['onboarding_status'], 'verified')
self.assertEqual(response_data['verified_in']['course_id'], other_course_id)
# assert that most recent enrollment (current status) has other_course_approved status
self.assertEqual(response_data['current_status']['onboarding_status'], 'other_course_approved')
self.assertEqual(response_data['current_status']['course_id'], self.course_id)
class ResetCourseViewTestBase(SupportViewTestCase):
"""
Shared base class for course reset view tests
"""
def _url(self, username):
""" Helper to generate URL """
return reverse("support:course_reset", kwargs={'username_or_email': username})
def setUp(self):
"""
Set permissions, create an open course and learner, enroll learner and opt into course reset
"""
super().setUp()
SupportStaffRole().add_users(self.user)
self.now = datetime.now().replace(tzinfo=UTC)
self.course = CourseFactory.create(
start=self.now - timedelta(days=90),
end=self.now + timedelta(days=90),
)
self.course_id = str(self.course.id)
self.course_overview = CourseOverview.get_from_id(self.course.id)
self.learner = UserFactory.create()
self.enrollment = CourseEnrollmentFactory.create(user=self.learner, course_id=self.course.id)
self.opt_in = CourseResetCourseOptInFactory.create(course_id=self.course.id)
@ddt.ddt
class TestResetCourseListView(ResetCourseViewTestBase):
""" Tests for the list endpoint for course reset """
def assert_course_ids(self, expected_course_ids, learner=None):
""" Helper that asserts the course ids that will be returned from the listing endpoint """
learner = learner or self.learner
response = self.client.get(self._url(learner))
self.assertEqual(response.status_code, 200)
actual_course_ids = [course['course_id'] for course in response.json()]
self.assertEqual(expected_course_ids, actual_course_ids)
def test_no_enrollments(self):
""" When a learner has no enrollments, the endpoint should return an empty list """
no_enrollment_learner = UserFactory.create()
self.assert_course_ids([], learner=no_enrollment_learner)
def test_not_opted_in(self):
"""
If a learner is enrolled in a course that is not opted into the course reset feature,
it will not be returned by the endpoint
"""
non_opted_in_course = CourseFactory.create()
CourseEnrollmentFactory.create(user=self.learner, course_id=non_opted_in_course.id)
self.assert_course_ids([self.course_id])
def test_deactivated_opt_in(self):
"""
If a learner is enrolled in a course that has opted in, but that opt-in is
deactivated, it will not be returned from the endpoint
"""
self.assert_course_ids([self.course_id])
self.opt_in.active = False
self.opt_in.save()
self.assert_course_ids([])
def test_deactivated_enrollment(self):
"""
If a learner's enrollment in an opted in course is deactivated,
the course will not be returned by the endpoint
"""
self.assert_course_ids([self.course_id])
self.enrollment.is_active = False
self.enrollment.save()
self.assert_course_ids([])
def assertResponse(self, expected_response):
""" Helper to assert the contents of the response from the listing endpoint """
response = self.client.get(self._url(self.learner))
self.assertEqual(response.status_code, 200)
actual_response = response.json()
self.assertEqual(expected_response, actual_response)
return actual_response
def test_course_not_started(self):
""" If a course is opted in but has not started, it should not be resettable """
self.course_overview.start = self.now + timedelta(days=10)
self.course_overview.end = self.now + timedelta(days=11)
self.course_overview.save()
self.assertResponse([{
'course_id': self.course_id,
'display_name': self.course_overview.display_name,
'can_reset': False,
'comment': '',
'status': 'Course Not Started'
}])
def test_course_ended(self):
""" If a course is opted in but has ended, it should not be resettable """
self.course_overview.start = self.now - timedelta(days=11)
self.course_overview.end = self.now - timedelta(days=10)
self.course_overview.save()
self.assertResponse([
{
'course_id': self.course_id,
'display_name': self.course_overview.display_name,
'can_reset': False,
'comment': '',
'status': 'Course Ended'
}
])
@patch('lms.djangoapps.support.views.course_reset.user_has_passing_grade_in_course', return_value=True)
def test_user_has_passing_grade(self, _):
""" If a course is opted in but the learner has a passing grade, it should not be resettable """
self.assertResponse([{
'course_id': self.course_id,
'display_name': self.course_overview.display_name,
'can_reset': False,
'comment': '',
'status': 'Learner Has Passing Grade'
}])
@patch('lms.djangoapps.support.views.course_reset.user_has_passing_grade_in_course', return_value=True)
def test_ended_with_passing_grade(self, _):
"""
If a course has ended and the learner has a passing grade,
the passing grade message should override the ended message
"""
self.course_overview.start = self.now - timedelta(days=11)
self.course_overview.end = self.now - timedelta(days=10)
self.assertResponse([{
'course_id': self.course_id,
'display_name': self.course_overview.display_name,
'can_reset': False,
'comment': '',
'status': 'Learner Has Passing Grade'
}])
def test_available_course(self):
""" If a course is opted in and had nothing stopping it from being reset, it should be resettable """
self.assertResponse([{
'course_id': self.course_id,
'display_name': self.course.display_name,
'can_reset': True,
'comment': '',
'status': 'Available'
}])
@ddt.unpack
@ddt.data(
(CourseResetAudit.CourseResetStatus.ENQUEUED, False),
(CourseResetAudit.CourseResetStatus.IN_PROGRESS, False),
(CourseResetAudit.CourseResetStatus.FAILED, True),
(CourseResetAudit.CourseResetStatus.COMPLETE, False),
)
def test_audit(self, audit_status, expected_can_reset):
"""
If a course enrollment has a CourseResetAudit associated with it,
it should not be resettable unless the audit is FAILED
"""
audit = CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
status=audit_status,
)
self.assertResponse([{
'course_id': self.course_id,
'display_name': self.course.display_name,
'can_reset': expected_can_reset,
'comment': audit.comment,
'status': audit.status_message()
}])
def _set_up_course(self, opt_in=True):
"""
Make a course, enroll self.learner, and optionally opt into course reset
"""
course = CourseFactory.create(start=self.course.start, end=self.course.end)
CourseEnrollmentFactory.create(course_id=course.id, user=self.learner)
if opt_in:
CourseResetCourseOptInFactory.create(course_id=course.id)
return course
def test_multiple_courses(self):
""" Test for the behavior of multiple courses """
# Create four opted in courses and four non-opted-in courses
opted_in_courses = [self._set_up_course(opt_in=True) for _ in range(4)]
for _ in range(4):
self._set_up_course(opt_in=False)
expected_response = [{
'course_id': self.course_id,
'display_name': self.course.display_name,
'can_reset': True,
'comment': '',
'status': 'Available'
}]
for course in opted_in_courses:
expected_response.append({
'course_id': str(course.id),
'display_name': course.display_name,
'comment': '',
'can_reset': True,
'status': 'Available'
})
self.assertResponse(expected_response)
def test_multiple_audits(self):
"""
If you have multiple audits for an enrollment (should only happen if process fails)
the information returned should be for the most recent ONLY
"""
daysago = lambda x: self.now - timedelta(days=x)
CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
status=CourseResetAudit.CourseResetStatus.FAILED,
created=daysago(3),
modified=daysago(3),
)
CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
status=CourseResetAudit.CourseResetStatus.FAILED,
created=daysago(2),
modified=daysago(2),
)
most_recent_audit = CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
status=CourseResetAudit.CourseResetStatus.IN_PROGRESS,
)
self.assertResponse([{
'course_id': self.course_id,
'display_name': self.course.display_name,
'can_reset': False,
'comment': most_recent_audit.comment,
'status': most_recent_audit.status_message()
}])
def test_multiple_failed_audits(self):
"""
If you have multiple audits for an enrollment and the most recent was a failure,
you should still be able to reset the course
"""
daysago = lambda x: self.now - timedelta(days=x)
CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
status=CourseResetAudit.CourseResetStatus.FAILED,
created=daysago(3),
modified=daysago(3),
)
CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
status=CourseResetAudit.CourseResetStatus.FAILED,
created=daysago(2),
modified=daysago(2),
)
most_recent_audit = CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
status=CourseResetAudit.CourseResetStatus.FAILED,
)
self.assertResponse([{
'course_id': self.course_id,
'display_name': self.course.display_name,
'can_reset': True,
'comment': most_recent_audit.comment,
'status': most_recent_audit.status_message()
}])
class TestResetCourseCreateView(ResetCourseViewTestBase):
"""
Tests for POST endpoint for performing course reset
"""
def request(self, username=None, course_id=None, comment=None):
""" Helper to perform request """
username = username or self.learner.username
return self.client.post(
self._url(username),
data={
"course_id": course_id if course_id else self.course_id,
"comment": comment if comment else ""
}
)
def assert_error_response(self, response, expected_status_code, expected_error_message):
""" Helper to assert status code and error message """
self.assertEqual(response.status_code, expected_status_code)
self.assertEqual(response.data['error'], expected_error_message)
def test_wrong_username(self):
""" A request with a username which does not exits returns 404 """
response = self.request(username='does_not_exist')
self.assert_error_response(response, 404, "User does not exist")
def test_invalid_course_id(self):
""" A request for an invalid course id returns 400 """
response = self.request(course_id='thisisnotacourseid')
self.assert_error_response(response, 400, "invalid course id")
def test_missing_course_id(self):
""" A request without a course id returns 400 """
response = self.client.post(self._url(self.learner.username))
self.assert_error_response(response, 400, "Must specify course id")
def test_course_not_opt_in(self):
""" A request for a course which isn't opted into the feature returns 404 """
self.opt_in.active = False
self.opt_in.save()
response = self.request()
self.assert_error_response(response, 404, "Course is not eligible")
def test_unenrolled(self):
""" A request for a learner who isn't enrolled in the given course returns a 404 """
self.enrollment.is_active = False
self.enrollment.save()
response = self.request()
self.assert_error_response(response, 404, "Learner is not enrolled in course")
@patch('lms.djangoapps.support.views.course_reset.can_enrollment_be_reset')
def test_cannot_reset(self, mock_can_reset):
""" A request for a course which isn't able to be reset returns a 404 """
mock_status = str(uuid4())
mock_can_reset.return_value = (False, mock_status)
response = self.request()
self.assert_error_response(response, 400, f"Cannot reset course: {mock_status}")
@patch('lms.djangoapps.support.views.course_reset.reset_student_course')
def test_learner_course_reset(self, mock_reset_student_course):
""" Happy path test """
comment = str(uuid4())
# A request for a given learner and course with a comment should return a 201
response = self.request(comment=comment)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data, {
'course_id': self.course_id,
'status': response.data['status'],
'can_reset': False,
'comment': comment,
'display_name': self.course.display_name
})
# The reset task should be queued
mock_reset_student_course.delay.assert_called_once_with(self.course_id, self.learner.email, self.user.email)
# And an audit should be created as ENQUEUED
self.assertEqual(
self.enrollment.courseresetaudit_set.first().status,
CourseResetAudit.CourseResetStatus.ENQUEUED
)
@patch('lms.djangoapps.support.views.course_reset.reset_student_course')
def test_course_reset_failed(self, mock_reset_student_course):
""" An audit that has failed previously should be able to be run successfully """
CourseResetAudit.objects.create(
course=self.opt_in,
course_enrollment=self.enrollment,
reset_by=self.user,
status=CourseResetAudit.CourseResetStatus.FAILED
)
response = self.request()
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data, {
'course_id': self.course_id,
'status': response.data['status'],
'can_reset': False,
'comment': '',
'display_name': self.course.display_name
})
mock_reset_student_course.delay.assert_called_once_with(self.course_id, self.learner.email, self.user.email)
def test_course_reset_already_reset(self):
""" A course that has an audit that hasn't failed should not be allowed to be run again """
additional_audit = CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
status=CourseResetAudit.CourseResetStatus.ENQUEUED
)
response = self.request()
self.assert_error_response(response, 400, f"Cannot reset course: {additional_audit.status_message()}")