diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index a3414266bd47..b2e748fd5556 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -327,6 +327,7 @@ def test_change_after_first_sync(self): self.assertEqual(html_block.data, data_value) def test_refreshes_children_if_libraries_change(self): + """ Tests that children are automatically refreshed if libraries list changes """ library2key = self._create_library("org2", "lib2", "Library2") library2 = modulestore().get_library(library2key) data1, data2 = "Hello world!", "Hello other world!" @@ -370,6 +371,7 @@ def test_refreshes_children_if_libraries_change(self): self.assertEqual(html_block.data, data2) def test_refreshes_children_if_capa_type_change(self): + """ Tests that children are automatically refreshed if capa type field changes """ name1, name2 = "Option Problem", "Multiple Choice Problem" ItemFactory.create( category="problem", @@ -419,6 +421,26 @@ def test_refreshes_children_if_capa_type_change(self): html_block = modulestore().get_item(lc_block.children[0]) self.assertEqual(html_block.display_name, name2) + def test_refresh_fails_for_unknown_library(self): + """ Tests that refresh children fails if unknown library is configured """ + # Create a course: + with modulestore().default_store(ModuleStoreEnum.Type.split): + course = CourseFactory.create() + + # Add a LibraryContent block to the course: + lc_block = self._add_library_content_block(course, self.lib_key) + lc_block = self._refresh_children(lc_block) + self.assertEqual(len(lc_block.children), 0) + + # Now, change the block settings to have an invalid library key: + resp = self._update_item( + lc_block.location, + {"source_libraries": [["library-v1:NOT+FOUND", None]]}, + ) + self.assertEqual(resp.status_code, 200) + with self.assertRaises(ValueError): + self._refresh_children(lc_block, status_code_expected=400) + @ddt.ddt class TestLibraryAccess(LibraryTestCase): diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 6b595ae00755..69898bfbe2fb 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import ddt -from mock import patch +from mock import patch, Mock, PropertyMock from pytz import UTC from webob import Response @@ -18,6 +18,7 @@ component_handler, get_component_templates ) + from contentstore.views.item import create_xblock_info, ALWAYS, VisibilityState, _xblock_type_and_display_name from contentstore.tests.utils import CourseTestCase from student.tests.factories import UserFactory @@ -86,12 +87,18 @@ def _create_vertical(self, parent_usage_key=None): class GetItemTest(ItemTest): """Tests for '/xblock' GET url.""" - def _get_container_preview(self, usage_key): + def _get_preview(self, usage_key, data=None): + """ Makes a request to xblock preview handler """ + preview_url = reverse_usage_url("xblock_view_handler", usage_key, {'view_name': 'container_preview'}) + data = data if data else {} + resp = self.client.get(preview_url, data, HTTP_ACCEPT='application/json') + return resp + + def _get_container_preview(self, usage_key, data=None): """ Returns the HTML and resources required for the xblock at the specified UsageKey """ - preview_url = reverse_usage_url("xblock_view_handler", usage_key, {'view_name': 'container_preview'}) - resp = self.client.get(preview_url, HTTP_ACCEPT='application/json') + resp = self._get_preview(usage_key, data) self.assertEqual(resp.status_code, 200) resp_content = json.loads(resp.content) html = resp_content['html'] @@ -100,6 +107,14 @@ def _get_container_preview(self, usage_key): self.assertIsNotNone(resources) return html, resources + def _get_container_preview_with_error(self, usage_key, expected_code, data=None, content_contains=None): + """ Make request and asserts on response code and response contents """ + resp = self._get_preview(usage_key, data) + self.assertEqual(resp.status_code, expected_code) + if content_contains: + self.assertIn(content_contains, resp.content) + return resp + @ddt.data( (1, 21, 23, 35, 37), (2, 22, 24, 38, 39), @@ -247,6 +262,40 @@ def test_split_test_edited(self): self.assertIn('New_NAME_A', html) self.assertIn('New_NAME_B', html) + def test_valid_paging(self): + """ + Tests that valid paging is passed along to underlying block + """ + with patch('contentstore.views.item.get_preview_fragment') as patched_get_preview_fragment: + retval = Mock() + type(retval).content = PropertyMock(return_value="Some content") + type(retval).resources = PropertyMock(return_value=[]) + patched_get_preview_fragment.return_value = retval + + root_usage_key = self._create_vertical() + _, _ = self._get_container_preview( + root_usage_key, + {'enable_paging': 'true', 'page_number': 0, 'page_size': 2} + ) + call_args = patched_get_preview_fragment.call_args[0] + _, _, context = call_args + self.assertIn('paging', context) + self.assertEqual({'page_number': 0, 'page_size': 2}, context['paging']) + + @ddt.data([1, 'invalid'], ['invalid', 2]) + @ddt.unpack + def test_invalid_paging(self, page_number, page_size): + """ + Tests that valid paging is passed along to underlying block + """ + root_usage_key = self._create_vertical() + self._get_container_preview_with_error( + root_usage_key, + 400, + data={'enable_paging': 'true', 'page_number': page_number, 'page_size': page_size}, + content_contains="Couldn't parse paging parameters" + ) + class DeleteItem(ItemTest): """Tests for '/xblock' DELETE url.""" diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py b/common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py index ef8a6d4d69ca..7381692239af 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py @@ -6,14 +6,11 @@ """ from bson.objectid import ObjectId import ddt -from mock import patch from opaque_keys.edx.locator import LibraryLocator -from xblock.fragment import Fragment -from xblock.runtime import Runtime as VanillaRuntime + from xmodule.modulestore.exceptions import DuplicateCourseError from xmodule.modulestore.tests.factories import LibraryFactory, ItemFactory, check_mongo_calls from xmodule.modulestore.tests.utils import MixedSplitTestCase -from xmodule.x_module import AUTHOR_VIEW @ddt.ddt @@ -179,34 +176,6 @@ def test_get_lib_version(self): version = lib.location.library_key.version_guid self.assertIsInstance(version, ObjectId) - @patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.render', VanillaRuntime.render) - def test_library_author_view(self): - """ - Test that LibraryRoot.author_view can run and includes content from its - children. - We have to patch the runtime (module system) in order to be able to - render blocks in our test environment. - """ - library = LibraryFactory.create(modulestore=self.store) - # Add one HTML block to the library: - ItemFactory.create( - category="html", - parent_location=library.location, - user_id=self.user_id, - publish_item=False, - modulestore=self.store, - ) - library = self.store.get_library(library.location.library_key) - - context = {'reorderable_items': set(), } - # Patch the HTML block to always render "Hello world" - message = u"Hello world" - hello_render = lambda _, context: Fragment(message) - with patch('xmodule.html_module.HtmlDescriptor.author_view', hello_render, create=True): - with patch('xmodule.x_module.DescriptorSystem.applicable_aside_types', lambda self, block: []): - result = library.render(AUTHOR_VIEW, context) - self.assertIn(message, result.content) - def test_xblock_in_lib_have_published_version_returns_false(self): library = LibraryFactory.create(modulestore=self.store) block = ItemFactory.create( diff --git a/common/lib/xmodule/xmodule/tests/test_library_content.py b/common/lib/xmodule/xmodule/tests/test_library_content.py index a9b924503d20..5f32dab130e7 100644 --- a/common/lib/xmodule/xmodule/tests/test_library_content.py +++ b/common/lib/xmodule/xmodule/tests/test_library_content.py @@ -4,21 +4,33 @@ Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`. """ -import ddt -from xmodule.library_content_module import LibraryVersionReference, ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor +from bson.objectid import ObjectId +from mock import patch +from opaque_keys.edx.locator import LibraryLocator +from unittest import TestCase + +from xblock.fragment import Fragment +from xblock.runtime import Runtime as VanillaRuntime + +from xmodule.x_module import AUTHOR_VIEW +from xmodule.library_content_module import ( + LibraryVersionReference, LibraryList, ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor +) from xmodule.modulestore.tests.factories import LibraryFactory, CourseFactory, ItemFactory from xmodule.modulestore.tests.utils import MixedSplitTestCase from xmodule.tests import get_test_system from xmodule.validation import StudioValidationMessage -@ddt.ddt -class TestLibraries(MixedSplitTestCase): +dummy_render = lambda block, _: Fragment(block.data) # pylint: disable=invalid-name + + +class LibraryContentTest(MixedSplitTestCase): """ - Basic unit tests for LibraryContentModule (library_content_module.py) + Base class for tests of LibraryContentModule (library_content_module.py) """ def setUp(self): - super(TestLibraries, self).setUp() + super(LibraryContentTest, self).setUp() self.library = LibraryFactory.create(modulestore=self.store) self.lib_blocks = [ @@ -108,6 +120,11 @@ def _create_capa_problems(self): modulestore=self.store, ) + +class TestLibraryContentModule(LibraryContentTest): + """ + Basic unit tests for LibraryContentModule + """ def test_lib_content_block(self): """ Test that blocks from a library are copied and added as children @@ -250,3 +267,71 @@ def test_non_editable_settings(self): non_editable_metadata_fields = self.lc_block.non_editable_metadata_fields self.assertIn(LibraryContentDescriptor.mode, non_editable_metadata_fields) self.assertNotIn(LibraryContentDescriptor.display_name, non_editable_metadata_fields) + + +@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.render', VanillaRuntime.render) +@patch('xmodule.html_module.HtmlModule.author_view', dummy_render, create=True) +@patch('xmodule.x_module.DescriptorSystem.applicable_aside_types', lambda self, block: []) +class TestLibraryContentRender(LibraryContentTest): + """ + Rendering unit tests for LibraryContentModule + """ + def test_preivew_view(self): + """ Test preview view rendering """ + self.lc_block.refresh_children() + self.lc_block = self.store.get_item(self.lc_block.location) + self.assertEqual(len(self.lc_block.children), len(self.lib_blocks)) + self._bind_course_module(self.lc_block) + rendered = self.lc_block.render(AUTHOR_VIEW, {'root_xblock': self.lc_block}) + self.assertIn("Hello world from block 1", rendered.content) + + def test_author_view(self): + """ Test author view rendering """ + self.lc_block.refresh_children() + self.lc_block = self.store.get_item(self.lc_block.location) + self.assertEqual(len(self.lc_block.children), len(self.lib_blocks)) + self._bind_course_module(self.lc_block) + rendered = self.lc_block.render(AUTHOR_VIEW, {}) + self.assertEqual("", rendered.content) # content should be empty + self.assertEqual("LibraryContentAuthorView", rendered.js_init_fn) # but some js initialization should happen + + +class TestLibraryList(TestCase): + """ Tests for LibraryList XBlock Field """ + def test_from_json_runtime_style(self): + """ + Test that LibraryList can parse raw libraries list as passed by runtime + """ + lib_list = LibraryList() + lib1_key, lib1_version = u'library-v1:Org1+Lib1', '5436ffec56c02c13806a4c1b' + lib2_key, lib2_version = u'library-v1:Org2+Lib2', '112dbaf312c0daa019ce9992' + raw = [[lib1_key, lib1_version], [lib2_key, lib2_version]] + parsed = lib_list.from_json(raw) + self.assertEqual(len(parsed), 2) + self.assertEquals(parsed[0].library_id, LibraryLocator.from_string(lib1_key)) + self.assertEquals(parsed[0].version, ObjectId(lib1_version)) + self.assertEquals(parsed[1].library_id, LibraryLocator.from_string(lib2_key)) + self.assertEquals(parsed[1].version, ObjectId(lib2_version)) + + def test_from_json_studio_editor_style(self): + """ + Test that LibraryList can parse raw libraries list as passed by studio editor + """ + lib_list = LibraryList() + lib1_key, lib1_version = u'library-v1:Org1+Lib1', '5436ffec56c02c13806a4c1b' + lib2_key, lib2_version = u'library-v1:Org2+Lib2', '112dbaf312c0daa019ce9992' + raw = [lib1_key + ',' + lib1_version, lib2_key + ',' + lib2_version] + parsed = lib_list.from_json(raw) + self.assertEqual(len(parsed), 2) + self.assertEquals(parsed[0].library_id, LibraryLocator.from_string(lib1_key)) + self.assertEquals(parsed[0].version, ObjectId(lib1_version)) + self.assertEquals(parsed[1].library_id, LibraryLocator.from_string(lib2_key)) + self.assertEquals(parsed[1].version, ObjectId(lib2_version)) + + def test_from_json_invalid_value(self): + """ + Test that LibraryList raises Value error if invalid library key is given + """ + lib_list = LibraryList() + with self.assertRaises(ValueError): + lib_list.from_json(["Not-a-library-key,whatever"]) diff --git a/common/lib/xmodule/xmodule/tests/test_library_root.py b/common/lib/xmodule/xmodule/tests/test_library_root.py new file mode 100644 index 000000000000..8774e7bb3295 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_library_root.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" +Basic unit tests for LibraryRoot +""" +from mock import patch + +from xblock.fragment import Fragment +from xblock.runtime import Runtime as VanillaRuntime +from xmodule.x_module import AUTHOR_VIEW + +from xmodule.modulestore.tests.factories import LibraryFactory, ItemFactory +from xmodule.modulestore.tests.utils import MixedSplitTestCase + +dummy_render = lambda block, _: Fragment(block.data) # pylint: disable=invalid-name + + +@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.render', VanillaRuntime.render) +@patch('xmodule.html_module.HtmlDescriptor.author_view', dummy_render, create=True) +@patch('xmodule.x_module.DescriptorSystem.applicable_aside_types', lambda self, block: []) +class TestLibraryRoot(MixedSplitTestCase): + """ + Basic unit tests for LibraryRoot (library_root_xblock.py) + """ + def test_library_author_view(self): + """ + Test that LibraryRoot.author_view can run and includes content from its + children. + We have to patch the runtime (module system) in order to be able to + render blocks in our test environment. + """ + message = u"Hello world" + library = LibraryFactory.create(modulestore=self.store) + # Add one HTML block to the library: + ItemFactory.create( + category="html", + parent_location=library.location, + user_id=self.user_id, + publish_item=False, + modulestore=self.store, + data=message + ) + library = self.store.get_library(library.location.library_key) + + context = {'reorderable_items': set(), } + # Patch the HTML block to always render "Hello world" + + result = library.render(AUTHOR_VIEW, context) + self.assertIn(message, result.content) + + def test_library_author_view_with_paging(self): + """ + Test that LibraryRoot.author_view can apply paging + We have to patch the runtime (module system) in order to be able to + render blocks in our test environment. + """ + library = LibraryFactory.create(modulestore=self.store) + # Add five HTML blocks to the library: + blocks = [ + ItemFactory.create( + category="html", + parent_location=library.location, + user_id=self.user_id, + publish_item=False, + modulestore=self.store, + data="HtmlBlock" + str(i) + ) + for i in range(5) + ] + library = self.store.get_library(library.location.library_key) + + def render_and_check_contents(page, page_size): + """ Renders block and asserts on returned content """ + context = {'reorderable_items': set(), 'paging': {'page_number': page, 'page_size': page_size}} + expected_blocks = blocks[page_size * page:page_size * (page + 1)] + result = library.render(AUTHOR_VIEW, context) + + for expected_block in expected_blocks: + self.assertIn(expected_block.data, result.content) + + render_and_check_contents(0, 3) + render_and_check_contents(1, 3) + render_and_check_contents(0, 2) + render_and_check_contents(1, 2)