# -*- coding: utf-8 -*- """ Test for lms courseware app, module render unit """ import ddt import itertools import json from functools import partial from bson import ObjectId from django.http import Http404, HttpResponse from django.core.urlresolvers import reverse from django.conf import settings from django.test.client import RequestFactory from django.contrib.auth.models import AnonymousUser from mock import MagicMock, patch, Mock from opaque_keys.edx.keys import UsageKey, CourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey from xblock.field_data import FieldData from xblock.runtime import Runtime from xblock.fields import ScopeIds from xblock.core import XBlock from capa.tests.response_xml_factory import OptionResponseXMLFactory from courseware import module_render as render from courseware.courses import get_course_with_access, course_image_url, get_course_info_section from courseware.model_data import FieldDataCache from courseware.module_render import hash_resource, get_module_for_descriptor from courseware.models import StudentModule from courseware.tests.factories import StudentModuleFactory, UserFactory, GlobalStaffFactory from courseware.tests.tests import LoginEnrollmentTestCase from courseware.tests.test_submitting_problems import TestSubmittingProblems from lms.djangoapps.lms_xblock.runtime import quote_slashes from student.models import anonymous_id_for_user from xmodule.modulestore.tests.django_utils import ( TEST_DATA_MIXED_TOY_MODULESTORE, TEST_DATA_XML_MODULESTORE, ) from xmodule.lti_module import LTIDescriptor from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory, check_mongo_calls from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW, CombinedSystem TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT @XBlock.needs("field-data") @XBlock.needs("i18n") @XBlock.needs("fs") @XBlock.needs("user") class PureXBlock(XBlock): """ Pure XBlock to use in tests. """ pass class EmptyXModule(XModule): # pylint: disable=abstract-method """ Empty XModule for testing with no dependencies. """ pass class EmptyXModuleDescriptor(XModuleDescriptor): # pylint: disable=abstract-method """ Empty XModule for testing with no dependencies. """ module_class = EmptyXModule @ddt.ddt class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Tests of courseware.module_render """ # TODO: this test relies on the specific setup of the toy course. # It should be rewritten to build the course it needs and then test that. def setUp(self): """ Set up the course and user context """ super(ModuleRenderTestCase, self).setUp() self.course_key = self.create_toy_course() self.toy_course = modulestore().get_course(self.course_key) self.mock_user = UserFactory() self.mock_user.id = 1 self.request_factory = RequestFactory() # Construct a mock module for the modulestore to return self.mock_module = MagicMock() self.mock_module.id = 1 self.dispatch = 'score_update' # Construct a 'standard' xqueue_callback url self.callback_url = reverse('xqueue_callback', kwargs=dict(course_id=self.course_key.to_deprecated_string(), userid=str(self.mock_user.id), mod_id=self.mock_module.id, dispatch=self.dispatch)) def test_get_module(self): self.assertEqual( None, render.get_module('dummyuser', None, 'invalid location', None) ) def test_module_render_with_jump_to_id(self): """ This test validates that the /jump_to_id/ shorthand for intracourse linking works assertIn expected. Note there's a HTML element in the 'toy' course with the url_name 'toyjumpto' which defines this linkage """ mock_request = MagicMock() mock_request.user = self.mock_user course = get_course_with_access(self.mock_user, 'load', self.course_key) field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.course_key, self.mock_user, course, depth=2) module = render.get_module( self.mock_user, mock_request, self.course_key.make_usage_key('html', 'toyjumpto'), field_data_cache, ) # get the rendered HTML output which should have the rewritten link html = module.render(STUDENT_VIEW).content # See if the url got rewritten to the target link # note if the URL mapping changes then this assertion will break self.assertIn('/courses/' + self.course_key.to_deprecated_string() + '/jump_to_id/vertical_test', html) def test_xqueue_callback_success(self): """ Test for happy-path xqueue_callback """ fake_key = 'fake key' xqueue_header = json.dumps({'lms_key': fake_key}) data = { 'xqueue_header': xqueue_header, 'xqueue_body': 'hello world', } # Patch getmodule to return our mock module with patch('courseware.module_render.find_target_student_module') as get_fake_module: get_fake_module.return_value = self.mock_module # call xqueue_callback with our mocked information request = self.request_factory.post(self.callback_url, data) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) # Verify that handle ajax is called with the correct data request.POST['queuekey'] = fake_key self.mock_module.handle_ajax.assert_called_once_with(self.dispatch, request.POST) def test_xqueue_callback_missing_header_info(self): data = { 'xqueue_header': '{}', 'xqueue_body': 'hello world', } with patch('courseware.module_render.find_target_student_module') as get_fake_module: get_fake_module.return_value = self.mock_module # Test with missing xqueue data with self.assertRaises(Http404): request = self.request_factory.post(self.callback_url, {}) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) # Test with missing xqueue_header with self.assertRaises(Http404): request = self.request_factory.post(self.callback_url, data) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) def test_get_score_bucket(self): self.assertEquals(render.get_score_bucket(0, 10), 'incorrect') self.assertEquals(render.get_score_bucket(1, 10), 'partial') self.assertEquals(render.get_score_bucket(10, 10), 'correct') # get_score_bucket calls error cases 'incorrect' self.assertEquals(render.get_score_bucket(11, 10), 'incorrect') self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect') def test_anonymous_handle_xblock_callback(self): dispatch_url = reverse( 'xblock_handler', args=[ self.course_key.to_deprecated_string(), quote_slashes(self.course_key.make_usage_key('videosequence', 'Toy_Videos').to_deprecated_string()), 'xmodule_handler', 'goto_position' ] ) response = self.client.post(dispatch_url, {'position': 2}) self.assertEquals(403, response.status_code) self.assertEquals('Unauthenticated', response.content) def test_missing_position_handler(self): """ Test that sending POST request without or invalid position argument don't raise server error """ self.client.login(username=self.mock_user.username, password="test") dispatch_url = reverse( 'xblock_handler', args=[ self.course_key.to_deprecated_string(), quote_slashes(self.course_key.make_usage_key('videosequence', 'Toy_Videos').to_deprecated_string()), 'xmodule_handler', 'goto_position' ] ) response = self.client.post(dispatch_url) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': ''}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': '-1'}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': "string"}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': u"Φυσικά"}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) response = self.client.post(dispatch_url, {'position': None}) self.assertEqual(200, response.status_code) self.assertEqual(json.loads(response.content), {'success': True}) @ddt.data('pure', 'vertical') @XBlock.register_temp_plugin(PureXBlock, identifier='pure') def test_rebinding_same_user(self, block_type): request = self.request_factory.get('') request.user = self.mock_user course = CourseFactory() descriptor = ItemFactory(category=block_type, parent=course) field_data_cache = FieldDataCache([self.toy_course, descriptor], self.toy_course.id, self.mock_user) render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id) render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id) def test_hash_resource(self): """ Ensure that the resource hasher works and does not fail on unicode, decoded or otherwise. """ resources = ['ASCII text', u'❄ I am a special snowflake.', "❄ So am I, but I didn't tell you."] self.assertEqual(hash_resource(resources), 'a76e27c8e80ca3efd7ce743093aa59e0') class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test the handle_xblock_callback function """ def setUp(self): super(TestHandleXBlockCallback, self).setUp() self.course_key = self.create_toy_course() self.location = self.course_key.make_usage_key('chapter', 'Overview') self.toy_course = modulestore().get_course(self.course_key) self.mock_user = UserFactory() self.mock_user.id = 1 self.request_factory = RequestFactory() # Construct a mock module for the modulestore to return self.mock_module = MagicMock() self.mock_module.id = 1 self.dispatch = 'score_update' # Construct a 'standard' xqueue_callback url self.callback_url = reverse( 'xqueue_callback', kwargs={ 'course_id': self.course_key.to_deprecated_string(), 'userid': str(self.mock_user.id), 'mod_id': self.mock_module.id, 'dispatch': self.dispatch } ) def _mock_file(self, name='file', size=10): """Create a mock file object for testing uploads""" mock_file = MagicMock( size=size, read=lambda: 'x' * size ) # We can't use `name` as a kwarg to Mock to set the name attribute # because mock uses `name` to name the mock itself mock_file.name = name return mock_file def test_invalid_location(self): request = self.request_factory.post('dummy_url', data={'position': 1}) request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), 'invalid Location', 'dummy_handler' 'dummy_dispatch' ) def test_too_many_files(self): request = self.request_factory.post( 'dummy_url', data={'file_id': (self._mock_file(), ) * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)} ) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'dummy_handler' ).content, json.dumps({ 'success': 'Submission aborted! Maximum %d files may be submitted at once' % settings.MAX_FILEUPLOADS_PER_INPUT }, indent=2) ) def test_too_large_file(self): inputfile = self._mock_file(size=1 + settings.STUDENT_FILEUPLOAD_MAX_SIZE) request = self.request_factory.post( 'dummy_url', data={'file_id': inputfile} ) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'dummy_handler' ).content, json.dumps({ 'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2)) }, indent=2) ) def test_xmodule_dispatch(self): request = self.request_factory.post('dummy_url', data={'position': 1}) request.user = self.mock_user response = render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'goto_position', ) self.assertIsInstance(response, HttpResponse) def test_bad_course_id(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, 'bad_course_id', quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'goto_position', ) def test_bad_location(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.course_key.make_usage_key('chapter', 'bad_location').to_deprecated_string()), 'xmodule_handler', 'goto_position', ) def test_bad_xmodule_dispatch(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'bad_dispatch', ) def test_missing_handler(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'bad_handler', 'bad_dispatch', ) @patch.dict('django.conf.settings.FEATURES', {'ENABLE_XBLOCK_VIEW_ENDPOINT': True}) def test_xblock_view_handler(self): args = [ 'edX/toy/2012_Fall', quote_slashes('i4x://edX/toy/videosequence/Toy_Videos'), 'student_view' ] xblock_view_url = reverse( 'xblock_view', args=args ) request = self.request_factory.get(xblock_view_url) request.user = self.mock_user response = render.xblock_view(request, *args) self.assertEquals(200, response.status_code) expected = ['csrf_token', 'html', 'resources'] content = json.loads(response.content) for section in expected: self.assertIn(section, content) self.assertIn('