Skip to content

Commit

Permalink
test: [AXM-636] Cover Offline Mode API with unit tests (#2577)
Browse files Browse the repository at this point in the history
* test: [AXM-636] Add some tests to cover Offline Mode

* test: [AXM-636] Covered the all Ofline Mode app with tests

* test: [AXM-636] Add test to Blocks Info API

* style: [AXM-636] Improve code style

* style: [AXM-636] Ivan's code improving

Co-authored-by: Ivan Niedielnitsev <[email protected]>

* refactor: [AXM-636] Refactor tests, add new tests

* test: [AXM-653] Fix tests, refactor, add new functional tests

* fix: [AXM-653] Commit forgotten base file

* test: [AXM-653] Fix old tests, add new tests

---------

Co-authored-by: Ivan Niedielnitsev <[email protected]>
  • Loading branch information
KyryloKireiev and NiedielnitsevIvan authored Jun 24, 2024
1 parent 6fa315c commit cf5694e
Show file tree
Hide file tree
Showing 8 changed files with 1,126 additions and 4 deletions.
87 changes: 85 additions & 2 deletions lms/djangoapps/mobile_api/tests/test_course_info_views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Tests for course_info
"""
from unittest.mock import patch
from unittest.mock import MagicMock, patch


import ddt
Expand All @@ -17,11 +17,13 @@
from common.djangoapps.student.tests.factories import UserFactory # pylint: disable=unused-import
from common.djangoapps.util.course import get_link_for_about_page
from lms.djangoapps.mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
from lms.djangoapps.mobile_api.utils import API_V1, API_V05
from lms.djangoapps.mobile_api.utils import API_V05, API_V1, API_V2, API_V3, API_V4
from lms.djangoapps.mobile_api.course_info.views import BlocksInfoInCourseView
from lms.djangoapps.course_api.blocks.tests.test_views import TestBlocksInCourseView
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.features.course_experience import ENABLE_COURSE_GOALS
from openedx.features.offline_mode.constants import DEFAULT_OFFLINE_SUPPORTED_XBLOCKS
from openedx.features.offline_mode.toggles import ENABLE_OFFLINE_MODE
from xmodule.html_block import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -450,3 +452,84 @@ def test_extend_sequential_info_with_assignment_progress_for_other_types(self, b
self.assertEqual(response.status_code, status.HTTP_200_OK)
for block_info in response.data['blocks'].values():
self.assertNotEqual('assignment_progress', block_info)

@patch('lms.djangoapps.mobile_api.course_info.views.default_storage')
@patch('lms.djangoapps.mobile_api.course_info.views.get_offline_block_content_path')
@patch('lms.djangoapps.mobile_api.course_info.views.is_offline_mode_enabled')
def test_extend_block_info_with_offline_data(
self,
is_offline_mode_enabled_mock: MagicMock,
get_offline_block_content_path_mock: MagicMock,
default_storage_mock: MagicMock,
) -> None:
url = reverse('blocks_info_in_course', kwargs={'api_version': API_V4})
offline_content_path_mock = '/offline_content_path_mock/'
created_time_mock = 'created_time_mock'
size_mock = 'size_mock'
get_offline_block_content_path_mock.return_value = offline_content_path_mock
default_storage_mock.get_modified_time.return_value = created_time_mock
default_storage_mock.size.return_value = size_mock

expected_offline_download_data = {
'file_url': offline_content_path_mock,
'last_modified': created_time_mock,
'file_size': size_mock
}

response = self.verify_response(url=url)

is_offline_mode_enabled_mock.assert_called_once_with(self.course.course_id)
self.assertEqual(response.status_code, status.HTTP_200_OK)
for block_info in response.data['blocks'].values():
self.assertDictEqual(block_info['offline_download'], expected_offline_download_data)

@patch('lms.djangoapps.mobile_api.course_info.views.is_offline_mode_enabled')
@ddt.data(
(API_V05, True),
(API_V05, False),
(API_V1, True),
(API_V1, False),
(API_V2, True),
(API_V2, False),
(API_V3, True),
(API_V3, False),
)
@ddt.unpack
def test_not_extend_block_info_with_offline_data_for_version_less_v4_and_any_waffle_flag(
self,
api_version: str,
offline_mode_waffle_flag_mock: MagicMock,
is_offline_mode_enabled_mock: MagicMock,
) -> None:
url = reverse('blocks_info_in_course', kwargs={'api_version': api_version})
is_offline_mode_enabled_mock.return_value = offline_mode_waffle_flag_mock

response = self.verify_response(url=url)

self.assertEqual(response.status_code, status.HTTP_200_OK)
for block_info in response.data['blocks'].values():
self.assertNotIn('offline_download', block_info)

@override_waffle_flag(ENABLE_OFFLINE_MODE, active=True)
@patch('openedx.features.offline_mode.html_manipulator.save_mathjax_to_xblock_assets')
def test_create_offline_content_integration_test(self, save_mathjax_to_xblock_assets_mock: MagicMock) -> None:
UserFactory.create(username='offline_mode_worker', password='password', is_staff=True)
handle_course_published_url = reverse('offline_mode:handle_course_published')
self.client.login(username='offline_mode_worker', password='password')

handler_response = self.client.post(handle_course_published_url, {'course_id': str(self.course.id)})
self.assertEqual(handler_response.status_code, status.HTTP_200_OK)

url = reverse('blocks_info_in_course', kwargs={'api_version': API_V4})

response = self.verify_response(url=url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
for block_info in response.data['blocks'].values():
if block_type := block_info.get('type'):
if block_type in DEFAULT_OFFLINE_SUPPORTED_XBLOCKS:
expected_offline_content_url = f'/uploads/{self.course.id}/{block_info["block_id"]}.zip'
self.assertIn('offline_download', block_info)
self.assertIn('file_url', block_info['offline_download'])
self.assertIn('last_modified', block_info['offline_download'])
self.assertIn('file_size', block_info['offline_download'])
self.assertEqual(expected_offline_content_url, block_info['offline_download']['file_url'])
3 changes: 1 addition & 2 deletions openedx/features/offline_mode/assets_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def read_static_file(path):

def save_asset_file(temp_dir, xblock, path, filename):
"""
Saves an asset file to the default storage.
Saves an asset file to the temporary directory.
If the filename contains a '/', it reads the static file directly from the file system.
Otherwise, it fetches the asset from the AssetManager.
Expand Down Expand Up @@ -89,7 +89,6 @@ def clean_outdated_xblock_files(xblock):
base_path = block_storage_path(xblock)
offline_zip_path = os.path.join(base_path, f'{xblock.location.block_id}.zip')

# Delete the 'offline_content.zip' file if it exists
if default_storage.exists(offline_zip_path):
default_storage.delete(offline_zip_path)
log.info(f"Successfully deleted the file: {offline_zip_path}")
Expand Down
47 changes: 47 additions & 0 deletions openedx/features/offline_mode/tests/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Tests for the testing xBlock renderers for Offline Mode.
"""

from xmodule.capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory


class CourseForOfflineTestCase(ModuleStoreTestCase):
"""
Base class for creation course for Offline Mode testing.
"""

def setUp(self):
super().setUp()
default_store = self.store.default_modulestore.get_modulestore_type()
with self.store.default_store(default_store):
self.course = CourseFactory.create( # lint-amnesty, pylint: disable=attribute-defined-outside-init
display_name='Offline Course',
org='RaccoonGang',
number='1',
run='2024',
)
chapter = BlockFactory.create(parent=self.course, category='chapter')
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
question_text='The correct answer is Choice 2',
choices=[False, False, True, False],
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
)
self.vertical_block = BlockFactory.create( # lint-amnesty, pylint: disable=attribute-defined-outside-init
parent_location=chapter.location,
category='vertical',
display_name='Vertical'
)
self.html_block = BlockFactory.create( # lint-amnesty, pylint: disable=attribute-defined-outside-init
parent=self.vertical_block,
category='html',
display_name='HTML xblock for Offline',
data='<p>Test HTML Content<p>'
)
self.problem_block = BlockFactory.create( # lint-amnesty, pylint: disable=attribute-defined-outside-init
parent=self.vertical_block,
category='problem',
display_name='Problem xblock for Offline',
data=problem_xml
)
Loading

0 comments on commit cf5694e

Please sign in to comment.