Skip to content

Commit a6c3c32

Browse files
authored
chore: make the tests run for both BuiltIn and Extracted LTI Blocks (#36020)
1 parent 68ba45a commit a6c3c32

File tree

4 files changed

+135
-29
lines changed

4 files changed

+135
-29
lines changed

lms/djangoapps/courseware/tests/test_lti_integration.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,47 @@
11
"""LTI integration tests"""
22

33

4+
import importlib
45
import json
6+
import re
57
from collections import OrderedDict
68

79
from unittest import mock
10+
from unittest.mock import patch
811
import urllib
912
import oauthlib
1013
from django.conf import settings
14+
from django.test import override_settings
1115
from django.urls import reverse
16+
from xblock import plugin
1217

1318
from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID
1419
from lms.djangoapps.courseware.tests.helpers import BaseTestXmodule
1520
from lms.djangoapps.courseware.views.views import get_course_lti_endpoints
1621
from openedx.core.lib.url_utils import quote_slashes
1722
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
1823
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
24+
from xmodule.tests.helpers import mock_render_template
25+
from xmodule import lti_block
1926

2027

21-
class TestLTI(BaseTestXmodule):
28+
class _TestLTIBase(BaseTestXmodule):
2229
"""
2330
Integration test for lti xmodule.
2431
2532
It checks overall code, by assuring that context that goes to template is correct.
2633
As part of that, checks oauth signature generation by mocking signing function
2734
of `oauthlib` library.
2835
"""
36+
__test__ = False
2937
CATEGORY = "lti"
3038

39+
@classmethod
40+
def setUpClass(cls):
41+
super().setUpClass()
42+
plugin.PLUGIN_CACHE = {}
43+
importlib.reload(lti_block)
44+
3145
def setUp(self):
3246
"""
3347
Mock oauth1 signing of requests library for testing.
@@ -115,21 +129,37 @@ def mocked_sign(self, *args, **kwargs):
115129
patcher.start()
116130
self.addCleanup(patcher.stop)
117131

118-
def test_lti_constructor(self):
132+
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
133+
def test_lti_constructor(self, mock_render_django_template):
119134
generated_content = self.block.student_view(None).content
120-
expected_content = self.runtime.render_template('lti.html', self.expected_context)
135+
136+
if settings.USE_EXTRACTED_LTI_BLOCK:
137+
# Remove i18n service from the extracted LTI Block's rendered `student_view` content
138+
generated_content = re.sub(r"\{.*?}", "{}", generated_content)
139+
expected_content = self.runtime.render_template('templates/lti.html', self.expected_context)
140+
mock_render_django_template.assert_called_once()
141+
else:
142+
expected_content = self.runtime.render_template('lti.html', self.expected_context)
121143
assert generated_content == expected_content
122144

123-
def test_lti_preview_handler(self):
145+
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
146+
def test_lti_preview_handler(self, mock_render_django_template):
124147
generated_content = self.block.preview_handler(None, None).body
125-
expected_content = self.runtime.render_template('lti_form.html', self.expected_context)
148+
149+
if settings.USE_EXTRACTED_LTI_BLOCK:
150+
expected_content = self.runtime.render_template('templates/lti_form.html', self.expected_context)
151+
mock_render_django_template.assert_called_once()
152+
else:
153+
expected_content = self.runtime.render_template('lti_form.html', self.expected_context)
126154
assert generated_content.decode('utf-8') == expected_content
127155

128156

129-
class TestLTIBlockListing(SharedModuleStoreTestCase):
157+
class _TestLTIBlockListingBase(SharedModuleStoreTestCase):
130158
"""
131159
a test for the rest endpoint that lists LTI blocks in a course
132160
"""
161+
162+
__test__ = False
133163
# arbitrary constant
134164
COURSE_SLUG = "100"
135165
COURSE_NAME = "test_course"
@@ -214,3 +244,23 @@ def test_lti_rest_non_get(self):
214244
request.method = method
215245
response = get_course_lti_endpoints(request, str(self.course.id))
216246
assert 405 == response.status_code
247+
248+
249+
@override_settings(USE_EXTRACTED_LTI_BLOCK=True)
250+
class TestLTIExtracted(_TestLTIBase):
251+
__test__ = True
252+
253+
254+
@override_settings(USE_EXTRACTED_LTI_BLOCK=False)
255+
class TestLTIBuiltIn(_TestLTIBase):
256+
__test__ = True
257+
258+
259+
@override_settings(USE_EXTRACTED_LTI_BLOCK=True)
260+
class TestLTIBlockListingExtracted(_TestLTIBlockListingBase):
261+
__test__ = True
262+
263+
264+
@override_settings(USE_EXTRACTED_LTI_BLOCK=False)
265+
class TestLTIBlockListingBuiltIn(_TestLTIBlockListingBase):
266+
__test__ = True

xmodule/lti_block.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -992,8 +992,17 @@ def is_past_due(self):
992992
return close_date is not None and datetime.datetime.now(ZoneInfo("UTC")) > close_date
993993

994994

995-
LTIBlock = (
996-
_ExtractedLTIBlock if settings.USE_EXTRACTED_LTI_BLOCK
997-
else _BuiltInLTIBlock
998-
)
995+
LTIBlock = None
996+
997+
998+
def reset_class():
999+
"""Reset class as per django settings flag"""
1000+
global LTIBlock
1001+
LTIBlock = (
1002+
_ExtractedLTIBlock if settings.USE_EXTRACTED_LTI_BLOCK
1003+
else _BuiltInLTIBlock
1004+
)
1005+
return LTIBlock
1006+
1007+
reset_class()
9991008
LTIBlock.__name__ = "LTIBlock"

xmodule/tests/test_lti20_unit.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,47 @@
33

44
import datetime
55
import textwrap
6-
import unittest
6+
from django.conf import settings
7+
from django.test import TestCase, override_settings
78
from unittest.mock import Mock
89
from zoneinfo import ZoneInfo
910

1011
from xblock.field_data import DictFieldData
1112

12-
from xmodule.lti_2_util import LTIError
13-
from xmodule.lti_block import LTIBlock
13+
from xmodule import lti_block
1414
from xmodule.tests.helpers import StubUserService
1515

1616
from . import get_test_system
1717

1818

19-
class LTI20RESTResultServiceTest(unittest.TestCase):
19+
from xmodule.lti_2_util import LTIError as BuiltInLTIError
20+
from xblocks_contrib.lti.lti_2_util import LTIError as ExtractedLTIError
21+
22+
23+
class _LTI20RESTResultServiceTestBase(TestCase):
2024
"""Logic tests for LTI block. LTI2.0 REST ResultService"""
2125

26+
__test__ = False
2227
USER_STANDIN = Mock()
2328
USER_STANDIN.id = 999
2429

30+
@classmethod
31+
def setUpClass(cls):
32+
super().setUpClass()
33+
cls.lti_class = lti_block.reset_class()
34+
if settings.USE_EXTRACTED_LTI_BLOCK:
35+
cls.LTIError = ExtractedLTIError
36+
else:
37+
cls.LTIError = BuiltInLTIError
38+
2539
def setUp(self):
2640
super().setUp()
2741
self.runtime = get_test_system(user=self.USER_STANDIN)
2842
self.environ = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'POST'}
2943
self.runtime.publish = Mock()
3044
self.runtime._services['rebind_user'] = Mock() # pylint: disable=protected-access
3145

32-
self.xblock = LTIBlock(self.runtime, DictFieldData({}), Mock())
46+
self.xblock = self.lti_class(self.runtime, DictFieldData({}), Mock())
3347
self.lti_id = self.xblock.lti_id
3448
self.xblock.due = None
3549
self.xblock.graceperiod = None
@@ -56,7 +70,7 @@ def test_lti20_rest_bad_contenttype(self):
5670
"""
5771
Input with bad content type
5872
"""
59-
with self.assertRaisesRegex(LTIError, "Content-Type must be"):
73+
with self.assertRaisesRegex(self.LTIError, "Content-Type must be"):
6074
request = Mock(headers={'Content-Type': 'Non-existent'})
6175
self.xblock.verify_lti_2_0_result_rest_headers(request)
6276

@@ -65,8 +79,8 @@ def test_lti20_rest_failed_oauth_body_verify(self):
6579
Input with bad oauth body hash verification
6680
"""
6781
err_msg = "OAuth body verification failed"
68-
self.xblock.verify_oauth_body_sign = Mock(side_effect=LTIError(err_msg))
69-
with self.assertRaisesRegex(LTIError, err_msg):
82+
self.xblock.verify_oauth_body_sign = Mock(side_effect=self.LTIError(err_msg))
83+
with self.assertRaisesRegex(self.LTIError, err_msg):
7084
request = Mock(headers={'Content-Type': 'application/vnd.ims.lis.v2.result+json'})
7185
self.xblock.verify_lti_2_0_result_rest_headers(request)
7286

@@ -99,7 +113,7 @@ def test_lti20_rest_bad_dispatch(self):
99113
fit the form user/<anon_id>
100114
"""
101115
for einput in self.BAD_DISPATCH_INPUTS:
102-
with self.assertRaisesRegex(LTIError, "No valid user id found in endpoint URL"):
116+
with self.assertRaisesRegex(self.LTIError, "No valid user id found in endpoint URL"):
103117
self.xblock.parse_lti_2_0_handler_suffix(einput)
104118

105119
GOOD_DISPATCH_INPUTS = [
@@ -160,7 +174,7 @@ def test_lti20_bad_json(self):
160174
"""
161175
for error_inputs, error_message in self.BAD_JSON_INPUTS:
162176
for einput in error_inputs:
163-
with self.assertRaisesRegex(LTIError, error_message):
177+
with self.assertRaisesRegex(self.LTIError, error_message):
164178
self.xblock.parse_lti_2_0_result_json(einput)
165179

166180
GOOD_JSON_INPUTS = [
@@ -341,7 +355,7 @@ def test_lti20_request_handler_bad_headers(self):
341355
Test that we get a 401 when header verification fails
342356
"""
343357
self.setup_system_xblock_mocks_for_lti20_request_test()
344-
self.xblock.verify_lti_2_0_result_rest_headers = Mock(side_effect=LTIError())
358+
self.xblock.verify_lti_2_0_result_rest_headers = Mock(side_effect=self.LTIError())
345359
mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT)
346360
response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd")
347361
assert response.status_code == 401
@@ -360,7 +374,7 @@ def test_lti20_request_handler_bad_json(self):
360374
Test that we get a 404 when json verification fails
361375
"""
362376
self.setup_system_xblock_mocks_for_lti20_request_test()
363-
self.xblock.parse_lti_2_0_result_json = Mock(side_effect=LTIError())
377+
self.xblock.parse_lti_2_0_result_json = Mock(side_effect=self.LTIError())
364378
mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT)
365379
response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd")
366380
assert response.status_code == 404
@@ -385,3 +399,13 @@ def test_lti20_request_handler_grade_past_due(self):
385399
mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT)
386400
response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd")
387401
assert response.status_code == 404
402+
403+
404+
@override_settings(USE_EXTRACTED_LTI_BLOCK=True)
405+
class TestLTI20RESTResultServiceWithExtracted(_LTI20RESTResultServiceTestBase):
406+
__test__ = True
407+
408+
409+
@override_settings(USE_EXTRACTED_LTI_BLOCK=False)
410+
class TestLTI20RESTResultServiceWithBuiltIn(_LTI20RESTResultServiceTestBase):
411+
__test__ = True

xmodule/tests/test_lti_unit.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,30 @@
2121

2222

2323
from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID
24-
from xmodule.lti_2_util import LTIError
25-
from xmodule.lti_block import LTIBlock
24+
from xmodule import lti_block
2625
from xmodule.tests.helpers import StubUserService
2726

2827
from . import get_test_system
2928

29+
from xmodule.lti_2_util import LTIError as BuiltInLTIError
30+
from xblocks_contrib.lti.lti_2_util import LTIError as ExtractedLTIError
31+
3032

3133
@override_settings(LMS_BASE="edx.org")
32-
class LTIBlockTest(TestCase):
34+
class _TestLTIBase(TestCase):
3335
"""Logic tests for LTI block."""
3436

37+
__test__ = False
38+
39+
@classmethod
40+
def setUpClass(cls):
41+
super().setUpClass()
42+
cls.lti_class = lti_block.reset_class()
43+
if settings.USE_EXTRACTED_LTI_BLOCK:
44+
cls.LTIError = ExtractedLTIError
45+
else:
46+
cls.LTIError = BuiltInLTIError
47+
3548
def setUp(self):
3649
super().setUp()
3750
self.environ = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'POST'}
@@ -66,7 +79,7 @@ def setUp(self):
6679
self.runtime.publish = Mock()
6780
self.runtime._services['rebind_user'] = Mock() # pylint: disable=protected-access
6881

69-
self.xblock = LTIBlock(
82+
self.xblock = self.lti_class(
7083
self.runtime,
7184
DictFieldData({}),
7285
ScopeIds(None, None, None, BlockUsageLocator(self.course_id, 'lti', 'name'))
@@ -374,7 +387,7 @@ def test_bad_client_key_secret(self):
374387
runtime = Mock(modulestore=modulestore)
375388
self.xblock.runtime = runtime
376389
self.xblock.lti_id = 'lti_id'
377-
with pytest.raises(LTIError):
390+
with pytest.raises(self.LTIError):
378391
self.xblock.get_client_key_secret()
379392

380393
@patch('xmodule.lti_block.signature.verify_hmac_sha1', Mock(return_value=True))
@@ -468,7 +481,7 @@ def test_failed_verify_oauth_body_sign(self):
468481
"""
469482
Oauth signing verify fail.
470483
"""
471-
with pytest.raises(LTIError):
484+
with pytest.raises(self.LTIError):
472485
req = self.get_signed_grade_mock_request()
473486
self.xblock.verify_oauth_body_sign(req)
474487

@@ -523,7 +536,7 @@ def test_bad_custom_params(self):
523536
self.xblock.custom_parameters = bad_custom_params
524537
self.xblock.get_client_key_secret = Mock(return_value=('test_client_key', 'test_client_secret'))
525538
self.xblock.oauth_params = Mock()
526-
with pytest.raises(LTIError):
539+
with pytest.raises(self.LTIError):
527540
self.xblock.get_input_fields()
528541

529542
def test_max_score(self):
@@ -541,3 +554,13 @@ def test_context_id(self):
541554
Tests that LTI parameter context_id is equal to course_id.
542555
"""
543556
assert str(self.course_id) == self.xblock.context_id
557+
558+
559+
@override_settings(USE_EXTRACTED_LTI_BLOCK=True)
560+
class TestLTIExtracted(_TestLTIBase):
561+
__test__ = True
562+
563+
564+
@override_settings(USE_EXTRACTED_LTI_BLOCK=False)
565+
class TestLTIBuiltIn(_TestLTIBase):
566+
__test__ = True

0 commit comments

Comments
 (0)