Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions lms/djangoapps/courseware/tests/test_lti_integration.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
"""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
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.

It checks overall code, by assuring that context that goes to template is correct.
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.
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
17 changes: 13 additions & 4 deletions xmodule/lti_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
48 changes: 36 additions & 12 deletions xmodule/tests/test_lti20_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,47 @@

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)
self.environ = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'POST'}
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
Expand All @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -99,7 +113,7 @@ def test_lti20_rest_bad_dispatch(self):
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 = [
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
37 changes: 30 additions & 7 deletions xmodule/tests/test_lti_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand All @@ -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
Loading