From a42e959b32e975ac12a5f9dea1b2c6d93763f674 Mon Sep 17 00:00:00 2001 From: Muhammad Tayayb Tahir Qureshi Date: Mon, 25 Nov 2024 14:09:33 +0500 Subject: [PATCH] fix: fix --- xblocks_contrib/lti/lti.py | 21 ++ xblocks_contrib/lti/tests/helpers.py | 10 - ...{test_lti_2_unit.py => test_lti20_unit.py} | 126 ++++--- xblocks_contrib/lti/tests/test_lti_unit.py | 316 ++++++++++-------- 4 files changed, 284 insertions(+), 189 deletions(-) rename xblocks_contrib/lti/tests/{test_lti_2_unit.py => test_lti20_unit.py} (81%) diff --git a/xblocks_contrib/lti/lti.py b/xblocks_contrib/lti/lti.py index 3ea3bb6..48b8bbd 100644 --- a/xblocks_contrib/lti/lti.py +++ b/xblocks_contrib/lti/lti.py @@ -70,6 +70,7 @@ from django.conf import settings from lxml import etree from oauthlib.oauth1.rfc5849 import signature +from opaque_keys.edx.keys import UsageKey from pytz import UTC from webob import Response from web_fragments.fragment import Fragment @@ -374,6 +375,26 @@ class LTIBlock( "ask_to_send_email", "ask_to_send_username", "has_score", "weight", ) + @property + def course_id(self): + return self.location.course_key + + @property + def category(self): + return self.scope_ids.block_type + + @property + def location(self): + return self.scope_ids.usage_id + + @location.setter + def location(self, value): + assert isinstance(value, UsageKey) + self.scope_ids = self.scope_ids._replace( + def_id=value, # Note: assigning a UsageKey as def_id is OK in old mongo / import system but wrong in split + usage_id=value, + ) + def max_score(self): return self.weight if self.has_score else None diff --git a/xblocks_contrib/lti/tests/helpers.py b/xblocks_contrib/lti/tests/helpers.py index d9d8dca..a4863eb 100644 --- a/xblocks_contrib/lti/tests/helpers.py +++ b/xblocks_contrib/lti/tests/helpers.py @@ -139,23 +139,17 @@ def get_test_system( course_id=CourseKey.from_string("/".join(["org", "course", "run"])), user=None, user_is_staff=False, - user_location=None, ): """Construct a minimal test system for the LTIBlockTest.""" - # course_id = course_id or CourseKey.from_string("org/course/run") - # user = user or Mock(id="student", is_staff=user_is_staff) if not user: user = Mock(name='get_test_system.user', is_staff=False) - if not user_location: - user_location = Mock(name='get_test_system.user_location') user_service = StubUserService( user=user, anonymous_user_id='student', deprecated_anonymous_user_id='student', user_is_staff=user_is_staff, user_role='student', - request_country_code=user_location, ) runtime = MockRuntime( anonymous_student_id="student", @@ -164,8 +158,4 @@ def get_test_system( } ) - # Add necessary mocks - runtime.publish = Mock(name="publish") - runtime._services["rebind_user"] = Mock(name="rebind_user") - return runtime diff --git a/xblocks_contrib/lti/tests/test_lti_2_unit.py b/xblocks_contrib/lti/tests/test_lti20_unit.py similarity index 81% rename from xblocks_contrib/lti/tests/test_lti_2_unit.py rename to xblocks_contrib/lti/tests/test_lti20_unit.py index b45e667..85f9641 100644 --- a/xblocks_contrib/lti/tests/test_lti_2_unit.py +++ b/xblocks_contrib/lti/tests/test_lti20_unit.py @@ -1,12 +1,12 @@ """Tests for LTI Xmodule LTIv2.0 functional logic.""" - import datetime import textwrap -import unittest from unittest.mock import Mock from pytz import UTC +from django.conf import settings +from django.test import TestCase, override_settings from xblock.field_data import DictFieldData from xblocks_contrib.lti.lti_2_util import LTIError @@ -14,7 +14,8 @@ from .helpers import StubUserService, get_test_system -class LTI20RESTResultServiceTest(unittest.TestCase): +@override_settings(LMS_BASE="edx.org") +class LTI20RESTResultServiceTest(TestCase): """Logic tests for LTI block. LTI2.0 REST ResultService""" USER_STANDIN = Mock() @@ -23,20 +24,27 @@ class LTI20RESTResultServiceTest(unittest.TestCase): def setUp(self): super().setUp() self.runtime = get_test_system(user=self.USER_STANDIN) - self.environ = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'POST'} + self.environ = {"wsgi.url_scheme": "http", "REQUEST_METHOD": "POST"} self.runtime.publish = Mock() - self.runtime._services['rebind_user'] = Mock() # pylint: disable=protected-access + self.runtime._services["rebind_user"] = Mock() # pylint: disable=protected-access self.xblock = LTIBlock(self.runtime, DictFieldData({}), Mock()) self.lti_id = self.xblock.lti_id + + self.unquoted_resource_link_id = ( + "{}-i4x-2-3-lti-31de800015cf4afb973356dbe81496df".format(settings.LMS_BASE) + ) + self.xblock.due = None self.xblock.graceperiod = None def test_sanitize_get_context(self): """Tests that the get_context function does basic sanitization""" # get_context, unfortunately, requires a lot of mocking machinery - mocked_course = Mock(name='mocked_course', lti_passports=['lti_id:test_client:test_secret']) - modulestore = Mock(name='modulestore') + mocked_course = Mock( + name="mocked_course", lti_passports=["lti_id:test_client:test_secret"] + ) + modulestore = Mock(name="modulestore") modulestore.get_course.return_value = mocked_course self.xblock.runtime.modulestore = modulestore self.xblock.lti_id = "lti_id" @@ -48,14 +56,14 @@ def test_sanitize_get_context(self): ) for case in test_cases: self.xblock.score_comment = case[0] - assert case[1] == self.xblock.get_context()['comment'] + assert case[1] == self.xblock.get_context()["comment"] def test_lti20_rest_bad_contenttype(self): """ Input with bad content type """ with self.assertRaisesRegex(LTIError, "Content-Type must be"): - request = Mock(headers={'Content-Type': 'Non-existent'}) + request = Mock(headers={"Content-Type": "Non-existent"}) self.xblock.verify_lti_2_0_result_rest_headers(request) def test_lti20_rest_failed_oauth_body_verify(self): @@ -65,7 +73,9 @@ def test_lti20_rest_failed_oauth_body_verify(self): err_msg = "OAuth body verification failed" self.xblock.verify_oauth_body_sign = Mock(side_effect=LTIError(err_msg)) with self.assertRaisesRegex(LTIError, err_msg): - request = Mock(headers={'Content-Type': 'application/vnd.ims.lis.v2.result+json'}) + request = Mock( + headers={"Content-Type": "application/vnd.ims.lis.v2.result+json"} + ) self.xblock.verify_lti_2_0_result_rest_headers(request) def test_lti20_rest_good_headers(self): @@ -74,7 +84,9 @@ def test_lti20_rest_good_headers(self): """ self.xblock.verify_oauth_body_sign = Mock(return_value=True) - request = Mock(headers={'Content-Type': 'application/vnd.ims.lis.v2.result+json'}) + request = Mock( + headers={"Content-Type": "application/vnd.ims.lis.v2.result+json"} + ) self.xblock.verify_lti_2_0_result_rest_headers(request) # We just want the above call to complete without exceptions, and to have called verify_oauth_body_sign assert self.xblock.verify_oauth_body_sign.called @@ -88,7 +100,7 @@ def test_lti20_rest_good_headers(self): "user//" "user/gbere/" "user/gbere/xsdf" - "user/ಠ益ಠ" # not alphanumeric + "user/ಠ益ಠ", # not alphanumeric ] def test_lti20_rest_bad_dispatch(self): @@ -202,7 +214,7 @@ def test_lti20_good_json(self): "comment": "ಠ益ಠ"} """).encode('utf-8') - def get_signed_lti20_mock_request(self, body, method='PUT'): + def get_signed_lti20_mock_request(self, body, method="PUT"): """ Example of signed from LTI 2.0 Provider. Signatures and hashes are example only and won't verify """ @@ -216,9 +228,9 @@ def get_signed_lti20_mock_request(self, body, method='PUT'): 'oauth_consumer_key="test_client_key", ' 'oauth_signature="my_signature%3D", ' 'oauth_body_hash="gz+PeJZuF2//n9hNUnDj2v5kN70="' - ) + ), } - mock_request.url = 'http://testurl' + mock_request.url = "http://testurl" mock_request.http_method = method mock_request.method = method mock_request.body = body @@ -229,7 +241,9 @@ def setup_system_xblock_mocks_for_lti20_request_test(self): Helper fn to set up mocking for lti 2.0 request test """ self.xblock.max_score = Mock(return_value=1.0) - self.xblock.get_client_key_secret = Mock(return_value=('test_client_key', 'test_client_secret')) + self.xblock.get_client_key_secret = Mock( + return_value=("test_client_key", "test_client_secret") + ) self.xblock.verify_oauth_body_sign = Mock() def test_lti20_put_like_delete_success(self): @@ -241,17 +255,25 @@ def test_lti20_put_like_delete_success(self): COMMENT = "ಠ益ಠ" # pylint: disable=invalid-name self.xblock.module_score = SCORE self.xblock.score_comment = COMMENT - mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT_LIKE_DELETE) + mock_request = self.get_signed_lti20_mock_request( + self.GOOD_JSON_PUT_LIKE_DELETE + ) # Now call the handler response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd") # Now assert there's no score assert response.status_code == 200 assert self.xblock.module_score is None - assert self.xblock.score_comment == '' - (_, evt_type, called_grade_obj), _ = self.runtime.publish.call_args # pylint: disable=unpacking-non-sequence - assert called_grade_obj ==\ - {'user_id': self.USER_STANDIN.id, 'value': None, 'max_value': None, 'score_deleted': True} - assert evt_type == 'grade' + assert self.xblock.score_comment == "" + (_, evt_type, called_grade_obj), _ = ( + self.runtime.publish.call_args + ) # pylint: disable=unpacking-non-sequence + assert called_grade_obj == { + "user_id": self.USER_STANDIN.id, + "value": None, + "max_value": None, + "score_deleted": True, + } + assert evt_type == "grade" def test_lti20_delete_success(self): """ @@ -262,17 +284,23 @@ def test_lti20_delete_success(self): COMMENT = "ಠ益ಠ" # pylint: disable=invalid-name self.xblock.module_score = SCORE self.xblock.score_comment = COMMENT - mock_request = self.get_signed_lti20_mock_request(b"", method='DELETE') + mock_request = self.get_signed_lti20_mock_request(b"", method="DELETE") # Now call the handler response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd") # Now assert there's no score assert response.status_code == 200 assert self.xblock.module_score is None - assert self.xblock.score_comment == '' - (_, evt_type, called_grade_obj), _ = self.runtime.publish.call_args # pylint: disable=unpacking-non-sequence - assert called_grade_obj ==\ - {'user_id': self.USER_STANDIN.id, 'value': None, 'max_value': None, 'score_deleted': True} - assert evt_type == 'grade' + assert self.xblock.score_comment == "" + (_, evt_type, called_grade_obj), _ = ( + self.runtime.publish.call_args + ) # pylint: disable=unpacking-non-sequence + assert called_grade_obj == { + "user_id": self.USER_STANDIN.id, + "value": None, + "max_value": None, + "score_deleted": True, + } + assert evt_type == "grade" def test_lti20_put_set_score_success(self): """ @@ -285,23 +313,32 @@ def test_lti20_put_set_score_success(self): # Now assert assert response.status_code == 200 assert self.xblock.module_score == 0.1 - assert self.xblock.score_comment == 'ಠ益ಠ' - (_, evt_type, called_grade_obj), _ = self.runtime.publish.call_args # pylint: disable=unpacking-non-sequence - assert evt_type == 'grade' - assert called_grade_obj ==\ - {'user_id': self.USER_STANDIN.id, 'value': 0.1, 'max_value': 1.0, 'score_deleted': False} + assert self.xblock.score_comment == "ಠ益ಠ" + (_, evt_type, called_grade_obj), _ = ( + self.runtime.publish.call_args + ) # pylint: disable=unpacking-non-sequence + assert evt_type == "grade" + assert called_grade_obj == { + "user_id": self.USER_STANDIN.id, + "value": 0.1, + "max_value": 1.0, + "score_deleted": False, + } def test_lti20_get_no_score_success(self): """ The happy path for LTI 2.0 GET when there's no score """ self.setup_system_xblock_mocks_for_lti20_request_test() - mock_request = self.get_signed_lti20_mock_request(b"", method='GET') + mock_request = self.get_signed_lti20_mock_request(b"", method="GET") # Now call the handler response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd") # Now assert assert response.status_code == 200 - assert response.json == {'@context': 'http://purl.imsglobal.org/ctx/lis/v2/Result', '@type': 'Result'} + assert response.json == { + "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result", + "@type": "Result", + } def test_lti20_get_with_score_success(self): """ @@ -312,14 +349,17 @@ def test_lti20_get_with_score_success(self): COMMENT = "ಠ益ಠ" # pylint: disable=invalid-name self.xblock.module_score = SCORE self.xblock.score_comment = COMMENT - mock_request = self.get_signed_lti20_mock_request(b"", method='GET') + mock_request = self.get_signed_lti20_mock_request(b"", method="GET") # Now call the handler response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd") # Now assert assert response.status_code == 200 - assert response.json ==\ - {'@context': 'http://purl.imsglobal.org/ctx/lis/v2/Result', - '@type': 'Result', 'resultScore': SCORE, 'comment': COMMENT} + assert response.json == { + "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result", + "@type": "Result", + "resultScore": SCORE, + "comment": COMMENT, + } UNSUPPORTED_HTTP_METHODS = ["OPTIONS", "HEAD", "POST", "TRACE", "CONNECT"] @@ -331,7 +371,9 @@ def test_lti20_unsupported_method_error(self): mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) for bad_method in self.UNSUPPORTED_HTTP_METHODS: mock_request.method = bad_method - response = self.xblock.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xblock.lti_2_0_result_rest_handler( + mock_request, "user/abcd" + ) assert response.status_code == 404 def test_lti20_request_handler_bad_headers(self): @@ -368,7 +410,9 @@ def test_lti20_request_handler_bad_user(self): Test that we get a 404 when the supplied user does not exist """ self.setup_system_xblock_mocks_for_lti20_request_test() - self.runtime._services['user'] = StubUserService(user=None) # pylint: disable=protected-access + self.runtime._services["user"] = StubUserService( + user=None + ) # pylint: disable=protected-access 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 diff --git a/xblocks_contrib/lti/tests/test_lti_unit.py b/xblocks_contrib/lti/tests/test_lti_unit.py index 4ac3a8f..5ad3ea5 100644 --- a/xblocks_contrib/lti/tests/test_lti_unit.py +++ b/xblocks_contrib/lti/tests/test_lti_unit.py @@ -1,6 +1,5 @@ """Test for LTI Xmodule functional logic.""" - import datetime import textwrap from copy import copy @@ -23,7 +22,7 @@ from xblocks_contrib.lti.lti import LTIBlock from .helpers import StubUserService, Timedelta, get_test_system -ATTR_KEY_ANONYMOUS_USER_ID = 'edx-platform.anonymous_user_id' +ATTR_KEY_ANONYMOUS_USER_ID = "edx-platform.anonymous_user_id" @override_settings(LMS_BASE="edx.org") @@ -32,8 +31,9 @@ class LTIBlockTest(TestCase): def setUp(self): super().setUp() - self.environ = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'POST'} - self.request_body_xml_template = textwrap.dedent(""" + self.environ = {"wsgi.url_scheme": "http", "REQUEST_METHOD": "POST"} + self.request_body_xml_template = textwrap.dedent( + """ @@ -67,24 +67,29 @@ def setUp(self): self.xblock = LTIBlock( self.runtime, DictFieldData({}), - ScopeIds(None, None, None, BlockUsageLocator(self.course_id, 'lti', 'name')) + ScopeIds( + None, None, None, BlockUsageLocator(self.course_id, "lti", "name") + ), ) - current_user = self.runtime.service(self.xblock, 'user').get_current_user() + current_user = self.runtime.service(self.xblock, "user").get_current_user() self.user_id = current_user.opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) self.lti_id = self.xblock.lti_id - self.unquoted_resource_link_id = '{}-i4x-2-3-lti-31de800015cf4afb973356dbe81496df'.format( - settings.LMS_BASE + self.unquoted_resource_link_id = ( + "{}-i4x-2-3-lti-31de800015cf4afb973356dbe81496df".format(settings.LMS_BASE) ) - sourced_id = ':'.join(parse.quote(i) for i in (self.lti_id, self.unquoted_resource_link_id, self.user_id)) # lint-amnesty, pylint: disable=line-too-long + sourced_id = ":".join( + parse.quote(i) + for i in (self.lti_id, self.unquoted_resource_link_id, self.user_id) + ) # lint-amnesty, pylint: disable=line-too-long self.defaults = { - 'namespace': "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0", - 'sourcedId': sourced_id, - 'action': 'replaceResultRequest', - 'grade': 0.5, - 'messageIdentifier': '528243ba5241b', + "namespace": "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0", + "sourcedId": sourced_id, + "action": "replaceResultRequest", + "grade": 0.5, + "messageIdentifier": "528243ba5241b", } self.xblock.due = None @@ -97,35 +102,41 @@ def get_request_body(self, params=None): data = copy(self.defaults) data.update(params) - return self.request_body_xml_template.format(**data).encode('utf-8') + return self.request_body_xml_template.format(**data).encode("utf-8") def get_response_values(self, response): """Gets the values from the given response""" - parser = etree.XMLParser(ns_clean=True, recover=True, encoding='utf-8') + parser = etree.XMLParser(ns_clean=True, recover=True, encoding="utf-8") root = etree.fromstring(response.body.strip(), parser=parser) lti_spec_namespace = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0" - namespaces = {'def': lti_spec_namespace} + namespaces = {"def": lti_spec_namespace} code_major = root.xpath("//def:imsx_codeMajor", namespaces=namespaces)[0].text - description = root.xpath("//def:imsx_description", namespaces=namespaces)[0].text - message_identifier = root.xpath("//def:imsx_messageIdentifier", namespaces=namespaces)[0].text + description = root.xpath("//def:imsx_description", namespaces=namespaces)[ + 0 + ].text + message_identifier = root.xpath( + "//def:imsx_messageIdentifier", namespaces=namespaces + )[0].text imsx_pox_body = root.xpath("//def:imsx_POXBody", namespaces=namespaces)[0] try: - action = imsx_pox_body.getchildren()[0].tag.replace('{' + lti_spec_namespace + '}', '') + action = imsx_pox_body.getchildren()[0].tag.replace( + "{" + lti_spec_namespace + "}", "" + ) except Exception: # pylint: disable=broad-except action = None return { - 'code_major': code_major, - 'description': description, - 'messageIdentifier': message_identifier, - 'action': action + "code_major": code_major, + "description": description, + "messageIdentifier": message_identifier, + "action": action, } @patch( - 'xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret', - return_value=('test_client_key', 'test_client_secret') + "xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret", + return_value=("test_client_key", "test_client_secret"), ) def test_authorization_header_not_present(self, _get_key_secret): """ @@ -135,21 +146,21 @@ def test_authorization_header_not_present(self, _get_key_secret): """ request = Request(self.environ) request.body = self.get_request_body() - response = self.xblock.grade_handler(request, '') + response = self.xblock.grade_handler(request, "") real_response = self.get_response_values(response) expected_response = { - 'action': None, - 'code_major': 'failure', - 'description': 'OAuth verification error: Malformed authorization header', - 'messageIdentifier': self.defaults['messageIdentifier'], + "action": None, + "code_major": "failure", + "description": "OAuth verification error: Malformed authorization header", + "messageIdentifier": self.defaults["messageIdentifier"], } assert response.status_code == 200 self.assertDictEqual(expected_response, real_response) @patch( - 'xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret', - return_value=('test_client_key', 'test_client_secret') + "xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret", + return_value=("test_client_key", "test_client_secret"), ) def test_authorization_header_empty(self, _get_key_secret): """ @@ -160,13 +171,13 @@ def test_authorization_header_empty(self, _get_key_secret): request = Request(self.environ) request.authorization = "bad authorization header" request.body = self.get_request_body() - response = self.xblock.grade_handler(request, '') + response = self.xblock.grade_handler(request, "") real_response = self.get_response_values(response) expected_response = { - 'action': None, - 'code_major': 'failure', - 'description': 'OAuth verification error: Malformed authorization header', - 'messageIdentifier': self.defaults['messageIdentifier'], + "action": None, + "code_major": "failure", + "description": "OAuth verification error: Malformed authorization header", + "messageIdentifier": self.defaults["messageIdentifier"], } assert response.status_code == 200 self.assertDictEqual(expected_response, real_response) @@ -175,18 +186,20 @@ def test_real_user_is_none(self): """ If we have no real user, we should send back failure response. """ - self.runtime._services['user'] = StubUserService(user=None) # pylint: disable=protected-access + self.runtime._services["user"] = StubUserService( + user=None + ) # pylint: disable=protected-access self.xblock.verify_oauth_body_sign = Mock() self.xblock.has_score = True request = Request(self.environ) request.body = self.get_request_body() - response = self.xblock.grade_handler(request, '') + response = self.xblock.grade_handler(request, "") real_response = self.get_response_values(response) expected_response = { - 'action': None, - 'code_major': 'failure', - 'description': 'User not found.', - 'messageIdentifier': self.defaults['messageIdentifier'], + "action": None, + "code_major": "failure", + "description": "User not found.", + "messageIdentifier": self.defaults["messageIdentifier"], } assert response.status_code == 200 self.assertDictEqual(expected_response, real_response) @@ -200,13 +213,13 @@ def test_grade_past_due(self): self.xblock.graceperiod = Timedelta().from_json("0 seconds") request = Request(self.environ) request.body = self.get_request_body() - response = self.xblock.grade_handler(request, '') + response = self.xblock.grade_handler(request, "") real_response = self.get_response_values(response) expected_response = { - 'action': None, - 'code_major': 'failure', - 'description': 'Grade is past due', - 'messageIdentifier': 'unknown', + "action": None, + "code_major": "failure", + "description": "Grade is past due", + "messageIdentifier": "unknown", } assert response.status_code == 200 assert expected_response == real_response @@ -217,14 +230,14 @@ def test_grade_not_in_range(self): """ self.xblock.verify_oauth_body_sign = Mock() request = Request(self.environ) - request.body = self.get_request_body(params={'grade': '10'}) - response = self.xblock.grade_handler(request, '') + request.body = self.get_request_body(params={"grade": "10"}) + response = self.xblock.grade_handler(request, "") real_response = self.get_response_values(response) expected_response = { - 'action': None, - 'code_major': 'failure', - 'description': 'Request body XML parsing error: score value outside the permitted range of 0-1.', - 'messageIdentifier': 'unknown', + "action": None, + "code_major": "failure", + "description": "Request body XML parsing error: score value outside the permitted range of 0-1.", + "messageIdentifier": "unknown", } assert response.status_code == 200 self.assertDictEqual(expected_response, real_response) @@ -235,15 +248,15 @@ def test_bad_grade_decimal(self): """ self.xblock.verify_oauth_body_sign = Mock() request = Request(self.environ) - request.body = self.get_request_body(params={'grade': '0,5'}) - response = self.xblock.grade_handler(request, '') + request.body = self.get_request_body(params={"grade": "0,5"}) + response = self.xblock.grade_handler(request, "") real_response = self.get_response_values(response) msg = "could not convert string to float: '0,5'" expected_response = { - 'action': None, - 'code_major': 'failure', - 'description': f'Request body XML parsing error: {msg}', - 'messageIdentifier': 'unknown', + "action": None, + "code_major": "failure", + "description": f"Request body XML parsing error: {msg}", + "messageIdentifier": "unknown", } assert response.status_code == 200 self.assertDictEqual(expected_response, real_response) @@ -255,14 +268,14 @@ def test_unsupported_action(self): """ self.xblock.verify_oauth_body_sign = Mock() request = Request(self.environ) - request.body = self.get_request_body({'action': 'wrongAction'}) - response = self.xblock.grade_handler(request, '') + request.body = self.get_request_body({"action": "wrongAction"}) + response = self.xblock.grade_handler(request, "") real_response = self.get_response_values(response) expected_response = { - 'action': None, - 'code_major': 'unsupported', - 'description': 'Target does not support the requested operation.', - 'messageIdentifier': self.defaults['messageIdentifier'], + "action": None, + "code_major": "unsupported", + "description": "Target does not support the requested operation.", + "messageIdentifier": self.defaults["messageIdentifier"], } assert response.status_code == 200 self.assertDictEqual(expected_response, real_response) @@ -275,22 +288,22 @@ def test_good_request(self): self.xblock.has_score = True request = Request(self.environ) request.body = self.get_request_body() - response = self.xblock.grade_handler(request, '') - description_expected = 'Score for {sourcedId} is now {score}'.format( - sourcedId=self.defaults['sourcedId'], - score=self.defaults['grade'], + response = self.xblock.grade_handler(request, "") + description_expected = "Score for {sourcedId} is now {score}".format( + sourcedId=self.defaults["sourcedId"], + score=self.defaults["grade"], ) real_response = self.get_response_values(response) expected_response = { - 'action': 'replaceResultResponse', - 'code_major': 'success', - 'description': description_expected, - 'messageIdentifier': self.defaults['messageIdentifier'], + "action": "replaceResultResponse", + "code_major": "success", + "description": description_expected, + "messageIdentifier": self.defaults["messageIdentifier"], } assert response.status_code == 200 self.assertDictEqual(expected_response, real_response) - assert self.xblock.module_score == float(self.defaults['grade']) + assert self.xblock.module_score == float(self.defaults["grade"]) def test_user_id(self): expected_user_id = str(parse.quote(self.xblock.runtime.anonymous_student_id)) @@ -298,30 +311,41 @@ def test_user_id(self): assert real_user_id == expected_user_id def test_outcome_service_url(self): - mock_url_prefix = 'https://hostname/' + mock_url_prefix = "https://hostname/" test_service_name = "test_service" - def mock_handler_url(block, handler_name, **kwargs): # pylint: disable=unused-argument + def mock_handler_url( + block, handler_name, **kwargs + ): # pylint: disable=unused-argument """Mock function for returning fully-qualified handler urls""" return mock_url_prefix + handler_name self.xblock.runtime.handler_url = Mock(side_effect=mock_handler_url) - real_outcome_service_url = self.xblock.get_outcome_service_url(service_name=test_service_name) + real_outcome_service_url = self.xblock.get_outcome_service_url( + service_name=test_service_name + ) assert real_outcome_service_url == (mock_url_prefix + test_service_name) def test_resource_link_id(self): - with patch('xblocks_contrib.lti.lti.LTIBlock.location', new_callable=PropertyMock): - self.xblock.location.html_id = lambda: 'i4x-2-3-lti-31de800015cf4afb973356dbe81496df' + with patch( + "xblocks_contrib.lti.lti.LTIBlock.location", new_callable=PropertyMock + ): + self.xblock.location.html_id = ( + lambda: "i4x-2-3-lti-31de800015cf4afb973356dbe81496df" + ) expected_resource_link_id = str(parse.quote(self.unquoted_resource_link_id)) real_resource_link_id = self.xblock.get_resource_link_id() assert real_resource_link_id == expected_resource_link_id def test_lis_result_sourcedid(self): - expected_sourced_id = ':'.join(parse.quote(i) for i in ( - str(self.course_id), - self.xblock.get_resource_link_id(), - self.user_id - )) + expected_sourced_id = ":".join( + parse.quote(i) + for i in ( + str(self.course_id), + self.xblock.get_resource_link_id(), + self.user_id, + ) + ) real_lis_result_sourcedid = self.xblock.get_lis_result_sourcedid() assert real_lis_result_sourcedid == expected_sourced_id @@ -329,15 +353,15 @@ def test_client_key_secret(self): """ LTI block gets client key and secret provided. """ - #this adds lti passports to system - mocked_course = Mock(lti_passports=['lti_id:test_client:test_secret']) + # this adds lti passports to system + mocked_course = Mock(lti_passports=["lti_id:test_client:test_secret"]) modulestore = Mock() modulestore.get_course.return_value = mocked_course runtime = Mock(modulestore=modulestore) self.xblock.runtime = runtime self.xblock.lti_id = "lti_id" key, secret = self.xblock.get_client_key_secret() - expected = ('test_client', 'test_secret') + expected = ("test_client", "test_secret") assert expected == (key, secret) def test_client_key_secret_not_provided(self): @@ -348,7 +372,7 @@ def test_client_key_secret_not_provided(self): """ # this adds lti passports to system - mocked_course = Mock(lti_passports=['test_id:test_client:test_secret']) + mocked_course = Mock(lti_passports=["test_id:test_client:test_secret"]) modulestore = Mock() modulestore.get_course.return_value = mocked_course runtime = Mock(modulestore=modulestore) @@ -356,7 +380,7 @@ def test_client_key_secret_not_provided(self): # set another lti_id self.xblock.lti_id = "another_lti_id" key_secret = self.xblock.get_client_key_secret() - expected = ('', '') + expected = ("", "") assert expected == key_secret def test_bad_client_key_secret(self): @@ -366,19 +390,21 @@ def test_bad_client_key_secret(self): There are key and secret provided in wrong format. """ # this adds lti passports to system - mocked_course = Mock(lti_passports=['test_id_test_client_test_secret']) + mocked_course = Mock(lti_passports=["test_id_test_client_test_secret"]) modulestore = Mock() modulestore.get_course.return_value = mocked_course runtime = Mock(modulestore=modulestore) self.xblock.runtime = runtime - self.xblock.lti_id = 'lti_id' + self.xblock.lti_id = "lti_id" with pytest.raises(LTIError): self.xblock.get_client_key_secret() - @patch('xblocks_contrib.lti.lti.signature.verify_hmac_sha1', Mock(return_value=True)) @patch( - 'xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret', - Mock(return_value=('test_client_key', 'test_client_secret')) + "xblocks_contrib.lti.lti.signature.verify_hmac_sha1", Mock(return_value=True) + ) + @patch( + "xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret", + Mock(return_value=("test_client_key", "test_client_secret")), ) def test_successful_verify_oauth_body_sign(self): """ @@ -386,9 +412,14 @@ def test_successful_verify_oauth_body_sign(self): """ self.xblock.verify_oauth_body_sign(self.get_signed_grade_mock_request()) - @patch('xblocks_contrib.lti.lti.LTIBlock.get_outcome_service_url', Mock(return_value='https://testurl/')) - @patch('xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret', - Mock(return_value=('__consumer_key__', '__lti_secret__'))) + @patch( + "xblocks_contrib.lti.lti.LTIBlock.get_outcome_service_url", + Mock(return_value="https://testurl/"), + ) + @patch( + "xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret", + Mock(return_value=("__consumer_key__", "__lti_secret__")), + ) def test_failed_verify_oauth_body_sign_proxy_mangle_url(self): """ Oauth signing verify fail. @@ -398,7 +429,7 @@ def test_failed_verify_oauth_body_sign_proxy_mangle_url(self): # we should verify against get_outcome_service_url not # request url proxy and load balancer along the way may # change url presented to the method - request.url = 'http://testurl/' + request.url = "http://testurl/" self.xblock.verify_oauth_body_sign(request) def get_signed_grade_mock_request_with_correct_signature(self): @@ -407,29 +438,29 @@ def get_signed_grade_mock_request_with_correct_signature(self): """ mock_request = Mock() mock_request.headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': ( + "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": ( 'OAuth realm="https://testurl/", oauth_body_hash="wwzA3s8gScKD1VpJ7jMt9b%2BMj9Q%3D",' 'oauth_nonce="18821463", oauth_timestamp="1409321145", ' 'oauth_consumer_key="__consumer_key__", oauth_signature_method="HMAC-SHA1", ' 'oauth_version="1.0", oauth_signature="fHsE1hhIz76/msUoMR3Lyb7Aou4%3D"' - ) + ), } - mock_request.url = 'https://testurl' - mock_request.http_method = 'POST' + mock_request.url = "https://testurl" + mock_request.http_method = "POST" mock_request.method = mock_request.http_method mock_request.body = ( - b'\n' + b"\n" b'' - b'V1.0' - b'edX_fix' - b'' - b'MITxLTI/MITxLTI/201x:localhost%3A8000-i4x-MITxLTI-MITxLTI-lti-3751833a214a4f66a0d18f63234207f2' - b':363979ef768ca171b50f9d1bfb322131' - b'en0.32' - b'' + b"V1.0" + b"edX_fix" + b"" + b"MITxLTI/MITxLTI/201x:localhost%3A8000-i4x-MITxLTI-MITxLTI-lti-3751833a214a4f66a0d18f63234207f2" + b":363979ef768ca171b50f9d1bfb322131" + b"en0.32" + b"" ) return mock_request @@ -441,7 +472,9 @@ def test_wrong_xml_namespace(self): Tests that tool provider returned grade back with wrong XML Namespace. """ with pytest.raises(IndexError): - mocked_request = self.get_signed_grade_mock_request(namespace_lti_v1p1=False) + mocked_request = self.get_signed_grade_mock_request( + namespace_lti_v1p1=False + ) self.xblock.parse_grade_xml_body(mocked_request.body) def test_parse_grade_xml_body(self): @@ -451,16 +484,20 @@ def test_parse_grade_xml_body(self): Tests that xml body was parsed successfully. """ mocked_request = self.get_signed_grade_mock_request() - message_identifier, sourced_id, grade, action = self.xblock.parse_grade_xml_body(mocked_request.body) - assert self.defaults['messageIdentifier'] == message_identifier - assert self.defaults['sourcedId'] == sourced_id - assert self.defaults['grade'] == grade - assert self.defaults['action'] == action + message_identifier, sourced_id, grade, action = ( + self.xblock.parse_grade_xml_body(mocked_request.body) + ) + assert self.defaults["messageIdentifier"] == message_identifier + assert self.defaults["sourcedId"] == sourced_id + assert self.defaults["grade"] == grade + assert self.defaults["action"] == action - @patch('xblocks_contrib.lti.lti.signature.verify_hmac_sha1', Mock(return_value=False)) @patch( - 'xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret', - Mock(return_value=('test_client_key', 'test_client_secret')) + "xblocks_contrib.lti.lti.signature.verify_hmac_sha1", Mock(return_value=False) + ) + @patch( + "xblocks_contrib.lti.lti.LTIBlock.get_client_key_secret", + Mock(return_value=("test_client_key", "test_client_secret")), ) def test_failed_verify_oauth_body_sign(self): """ @@ -479,23 +516,21 @@ def get_signed_grade_mock_request(self, namespace_lti_v1p1=True): """ mock_request = Mock() mock_request.headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': 'OAuth oauth_nonce="135685044251684026041377608307", \ + "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": 'OAuth oauth_nonce="135685044251684026041377608307", \ oauth_timestamp="1234567890", oauth_version="1.0", \ oauth_signature_method="HMAC-SHA1", \ oauth_consumer_key="test_client_key", \ oauth_signature="my_signature%3D", \ - oauth_body_hash="JEpIArlNCeV4ceXxric8gJQCnBw="' + oauth_body_hash="JEpIArlNCeV4ceXxric8gJQCnBw="', } - mock_request.url = 'http://testurl' - mock_request.http_method = 'POST' + mock_request.url = "http://testurl" + mock_request.http_method = "POST" params = {} if not namespace_lti_v1p1: - params = { - 'namespace': "http://www.fakenamespace.com/fake" - } + params = {"namespace": "http://www.fakenamespace.com/fake"} mock_request.body = self.get_request_body(params) return mock_request @@ -504,22 +539,27 @@ def test_good_custom_params(self): """ Custom parameters are presented in right format. """ - self.xblock.custom_parameters = ['test_custom_params=test_custom_param_value'] - self.xblock.get_client_key_secret = Mock(return_value=('test_client_key', 'test_client_secret')) + self.xblock.custom_parameters = ["test_custom_params=test_custom_param_value"] + self.xblock.get_client_key_secret = Mock( + return_value=("test_client_key", "test_client_secret") + ) self.xblock.oauth_params = Mock() self.xblock.get_input_fields() self.xblock.oauth_params.assert_called_with( - {'custom_test_custom_params': 'test_custom_param_value'}, - 'test_client_key', 'test_client_secret' + {"custom_test_custom_params": "test_custom_param_value"}, + "test_client_key", + "test_client_secret", ) def test_bad_custom_params(self): """ Custom parameters are presented in wrong format. """ - bad_custom_params = ['test_custom_params: test_custom_param_value'] + bad_custom_params = ["test_custom_params: test_custom_param_value"] self.xblock.custom_parameters = bad_custom_params - self.xblock.get_client_key_secret = Mock(return_value=('test_client_key', 'test_client_secret')) + self.xblock.get_client_key_secret = Mock( + return_value=("test_client_key", "test_client_secret") + ) self.xblock.oauth_params = Mock() with pytest.raises(LTIError): self.xblock.get_input_fields()