diff --git a/cms/djangoapps/contentstore/core/course_optimizer_provider.py b/cms/djangoapps/contentstore/core/course_optimizer_provider.py index 9a6ecfdaebeb..9bf3647bafdb 100644 --- a/cms/djangoapps/contentstore/core/course_optimizer_provider.py +++ b/cms/djangoapps/contentstore/core/course_optimizer_provider.py @@ -59,7 +59,7 @@ def generate_broken_links_descriptor(json_content, request_user): usage_key = usage_key_with_run(block_id) block = get_xblock(usage_key, request_user) - _update_node_tree_and_dictionary( + xblock_node_tree, xblock_dictionary = _update_node_tree_and_dictionary( block=block, link=link, is_locked=is_locked_flag, @@ -103,14 +103,16 @@ def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictiona ..., } """ + updated_tree, updated_dictionary = node_tree, dictionary + path = _get_node_path(block) - current_node = node_tree + current_node = updated_tree xblock_id = '' # Traverse the path and build the tree structure for xblock in path: xblock_id = xblock.location.block_id - dictionary.setdefault(xblock_id, + updated_dictionary.setdefault(xblock_id, { 'display_name': xblock.display_name, 'category': getattr(xblock, 'category', ''), @@ -120,18 +122,20 @@ def _update_node_tree_and_dictionary(block, link, is_locked, node_tree, dictiona current_node = current_node.setdefault(xblock_id, {}) # Add block-level details for the last xblock in the path (URL and broken/locked links) - dictionary[xblock_id].setdefault('url', + updated_dictionary[xblock_id].setdefault('url', f'/course/{block.course_id}/editor/{block.category}/{block.location}' ) if is_locked: - dictionary[xblock_id].setdefault('locked_links', []).append(link) + updated_dictionary[xblock_id].setdefault('locked_links', []).append(link) else: - dictionary[xblock_id].setdefault('broken_links', []).append(link) + updated_dictionary[xblock_id].setdefault('broken_links', []).append(link) + + return updated_tree, updated_dictionary def _get_node_path(block): """ - Retrieves the path frmo the course root node to a specific block, excluding the root. + Retrieves the path from the course root node to a specific block, excluding the root. ** Example Path structure ** [chapter_node, sequential_node, vertical_node, html_node] diff --git a/cms/djangoapps/contentstore/core/tests/test_course_optimizer_provider.py b/cms/djangoapps/contentstore/core/tests/test_course_optimizer_provider.py index a0524df4081f..1bd1c6ef2f34 100644 --- a/cms/djangoapps/contentstore/core/tests/test_course_optimizer_provider.py +++ b/cms/djangoapps/contentstore/core/tests/test_course_optimizer_provider.py @@ -5,34 +5,195 @@ import unittest from unittest.mock import Mock, patch -from cms.djangoapps.contentstore.core.course_optimizer_provider import generate_broken_links_descriptor -# from ..course_optimizer_provider import generate_broken_links_descriptor +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.core.course_optimizer_provider import ( + generate_broken_links_descriptor, + _update_node_tree_and_dictionary, + _get_node_path, + _create_dto_from_node_tree_recursive +) -class TestLinkCheck(ModuleStoreTestCase): +class TestLinkCheck(CourseTestCase): """ Tests for the link check functionality """ - @patch('cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers.get_xblock') - @patch('cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers.usage_key_with_run') - def test_success(self): + def setUp(self): + global MOCK_TREE + global MOCK_XBLOCK_DICTIONARY + MOCK_TREE = { + 'chapter_1': { + 'sequential_1': { + 'vertical_1': { + 'block_1': {} + } + } + } + } + MOCK_XBLOCK_DICTIONARY = { + 'chapter_1': { + 'display_name': 'Chapter 1', + 'category': 'chapter' + }, + 'sequential_1': { + 'display_name': 'Sequential 1', + 'category': 'sequential' + }, + 'vertical_1': { + 'display_name': 'Vertical 1', + 'category': 'vertical' + }, + 'block_1': { + 'display_name': 'Block 1', + 'url': '/block/1', + 'broken_links': ['broken_link_1', 'broken_link_2'], + 'locked_links': ['locked_link'] + } + } + + + def test_recursive_empty(self): + expected = _create_dto_from_node_tree_recursive({}, {}) + self.assertEqual(None, expected) + + + def test_recursive_leaf_node(self): + expected_result = { + 'blocks': [ + { + 'id': 'block_1', + 'displayName': 'Block 1', + 'url': '/block/1', + 'brokenLinks': ['broken_link_1', 'broken_link_2'], + 'lockedLinks': ['locked_link'] + } + ] + } + expected = _create_dto_from_node_tree_recursive( + MOCK_TREE['chapter_1']['sequential_1']['vertical_1'], + MOCK_XBLOCK_DICTIONARY + ) + self.assertEqual(expected_result, expected) + + + def test_recursive_full_tree(self): + expected_result = { + 'sections': [ + { + 'id': 'chapter_1', + 'displayName': 'Chapter 1', + 'subsections': [ + { + 'id': 'sequential_1', + 'displayName': 'Sequential 1', + 'units': [ + { + 'id': 'vertical_1', + 'displayName': 'Vertical 1', + 'blocks': [ + { + 'id': 'block_1', + 'displayName': 'Block 1', + 'url': '/block/1', + 'brokenLinks': ['broken_link_1', 'broken_link_2'], + 'lockedLinks': ['locked_link'] + } + ] + } + ] + } + ] + } + ] + } + + expected = _create_dto_from_node_tree_recursive(MOCK_TREE, MOCK_XBLOCK_DICTIONARY) + self.assertEqual(expected_result, expected) + + + def test_get_node_path(self): + mock_course = Mock() + mock_section = Mock( + location=Mock(block_id='section_id'), + display_name='Section Name' + ) + mock_subsection = Mock( + location=Mock(block_id='subsection_id'), + display_name='Subsection Name' + ) + mock_unit = Mock( + location=Mock(block_id='unit_id'), + display_name='Unit Name' + ) + mock_block = Mock( + course_id='course-v1:test+course+2024', + location=Mock(block_id='block_id'), + display_name='Block Name', + category='html' + ) + mock_course.get_parent.return_value = None + mock_section.get_parent.return_value = mock_course + mock_subsection.get_parent.return_value = mock_section + mock_unit.get_parent.return_value = mock_subsection + mock_block.get_parent.return_value = mock_unit + + expected_result = [mock_course, mock_section, mock_subsection, mock_unit, mock_block] + + result = _get_node_path(mock_unit) + self.assertEqual(expected_result, result) + + + # @patch('cms.djangoapps.contentstore.core.course_optimizer_provider._create_dto_from_node_tree_recursive') + @patch('cms.djangoapps.contentstore.core.course_optimizer_provider._update_node_tree_and_dictionary') + @patch('cms.djangoapps.contentstore.core.course_optimizer_provider.get_xblock') + @patch('cms.djangoapps.contentstore.core.course_optimizer_provider.usage_key_with_run') + def test_generate_broken_links_descriptor_returns_correct_result( + self, + mock_usage_key_with_run, + mock_get_xblock, + mock_update_node_tree_and_dictionary, + # mock_create_dto_from_node_tree_recursive + ): + """ + Test generate_broken_links_descriptor to return expected dto + """ # Mock data - mock_block = Mock() - mock_block.location.block_id = "block_id" - mock_block.display_name = "Block Name" - mock_block.course_id = "course-v1:test+course+2024" - mock_block.category = "html" - mock_block.get_parent.side_effect = [ - Mock(location=Mock(block_id="unit_id"), display_name="Unit Name"), - Mock(location=Mock(block_id="subsection_id"), display_name="Subsection Name"), - Mock(location=Mock(block_id="section_id"), display_name="Section Name"), - None, - ] + mock_course = Mock() + mock_section = Mock( + location=Mock(block_id='section_id'), + display_name='Section Name' + ) + mock_section.get_parent.side_effect = mock_course + mock_subsection = Mock( + location=Mock(block_id='subsection_id'), + display_name='Subsection Name' + ) + mock_subsection.get_parent.side_effect = mock_section + mock_unit = Mock( + location=Mock(block_id='unit_id'), + display_name='Unit Name' + ) + mock_unit.get_parent.side_effect = mock_subsection + mock_block = Mock( + course_id='course-v1:test+course+2024', + location=Mock(block_id='block_id'), + display_name='Block Name' + ) + mock_block.get_parent.side_effect = mock_unit + mock_block.category = 'html' + # mock_block.get_parent.side_effect = [ + # Mock(location=Mock(block_id="unit_id"), display_name="Unit Name"), + # Mock(location=Mock(block_id="subsection_id"), display_name="Subsection Name"), + # Mock(location=Mock(block_id="section_id"), display_name="Section Name"), + # None, + # ] - # Mocking + # Mock functions mock_usage_key_with_run.return_value = "mock_usage_key" mock_get_xblock.return_value = mock_block + mock_update_node_tree_and_dictionary.return_value = Mock() + # mock_create_dto_from_node_tree_recursive.return_value = 'test' - # Test input + # Mock input mock_json_content = [ ["block_id", "http://example.com/broken-link1", False], ["block_id", "http://example.com/locked-link1", True], @@ -40,10 +201,7 @@ def test_success(self): ] request_user = Mock() - # Call the function - result = generate_broken_links_descriptor(mock_json_content, request_user) - - # Expected structure + # Expected output expected_result = { 'sections': [ { @@ -77,10 +235,7 @@ def test_success(self): ] } - self.assertEqual(result, expected_result) - - - # def test_exception(self): + # Call the function + result = generate_broken_links_descriptor(mock_json_content, request_user) -# if __name__ == '__main__': -# unittest.main() + self.assertEqual(result, expected_result) diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 506566727d63..451bc9221ea0 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -1110,13 +1110,6 @@ def check_broken_links(self, user_id, course_key_string, language): """ Checks for broken links in a course. Store the results in a file. """ - URL_STATUS = { - 'success': '200 OK', - 'forbidden': '403 Forbidden', - 'failure': 'Request Failed', - 'error': 'Request Error' - } - def validate_user(): """Validate if the user exists. Otherwise log error. """ try: diff --git a/cms/djangoapps/contentstore/tests/test_tasks.py b/cms/djangoapps/contentstore/tests/test_tasks.py index cf82a6d16571..658c7f24e7aa 100644 --- a/cms/djangoapps/contentstore/tests/test_tasks.py +++ b/cms/djangoapps/contentstore/tests/test_tasks.py @@ -199,3 +199,60 @@ def test_register_exams_failure(self, _mock_register_exams_proctoring, _mock_reg _mock_register_exams_proctoring.side_effect = Exception('boom!') update_special_exams_and_publish(str(self.course.id)) course_publish.assert_called() + + +@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) +class CourseLinkCheckTestCase(CourseTestCase): + def test_user_does_not_exist_raises_exception(self): + raise NotImplementedError + + def test_no_course_access_raises_exception(self): + raise NotImplementedError + + def test_hash_tags_stripped_from_url_lists(self): + raise NotImplementedError + + def test_urls_out_count_equals_urls_in_count_when_no_hashtags(self): + raise NotImplementedError + + def test_http_and_https_recognized_as_studio_url_schemes(self): + raise NotImplementedError + + def test_file_not_recognized_as_studio_url_scheme(self): + raise NotImplementedError + + def test_url_substitution_on_static_prefixes(self): + raise NotImplementedError + + def test_url_substitution_on_forward_slash_prefixes(self): + raise NotImplementedError + + def test_url_subsitution_on_containers(self): + raise NotImplementedError + + def test_optimization_occurs_on_published_version(self): + raise NotImplementedError + + def test_number_of_scanned_blocks_equals_blocks_in_course(self): + raise NotImplementedError + + def test_every_detected_link_is_validated(self): + raise NotImplementedError + + def test_link_validation_is_batched(self): + raise NotImplementedError + + def test_all_links_in_link_list_longer_than_batch_size_are_validated(self): + raise NotImplementedError + + def test_no_retries_on_403_access_denied_links(self): + raise NotImplementedError + + def test_retries_attempted_on_connection_errors(self): + raise NotImplementedError + + def test_max_number_of_retries_is_respected(self): + raise NotImplementedError + + def test_scan_generates_file_named_by_course_key(self): + raise NotImplementedError \ No newline at end of file