Commit f6867da5 by Valera Rozuvan Committed by Alexander Kryklia

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

BLD-564.
parent a78400a8
...@@ -5,13 +5,15 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,13 +5,15 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Blades: LTI additional Python tests. LTI must use HTTPS for
lis_outcome_service_url. BLD-564.
Blades: Fix bug when Image mapping problems are not working for students in IE. BLD-413. Blades: Fix bug when Image mapping problems are not working for students in IE. BLD-413.
Blades: Add template that displays the most up-to-date features of Blades: Add template that displays the most up-to-date features of
drag-and-drop. BLD-479. drag-and-drop. BLD-479.
Blades: LTI additional Python tests. LTI fix bug e-reader error when popping Blades: LTI fix bug e-reader error when popping out window. BLD-465.
out window. BLD-465.
Common: Switch from mitx.db to edx.db for sqlite databases. This will effectively Common: Switch from mitx.db to edx.db for sqlite databases. This will effectively
reset state for local instances of the code, unless you manually rename your reset state for local instances of the code, unless you manually rename your
......
...@@ -259,37 +259,22 @@ class LTIModule(LTIFields, XModule): ...@@ -259,37 +259,22 @@ class LTIModule(LTIFields, XModule):
'element_class': self.category, 'element_class': self.category,
'open_in_a_new_page': self.open_in_a_new_page, 'open_in_a_new_page': self.open_in_a_new_page,
'display_name': self.display_name, '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): def get_html(self):
""" """
Renders parameters to template. Renders parameters to template.
""" """
return self.system.render_template('lti.html', self.get_context()) 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 @XBlock.handler
def preview_handler(self, request, dispatch): def preview_handler(self, _, __):
""" """
Ajax handler. This is called to get context with new oauth params to iframe.
Args:
dispatch: string request slug
Returns:
json string
""" """
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): def get_user_id(self):
user_id = self.runtime.anonymous_student_id user_id = self.runtime.anonymous_student_id
...@@ -299,11 +284,18 @@ class LTIModule(LTIFields, XModule): ...@@ -299,11 +284,18 @@ class LTIModule(LTIFields, XModule):
def get_outcome_service_url(self): def get_outcome_service_url(self):
""" """
Return URL for storing grades. 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( scheme = 'http' if 'sandbox' in self.system.hostname else 'https'
host=self.system.hostname, uri = '{scheme}://{host}{path}'.format(
path=self.runtime.handler_url(self, 'grade_handler', thirdparty=True).rstrip('/?') scheme=scheme,
) host=self.system.hostname,
path=self.runtime.handler_url(self, 'grade_handler', thirdparty=True).rstrip('/?')
)
return uri return uri
def get_resource_link_id(self): def get_resource_link_id(self):
...@@ -449,7 +441,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'} ...@@ -449,7 +441,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'}
Example of correct/incorrect answer XML body:: see response_xml_template. 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"?> <?xml version="1.0" encoding="UTF-8"?>
<imsx_POXEnvelopeResponse xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0"> <imsx_POXEnvelopeResponse xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
<imsx_POXHeader> <imsx_POXHeader>
...@@ -578,7 +570,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'} ...@@ -578,7 +570,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'}
sha1 = hashlib.sha1() sha1 = hashlib.sha1()
sha1.update(request.body) 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_params = signature.collect_parameters(headers=headers, exclude_oauth_signature=False)
oauth_headers =dict(oauth_params) oauth_headers =dict(oauth_params)
......
...@@ -2,14 +2,21 @@ ...@@ -2,14 +2,21 @@
"""Test for LTI Xmodule functional logic.""" """Test for LTI Xmodule functional logic."""
from mock import Mock, patch, PropertyMock from mock import Mock, patch, PropertyMock
import mock
import textwrap import textwrap
import json import json
from lxml import etree from lxml import etree
import json
from webob.request import Request from webob.request import Request
from copy import copy from copy import copy
from collections import OrderedDict
import urllib import urllib
import oauthlib
import hashlib
import base64
from xmodule.lti_module import LTIDescriptor from xmodule.lti_module import LTIDescriptor, LTIError
from . import LogicTest from . import LogicTest
...@@ -48,7 +55,6 @@ class LTIModuleTest(LogicTest): ...@@ -48,7 +55,6 @@ class LTIModuleTest(LogicTest):
</imsx_POXEnvelopeRequest> </imsx_POXEnvelopeRequest>
""") """)
self.system.get_real_user = Mock() self.system.get_real_user = Mock()
self.xmodule.get_client_key_secret = Mock(return_value=('key', 'secret'))
self.system.publish = Mock() self.system.publish = Mock()
self.user_id = self.xmodule.runtime.anonymous_student_id self.user_id = self.xmodule.runtime.anonymous_student_id
...@@ -96,6 +102,7 @@ class LTIModuleTest(LogicTest): ...@@ -96,6 +102,7 @@ class LTIModuleTest(LogicTest):
def test_authorization_header_not_present(self): def test_authorization_header_not_present(self):
""" """
Request has no Authorization header. Request has no Authorization header.
This is an unknown service request, i.e., it is not a part of the original service specification. This is an unknown service request, i.e., it is not a part of the original service specification.
""" """
request = Request(self.environ) request = Request(self.environ)
...@@ -115,6 +122,7 @@ class LTIModuleTest(LogicTest): ...@@ -115,6 +122,7 @@ class LTIModuleTest(LogicTest):
def test_authorization_header_empty(self): def test_authorization_header_empty(self):
""" """
Request Authorization header has no value. Request Authorization header has no value.
This is an unknown service request, i.e., it is not a part of the original service specification. This is an unknown service request, i.e., it is not a part of the original service specification.
""" """
request = Request(self.environ) request = Request(self.environ)
...@@ -128,7 +136,6 @@ class LTIModuleTest(LogicTest): ...@@ -128,7 +136,6 @@ class LTIModuleTest(LogicTest):
'description': 'The request has failed.', 'description': 'The request has failed.',
'messageIdentifier': self.DEFAULTS['messageIdentifier'], 'messageIdentifier': self.DEFAULTS['messageIdentifier'],
} }
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictEqual(expected_response, real_response) self.assertDictEqual(expected_response, real_response)
...@@ -147,7 +154,6 @@ class LTIModuleTest(LogicTest): ...@@ -147,7 +154,6 @@ class LTIModuleTest(LogicTest):
'description': 'The request has failed.', 'description': 'The request has failed.',
'messageIdentifier': 'unknown', 'messageIdentifier': 'unknown',
} }
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictEqual(expected_response, real_response) self.assertDictEqual(expected_response, real_response)
...@@ -166,7 +172,6 @@ class LTIModuleTest(LogicTest): ...@@ -166,7 +172,6 @@ class LTIModuleTest(LogicTest):
'description': 'The request has failed.', 'description': 'The request has failed.',
'messageIdentifier': 'unknown', 'messageIdentifier': 'unknown',
} }
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictEqual(expected_response, real_response) self.assertDictEqual(expected_response, real_response)
...@@ -186,7 +191,6 @@ class LTIModuleTest(LogicTest): ...@@ -186,7 +191,6 @@ class LTIModuleTest(LogicTest):
'description': 'Target does not support the requested operation.', 'description': 'Target does not support the requested operation.',
'messageIdentifier': self.DEFAULTS['messageIdentifier'], 'messageIdentifier': self.DEFAULTS['messageIdentifier'],
} }
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictEqual(expected_response, real_response) self.assertDictEqual(expected_response, real_response)
...@@ -199,7 +203,6 @@ class LTIModuleTest(LogicTest): ...@@ -199,7 +203,6 @@ class LTIModuleTest(LogicTest):
request = Request(self.environ) request = Request(self.environ)
request.body = self.get_request_body() request.body = self.get_request_body()
response = self.xmodule.grade_handler(request, '') 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( description_expected = 'Score for {sourcedId} is now {score}'.format(
sourcedId=self.DEFAULTS['sourcedId'], sourcedId=self.DEFAULTS['sourcedId'],
score=self.DEFAULTS['grade'], score=self.DEFAULTS['grade'],
...@@ -221,20 +224,13 @@ class LTIModuleTest(LogicTest): ...@@ -221,20 +224,13 @@ class LTIModuleTest(LogicTest):
self.assertEqual(real_user_id, expected_user_id) self.assertEqual(real_user_id, expected_user_id)
def test_outcome_service_url(self): 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, host=self.xmodule.runtime.hostname,
path=self.xmodule.runtime.handler_url(self.xmodule, 'grade_handler', thirdparty=True).rstrip('/?') path=self.xmodule.runtime.handler_url(self.xmodule, 'grade_handler', thirdparty=True).rstrip('/?')
) )
real_outcome_service_url = self.xmodule.get_outcome_service_url() real_outcome_service_url = self.xmodule.get_outcome_service_url()
self.assertEqual(real_outcome_service_url, expected_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): def test_resource_link_id(self):
with patch('xmodule.lti_module.LTIModule.id', new_callable=PropertyMock) as mock_id: with patch('xmodule.lti_module.LTIModule.id', new_callable=PropertyMock) as mock_id:
mock_id.return_value = self.module_id mock_id.return_value = self.module_id
...@@ -242,7 +238,6 @@ class LTIModuleTest(LogicTest): ...@@ -242,7 +238,6 @@ class LTIModuleTest(LogicTest):
real_resource_link_id = self.xmodule.get_resource_link_id() real_resource_link_id = self.xmodule.get_resource_link_id()
self.assertEqual(real_resource_link_id, expected_resource_link_id) self.assertEqual(real_resource_link_id, expected_resource_link_id)
def test_lis_result_sourcedid(self): def test_lis_result_sourcedid(self):
with patch('xmodule.lti_module.LTIModule.id', new_callable=PropertyMock) as mock_id: with patch('xmodule.lti_module.LTIModule.id', new_callable=PropertyMock) as mock_id:
mock_id.return_value = self.module_id mock_id.return_value = self.module_id
...@@ -251,11 +246,127 @@ class LTIModuleTest(LogicTest): ...@@ -251,11 +246,127 @@ class LTIModuleTest(LogicTest):
self.assertEqual(real_lis_result_sourcedid, expected_sourcedId) self.assertEqual(real_lis_result_sourcedid, expected_sourcedId)
def test_verify_oauth_body_sign(self): @patch('xmodule.course_module.CourseDescriptor.id_to_location')
pass 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))
@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.
def test_client_key_secret(self): There are key and secret provided in wrong format.
pass """
#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): def test_max_score(self):
self.xmodule.weight = 100.0 self.xmodule.weight = 100.0
......
...@@ -38,6 +38,10 @@ def setup_mock_lti_server(): ...@@ -38,6 +38,10 @@ def setup_mock_lti_server():
'lti_endpoint': 'correct_lti_endpoint' 'lti_endpoint': 'correct_lti_endpoint'
} }
# Flag for acceptance tests used for creating right callback_url and sending
# graded result. Used in MockLTIRequestHandler.
server.test_mode = True
# Store the server instance in lettuce's world # Store the server instance in lettuce's world
# so that other steps can access it # so that other steps can access it
# (and we can shut it down later) # (and we can shut it down later)
......
"""
LTI Server
What is supported:
------------------
1.) This LTI Provider can service only one Tool Consumer at the same time. It is
not possible to have this LTI multiple times on a single page in LMS.
"""
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from uuid import uuid4 from uuid import uuid4
import textwrap import textwrap
...@@ -23,6 +33,7 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler): ...@@ -23,6 +33,7 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
protocol = "HTTP/1.0" protocol = "HTTP/1.0"
callback_url = None callback_url = None
def log_message(self, format, *args): def log_message(self, format, *args):
"""Log an arbitrary message.""" """Log an arbitrary message."""
# Code copied from BaseHTTPServer.py. Changed to write to sys.stdout # Code copied from BaseHTTPServer.py. Changed to write to sys.stdout
...@@ -35,6 +46,8 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler): ...@@ -35,6 +46,8 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
''' '''
Handle a GET request from the client and sends response back. Handle a GET request from the client and sends response back.
Used for checking LTI Provider started correctly.
''' '''
self.send_response(200, 'OK') self.send_response(200, 'OK')
...@@ -42,29 +55,20 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler): ...@@ -42,29 +55,20 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
response_str = """<html><head><title>TEST TITLE</title></head> response_str = """<html><head><title>TEST TITLE</title></head>
<body>I have stored grades.</body></html>""" <body>This is LTI Provider.</body></html>"""
self.wfile.write(response_str) self.wfile.write(response_str)
self._send_graded_result()
def do_POST(self): def do_POST(self):
''' '''
Handle a POST request from the client and sends response back. Handle a POST request from the client and sends response back.
''' '''
'''
logger.debug("LTI provider received POST request {} to path {}".format(
str(self.post_dict),
self.path)
) # Log the request
'''
# Respond to grade request
if 'grade' in self.path and self._send_graded_result().status_code == 200: if 'grade' in self.path and self._send_graded_result().status_code == 200:
status_message = 'LTI consumer (edX) responded with XML content:<br>' + self.server.grade_data['TC answer'] status_message = 'LTI consumer (edX) responded with XML content:<br>' + self.server.grade_data['TC answer']
self.server.grade_data['callback_url'] = None self.server.grade_data['callback_url'] = None
self._send_response(status_message, 200)
# Respond to request with correct lti endpoint: # Respond to request with correct lti endpoint:
elif self._is_correct_lti_request(): elif self._is_correct_lti_request():
self.post_dict = self._post_dict() self.post_dict = self._post_dict()
...@@ -97,26 +101,19 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler): ...@@ -97,26 +101,19 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
# set data for grades # set data for grades
# what need to be stored as server data # what need to be stored as server data
self.server.grade_data = { self.server.grade_data = {
'callback_url': self.post_dict["lis_outcome_service_url"], 'callback_url': self.post_dict.get('lis_outcome_service_url'),
'sourcedId': self.post_dict['lis_result_sourcedid'] 'sourcedId': self.post_dict.get('lis_result_sourcedid')
} }
self._send_response(status_message, 200)
else: else:
status_message = "Invalid request URL" status_message = "Invalid request URL"
self._send_response(status_message, 500)
self._send_head() def _send_head(self, status_code):
self._send_response(status_message)
def _send_head(self):
''' '''
Send the response code and MIME headers Send the response code and MIME headers
''' '''
self.send_response(200) self.send_response(status_code)
'''
if self._is_correct_lti_request():
self.send_response(200)
else:
self.send_response(500)
'''
self.send_header('Content-type', 'text/html') self.send_header('Content-type', 'text/html')
self.end_headers() self.end_headers()
...@@ -182,15 +179,22 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler): ...@@ -182,15 +179,22 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
</imsx_POXEnvelopeRequest> </imsx_POXEnvelopeRequest>
""") """)
data = payload.format(**values) data = payload.format(**values)
# temporarily changed to get for easy view in browser
# get relative part, because host name is different in a) manual tests b) acceptance tests c) demos # get relative part, because host name is different in a) manual tests b) acceptance tests c) demos
relative_url = urlparse.urlparse(self.server.grade_data['callback_url']).path if getattr(self.server, 'test_mode', None):
url = self.server.referer_host + relative_url relative_url = urlparse.urlparse(self.server.grade_data['callback_url']).path
url = self.server.referer_host + relative_url
else:
url = self.server.grade_data['callback_url']
headers = {'Content-Type': 'application/xml', 'X-Requested-With': 'XMLHttpRequest'} headers = {'Content-Type': 'application/xml', 'X-Requested-With': 'XMLHttpRequest'}
headers['Authorization'] = self.oauth_sign(url, data) headers['Authorization'] = self.oauth_sign(url, data)
# We can't mock requests in unit tests, because we use them, but we need
# them to be mocked only for this one case.
if getattr(self.server, 'run_inside_unittest_flag', None):
response = mock.Mock(status_code=200, url=url, data=data, headers=headers)
return response
response = requests.post( response = requests.post(
url, url,
data=data, data=data,
...@@ -199,11 +203,11 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler): ...@@ -199,11 +203,11 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
self.server.grade_data['TC answer'] = response.content self.server.grade_data['TC answer'] = response.content
return response return response
def _send_response(self, message): def _send_response(self, message, status_code):
''' '''
Send message back to the client Send message back to the client
''' '''
self._send_head(status_code)
if self.server.grade_data['callback_url']: if self.server.grade_data['callback_url']:
response_str = """<html><head><title>TEST TITLE</title></head> response_str = """<html><head><title>TEST TITLE</title></head>
<body> <body>
...@@ -250,7 +254,7 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler): ...@@ -250,7 +254,7 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
#Calculate and encode body hash. See http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html #Calculate and encode body hash. See http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
sha1 = hashlib.sha1() sha1 = hashlib.sha1()
sha1.update(body) sha1.update(body)
oauth_body_hash = base64.b64encode(sha1.hexdigest()) oauth_body_hash = base64.b64encode(sha1.digest())
__, headers, __ = client.sign( __, headers, __ = client.sign(
unicode(url.strip()), unicode(url.strip()),
http_method=u'POST', http_method=u'POST',
......
""" """
Mock LTI server for manual testing. Mock LTI server for manual testing.
Used for manual testing and testing on sandbox.
""" """
import threading import threading
...@@ -18,6 +20,10 @@ server.oauth_settings = { ...@@ -18,6 +20,10 @@ server.oauth_settings = {
} }
server.server_host = server_host server.server_host = server_host
# If in test mode mock lti server will make callback url using referer host.
# Used in MockLTIRequestHandler when sending graded result.
server.test_mode = True
try: try:
server.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
......
...@@ -11,7 +11,6 @@ import requests ...@@ -11,7 +11,6 @@ import requests
from mock_lti_server import MockLTIServer from mock_lti_server import MockLTIServer
class MockLTIServerTest(unittest.TestCase): class MockLTIServerTest(unittest.TestCase):
''' '''
A mock version of the LTI provider server that listens on a local A mock version of the LTI provider server that listens on a local
...@@ -33,6 +32,10 @@ class MockLTIServerTest(unittest.TestCase): ...@@ -33,6 +32,10 @@ class MockLTIServerTest(unittest.TestCase):
'lti_base': 'http://{}:{}/'.format(server_host, server_port), 'lti_base': 'http://{}:{}/'.format(server_host, server_port),
'lti_endpoint': 'correct_lti_endpoint' 'lti_endpoint': 'correct_lti_endpoint'
} }
self.server.run_inside_unittest_flag = True
#flag for creating right callback_url
self.server.test_mode = True
# Start the server in a separate daemon thread # Start the server in a separate daemon thread
server_thread = threading.Thread(target=self.server.serve_forever) server_thread = threading.Thread(target=self.server.serve_forever)
server_thread.daemon = True server_thread.daemon = True
...@@ -43,6 +46,24 @@ class MockLTIServerTest(unittest.TestCase): ...@@ -43,6 +46,24 @@ class MockLTIServerTest(unittest.TestCase):
# Stop the server, freeing up the port # Stop the server, freeing up the port
self.server.shutdown() self.server.shutdown()
def test_wrong_header(self):
"""
Tests that LTI server processes request with right program
path and responses with wrong header.
"""
#wrong number of params
payload = {
'user_id': 'default_user_id',
'role': 'student',
'oauth_nonce': '',
'oauth_timestamp': '',
}
uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint']
headers = {'referer': 'http://localhost:8000/'}
response = requests.post(uri, data=payload, headers=headers)
self.assertIn('Incorrect LTI header', response.content)
def test_wrong_signature(self): def test_wrong_signature(self):
""" """
Tests that LTI server processes request with right program Tests that LTI server processes request with right program
...@@ -65,18 +86,42 @@ class MockLTIServerTest(unittest.TestCase): ...@@ -65,18 +86,42 @@ class MockLTIServerTest(unittest.TestCase):
'lis_result_sourcedid': '', 'lis_result_sourcedid': '',
'resource_link_id':'', 'resource_link_id':'',
} }
uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint'] uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint']
headers = {'referer': 'http://localhost:8000/'} headers = {'referer': 'http://localhost:8000/'}
response = requests.post(uri, data=payload, headers=headers) response = requests.post(uri, data=payload, headers=headers)
self.assertIn('Wrong LTI signature', response.content)
self.assertTrue('Wrong LTI signature' in response.content)
def test_success_response_launch_lti(self): def test_success_response_launch_lti(self):
""" """
Success lti launch. Success lti launch.
""" """
payload = {
'user_id': 'default_user_id',
'role': 'student',
'oauth_nonce': '',
'oauth_timestamp': '',
'oauth_consumer_key': 'client_key',
'lti_version': 'LTI-1p0',
'oauth_signature_method': 'HMAC-SHA1',
'oauth_version': '1.0',
'oauth_signature': '',
'lti_message_type': 'basic-lti-launch-request',
'oauth_callback': 'about:blank',
'launch_presentation_return_url': '',
'lis_outcome_service_url': '',
'lis_result_sourcedid': '',
'resource_link_id':'',
"lis_outcome_service_url": '',
}
self.server.check_oauth_signature = Mock(return_value=True)
uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint']
headers = {'referer': 'http://localhost:8000/'}
response = requests.post(uri, data=payload, headers=headers)
self.assertIn('This is LTI tool. Success.', response.content)
def test_send_graded_result(self):
payload = { payload = {
'user_id': 'default_user_id', 'user_id': 'default_user_id',
...@@ -97,11 +142,16 @@ class MockLTIServerTest(unittest.TestCase): ...@@ -97,11 +142,16 @@ class MockLTIServerTest(unittest.TestCase):
"lis_outcome_service_url": '', "lis_outcome_service_url": '',
} }
self.server.check_oauth_signature = Mock(return_value=True) self.server.check_oauth_signature = Mock(return_value=True)
uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint'] uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint']
#this is the uri for sending grade from lti
headers = {'referer': 'http://localhost:8000/'} headers = {'referer': 'http://localhost:8000/'}
response = requests.post(uri, data=payload, headers=headers) response = requests.post(uri, data=payload, headers=headers)
self.assertTrue('This is LTI tool. Success.' in response.content) self.assertTrue('This is LTI tool. Success.' in response.content)
self.server.grade_data['TC answer'] = "Test response"
graded_response = requests.post('http://127.0.0.1:8034/grade')
self.assertIn('Test response', graded_response.content)
...@@ -33,7 +33,7 @@ class TestLTI(BaseTestXmodule): ...@@ -33,7 +33,7 @@ class TestLTI(BaseTestXmodule):
sourcedId = u':'.join(urllib.quote(i) for i in (lti_id, module_id, user_id)) sourcedId = u':'.join(urllib.quote(i) for i in (lti_id, module_id, user_id))
lis_outcome_service_url = 'http://{host}{path}'.format( lis_outcome_service_url = 'https://{host}{path}'.format(
host=self.item_descriptor.xmodule_runtime.hostname, host=self.item_descriptor.xmodule_runtime.hostname,
path=self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'grade_handler', thirdparty=True).rstrip('/?') path=self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'grade_handler', thirdparty=True).rstrip('/?')
) )
...@@ -59,6 +59,16 @@ class TestLTI(BaseTestXmodule): ...@@ -59,6 +59,16 @@ class TestLTI(BaseTestXmodule):
saved_sign = oauthlib.oauth1.Client.sign saved_sign = oauthlib.oauth1.Client.sign
self.expected_context = {
'display_name': self.item_module.display_name,
'input_fields': self.correct_headers,
'element_class': self.item_module.category,
'element_id': self.item_module.location.html_id(),
'launch_url': 'http://www.example.com', # default value
'open_in_a_new_page': True,
'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'preview_handler').rstrip('/?'),
}
def mocked_sign(self, *args, **kwargs): def mocked_sign(self, *args, **kwargs):
""" """
Mocked oauth1 sign function. Mocked oauth1 sign function.
...@@ -79,21 +89,11 @@ class TestLTI(BaseTestXmodule): ...@@ -79,21 +89,11 @@ class TestLTI(BaseTestXmodule):
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
def test_lti_constructor(self): def test_lti_constructor(self):
""" generated_content = self.item_module.render('student_view').content
Makes sure that all parameters extracted. expected_content = self.runtime.render_template('lti.html', self.expected_context)
""" self.assertEqual(generated_content, expected_content)
generated_context = self.item_module.render('student_view').content
expected_context = {
'display_name': self.item_module.display_name,
'input_fields': self.correct_headers,
'element_class': self.item_module.category,
'element_id': self.item_module.location.html_id(),
'launch_url': 'http://www.example.com', # default value
'open_in_a_new_page': True,
'form_url': self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'preview_handler').rstrip('/?'),
}
self.assertEqual( def test_lti_preview_handler(self):
generated_context, generated_content = self.item_module.preview_handler(None, None).body
self.runtime.render_template('lti.html', expected_context), expected_content = self.runtime.render_template('lti_form.html', self.expected_context)
) self.assertEqual(generated_content, expected_content)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment