""" Unit tests for course import and export Celery tasks """ import copy import json from unittest import mock from uuid import uuid4 from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.test.utils import override_settings from opaque_keys.edx.locator import CourseLocator from organizations.models import OrganizationCourse from organizations.tests.factories import OrganizationFactory from user_tasks.models import UserTaskArtifact, UserTaskStatus from cms.djangoapps.contentstore.tasks import export_olx, rerun_course from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase from cms.djangoapps.contentstore.tests.utils import CourseTestCase from common.djangoapps.course_action_state.models import CourseRerunState from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.embargo.models import Country, CountryAccessRule, RestrictedCourse from xmodule.modulestore.django import modulestore TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex def side_effect_exception(*args, **kwargs): """ Side effect for mocking which raises an exception """ raise Exception('Boom!') @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ExportCourseTestCase(CourseTestCase): """ Tests of the export_olx task applied to courses """ def test_success(self): """ Verify that a routine course export task succeeds """ key = str(self.course.location.course_key) result = export_olx.delay(self.user.id, key, 'en') status = UserTaskStatus.objects.get(task_id=result.id) self.assertEqual(status.state, UserTaskStatus.SUCCEEDED) artifacts = UserTaskArtifact.objects.filter(status=status) self.assertEqual(len(artifacts), 1) output = artifacts[0] self.assertEqual(output.name, 'Output') @mock.patch('cms.djangoapps.contentstore.tasks.export_course_to_xml', side_effect=side_effect_exception) def test_exception(self, mock_export): # pylint: disable=unused-argument """ The export task should fail gracefully if an exception is thrown """ key = str(self.course.location.course_key) result = export_olx.delay(self.user.id, key, 'en') self._assert_failed(result, json.dumps({'raw_error_msg': 'Boom!'})) @mock.patch('cms.djangoapps.contentstore.tasks.User.objects.get', side_effect=User.DoesNotExist) def test_invalid_user_id(self, mock_raise_exc): # pylint: disable=unused-argument """ Verify that attempts to export a course as an invalid user fail """ user = UserFactory(id=User.objects.order_by('-id').first().pk + 100) key = str(self.course.location.course_key) result = export_olx.delay(user.id, key, 'en') self._assert_failed(result, f'Unknown User ID: {user.id}') def test_non_course_author(self): """ Verify that users who aren't authors of the course are unable to export it """ _, nonstaff_user = self.create_non_staff_authed_user_client() key = str(self.course.location.course_key) result = export_olx.delay(nonstaff_user.id, key, 'en') self._assert_failed(result, 'Permission denied') def _assert_failed(self, task_result, error_message): """ Verify that a task failed with the specified error message """ status = UserTaskStatus.objects.get(task_id=task_result.id) self.assertEqual(status.state, UserTaskStatus.FAILED) artifacts = UserTaskArtifact.objects.filter(status=status) self.assertEqual(len(artifacts), 1) error = artifacts[0] self.assertEqual(error.name, 'Error') self.assertEqual(error.text, error_message) @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ExportLibraryTestCase(LibraryTestCase): """ Tests of the export_olx task applied to libraries """ def test_success(self): """ Verify that a routine library export task succeeds """ key = str(self.lib_key) result = export_olx.delay(self.user.id, key, 'en') status = UserTaskStatus.objects.get(task_id=result.id) self.assertEqual(status.state, UserTaskStatus.SUCCEEDED) artifacts = UserTaskArtifact.objects.filter(status=status) self.assertEqual(len(artifacts), 1) output = artifacts[0] self.assertEqual(output.name, 'Output') @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class RerunCourseTaskTestCase(CourseTestCase): # lint-amnesty, pylint: disable=missing-class-docstring def _rerun_course(self, old_course_key, new_course_key): CourseRerunState.objects.initiated(old_course_key, new_course_key, self.user, 'Test Re-run') rerun_course(str(old_course_key), str(new_course_key), self.user.id) def test_success(self): """ The task should clone the OrganizationCourse and RestrictedCourse data. """ old_course_key = self.course.id new_course_key = CourseLocator(org=old_course_key.org, course=old_course_key.course, run='rerun') old_course_id = str(old_course_key) new_course_id = str(new_course_key) organization = OrganizationFactory(short_name=old_course_key.org) OrganizationCourse.objects.create(course_id=old_course_id, organization=organization) restricted_course = RestrictedCourse.objects.create(course_key=self.course.id) restricted_country = Country.objects.create(country='US') CountryAccessRule.objects.create( rule_type=CountryAccessRule.BLACKLIST_RULE, restricted_course=restricted_course, country=restricted_country ) # Run the task! self._rerun_course(old_course_key, new_course_key) # Verify the new course run exists course = modulestore().get_course(new_course_key) self.assertIsNotNone(course) # Verify the OrganizationCourse is cloned self.assertEqual(OrganizationCourse.objects.count(), 2) # This will raise an error if the OrganizationCourse object was not cloned OrganizationCourse.objects.get(course_id=new_course_id, organization=organization) # Verify the RestrictedCourse and related objects are cloned self.assertEqual(RestrictedCourse.objects.count(), 2) restricted_course = RestrictedCourse.objects.get(course_key=new_course_key) self.assertEqual(CountryAccessRule.objects.count(), 2) CountryAccessRule.objects.get( rule_type=CountryAccessRule.BLACKLIST_RULE, restricted_course=restricted_course, country=restricted_country )