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

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

View File

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

View File

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

View File

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

View File

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