Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: [AXM-636] Cover Offline Mode API with unit tests #2577

Merged
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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth adding a test to check for the absence of a new field in version 3 of this API

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests for versions less then v4

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
Loading