chore: make the tests run for both BuiltIn and Extracted LTI Blocks (#36020)

This commit is contained in:
M. Tayyab Tahir Qureshi
2026-01-19 16:07:06 +05:00
committed by GitHub
parent 68ba45a858
commit a6c3c3236d
4 changed files with 135 additions and 29 deletions

View File

@@ -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 lms.djangoapps.courseware.views.views import get_course_lti_endpoints
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 @@ class TestLTI(BaseTestXmodule):
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 @@ class TestLTIBlockListing(SharedModuleStoreTestCase):
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

View File

@@ -992,8 +992,17 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'}
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"

View File

@@ -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 @@ class LTI20RESTResultServiceTest(unittest.TestCase):
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 @@ class LTI20RESTResultServiceTest(unittest.TestCase):
"""
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 @@ class LTI20RESTResultServiceTest(unittest.TestCase):
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 @@ class LTI20RESTResultServiceTest(unittest.TestCase):
fit the form user/<anon_id>
"""
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 @@ class LTI20RESTResultServiceTest(unittest.TestCase):
"""
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 @@ class LTI20RESTResultServiceTest(unittest.TestCase):
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 @@ class LTI20RESTResultServiceTest(unittest.TestCase):
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 @@ class LTI20RESTResultServiceTest(unittest.TestCase):
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

View File

@@ -21,17 +21,30 @@ from xblock.fields import ScopeIds, Timedelta
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 @@ class LTIBlockTest(TestCase):
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 @@ class LTIBlockTest(TestCase):
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 @@ class LTIBlockTest(TestCase):
"""
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 @@ class LTIBlockTest(TestCase):
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 @@ class LTIBlockTest(TestCase):
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