From 5139b2f36e2dac8751a5175837148a4f54680a2c Mon Sep 17 00:00:00 2001 From: farhan Date: Wed, 13 May 2026 10:52:45 +0500 Subject: [PATCH 1/3] test: Adds test case for studio_metadata_mixin class --- .../video/tests/test_studio_metadata_mixin.py | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 xblocks_contrib/video/tests/test_studio_metadata_mixin.py diff --git a/xblocks_contrib/video/tests/test_studio_metadata_mixin.py b/xblocks_contrib/video/tests/test_studio_metadata_mixin.py new file mode 100644 index 00000000..28876d53 --- /dev/null +++ b/xblocks_contrib/video/tests/test_studio_metadata_mixin.py @@ -0,0 +1,210 @@ +# pylint: disable=protected-access +"""Tests for StudioMetadataMixin.editable_metadata_fields (VideoBlock).""" +from unittest.mock import Mock, patch + +from django.test import SimpleTestCase +from django.test.utils import override_settings + +from xblocks_contrib.video.exceptions import TranscriptNotFoundError +from xblocks_contrib.video.tests.test_utils import DummyRuntime +from xblocks_contrib.video.video import VideoBlock +from xblock.field_data import DictFieldData +from xblock.fields import ScopeIds +from opaque_keys.edx.locator import CourseLocator + +ALL_LANGUAGES = ( + ["en", "English"], + ["eo", "Esperanto"], + ["ur", "Urdu"], +) + + +def instantiate_block(**field_data): + """Instantiate a VideoBlock with a DummyRuntime.""" + system = DummyRuntime() + course_key = CourseLocator('org', 'course', 'run') + usage_key = course_key.make_usage_key('video', 'SampleProblem') + return system.construct_xblock_from_class( + VideoBlock, + scope_ids=ScopeIds(None, None, usage_key, usage_key), + field_data=DictFieldData(field_data), + ) + + +@override_settings(ALL_LANGUAGES=ALL_LANGUAGES) +class TestEditableMetadataFieldsProperty(SimpleTestCase): + """ + Unit tests for VideoBlock.editable_metadata_fields property. + + The property enriches the raw editable fields returned by + _get_editable_metadata_fields with video-specific customisations: + transcript language lists, special types for certain fields, etc. + """ + + def setUp(self): + super().setUp() + self.block = instantiate_block() + + def _base_fields(self, include_license=True): + """Return a minimal editable-fields dict that satisfies the property's expectations.""" + fields = { + 'sub': {'type': 'Generic', 'value': ''}, + 'transcripts': {'type': 'Dict', 'value': {}}, + 'edx_video_id': {'type': 'Generic', 'value': ''}, + 'public_access': {'type': 'Select', 'value': False}, + 'handout': {'type': 'Generic', 'value': ''}, + } + if include_license: + fields['license'] = {'type': 'License', 'value': ''} + return fields + + def _get_fields(self, include_license=False, public_url=None, transcripts=None): + """ + Call editable_metadata_fields with standard mocks applied. + + All runtime services return None (no settings service, no video_config), + so license checks and English-transcript lookups are skipped. + """ + with patch.object(self.block, '_get_editable_metadata_fields', + return_value=self._base_fields(include_license)): + with patch.object(self.block, 'get_transcripts_info', + return_value={'sub': '', 'transcripts': transcripts or {}}): + with patch.object(self.block, 'get_public_video_url', return_value=public_url): + with patch.object(self.block.runtime, 'service', return_value=None): + return self.block.editable_metadata_fields + + # ------------------------------------------------------------------ + # Field-level modifications + # ------------------------------------------------------------------ + + def test_sub_field_is_removed(self): + """'sub' is deprecated and must be absent from the returned fields.""" + fields = self._get_fields() + assert 'sub' not in fields + + def test_transcripts_custom_flag_and_type(self): + """'transcripts' field gets custom=True and type='VideoTranslations'.""" + fields = self._get_fields() + assert fields['transcripts']['custom'] is True + assert fields['transcripts']['type'] == 'VideoTranslations' + + def test_transcripts_languages_sorted_by_label(self): + """Language list injected into 'transcripts' must be sorted by label.""" + fields = self._get_fields() + assert fields['transcripts']['languages'] == [ + {'label': 'English', 'code': 'en'}, + {'label': 'Esperanto', 'code': 'eo'}, + {'label': 'Urdu', 'code': 'ur'}, + ] + + def test_transcripts_value_comes_from_get_transcripts_info(self): + """'transcripts.value' must reflect the dict returned by get_transcripts_info.""" + fields = self._get_fields(transcripts={'fr': 'french.srt'}) + assert fields['transcripts']['value'] == {'fr': 'french.srt'} + + def test_transcripts_url_root_from_handler_url(self): + """'transcripts.urlRoot' must be built from runtime.handler_url (trailing /? stripped).""" + fields = self._get_fields() + # DummyRuntime.handler_url returns '/handler/block/handler' + assert fields['transcripts']['urlRoot'] == '/handler/block/handler' + + def test_edx_video_id_type_is_video_id(self): + """'edx_video_id' type must be changed to 'VideoID'.""" + fields = self._get_fields() + assert fields['edx_video_id']['type'] == 'VideoID' + + def test_public_access_type_and_url(self): + """'public_access' type becomes 'PublicAccess' and url is set from get_public_video_url.""" + fields = self._get_fields(public_url='https://example.com/video') + assert fields['public_access']['type'] == 'PublicAccess' + assert fields['public_access']['url'] == 'https://example.com/video' + + def test_handout_type_is_file_uploader(self): + """'handout' type must be changed to 'FileUploader'.""" + fields = self._get_fields() + assert fields['handout']['type'] == 'FileUploader' + + # ------------------------------------------------------------------ + # License field handling + # ------------------------------------------------------------------ + + def test_license_removed_when_licensing_disabled(self): + """'license' is removed when the settings service reports licensing_enabled=False.""" + settings_service = Mock() + settings_service.get_settings_bucket.return_value = {'licensing_enabled': False} + + def _service(_block, name): + return settings_service if name == 'settings' else None + + with patch.object(self.block, '_get_editable_metadata_fields', + return_value=self._base_fields(include_license=True)): + with patch.object(self.block, 'get_transcripts_info', + return_value={'sub': '', 'transcripts': {}}): + with patch.object(self.block, 'get_public_video_url', return_value=None): + with patch.object(self.block.runtime, 'service', side_effect=_service): + fields = self.block.editable_metadata_fields + + assert 'license' not in fields + + def test_license_kept_when_licensing_enabled(self): + """'license' is kept when the settings service reports licensing_enabled=True.""" + settings_service = Mock() + settings_service.get_settings_bucket.return_value = {'licensing_enabled': True} + + def _service(_block, name): + return settings_service if name == 'settings' else None + + with patch.object(self.block, '_get_editable_metadata_fields', + return_value=self._base_fields(include_license=True)): + with patch.object(self.block, 'get_transcripts_info', + return_value={'sub': '', 'transcripts': {}}): + with patch.object(self.block, 'get_public_video_url', return_value=None): + with patch.object(self.block.runtime, 'service', side_effect=_service): + fields = self.block.editable_metadata_fields + + assert 'license' in fields + + # ------------------------------------------------------------------ + # English transcript lookup via video_config service + # ------------------------------------------------------------------ + + def test_english_transcript_found_added_to_value(self): + """When video_config returns an English transcript, it is added to transcripts.value.""" + video_config = Mock() + video_config.get_transcript.return_value = ('content', 'en_subs_id', 'txt') + + def _service(_block, name): + return video_config if name == 'video_config' else None + + self.block.sub = 'some_sub_id' # non-empty so possible_sub_ids is non-empty + + with patch.object(self.block, '_get_editable_metadata_fields', + return_value=self._base_fields(include_license=False)): + with patch.object(self.block, 'get_transcripts_info', + return_value={'sub': 'some_sub_id', 'transcripts': {}}): + with patch.object(self.block, 'get_public_video_url', return_value=None): + with patch.object(self.block.runtime, 'service', side_effect=_service): + fields = self.block.editable_metadata_fields + + assert fields['transcripts']['value'] == {'en': 'en_subs_id'} + + def test_transcript_not_found_leaves_value_unchanged(self): + """When video_config raises TranscriptNotFoundError, transcripts.value is not modified.""" + video_config = Mock() + video_config.get_transcript.side_effect = TranscriptNotFoundError + + def _service(_block, name): + return video_config if name == 'video_config' else None + + self.block.sub = 'some_sub_id' + + with patch.object(self.block, '_get_editable_metadata_fields', + return_value=self._base_fields(include_license=False)): + with patch.object(self.block, 'get_transcripts_info', + return_value={'sub': 'some_sub_id', 'transcripts': {'fr': 'french.srt'}}): + with patch.object(self.block, 'get_public_video_url', return_value=None): + with patch.object(self.block.runtime, 'service', side_effect=_service): + fields = self.block.editable_metadata_fields + + assert 'en' not in fields['transcripts']['value'] + assert fields['transcripts']['value'] == {'fr': 'french.srt'} From 1740777fcd251598cd2acae15114b9430f9a15fb Mon Sep 17 00:00:00 2001 From: farhan Date: Thu, 14 May 2026 19:37:21 +0500 Subject: [PATCH 2/3] style: fix import order and remove useless pylint suppression in test_studio_metadata_mixin Co-Authored-By: Claude Sonnet 4.6 --- xblocks_contrib/video/tests/test_studio_metadata_mixin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/xblocks_contrib/video/tests/test_studio_metadata_mixin.py b/xblocks_contrib/video/tests/test_studio_metadata_mixin.py index 28876d53..0a3710a2 100644 --- a/xblocks_contrib/video/tests/test_studio_metadata_mixin.py +++ b/xblocks_contrib/video/tests/test_studio_metadata_mixin.py @@ -1,16 +1,15 @@ -# pylint: disable=protected-access """Tests for StudioMetadataMixin.editable_metadata_fields (VideoBlock).""" from unittest.mock import Mock, patch from django.test import SimpleTestCase from django.test.utils import override_settings +from opaque_keys.edx.locator import CourseLocator +from xblock.field_data import DictFieldData +from xblock.fields import ScopeIds from xblocks_contrib.video.exceptions import TranscriptNotFoundError from xblocks_contrib.video.tests.test_utils import DummyRuntime from xblocks_contrib.video.video import VideoBlock -from xblock.field_data import DictFieldData -from xblock.fields import ScopeIds -from opaque_keys.edx.locator import CourseLocator ALL_LANGUAGES = ( ["en", "English"], From b6ae3e9173ce4ce9997a6dc13a26e0ca7dcc3d7f Mon Sep 17 00:00:00 2001 From: farhan Date: Mon, 18 May 2026 15:34:10 +0500 Subject: [PATCH 3/3] test: refactor test_studio_metadata_mixin for speed and clarity - Consolidate 8 single-assertion methods into fewer grouped tests - Extract _service_stub helper to eliminate repeated inline lambdas - Extend _get_fields with service param to replace verbose patch blocks - Add missing merge-case test for English transcript + existing transcripts --- .../video/tests/test_studio_metadata_mixin.py | 130 +++++++----------- 1 file changed, 46 insertions(+), 84 deletions(-) diff --git a/xblocks_contrib/video/tests/test_studio_metadata_mixin.py b/xblocks_contrib/video/tests/test_studio_metadata_mixin.py index 0a3710a2..2142ae14 100644 --- a/xblocks_contrib/video/tests/test_studio_metadata_mixin.py +++ b/xblocks_contrib/video/tests/test_studio_metadata_mixin.py @@ -57,72 +57,57 @@ def _base_fields(self, include_license=True): fields['license'] = {'type': 'License', 'value': ''} return fields - def _get_fields(self, include_license=False, public_url=None, transcripts=None): + @staticmethod + def _service_stub(service_name, service_obj): + """Return a runtime.service stub that serves service_obj only for service_name.""" + return lambda _block, name: service_obj if name == service_name else None + + def _get_fields(self, include_license=False, public_url=None, transcripts=None, service=None): """ Call editable_metadata_fields with standard mocks applied. - All runtime services return None (no settings service, no video_config), - so license checks and English-transcript lookups are skipped. + Pass service= to simulate a real runtime service (e.g. for + licensing or video_config tests); omit it to have all service calls + return None (skipping license and English-transcript logic). """ + service_kwargs = {'side_effect': service} if service else {'return_value': None} with patch.object(self.block, '_get_editable_metadata_fields', return_value=self._base_fields(include_license)): with patch.object(self.block, 'get_transcripts_info', return_value={'sub': '', 'transcripts': transcripts or {}}): with patch.object(self.block, 'get_public_video_url', return_value=public_url): - with patch.object(self.block.runtime, 'service', return_value=None): + with patch.object(self.block.runtime, 'service', **service_kwargs): return self.block.editable_metadata_fields # ------------------------------------------------------------------ # Field-level modifications # ------------------------------------------------------------------ - def test_sub_field_is_removed(self): - """'sub' is deprecated and must be absent from the returned fields.""" + def test_default_field_modifications(self): + """Verify all field-level changes made by editable_metadata_fields with default args.""" fields = self._get_fields() assert 'sub' not in fields - - def test_transcripts_custom_flag_and_type(self): - """'transcripts' field gets custom=True and type='VideoTranslations'.""" - fields = self._get_fields() assert fields['transcripts']['custom'] is True assert fields['transcripts']['type'] == 'VideoTranslations' - - def test_transcripts_languages_sorted_by_label(self): - """Language list injected into 'transcripts' must be sorted by label.""" - fields = self._get_fields() assert fields['transcripts']['languages'] == [ {'label': 'English', 'code': 'en'}, {'label': 'Esperanto', 'code': 'eo'}, {'label': 'Urdu', 'code': 'ur'}, ] - - def test_transcripts_value_comes_from_get_transcripts_info(self): - """'transcripts.value' must reflect the dict returned by get_transcripts_info.""" - fields = self._get_fields(transcripts={'fr': 'french.srt'}) - assert fields['transcripts']['value'] == {'fr': 'french.srt'} - - def test_transcripts_url_root_from_handler_url(self): - """'transcripts.urlRoot' must be built from runtime.handler_url (trailing /? stripped).""" - fields = self._get_fields() # DummyRuntime.handler_url returns '/handler/block/handler' assert fields['transcripts']['urlRoot'] == '/handler/block/handler' - - def test_edx_video_id_type_is_video_id(self): - """'edx_video_id' type must be changed to 'VideoID'.""" - fields = self._get_fields() assert fields['edx_video_id']['type'] == 'VideoID' + assert fields['handout']['type'] == 'FileUploader' + + def test_field_modifications_with_custom_args(self): + """Verify transcripts.value passthrough and public_access enrichment.""" + fields = self._get_fields(transcripts={'fr': 'french.srt'}) + assert fields['transcripts']['value'] == {'fr': 'french.srt'} - def test_public_access_type_and_url(self): - """'public_access' type becomes 'PublicAccess' and url is set from get_public_video_url.""" fields = self._get_fields(public_url='https://example.com/video') assert fields['public_access']['type'] == 'PublicAccess' assert fields['public_access']['url'] == 'https://example.com/video' - def test_handout_type_is_file_uploader(self): - """'handout' type must be changed to 'FileUploader'.""" - fields = self._get_fields() - assert fields['handout']['type'] == 'FileUploader' - # ------------------------------------------------------------------ # License field handling # ------------------------------------------------------------------ @@ -131,36 +116,20 @@ def test_license_removed_when_licensing_disabled(self): """'license' is removed when the settings service reports licensing_enabled=False.""" settings_service = Mock() settings_service.get_settings_bucket.return_value = {'licensing_enabled': False} - - def _service(_block, name): - return settings_service if name == 'settings' else None - - with patch.object(self.block, '_get_editable_metadata_fields', - return_value=self._base_fields(include_license=True)): - with patch.object(self.block, 'get_transcripts_info', - return_value={'sub': '', 'transcripts': {}}): - with patch.object(self.block, 'get_public_video_url', return_value=None): - with patch.object(self.block.runtime, 'service', side_effect=_service): - fields = self.block.editable_metadata_fields - + fields = self._get_fields( + include_license=True, + service=self._service_stub('settings', settings_service), + ) assert 'license' not in fields def test_license_kept_when_licensing_enabled(self): """'license' is kept when the settings service reports licensing_enabled=True.""" settings_service = Mock() settings_service.get_settings_bucket.return_value = {'licensing_enabled': True} - - def _service(_block, name): - return settings_service if name == 'settings' else None - - with patch.object(self.block, '_get_editable_metadata_fields', - return_value=self._base_fields(include_license=True)): - with patch.object(self.block, 'get_transcripts_info', - return_value={'sub': '', 'transcripts': {}}): - with patch.object(self.block, 'get_public_video_url', return_value=None): - with patch.object(self.block.runtime, 'service', side_effect=_service): - fields = self.block.editable_metadata_fields - + fields = self._get_fields( + include_license=True, + service=self._service_stub('settings', settings_service), + ) assert 'license' in fields # ------------------------------------------------------------------ @@ -171,39 +140,32 @@ def test_english_transcript_found_added_to_value(self): """When video_config returns an English transcript, it is added to transcripts.value.""" video_config = Mock() video_config.get_transcript.return_value = ('content', 'en_subs_id', 'txt') - - def _service(_block, name): - return video_config if name == 'video_config' else None - self.block.sub = 'some_sub_id' # non-empty so possible_sub_ids is non-empty - - with patch.object(self.block, '_get_editable_metadata_fields', - return_value=self._base_fields(include_license=False)): - with patch.object(self.block, 'get_transcripts_info', - return_value={'sub': 'some_sub_id', 'transcripts': {}}): - with patch.object(self.block, 'get_public_video_url', return_value=None): - with patch.object(self.block.runtime, 'service', side_effect=_service): - fields = self.block.editable_metadata_fields - + fields = self._get_fields( + transcripts={}, + service=self._service_stub('video_config', video_config), + ) assert fields['transcripts']['value'] == {'en': 'en_subs_id'} + def test_english_transcript_merged_with_existing_transcripts(self): + """English transcript from video_config is merged with, not replacing, existing transcripts.""" + video_config = Mock() + video_config.get_transcript.return_value = ('content', 'en_subs_id', 'txt') + self.block.sub = 'some_sub_id' + fields = self._get_fields( + transcripts={'fr': 'french.srt'}, + service=self._service_stub('video_config', video_config), + ) + assert fields['transcripts']['value'] == {'fr': 'french.srt', 'en': 'en_subs_id'} + def test_transcript_not_found_leaves_value_unchanged(self): """When video_config raises TranscriptNotFoundError, transcripts.value is not modified.""" video_config = Mock() video_config.get_transcript.side_effect = TranscriptNotFoundError - - def _service(_block, name): - return video_config if name == 'video_config' else None - self.block.sub = 'some_sub_id' - - with patch.object(self.block, '_get_editable_metadata_fields', - return_value=self._base_fields(include_license=False)): - with patch.object(self.block, 'get_transcripts_info', - return_value={'sub': 'some_sub_id', 'transcripts': {'fr': 'french.srt'}}): - with patch.object(self.block, 'get_public_video_url', return_value=None): - with patch.object(self.block.runtime, 'service', side_effect=_service): - fields = self.block.editable_metadata_fields - + fields = self._get_fields( + transcripts={'fr': 'french.srt'}, + service=self._service_stub('video_config', video_config), + ) assert 'en' not in fields['transcripts']['value'] assert fields['transcripts']['value'] == {'fr': 'french.srt'}