From e1f6ca93ec36481a7eabcadd439d0ab4ca270f46 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Sat, 1 Nov 2014 19:47:28 -0700 Subject: [PATCH] Unit and integration tests of content libraries --- .../contentstore/tests/test_libraries.py | 256 ++++++++++++++++++ .../xmodule/tests/test_library_content.py | 142 ++++++++++ 2 files changed, 398 insertions(+) create mode 100644 cms/djangoapps/contentstore/tests/test_libraries.py create mode 100644 common/lib/xmodule/xmodule/tests/test_library_content.py diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py new file mode 100644 index 0000000000..b6c6119ed1 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -0,0 +1,256 @@ +""" +Content library unit tests that require the CMS runtime. +""" +from contentstore.tests.utils import AjaxEnabledTestClient, parse_json +from contentstore.utils import reverse_usage_url +from contentstore.views.preview import _load_preview_module +from contentstore.views.tests.test_library import LIBRARY_REST_URL +import ddt +from xmodule.library_content_module import LibraryVersionReference +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 CourseFactory, ItemFactory +from xmodule.tests import get_test_system +from mock import Mock +from opaque_keys.edx.locator import CourseKey, LibraryLocator + + +@ddt.ddt +class TestLibraries(ModuleStoreTestCase): + """ + High-level tests for libraries + """ + def setUp(self): + user_password = super(TestLibraries, self).setUp() + + self.client = AjaxEnabledTestClient() + self.client.login(username=self.user.username, password=user_password) + + self.lib_key = self._create_library() + self.library = modulestore().get_library(self.lib_key) + + def _create_library(self, org="org", library="lib", display_name="Test Library"): + """ + Helper method used to create a library. Uses the REST API. + """ + response = self.client.ajax_post(LIBRARY_REST_URL, { + 'org': org, + 'library': library, + 'display_name': display_name, + }) + self.assertEqual(response.status_code, 200) + lib_info = parse_json(response) + lib_key = CourseKey.from_string(lib_info['library_key']) + self.assertIsInstance(lib_key, LibraryLocator) + return lib_key + + def _add_library_content_block(self, course, library_key, other_settings=None): + """ + Helper method to add a LibraryContent block to a course. + The block will be configured to select content from the library + specified by library_key. + other_settings can be a dict of Scope.settings fields to set on the block. + """ + return ItemFactory.create( + category='library_content', + parent_location=course.location, + user_id=self.user.id, + publish_item=False, + source_libraries=[LibraryVersionReference(library_key)], + **(other_settings or {}) + ) + + def _refresh_children(self, lib_content_block): + """ + Helper method: Uses the REST API to call the 'refresh_children' handler + of a LibraryContent block + """ + if 'user' not in lib_content_block.runtime._services: # pylint: disable=protected-access + lib_content_block.runtime._services['user'] = Mock(user_id=self.user.id) # pylint: disable=protected-access + handler_url = reverse_usage_url('component_handler', lib_content_block.location, kwargs={'handler': 'refresh_children'}) + response = self.client.ajax_post(handler_url) + self.assertEqual(response.status_code, 200) + return modulestore().get_item(lib_content_block.location) + + @ddt.data( + (2, 1, 1), + (2, 2, 2), + (2, 20, 2), + ) + @ddt.unpack + def test_max_items(self, num_to_create, num_to_select, num_expected): + """ + Test the 'max_count' property of LibraryContent blocks. + """ + for _ in range(0, num_to_create): + ItemFactory.create(category="html", parent_location=self.library.location, user_id=self.user.id, publish_item=False) + + with modulestore().default_store(ModuleStoreEnum.Type.split): + course = CourseFactory.create() + + lc_block = self._add_library_content_block(course, self.lib_key, {'max_count': num_to_select}) + self.assertEqual(len(lc_block.children), 0) + lc_block = self._refresh_children(lc_block) + + # Now, we want to make sure that .children has the total # of potential + # children, and that get_child_descriptors() returns the actual children + # chosen for a given student. + # In order to be able to call get_child_descriptors(), we must first + # call bind_for_student: + lc_block.bind_for_student(get_test_system(), lc_block._field_data) # pylint: disable=protected-access + self.assertEqual(len(lc_block.children), num_to_create) + self.assertEqual(len(lc_block.get_child_descriptors()), num_expected) + + def test_consistent_children(self): + """ + Test that the same student will always see the same selected child block + """ + session_data = {} + + def bind_module(descriptor): + """ + Helper to use the CMS's module system so we can access student-specific fields. + """ + request = Mock(user=self.user, session=session_data) + return _load_preview_module(request, descriptor) # pylint: disable=protected-access + + # Create many blocks in the library and add them to a course: + for num in range(0, 8): + ItemFactory.create( + data="This is #{}".format(num + 1), + category="html", parent_location=self.library.location, user_id=self.user.id, publish_item=False + ) + + with modulestore().default_store(ModuleStoreEnum.Type.split): + course = CourseFactory.create() + + lc_block = self._add_library_content_block(course, self.lib_key, {'max_count': 1}) + lc_block_key = lc_block.location + lc_block = self._refresh_children(lc_block) + + def get_child_of_lc_block(block): + """ + Fetch the child shown to the current user. + """ + children = block.get_child_descriptors() + self.assertEqual(len(children), 1) + return children[0] + + # Check which child a student will see: + bind_module(lc_block) + chosen_child = get_child_of_lc_block(lc_block) + chosen_child_defn_id = chosen_child.definition_locator.definition_id + lc_block.save() + + modulestore().update_item(lc_block, self.user.id) + + # Now re-load the block and try again: + def check(): + """ + Confirm that chosen_child is still the child seen by the test student + """ + for _ in range(0, 6): # Repeat many times b/c blocks are randomized + lc_block = modulestore().get_item(lc_block_key) # Reload block from the database + bind_module(lc_block) + current_child = get_child_of_lc_block(lc_block) + self.assertEqual(current_child.location, chosen_child.location) + self.assertEqual(current_child.data, chosen_child.data) + self.assertEqual(current_child.definition_locator.definition_id, chosen_child_defn_id) + + check() + # Refresh the children: + lc_block = self._refresh_children(lc_block) + # Now re-load the block and try yet again, in case refreshing the children changed anything: + check() + + def test_definition_shared_with_library(self): + """ + Test that the same block definition is used for the library and course[s] + """ + block1 = ItemFactory.create(category="html", parent_location=self.library.location, user_id=self.user.id, publish_item=False) + def_id1 = block1.definition_locator.definition_id + block2 = ItemFactory.create(category="html", parent_location=self.library.location, user_id=self.user.id, publish_item=False) + def_id2 = block2.definition_locator.definition_id + self.assertNotEqual(def_id1, def_id2) + + # Next, 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) + for child_key in lc_block.children: + child = modulestore().get_item(child_key) + def_id = child.definition_locator.definition_id + self.assertIn(def_id, (def_id1, def_id2)) + + def test_fields(self): + """ + Test that blocks used from a library have the same field values as + defined by the library author. + """ + data_value = "A Scope.content value" + name_value = "A Scope.settings value" + lib_block = ItemFactory.create( + category="html", + parent_location=self.library.location, + user_id=self.user.id, + publish_item=False, + display_name=name_value, + data=data_value, + ) + self.assertEqual(lib_block.data, data_value) + self.assertEqual(lib_block.display_name, name_value) + + # Next, 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) + course_block = modulestore().get_item(lc_block.children[0]) + + self.assertEqual(course_block.data, data_value) + self.assertEqual(course_block.display_name, name_value) + + def test_block_with_children(self): + """ + Test that blocks used from a library can have children. + """ + data_value = "A Scope.content value" + name_value = "A Scope.settings value" + # In the library, create a vertical block with a child: + vert_block = ItemFactory.create( + category="vertical", + parent_location=self.library.location, + user_id=self.user.id, + publish_item=False, + ) + child_block = ItemFactory.create( + category="html", + parent_location=vert_block.location, + user_id=self.user.id, + publish_item=False, + display_name=name_value, + data=data_value, + ) + self.assertEqual(child_block.data, data_value) + self.assertEqual(child_block.display_name, name_value) + + # Next, 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), 1) + course_vert_block = modulestore().get_item(lc_block.children[0]) + self.assertEqual(len(course_vert_block.children), 1) + course_child_block = modulestore().get_item(course_vert_block.children[0]) + + self.assertEqual(course_child_block.data, data_value) + self.assertEqual(course_child_block.display_name, name_value) diff --git a/common/lib/xmodule/xmodule/tests/test_library_content.py b/common/lib/xmodule/xmodule/tests/test_library_content.py new file mode 100644 index 0000000000..2b52386e37 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_library_content.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +Basic unit tests for LibraryContentModule + +Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`. +""" +import ddt +from xmodule.library_content_module import LibraryVersionReference +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): + """ + Basic unit tests for LibraryContentModule (library_content_module.py) + """ + def setUp(self): + super(TestLibraries, self).setUp() + + self.library = LibraryFactory.create(modulestore=self.store) + self.lib_blocks = [ + ItemFactory.create( + category="html", + parent_location=self.library.location, + user_id=self.user_id, + publish_item=False, + metadata={"data": "Hello world from block {}".format(i), }, + modulestore=self.store, + ) + for i in range(1, 5) + ] + self.course = CourseFactory.create(modulestore=self.store) + self.chapter = ItemFactory.create( + category="chapter", + parent_location=self.course.location, + user_id=self.user_id, + modulestore=self.store, + ) + self.sequential = ItemFactory.create( + category="sequential", + parent_location=self.chapter.location, + user_id=self.user_id, + modulestore=self.store, + ) + self.vertical = ItemFactory.create( + category="vertical", + parent_location=self.sequential.location, + user_id=self.user_id, + modulestore=self.store, + ) + self.lc_block = ItemFactory.create( + category="library_content", + parent_location=self.vertical.location, + user_id=self.user_id, + modulestore=self.store, + metadata={ + 'max_count': 1, + 'source_libraries': [LibraryVersionReference(self.library.location.library_key)] + } + ) + + def _bind_course_module(self, module): + """ + Bind a module (part of self.course) so we can access student-specific data. + """ + module_system = get_test_system(course_id=self.course.location.course_key) + module_system.descriptor_runtime = module.runtime + + def get_module(descriptor): + """Mocks module_system get_module function""" + sub_module_system = get_test_system(course_id=self.course.location.course_key) + sub_module_system.get_module = get_module + sub_module_system.descriptor_runtime = descriptor.runtime + descriptor.bind_for_student(sub_module_system, descriptor._field_data) # pylint: disable=protected-access + return descriptor + + module_system.get_module = get_module + module.xmodule_runtime = module_system + + def test_lib_content_block(self): + """ + Test that blocks from a library are copied and added as children + """ + # Check that the LibraryContent block has no children initially + # Normally the children get added when the "source_libraries" setting + # is updated, but the way we do it through a factory doesn't do that. + self.assertEqual(len(self.lc_block.children), 0) + # Update the LibraryContent module: + self.lc_block.refresh_children(None, None) + # Check that all blocks from the library are now children of the block: + self.assertEqual(len(self.lc_block.children), len(self.lib_blocks)) + + def test_children_seen_by_a_user(self): + """ + Test that each student sees only one block as a child of the LibraryContent block. + """ + self.lc_block.refresh_children(None, None) + self.lc_block = self.store.get_item(self.lc_block.location) + self._bind_course_module(self.lc_block) + # Make sure the runtime knows that the block's children vary per-user: + self.assertTrue(self.lc_block.has_dynamic_children()) + + self.assertEqual(len(self.lc_block.children), len(self.lib_blocks)) + + # Check how many children each user will see: + self.assertEqual(len(self.lc_block.get_child_descriptors()), 1) + # Check that get_content_titles() doesn't return titles for hidden/unused children + self.assertEqual(len(self.lc_block.get_content_titles()), 1) + + def test_validation(self): + """ + Test that the validation method of LibraryContent blocks is working. + """ + # When source_libraries is blank, the validation summary should say this block needs to be configured: + self.lc_block.source_libraries = [] + result = self.lc_block.validate() + self.assertFalse(result) # Validation fails due to at least one warning/message + self.assertTrue(result.summary) + self.assertEqual(StudioValidationMessage.NOT_CONFIGURED, result.summary.type) + + # When source_libraries references a non-existent library, we should get an error: + self.lc_block.source_libraries = [LibraryVersionReference("library-v1:BAD+WOLF")] + result = self.lc_block.validate() + self.assertFalse(result) # Validation fails due to at least one warning/message + self.assertTrue(result.summary) + self.assertEqual(StudioValidationMessage.ERROR, result.summary.type) + self.assertIn("invalid", result.summary.text) + + # When source_libraries is set but the block needs to be updated, the summary should say so: + self.lc_block.source_libraries = [LibraryVersionReference(self.library.location.library_key)] + result = self.lc_block.validate() + self.assertFalse(result) # Validation fails due to at least one warning/message + self.assertTrue(result.summary) + self.assertEqual(StudioValidationMessage.WARNING, result.summary.type) + self.assertIn("out of date", result.summary.text) + + # Now if we update the block, all validation should pass: + self.lc_block.refresh_children(None, None) + self.assertTrue(self.lc_block.validate())