LTI additional Python tests. LTI must use HTTPS for lis_outcome_service_url.

BLD-564.
This commit is contained in:
Valera Rozuvan
2013-11-27 17:08:03 +02:00
committed by Oleg Marshev
parent 11080f2872
commit 0079243746
8 changed files with 277 additions and 108 deletions

View File

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

View File

@@ -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