diff --git a/lms/djangoapps/courseware/tests/test_lti_integration.py b/lms/djangoapps/courseware/tests/test_lti_integration.py index c9a8a5450712..bdaf1a13549b 100644 --- a/lms/djangoapps/courseware/tests/test_lti_integration.py +++ b/lms/djangoapps/courseware/tests/test_lti_integration.py @@ -1,14 +1,19 @@ """LTI integration tests""" +import importlib import json +import re from collections import OrderedDict from unittest import mock +from unittest.mock import patch import urllib import oauthlib from django.conf import settings +from django.test import override_settings from django.urls import reverse +from xblock import plugin from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID from lms.djangoapps.courseware.tests.helpers import BaseTestXmodule @@ -16,9 +21,11 @@ from openedx.core.lib.url_utils import quote_slashes from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.tests.helpers import mock_render_template +from xmodule import lti_block -class TestLTI(BaseTestXmodule): +class _TestLTIBase(BaseTestXmodule): """ Integration test for lti xmodule. @@ -26,8 +33,15 @@ class TestLTI(BaseTestXmodule): As part of that, checks oauth signature generation by mocking signing function of `oauthlib` library. """ + __test__ = False CATEGORY = "lti" + @classmethod + def setUpClass(cls): + super().setUpClass() + plugin.PLUGIN_CACHE = {} + importlib.reload(lti_block) + def setUp(self): """ Mock oauth1 signing of requests library for testing. @@ -115,21 +129,37 @@ def mocked_sign(self, *args, **kwargs): patcher.start() self.addCleanup(patcher.stop) - def test_lti_constructor(self): + @patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template) + def test_lti_constructor(self, mock_render_django_template): generated_content = self.block.student_view(None).content - expected_content = self.runtime.render_template('lti.html', self.expected_context) + + if settings.USE_EXTRACTED_LTI_BLOCK: + # Remove i18n service from the extracted LTI Block's rendered `student_view` content + generated_content = re.sub(r"\{.*?}", "{}", generated_content) + expected_content = self.runtime.render_template('templates/lti.html', self.expected_context) + mock_render_django_template.assert_called_once() + else: + expected_content = self.runtime.render_template('lti.html', self.expected_context) assert generated_content == expected_content - def test_lti_preview_handler(self): + @patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template) + def test_lti_preview_handler(self, mock_render_django_template): generated_content = self.block.preview_handler(None, None).body - expected_content = self.runtime.render_template('lti_form.html', self.expected_context) + + if settings.USE_EXTRACTED_LTI_BLOCK: + expected_content = self.runtime.render_template('templates/lti_form.html', self.expected_context) + mock_render_django_template.assert_called_once() + else: + expected_content = self.runtime.render_template('lti_form.html', self.expected_context) assert generated_content.decode('utf-8') == expected_content -class TestLTIBlockListing(SharedModuleStoreTestCase): +class _TestLTIBlockListingBase(SharedModuleStoreTestCase): """ a test for the rest endpoint that lists LTI blocks in a course """ + + __test__ = False # arbitrary constant COURSE_SLUG = "100" COURSE_NAME = "test_course" @@ -214,3 +244,23 @@ def test_lti_rest_non_get(self): request.method = method response = get_course_lti_endpoints(request, str(self.course.id)) assert 405 == response.status_code + + +@override_settings(USE_EXTRACTED_LTI_BLOCK=True) +class TestLTIExtracted(_TestLTIBase): + __test__ = True + + +@override_settings(USE_EXTRACTED_LTI_BLOCK=False) +class TestLTIBuiltIn(_TestLTIBase): + __test__ = True + + +@override_settings(USE_EXTRACTED_LTI_BLOCK=True) +class TestLTIBlockListingExtracted(_TestLTIBlockListingBase): + __test__ = True + + +@override_settings(USE_EXTRACTED_LTI_BLOCK=False) +class TestLTIBlockListingBuiltIn(_TestLTIBlockListingBase): + __test__ = True diff --git a/xmodule/lti_block.py b/xmodule/lti_block.py index 2dfd258639fe..944b860f58b5 100644 --- a/xmodule/lti_block.py +++ b/xmodule/lti_block.py @@ -992,8 +992,17 @@ def is_past_due(self): return close_date is not None and datetime.datetime.now(ZoneInfo("UTC")) > close_date -LTIBlock = ( - _ExtractedLTIBlock if settings.USE_EXTRACTED_LTI_BLOCK - else _BuiltInLTIBlock -) +LTIBlock = None + + +def reset_class(): + """Reset class as per django settings flag""" + global LTIBlock + LTIBlock = ( + _ExtractedLTIBlock if settings.USE_EXTRACTED_LTI_BLOCK + else _BuiltInLTIBlock + ) + return LTIBlock + +reset_class() LTIBlock.__name__ = "LTIBlock" diff --git a/xmodule/tests/test_lti20_unit.py b/xmodule/tests/test_lti20_unit.py index 3d1ddb0c56b2..1734b938c453 100644 --- a/xmodule/tests/test_lti20_unit.py +++ b/xmodule/tests/test_lti20_unit.py @@ -3,25 +3,39 @@ import datetime import textwrap -import unittest +from django.conf import settings +from django.test import TestCase, override_settings from unittest.mock import Mock from zoneinfo import ZoneInfo from xblock.field_data import DictFieldData -from xmodule.lti_2_util import LTIError -from xmodule.lti_block import LTIBlock +from xmodule import lti_block from xmodule.tests.helpers import StubUserService from . import get_test_system -class LTI20RESTResultServiceTest(unittest.TestCase): +from xmodule.lti_2_util import LTIError as BuiltInLTIError +from xblocks_contrib.lti.lti_2_util import LTIError as ExtractedLTIError + + +class _LTI20RESTResultServiceTestBase(TestCase): """Logic tests for LTI block. LTI2.0 REST ResultService""" + __test__ = False USER_STANDIN = Mock() USER_STANDIN.id = 999 + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.lti_class = lti_block.reset_class() + if settings.USE_EXTRACTED_LTI_BLOCK: + cls.LTIError = ExtractedLTIError + else: + cls.LTIError = BuiltInLTIError + def setUp(self): super().setUp() self.runtime = get_test_system(user=self.USER_STANDIN) @@ -29,7 +43,7 @@ def setUp(self): self.runtime.publish = Mock() self.runtime._services['rebind_user'] = Mock() # pylint: disable=protected-access - self.xblock = LTIBlock(self.runtime, DictFieldData({}), Mock()) + self.xblock = self.lti_class(self.runtime, DictFieldData({}), Mock()) self.lti_id = self.xblock.lti_id self.xblock.due = None self.xblock.graceperiod = None @@ -56,7 +70,7 @@ def test_lti20_rest_bad_contenttype(self): """ Input with bad content type """ - with self.assertRaisesRegex(LTIError, "Content-Type must be"): + with self.assertRaisesRegex(self.LTIError, "Content-Type must be"): request = Mock(headers={'Content-Type': 'Non-existent'}) self.xblock.verify_lti_2_0_result_rest_headers(request) @@ -65,8 +79,8 @@ def test_lti20_rest_failed_oauth_body_verify(self): Input with bad oauth body hash verification """ err_msg = "OAuth body verification failed" - self.xblock.verify_oauth_body_sign = Mock(side_effect=LTIError(err_msg)) - with self.assertRaisesRegex(LTIError, err_msg): + self.xblock.verify_oauth_body_sign = Mock(side_effect=self.LTIError(err_msg)) + with self.assertRaisesRegex(self.LTIError, err_msg): request = Mock(headers={'Content-Type': 'application/vnd.ims.lis.v2.result+json'}) self.xblock.verify_lti_2_0_result_rest_headers(request) @@ -99,7 +113,7 @@ def test_lti20_rest_bad_dispatch(self): fit the form user/ """ for einput in self.BAD_DISPATCH_INPUTS: - with self.assertRaisesRegex(LTIError, "No valid user id found in endpoint URL"): + with self.assertRaisesRegex(self.LTIError, "No valid user id found in endpoint URL"): self.xblock.parse_lti_2_0_handler_suffix(einput) GOOD_DISPATCH_INPUTS = [ @@ -160,7 +174,7 @@ def test_lti20_bad_json(self): """ for error_inputs, error_message in self.BAD_JSON_INPUTS: for einput in error_inputs: - with self.assertRaisesRegex(LTIError, error_message): + with self.assertRaisesRegex(self.LTIError, error_message): self.xblock.parse_lti_2_0_result_json(einput) GOOD_JSON_INPUTS = [ @@ -341,7 +355,7 @@ def test_lti20_request_handler_bad_headers(self): Test that we get a 401 when header verification fails """ self.setup_system_xblock_mocks_for_lti20_request_test() - self.xblock.verify_lti_2_0_result_rest_headers = Mock(side_effect=LTIError()) + self.xblock.verify_lti_2_0_result_rest_headers = Mock(side_effect=self.LTIError()) mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd") assert response.status_code == 401 @@ -360,7 +374,7 @@ def test_lti20_request_handler_bad_json(self): Test that we get a 404 when json verification fails """ self.setup_system_xblock_mocks_for_lti20_request_test() - self.xblock.parse_lti_2_0_result_json = Mock(side_effect=LTIError()) + self.xblock.parse_lti_2_0_result_json = Mock(side_effect=self.LTIError()) mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd") assert response.status_code == 404 @@ -385,3 +399,13 @@ def test_lti20_request_handler_grade_past_due(self): mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd") assert response.status_code == 404 + + +@override_settings(USE_EXTRACTED_LTI_BLOCK=True) +class TestLTI20RESTResultServiceWithExtracted(_LTI20RESTResultServiceTestBase): + __test__ = True + + +@override_settings(USE_EXTRACTED_LTI_BLOCK=False) +class TestLTI20RESTResultServiceWithBuiltIn(_LTI20RESTResultServiceTestBase): + __test__ = True diff --git a/xmodule/tests/test_lti_unit.py b/xmodule/tests/test_lti_unit.py index be314cab7221..316ff58f4384 100644 --- a/xmodule/tests/test_lti_unit.py +++ b/xmodule/tests/test_lti_unit.py @@ -21,17 +21,30 @@ from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID -from xmodule.lti_2_util import LTIError -from xmodule.lti_block import LTIBlock +from xmodule import lti_block from xmodule.tests.helpers import StubUserService from . import get_test_system +from xmodule.lti_2_util import LTIError as BuiltInLTIError +from xblocks_contrib.lti.lti_2_util import LTIError as ExtractedLTIError + @override_settings(LMS_BASE="edx.org") -class LTIBlockTest(TestCase): +class _TestLTIBase(TestCase): """Logic tests for LTI block.""" + __test__ = False + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.lti_class = lti_block.reset_class() + if settings.USE_EXTRACTED_LTI_BLOCK: + cls.LTIError = ExtractedLTIError + else: + cls.LTIError = BuiltInLTIError + def setUp(self): super().setUp() self.environ = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'POST'} @@ -66,7 +79,7 @@ def setUp(self): self.runtime.publish = Mock() self.runtime._services['rebind_user'] = Mock() # pylint: disable=protected-access - self.xblock = LTIBlock( + self.xblock = self.lti_class( self.runtime, DictFieldData({}), ScopeIds(None, None, None, BlockUsageLocator(self.course_id, 'lti', 'name')) @@ -374,7 +387,7 @@ def test_bad_client_key_secret(self): runtime = Mock(modulestore=modulestore) self.xblock.runtime = runtime self.xblock.lti_id = 'lti_id' - with pytest.raises(LTIError): + with pytest.raises(self.LTIError): self.xblock.get_client_key_secret() @patch('xmodule.lti_block.signature.verify_hmac_sha1', Mock(return_value=True)) @@ -468,7 +481,7 @@ def test_failed_verify_oauth_body_sign(self): """ Oauth signing verify fail. """ - with pytest.raises(LTIError): + with pytest.raises(self.LTIError): req = self.get_signed_grade_mock_request() self.xblock.verify_oauth_body_sign(req) @@ -523,7 +536,7 @@ def test_bad_custom_params(self): self.xblock.custom_parameters = bad_custom_params self.xblock.get_client_key_secret = Mock(return_value=('test_client_key', 'test_client_secret')) self.xblock.oauth_params = Mock() - with pytest.raises(LTIError): + with pytest.raises(self.LTIError): self.xblock.get_input_fields() def test_max_score(self): @@ -541,3 +554,13 @@ def test_context_id(self): Tests that LTI parameter context_id is equal to course_id. """ assert str(self.course_id) == self.xblock.context_id + + +@override_settings(USE_EXTRACTED_LTI_BLOCK=True) +class TestLTIExtracted(_TestLTIBase): + __test__ = True + + +@override_settings(USE_EXTRACTED_LTI_BLOCK=False) +class TestLTIBuiltIn(_TestLTIBase): + __test__ = True