""" Tests student admin.py """ import datetime from unittest.mock import Mock import ddt import pytest from django.contrib.admin.sites import AdminSite from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.forms import ValidationError from django.test import TestCase, override_settings from django.urls import reverse from django.utils.timezone import now from edx_toggles.toggles.testutils import override_waffle_switch from pytz import UTC from common.djangoapps.student.admin import ( # lint-amnesty, pylint: disable=line-too-long COURSE_ENROLLMENT_ADMIN_SWITCH, AllowedAuthUserForm, CourseEnrollmentForm, UserAdmin ) from common.djangoapps.student.models import AllowedAuthUser, CourseEnrollment, LoginFailures from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory class AdminCourseRolesPageTest(SharedModuleStoreTestCase): """Test the django admin course roles form saving data in db. """ @classmethod def setUpClass(cls): super().setUpClass() cls.course = CourseFactory.create(org='edx') def setUp(self): super().setUp() self.user = UserFactory.create(is_staff=True, is_superuser=True) self.user.save() def test_save_valid_data(self): data = { 'course_id': str(self.course.id), 'role': 'finance_admin', 'org': 'edx', 'email': self.user.email } self.client.login(username=self.user.username, password='test') # # adding new role from django admin page response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data) self.assertRedirects(response, reverse('admin:student_courseaccessrole_changelist')) response = self.client.get(reverse('admin:student_courseaccessrole_changelist')) self.assertContains(response, 'Select course access role to change') self.assertContains(response, 'Add course access role') self.assertContains(response, 'finance_admin') self.assertContains(response, str(self.course.id)) self.assertContains(response, '1 course access role') #try adding with same information raise error. response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data) self.assertContains(response, 'Duplicate') def test_save_without_org_and_course_data(self): data = { 'role': 'staff', 'email': self.user.email, 'course_id': str(self.course.id) } self.client.login(username=self.user.username, password='test') # # adding new role from django admin page response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data) self.assertRedirects(response, reverse('admin:student_courseaccessrole_changelist')) response = self.client.get(reverse('admin:student_courseaccessrole_changelist')) self.assertContains(response, 'staff') self.assertContains(response, '1 course access role') def test_save_with_course_only(self): data = { 'role': 'beta_testers', 'email': self.user.email, } self.client.login(username=self.user.username, password='test') # # adding new role from django admin page response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data) self.assertRedirects(response, reverse('admin:student_courseaccessrole_changelist')) response = self.client.get(reverse('admin:student_courseaccessrole_changelist')) self.assertContains(response, 'beta_testers') self.assertContains(response, '1 course access role') def test_save_with_org_only(self): data = { 'role': 'beta_testers', 'email': self.user.email, 'org': 'myorg' } self.client.login(username=self.user.username, password='test') # # adding new role from django admin page response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data) self.assertRedirects(response, reverse('admin:student_courseaccessrole_changelist')) response = self.client.get(reverse('admin:student_courseaccessrole_changelist')) self.assertContains(response, 'myorg') self.assertContains(response, '1 course access role') def test_save_with_invalid_course(self): course = 'no/edx/course' email = "invalid@email.com" data = { 'course_id': course, 'role': 'finance_admin', 'org': 'edx', 'email': email } self.client.login(username=self.user.username, password='test') # Adding new role with invalid data response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data) self.assertContains( response, 'Course not found. Entered course id was: "{}".'.format( course ) ) self.assertContains( response, "Email does not exist. Could not find {}. Please re-enter email address".format( email ) ) def test_save_valid_course_invalid_org(self): data = { 'course_id': str(self.course.id), 'role': 'finance_admin', 'org': 'edxxx', 'email': self.user.email } self.client.login(username=self.user.username, password='test') # # adding new role from django admin page response = self.client.post(reverse('admin:student_courseaccessrole_add'), data=data) self.assertContains( response, 'Org name {} is not valid. Valid name is {}.'.format( 'edxxx', 'edx' ) ) class AdminUserPageTest(TestCase): """ Unit tests for the UserAdmin view. """ def setUp(self): super().setUp() self.admin = UserAdmin(User, AdminSite()) def test_username_is_writable_for_user_creation(self): """ Ensures that the username is not readonly, when admin creates new user. """ request = Mock() assert 'username' not in self.admin.get_readonly_fields(request) def test_username_is_readonly_for_user(self): """ Ensures that the username field is readonly, when admin open user which already exists. This hook used for skip Django validation in the `auth_user_change` view. Changing the username is still possible using the database or from the model directly. However, changing the username might cause issues with the logs and/or the cs_comments_service since it stores the username in a different database. """ request = Mock() user = Mock() assert 'username' in self.admin.get_readonly_fields(request, user) @ddt.ddt class CourseEnrollmentAdminTest(SharedModuleStoreTestCase): """ Unit tests for the CourseEnrollmentAdmin view. """ ADMIN_URLS = ( ('get', reverse('admin:student_courseenrollment_add')), ('get', reverse('admin:student_courseenrollment_changelist')), ('get', reverse('admin:student_courseenrollment_change', args=(1,))), ('get', reverse('admin:student_courseenrollment_delete', args=(1,))), ('post', reverse('admin:student_courseenrollment_add')), ('post', reverse('admin:student_courseenrollment_changelist')), ('post', reverse('admin:student_courseenrollment_change', args=(1,))), ('post', reverse('admin:student_courseenrollment_delete', args=(1,))), ) def setUp(self): super().setUp() self.user = UserFactory.create(is_staff=True, is_superuser=True) self.course = CourseFactory() self.course_enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, # pylint: disable=no-member ) self.client.login(username=self.user.username, password='test') @ddt.data(*ADMIN_URLS) @ddt.unpack def test_view_disabled(self, method, url): """ All CourseEnrollmentAdmin views are disabled by default. """ response = getattr(self.client, method)(url) assert response.status_code == 403 @ddt.data(*ADMIN_URLS) @ddt.unpack def test_view_enabled(self, method, url): """ Ensure CourseEnrollmentAdmin views can be enabled with the waffle switch. """ with override_waffle_switch(COURSE_ENROLLMENT_ADMIN_SWITCH, active=True): response = getattr(self.client, method)(url) assert response.status_code == 200 def test_username_exact_match(self): """ Ensure that course enrollment searches return exact matches on username first. """ user2 = UserFactory.create(username=f'aaa_{self.user.username}') CourseEnrollmentFactory( user=user2, course_id=self.course.id, # pylint: disable=no-member ) search_url = '{}?q={}'.format(reverse('admin:student_courseenrollment_changelist'), self.user.username) with override_waffle_switch(COURSE_ENROLLMENT_ADMIN_SWITCH, active=True): response = self.client.get(search_url) assert response.status_code == 200 # context['results'] is an array of arrays of HTML elements to be rendered assert len(response.context['results']) == 2 for idx, username in enumerate([self.user.username, user2.username]): # Locate the column containing the username user_field = next(col for col in response.context['results'][idx] if "field-user" in col) assert username in user_field def test_save_toggle_active(self): """ Edit a CourseEnrollment to toggle its is_active checkbox, save it and verify that it was toggled. When the form is saved, Django uses a QueryDict object which is immutable and needs special treatment. This test implicitly verifies that the POST parameters are handled correctly. """ # is_active will change from True to False assert self.course_enrollment.is_active data = { 'user': str(self.course_enrollment.user.id), 'course': str(self.course_enrollment.course.id), 'is_active': 'false', 'mode': self.course_enrollment.mode, } with override_waffle_switch(COURSE_ENROLLMENT_ADMIN_SWITCH, active=True): response = self.client.post( reverse('admin:student_courseenrollment_change', args=(self.course_enrollment.id, )), data=data, ) assert response.status_code == 302 self.course_enrollment.refresh_from_db() assert not self.course_enrollment.is_active def test_save_invalid_course_id(self): """ Send an invalid course ID instead of "org.0/course_0/Run_0" when saving, and verify that it fails. """ data = { 'user': str(self.course_enrollment.user.id), 'course': 'invalid-course-id', 'is_active': 'true', 'mode': self.course_enrollment.mode, } with override_waffle_switch(COURSE_ENROLLMENT_ADMIN_SWITCH, active=True): with pytest.raises(ValidationError): self.client.post( reverse('admin:student_courseenrollment_change', args=(self.course_enrollment.id, )), data=data, ) @ddt.ddt class LoginFailuresAdminTest(TestCase): """Test Login Failures Admin.""" @classmethod def setUpClass(cls): """Setup class""" super().setUpClass() cls.user = UserFactory.create(username='§', is_staff=True, is_superuser=True) cls.user.save() def setUp(self): """Setup.""" super().setUp() self.client.login(username=self.user.username, password='test') self.user2 = UserFactory.create(username='Zażółć gęślą jaźń') self.user_lockout_until = datetime.datetime.now(UTC) LoginFailures.objects.create(user=self.user, failure_count=10, lockout_until=self.user_lockout_until) LoginFailures.objects.create(user=self.user2, failure_count=2) def tearDown(self): """Tear Down.""" super().tearDown() LoginFailures.objects.all().delete() def test_unicode_username(self): """ Test if `__str__` method behaves correctly for unicode username. It shouldn't raise `TypeError`. """ assert str(LoginFailures.objects.get(user=self.user)) == f'§: 10 - {self.user_lockout_until.isoformat()}' assert str(LoginFailures.objects.get(user=self.user2)) == 'Zażółć gęślą jaźń: 2 - -' @override_settings(FEATURES={'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': True}) def test_feature_enabled(self): url = reverse('admin:student_loginfailures_changelist') response = self.client.get(url) assert response.status_code == 200 @ddt.data( reverse('admin:student_loginfailures_changelist'), reverse('admin:student_loginfailures_add'), reverse('admin:student_loginfailures_change', args=(1,)), reverse('admin:student_loginfailures_delete', args=(1,)), ) def test_feature_disabled(self, url): """Test if feature is disabled there's no access to the admin module.""" response = self.client.get(url) assert response.status_code == 403 response = self.client.post(url) assert response.status_code == 403 @override_settings(FEATURES={'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': True}) def test_unlock_student_accounts(self): """Test batch unlock student accounts.""" url = reverse('admin:student_loginfailures_changelist') self.client.post( url, data={ 'action': 'unlock_student_accounts', '_selected_action': [str(o.pk) for o in LoginFailures.objects.all()] }, follow=True ) count = LoginFailures.objects.count() assert count == 0 @override_settings(FEATURES={'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': True}) def test_unlock_account(self): """Test unlock single student account.""" url = reverse('admin:student_loginfailures_change', args=(1, )) start_count = LoginFailures.objects.count() self.client.post( url, data={'_unlock': 1} ) count = LoginFailures.objects.count() assert count == (start_count - 1) class CourseEnrollmentAdminFormTest(SharedModuleStoreTestCase): """ Unit test for CourseEnrollment admin ModelForm. """ @classmethod def setUpClass(cls): super().setUpClass() cls.course = CourseOverviewFactory(start=now()) def setUp(self): super().setUp() self.user = UserFactory.create() def test_admin_model_form_create(self): """ Test CourseEnrollmentAdminForm creation. """ assert CourseEnrollment.objects.count() == 0 form = CourseEnrollmentForm({ 'user': self.user.id, 'course': str(self.course.id), 'is_active': True, 'mode': 'audit', }) assert form.is_valid() enrollment = form.save() assert CourseEnrollment.objects.count() == 1 assert CourseEnrollment.objects.first() == enrollment def test_admin_model_form_update(self): """ Test CourseEnrollmentAdminForm update. """ enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course.id) count = CourseEnrollment.objects.count() form = CourseEnrollmentForm({ 'user': self.user.id, 'course': str(self.course.id), 'is_active': False, 'mode': 'audit'}, instance=enrollment ) assert form.is_valid() course_enrollment = form.save() assert count == CourseEnrollment.objects.count() assert not course_enrollment.is_active assert enrollment.id == course_enrollment.id class AllowedAuthUserFormTest(SiteMixin, TestCase): """ Unit test for AllowedAuthUserAdmin's ModelForm. """ @classmethod def setUpClass(cls): super().setUpClass() cls.email_domain_name = "dummy.com" cls.email_with_wrong_domain = "dummy@example.com" cls.valid_email = f"dummy@{cls.email_domain_name}" cls.other_valid_email = f"dummy1@{cls.email_domain_name}" UserFactory(email=cls.valid_email) UserFactory(email=cls.email_with_wrong_domain) def _update_site_configuration(self): """ Updates the site's configuration """ self.site.configuration.site_values = {'THIRD_PARTY_AUTH_ONLY_DOMAIN': self.email_domain_name} self.site.configuration.save() def _assert_form(self, site, email, is_valid_form=False): """ Asserts the form and returns the error if its not valid and instance if its valid """ error = '' instance = None form = AllowedAuthUserForm({'site': site.id, 'email': email}) if is_valid_form: assert form.is_valid() instance = form.save() else: assert not form.is_valid() error = form.errors['email'][0] return error, instance def test_form_with_invalid_site_configuration(self): """ Test form with wrong site's configuration. """ error, _ = self._assert_form(self.site, self.valid_email) assert error == "Please add a key/value 'THIRD_PARTY_AUTH_ONLY_DOMAIN/{site_email_domain}'" \ " in SiteConfiguration model's site_values field." def test_form_with_invalid_domain_name(self): """ Test form with email which has wrong email domain. """ self._update_site_configuration() error, _ = self._assert_form(self.site, self.email_with_wrong_domain) assert error == f"Email doesn't have {self.email_domain_name} domain name." def test_form_with_invalid_user(self): """ Test form with an email which is not associated with any user. """ self._update_site_configuration() error, _ = self._assert_form(self.site, self.other_valid_email) assert error == "User with this email doesn't exist in system." def test_form_creation(self): """ Test AllowedAuthUserForm creation. """ self._update_site_configuration() _, allowed_auth_user = self._assert_form(self.site, self.valid_email, is_valid_form=True) db_allowed_auth_user = AllowedAuthUser.objects.all().first() assert db_allowed_auth_user.site.id == allowed_auth_user.site.id assert db_allowed_auth_user.email == allowed_auth_user.email def test_form_update(self): """ Test AllowedAuthUserForm update. """ self._update_site_configuration() UserFactory(email=self.other_valid_email) _, allowed_auth_user = self._assert_form(self.site, self.valid_email, is_valid_form=True) assert AllowedAuthUser.objects.all().count() == 1 # update the object with new instance. form = AllowedAuthUserForm({'site': self.site.id, 'email': self.other_valid_email}, instance=allowed_auth_user) assert form.is_valid() form.save() db_allowed_auth_user = AllowedAuthUser.objects.all().first() assert AllowedAuthUser.objects.all().count() == 1 assert db_allowed_auth_user.email == self.other_valid_email