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
@@ -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)
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
"""
|
||||
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',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""
|
||||
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']
|
||||
headers = {'referer': 'http://localhost:8000/'}
|
||||
|
||||
response = requests.post(uri, data=payload, headers=headers)
|
||||
|
||||
self.assertTrue('This is LTI tool. Success.' in response.content)
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user