LTI additional Python tests. LTI must use HTTPS for lis_outcome_service_url.
BLD-564.
This commit is contained in:
committed by
Oleg Marshev
parent
11080f2872
commit
0079243746
@@ -259,37 +259,22 @@ class LTIModule(LTIFields, XModule):
|
||||
'element_class': self.category,
|
||||
'open_in_a_new_page': self.open_in_a_new_page,
|
||||
'display_name': self.display_name,
|
||||
'form_url': self.get_form_path(),
|
||||
'form_url': self.runtime.handler_url(self, 'preview_handler').rstrip('/?'),
|
||||
}
|
||||
|
||||
|
||||
def get_form_path(self):
|
||||
return self.runtime.handler_url(self, 'preview_handler').rstrip('/?')
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Renders parameters to template.
|
||||
"""
|
||||
return self.system.render_template('lti.html', self.get_context())
|
||||
|
||||
def get_form(self):
|
||||
"""
|
||||
Renders parameters to form template.
|
||||
"""
|
||||
return self.system.render_template('lti_form.html', self.get_context())
|
||||
|
||||
@XBlock.handler
|
||||
def preview_handler(self, request, dispatch):
|
||||
def preview_handler(self, _, __):
|
||||
"""
|
||||
Ajax handler.
|
||||
|
||||
Args:
|
||||
dispatch: string request slug
|
||||
|
||||
Returns:
|
||||
json string
|
||||
This is called to get context with new oauth params to iframe.
|
||||
"""
|
||||
return Response(self.get_form(), content_type='text/html')
|
||||
template = self.system.render_template('lti_form.html', self.get_context())
|
||||
return Response(template, content_type='text/html')
|
||||
|
||||
def get_user_id(self):
|
||||
user_id = self.runtime.anonymous_student_id
|
||||
@@ -299,11 +284,18 @@ class LTIModule(LTIFields, XModule):
|
||||
def get_outcome_service_url(self):
|
||||
"""
|
||||
Return URL for storing grades.
|
||||
|
||||
To test LTI on sandbox we must use http scheme.
|
||||
|
||||
While testing locally and on Jenkins, mock_lti_server use http.referer
|
||||
to obtain scheme, so it is ok to have http(s) anyway.
|
||||
"""
|
||||
uri = 'http://{host}{path}'.format(
|
||||
host=self.system.hostname,
|
||||
path=self.runtime.handler_url(self, 'grade_handler', thirdparty=True).rstrip('/?')
|
||||
)
|
||||
scheme = 'http' if 'sandbox' in self.system.hostname else 'https'
|
||||
uri = '{scheme}://{host}{path}'.format(
|
||||
scheme=scheme,
|
||||
host=self.system.hostname,
|
||||
path=self.runtime.handler_url(self, 'grade_handler', thirdparty=True).rstrip('/?')
|
||||
)
|
||||
return uri
|
||||
|
||||
def get_resource_link_id(self):
|
||||
@@ -449,7 +441,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'}
|
||||
|
||||
Example of correct/incorrect answer XML body:: see response_xml_template.
|
||||
"""
|
||||
response_xml_template = textwrap.dedent("""
|
||||
response_xml_template = textwrap.dedent("""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<imsx_POXEnvelopeResponse xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
|
||||
<imsx_POXHeader>
|
||||
@@ -578,7 +570,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'}
|
||||
|
||||
sha1 = hashlib.sha1()
|
||||
sha1.update(request.body)
|
||||
oauth_body_hash = base64.b64encode(sha1.hexdigest())
|
||||
oauth_body_hash = base64.b64encode(sha1.digest())
|
||||
|
||||
oauth_params = signature.collect_parameters(headers=headers, exclude_oauth_signature=False)
|
||||
oauth_headers =dict(oauth_params)
|
||||
|
||||
@@ -2,14 +2,21 @@
|
||||
"""Test for LTI Xmodule functional logic."""
|
||||
|
||||
from mock import Mock, patch, PropertyMock
|
||||
import mock
|
||||
import textwrap
|
||||
import json
|
||||
from lxml import etree
|
||||
import json
|
||||
from webob.request import Request
|
||||
from copy import copy
|
||||
from collections import OrderedDict
|
||||
import urllib
|
||||
import oauthlib
|
||||
import hashlib
|
||||
import base64
|
||||
|
||||
from xmodule.lti_module import LTIDescriptor
|
||||
|
||||
from xmodule.lti_module import LTIDescriptor, LTIError
|
||||
|
||||
from . import LogicTest
|
||||
|
||||
@@ -48,7 +55,6 @@ class LTIModuleTest(LogicTest):
|
||||
</imsx_POXEnvelopeRequest>
|
||||
""")
|
||||
self.system.get_real_user = Mock()
|
||||
self.xmodule.get_client_key_secret = Mock(return_value=('key', 'secret'))
|
||||
self.system.publish = Mock()
|
||||
|
||||
self.user_id = self.xmodule.runtime.anonymous_student_id
|
||||
@@ -96,6 +102,7 @@ class LTIModuleTest(LogicTest):
|
||||
def test_authorization_header_not_present(self):
|
||||
"""
|
||||
Request has no Authorization header.
|
||||
|
||||
This is an unknown service request, i.e., it is not a part of the original service specification.
|
||||
"""
|
||||
request = Request(self.environ)
|
||||
@@ -115,6 +122,7 @@ class LTIModuleTest(LogicTest):
|
||||
def test_authorization_header_empty(self):
|
||||
"""
|
||||
Request Authorization header has no value.
|
||||
|
||||
This is an unknown service request, i.e., it is not a part of the original service specification.
|
||||
"""
|
||||
request = Request(self.environ)
|
||||
@@ -128,7 +136,6 @@ class LTIModuleTest(LogicTest):
|
||||
'description': 'The request has failed.',
|
||||
'messageIdentifier': self.DEFAULTS['messageIdentifier'],
|
||||
}
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(expected_response, real_response)
|
||||
|
||||
@@ -147,7 +154,6 @@ class LTIModuleTest(LogicTest):
|
||||
'description': 'The request has failed.',
|
||||
'messageIdentifier': 'unknown',
|
||||
}
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(expected_response, real_response)
|
||||
|
||||
@@ -166,7 +172,6 @@ class LTIModuleTest(LogicTest):
|
||||
'description': 'The request has failed.',
|
||||
'messageIdentifier': 'unknown',
|
||||
}
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(expected_response, real_response)
|
||||
|
||||
@@ -186,7 +191,6 @@ class LTIModuleTest(LogicTest):
|
||||
'description': 'Target does not support the requested operation.',
|
||||
'messageIdentifier': self.DEFAULTS['messageIdentifier'],
|
||||
}
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(expected_response, real_response)
|
||||
|
||||
@@ -199,7 +203,6 @@ class LTIModuleTest(LogicTest):
|
||||
request = Request(self.environ)
|
||||
request.body = self.get_request_body()
|
||||
response = self.xmodule.grade_handler(request, '')
|
||||
code_major, description, messageIdentifier, action = self.get_response_values(response)
|
||||
description_expected = 'Score for {sourcedId} is now {score}'.format(
|
||||
sourcedId=self.DEFAULTS['sourcedId'],
|
||||
score=self.DEFAULTS['grade'],
|
||||
@@ -221,20 +224,13 @@ class LTIModuleTest(LogicTest):
|
||||
self.assertEqual(real_user_id, expected_user_id)
|
||||
|
||||
def test_outcome_service_url(self):
|
||||
expected_outcome_service_url = 'http://{host}{path}'.format(
|
||||
expected_outcome_service_url = 'https://{host}{path}'.format(
|
||||
host=self.xmodule.runtime.hostname,
|
||||
path=self.xmodule.runtime.handler_url(self.xmodule, 'grade_handler', thirdparty=True).rstrip('/?')
|
||||
)
|
||||
|
||||
real_outcome_service_url = self.xmodule.get_outcome_service_url()
|
||||
self.assertEqual(real_outcome_service_url, expected_outcome_service_url)
|
||||
|
||||
def test_get_form_path(self):
|
||||
expected_form_path = self.xmodule.runtime.handler_url(self.xmodule, 'preview_handler').rstrip('/?')
|
||||
|
||||
real_form_path = self.xmodule.get_form_path()
|
||||
self.assertEqual(real_form_path, expected_form_path)
|
||||
|
||||
def test_resource_link_id(self):
|
||||
with patch('xmodule.lti_module.LTIModule.id', new_callable=PropertyMock) as mock_id:
|
||||
mock_id.return_value = self.module_id
|
||||
@@ -242,7 +238,6 @@ class LTIModuleTest(LogicTest):
|
||||
real_resource_link_id = self.xmodule.get_resource_link_id()
|
||||
self.assertEqual(real_resource_link_id, expected_resource_link_id)
|
||||
|
||||
|
||||
def test_lis_result_sourcedid(self):
|
||||
with patch('xmodule.lti_module.LTIModule.id', new_callable=PropertyMock) as mock_id:
|
||||
mock_id.return_value = self.module_id
|
||||
@@ -251,11 +246,127 @@ class LTIModuleTest(LogicTest):
|
||||
self.assertEqual(real_lis_result_sourcedid, expected_sourcedId)
|
||||
|
||||
|
||||
def test_verify_oauth_body_sign(self):
|
||||
pass
|
||||
@patch('xmodule.course_module.CourseDescriptor.id_to_location')
|
||||
def test_client_key_secret(self, test):
|
||||
"""
|
||||
LTI module gets client key and secret provided.
|
||||
"""
|
||||
#this adds lti passports to system
|
||||
mocked_course = Mock(lti_passports = ['lti_id:test_client:test_secret'])
|
||||
modulestore = Mock()
|
||||
modulestore.get_item.return_value = mocked_course
|
||||
runtime = Mock(modulestore=modulestore)
|
||||
self.xmodule.descriptor.runtime = runtime
|
||||
self.xmodule.lti_id = "lti_id"
|
||||
key, secret = self.xmodule.get_client_key_secret()
|
||||
expected = ('test_client', 'test_secret')
|
||||
self.assertEqual(expected, (key, secret))
|
||||
|
||||
def test_client_key_secret(self):
|
||||
pass
|
||||
@patch('xmodule.course_module.CourseDescriptor.id_to_location')
|
||||
def test_client_key_secret_not_provided(self, test):
|
||||
"""
|
||||
LTI module attempts to get client key and secret provided in cms.
|
||||
|
||||
There are key and secret but not for specific LTI.
|
||||
"""
|
||||
|
||||
#this adds lti passports to system
|
||||
mocked_course = Mock(lti_passports = ['test_id:test_client:test_secret'])
|
||||
modulestore = Mock()
|
||||
modulestore.get_item.return_value = mocked_course
|
||||
runtime = Mock(modulestore=modulestore)
|
||||
self.xmodule.descriptor.runtime = runtime
|
||||
#set another lti_id
|
||||
self.xmodule.lti_id = "another_lti_id"
|
||||
key_secret = self.xmodule.get_client_key_secret()
|
||||
expected = ('','')
|
||||
self.assertEqual(expected, key_secret)
|
||||
|
||||
@patch('xmodule.course_module.CourseDescriptor.id_to_location')
|
||||
def test_bad_client_key_secret(self, test):
|
||||
"""
|
||||
LTI module attempts to get client key and secret provided in cms.
|
||||
|
||||
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'])
|
||||
modulestore = Mock()
|
||||
modulestore.get_item.return_value = mocked_course
|
||||
runtime = Mock(modulestore=modulestore)
|
||||
self.xmodule.descriptor.runtime = runtime
|
||||
self.xmodule.lti_id = 'lti_id'
|
||||
with self.assertRaises(LTIError):
|
||||
self.xmodule.get_client_key_secret()
|
||||
|
||||
@patch('xmodule.lti_module.signature.verify_hmac_sha1', return_value=True)
|
||||
@patch('xmodule.lti_module.LTIModule.get_client_key_secret', return_value=('test_client_key', u'test_client_secret'))
|
||||
def test_successful_verify_oauth_body_sign(self, get_key_secret, mocked_verify):
|
||||
"""
|
||||
Test if OAuth signing was successful.
|
||||
"""
|
||||
try:
|
||||
self.xmodule.verify_oauth_body_sign(self.get_signed_grade_mock_request())
|
||||
except LTIError:
|
||||
self.fail("verify_oauth_body_sign() raised LTIError unexpectedly!")
|
||||
|
||||
@patch('xmodule.lti_module.signature.verify_hmac_sha1', return_value=False)
|
||||
@patch('xmodule.lti_module.LTIModule.get_client_key_secret', return_value=('test_client_key', u'test_client_secret'))
|
||||
def test_failed_verify_oauth_body_sign(self, get_key_secret, mocked_verify):
|
||||
"""
|
||||
Oauth signing verify fail.
|
||||
"""
|
||||
with self.assertRaises(LTIError):
|
||||
req = self.get_signed_grade_mock_request()
|
||||
self.xmodule.verify_oauth_body_sign(req)
|
||||
|
||||
def get_signed_grade_mock_request(self):
|
||||
"""
|
||||
Example of signed request from LTI Provider.
|
||||
"""
|
||||
mock_request = Mock()
|
||||
mock_request.headers = {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/xml',
|
||||
'Authorization': u'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="gz+PeJZuF2//n9hNUnDj2v5kN70="'
|
||||
}
|
||||
mock_request.url = u'http://testurl'
|
||||
mock_request.http_method = u'POST'
|
||||
mock_request.body = textwrap.dedent("""
|
||||
<?xml version = "1.0" encoding = "UTF-8"?>
|
||||
<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
|
||||
</imsx_POXEnvelopeRequest>
|
||||
""")
|
||||
return mock_request
|
||||
|
||||
def test_good_custom_params(self):
|
||||
"""
|
||||
Custom parameters are presented in right format.
|
||||
"""
|
||||
self.xmodule.custom_parameters = ['test_custom_params=test_custom_param_value']
|
||||
self.xmodule.get_client_key_secret = Mock(return_value=('test_client_key', 'test_client_secret'))
|
||||
self.xmodule.oauth_params = Mock()
|
||||
self.xmodule.get_input_fields()
|
||||
self.xmodule.oauth_params.assert_called_with(
|
||||
{u'custom_test_custom_params': u'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']
|
||||
self.xmodule.custom_parameters = bad_custom_params
|
||||
self.xmodule.get_client_key_secret = Mock(return_value=('test_client_key', 'test_client_secret'))
|
||||
self.xmodule.oauth_params = Mock()
|
||||
with self.assertRaises(LTIError):
|
||||
self.xmodule.get_input_fields()
|
||||
|
||||
def test_max_score(self):
|
||||
self.xmodule.weight = 100.0
|
||||
|
||||
Reference in New Issue
Block a user