Commit 00792437 by Valera Rozuvan Committed by Oleg Marshev

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

BLD-564.
parent 11080f28
......@@ -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
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: Add template that displays the most up-to-date features of
drag-and-drop. BLD-479.
Blades: LTI additional Python tests. LTI fix bug e-reader error when popping
out window. BLD-465.
Blades: LTI fix bug e-reader error when popping out window. BLD-465.
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
......
......@@ -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)
......
......@@ -38,6 +38,10 @@ def setup_mock_lti_server():
'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
# so that other steps can access it
# (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 uuid import uuid4
import textwrap
......@@ -23,6 +33,7 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
protocol = "HTTP/1.0"
callback_url = None
def log_message(self, format, *args):
"""Log an arbitrary message."""
# Code copied from BaseHTTPServer.py. Changed to write to sys.stdout
......@@ -35,6 +46,8 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
'''
Handle a GET request from the client and sends response back.
Used for checking LTI Provider started correctly.
'''
self.send_response(200, 'OK')
......@@ -42,29 +55,20 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
self.end_headers()
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._send_graded_result()
def do_POST(self):
'''
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:
status_message = 'LTI consumer (edX) responded with XML content:<br>' + self.server.grade_data['TC answer']
self.server.grade_data['callback_url'] = None
self._send_response(status_message, 200)
# Respond to request with correct lti endpoint:
elif self._is_correct_lti_request():
self.post_dict = self._post_dict()
......@@ -97,26 +101,19 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
# set data for grades
# what need to be stored as server data
self.server.grade_data = {
'callback_url': self.post_dict["lis_outcome_service_url"],
'sourcedId': self.post_dict['lis_result_sourcedid']
'callback_url': self.post_dict.get('lis_outcome_service_url'),
'sourcedId': self.post_dict.get('lis_result_sourcedid')
}
self._send_response(status_message, 200)
else:
status_message = "Invalid request URL"
self._send_response(status_message, 500)
self._send_head()
self._send_response(status_message)
def _send_head(self):
def _send_head(self, status_code):
'''
Send the response code and MIME headers
'''
self.send_response(200)
'''
if self._is_correct_lti_request():
self.send_response(200)
else:
self.send_response(500)
'''
self.send_response(status_code)
self.send_header('Content-type', 'text/html')
self.end_headers()
......@@ -182,15 +179,22 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
</imsx_POXEnvelopeRequest>
""")
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
relative_url = urlparse.urlparse(self.server.grade_data['callback_url']).path
url = self.server.referer_host + relative_url
if getattr(self.server, 'test_mode', None):
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['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(
url,
data=data,
......@@ -199,11 +203,11 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
self.server.grade_data['TC answer'] = response.content
return response
def _send_response(self, message):
def _send_response(self, message, status_code):
'''
Send message back to the client
'''
self._send_head(status_code)
if self.server.grade_data['callback_url']:
response_str = """<html><head><title>TEST TITLE</title></head>
<body>
......@@ -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
sha1 = hashlib.sha1()
sha1.update(body)
oauth_body_hash = base64.b64encode(sha1.hexdigest())
oauth_body_hash = base64.b64encode(sha1.digest())
__, headers, __ = client.sign(
unicode(url.strip()),
http_method=u'POST',
......
"""
Mock LTI server for manual testing.
Used for manual testing and testing on sandbox.
"""
import threading
......@@ -18,6 +20,10 @@ server.oauth_settings = {
}
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:
server.serve_forever()
except KeyboardInterrupt:
......
......@@ -11,7 +11,6 @@ import requests
from mock_lti_server import MockLTIServer
class MockLTIServerTest(unittest.TestCase):
'''
A mock version of the LTI provider server that listens on a local
......@@ -33,6 +32,10 @@ class MockLTIServerTest(unittest.TestCase):
'lti_base': 'http://{}:{}/'.format(server_host, server_port),
'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
server_thread = threading.Thread(target=self.server.serve_forever)
server_thread.daemon = True
......@@ -43,6 +46,24 @@ class MockLTIServerTest(unittest.TestCase):
# Stop the server, freeing up the port
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):
"""
Tests that LTI server processes request with right program
......@@ -65,18 +86,42 @@ class MockLTIServerTest(unittest.TestCase):
'lis_result_sourcedid': '',
'resource_link_id':'',
}
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('Wrong LTI signature', response.content)
self.assertTrue('Wrong LTI signature' in response.content)
def test_success_response_launch_lti(self):
"""
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 = {
'user_id': 'default_user_id',
......@@ -97,11 +142,16 @@ class MockLTIServerTest(unittest.TestCase):
"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']
#this is the uri for sending grade from lti
headers = {'referer': 'http://localhost:8000/'}
response = requests.post(uri, data=payload, headers=headers)
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):
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,
path=self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'grade_handler', thirdparty=True).rstrip('/?')
)
......@@ -59,6 +59,16 @@ class TestLTI(BaseTestXmodule):
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):
"""
Mocked oauth1 sign function.
......@@ -79,21 +89,11 @@ class TestLTI(BaseTestXmodule):
self.addCleanup(patcher.stop)
def test_lti_constructor(self):
"""
Makes sure that all parameters extracted.
"""
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('/?'),
}
generated_content = self.item_module.render('student_view').content
expected_content = self.runtime.render_template('lti.html', self.expected_context)
self.assertEqual(generated_content, expected_content)
self.assertEqual(
generated_context,
self.runtime.render_template('lti.html', expected_context),
)
def test_lti_preview_handler(self):
generated_content = self.item_module.preview_handler(None, None).body
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