Commit b5dc03ec by Alexander Kryklia

Acceptance test for LTI module (not finished), but working

parent e4bad0a6
......@@ -83,6 +83,7 @@ class LTIModule(LTIFields, XModule):
# with 'Content-Type': 'application/x-www-form-urlencoded'
# so '='' becomes '%3D', but server waits for unencoded signature.
# Decode signature back:
# may be it may be encoded by browser again... check
params[u'oauth_signature'] = urllib.unquote(params[u'oauth_signature']).decode('utf8')
return params
......
Feature: LTI component
As a student, I want to view LTI component in LMS.
Scenario: LTI component in LMS is not rendered
Given the course has a LTI component with empty fields
Then I view the LTI and it is not rendered
Scenario: LTI component in LMS is rendered
Given the course has a LTI component filled with correct data
Then I view the LTI and it is rendered
#pylint: disable=C0111
from lettuce import world, step
from lettuce.django import django_url
from common import i_am_registered_for_the_course, section_location
@step('I view the LTI and it is not rendered')
def lti_is_not_rendered(_step):
# lti div has no class rendered
assert world.is_css_not_present('div.lti.rendered')
# error is shown
assert world.css_visible('.error_message')
# iframe is not visible
assert (not world.css_visible('iframe'))
#inside iframe test content is not presented
with world.browser.get_iframe('ltiLaunchFrame') as iframe:
# iframe does not contain functions from terrain/ui_helpers.py
assert iframe.is_element_not_present_by_css('.result', wait_time=5)
@step('I view the LTI and it is rendered')
def lti_is_rendered(_step):
# lti div has class rendered
assert world.is_css_present('div.lti.rendered')
# error is hidden
assert (not world.css_visible('.error_message'))
# iframe is visible
assert world.css_visible('iframe')
#inside iframe test content is presented
with world.browser.get_iframe('ltiLaunchFrame') as iframe:
# iframe does not contain functions from terrain/ui_helpers.py
assert iframe.is_element_present_by_css('.result', wait_time=5)
@step('the course has a LTI component filled with correct data')
def view_lti_with_data(_step):
coursenum = 'test_course'
i_am_registered_for_the_course(_step, coursenum)
add_correct_lti_to_course(coursenum)
chapter_name = world.scenario_dict['SECTION'].display_name.replace(
" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % (
world.scenario_dict['COURSE'].org,
world.scenario_dict['COURSE'].number,
world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
chapter_name, section_name,)
)
world.browser.visit(url)
@step('the course has a LTI component with empty fields')
def view_default_lti(_step):
coursenum = 'test_course'
i_am_registered_for_the_course(_step, coursenum)
add_default_lti_to_course(coursenum)
chapter_name = world.scenario_dict['SECTION'].display_name.replace(
" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % (
world.scenario_dict['COURSE'].org,
world.scenario_dict['COURSE'].number,
world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
chapter_name, section_name,)
)
world.browser.visit(url)
def add_correct_lti_to_course(course):
category = 'lti'
world.ItemFactory.create(
parent_location=section_location(course),
category=category,
display_name='LTI',
metadata={
'client_key': 'client_key',
'clent_secret': 'client_secret',
'lti_url': 'http://127.0.0.1:{}/correct_lti_endpoint'.format(world.lti_server_port)
}
)
def add_default_lti_to_course(course):
category = 'lti'
world.ItemFactory.create(
parent_location=section_location(course),
category=category,
display_name='LTI'
)
#pylint: disable=C0111
#pylint: disable=W0621
from courseware.mock_lti_server.mock_lti_server import MockLTIServer
from lettuce import before, after, world
from django.conf import settings
import threading
from logging import getLogger
logger = getLogger(__name__)
@before.all
def setup_mock_lti_server():
# Add +1 to XQUEUE random port number
server_port = settings.XQUEUE_PORT + 1
# Create the mock server instance
server = MockLTIServer(server_port)
logger.debug("LTI server started at {} port".format(str(server_port)))
# Start the server running in a separate daemon thread
# Because the thread is a daemon, it will terminate
# when the main thread terminates.
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
# Store the server instance in lettuce's world
# so that other steps can access it
# (and we can shut it down later)
world.lti_server = server
world.lti_server_port = server_port
@after.all
def teardown_mock_lti_server(total):
# Stop the LTI server and free up the port
world.lti_server.shutdown()
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import json
import urllib
import urlparse
import threading
from logging import getLogger
logger = getLogger(__name__)
# todo - implement oauth
class MockLTIRequestHandler(BaseHTTPRequestHandler):
'''
A handler for LTI POST requests.
'''
protocol = "HTTP/1.0"
def do_HEAD(self):
self._send_head()
def do_POST(self):
'''
Handle a POST request from the client and sends response back.
'''
self._send_head()
post_dict = self._post_dict() # Retrieve the POST data
# Log the request
logger.debug("LTI provider received POST request {} to path {}".format(
str(post_dict),
self.path)
)
# Respond only to requests with correct lti endpoint:
if self._is_correct_lti_request():
correct_dict = {
'user_id': 'default_user_id',
'oauth_nonce': '22990037033121997701377766132',
'oauth_timestamp': '1377766132',
'oauth_consumer_key': 'client_key',
'lti_version': 'LTI-1p0',
'oauth_signature_method': 'HMAC-SHA1',
'oauth_version': '1.0',
'oauth_signature': 'HGYMAU/G5EMxd0CDOvWubsqxLIY=',
'lti_message_type': 'basic-lti-launch-request',
'oauth_callback': 'about:blank'
}
if sorted(correct_dict.keys()) != sorted(post_dict.keys()):
error_message = "Incorrect LTI header"
else:
error_message = "This is LTI tool."
else:
error_message = "Invalid request URL"
self._send_response(error_message)
def _send_head(self):
'''
Send the response code and MIME headers
'''
if self._is_correct_lti_request():
self.send_response(200)
else:
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.end_headers()
def _post_dict(self):
'''
Retrieve the POST parameters from the client as a dictionary
'''
try:
length = int(self.headers.getheader('content-length'))
post_dict = urlparse.parse_qs(self.rfile.read(length))
# The POST dict will contain a list of values for each key.
# None of our parameters are lists, however, so we map [val] --> val.
#I f the list contains multiple entries, we pick the first one
post_dict = dict(
map(
lambda (key, list_val): (key, list_val[0]),
post_dict.items()
)
)
except:
# We return an empty dict here, on the assumption
# that when we later check that the request has
# the correct fields, it won't find them,
# and will therefore send an error response
return {}
return post_dict
def _send_response(self, message):
'''
Send message back to the client
'''
response_str = """<html><head><title>TEST TITLE</title></head>
<body>
<div><h2>IFrame loaded</h2> \
<h3>Server response is:</h3>\
<h3 class="result">{}</h3></div>
</body></html>""".format(message)
# Log the response
logger.debug("LTI: sent response {}".format(response_str))
self.wfile.write(response_str)
def _is_correct_lti_request(self):
'''If url to get LTI is correct.'''
return 'correct_lti_endpoint' in self.path
class MockLTIServer(HTTPServer):
'''
A mock LTI provider server that responds
to POST requests to localhost.
'''
def __init__(self, port_num, oauth={}):
'''
Initialize the mock XQueue server instance.
*port_num* is the localhost port to listen to
*grade_response_dict* is a dictionary that will be JSON-serialized
and sent in response to XQueue grading requests.
'''
self.clent_key = oauth.get('client_key', '')
self.clent_secret = oauth.get('client_secret', '')
self.check_oauth()
handler = MockLTIRequestHandler
address = ('', port_num)
HTTPServer.__init__(self, address, handler)
def shutdown(self):
'''
Stop the server and free up the port
'''
# First call superclass shutdown()
HTTPServer.shutdown(self)
# We also need to manually close the socket
self.socket.close()
def get_oauth_signature(self):
'''test'''
return self._signature
def check_oauth(self):
''' generate oauth signature '''
self._signature = '12345'
import mock
import unittest
import threading
import json
import urllib
import time
from mock_lti_server import MockLTIServer, MockLTIRequestHandler
from nose.plugins.skip import SkipTest
class MockLTIServerTest(unittest.TestCase):
'''
A mock version of the LTI provider server that listens on a local
port and responds with pre-defined grade messages.
Used for lettuce BDD tests in lms/courseware/features/lti.feature
'''
def setUp(self):
# This is a test of the test setup,
# so it does not need to run as part of the unit test suite
# You can re-enable it by commenting out the line below
# raise SkipTest
# Create the server
server_port = 8034
self.server_url = 'http://127.0.0.1:%d' % server_port
self.server = MockLTIServer(server_port, {'client_key': '', 'client_secret': ''})
# Start the server in a separate daemon thread
server_thread = threading.Thread(target=self.server.serve_forever)
server_thread.daemon = True
server_thread.start()
def tearDown(self):
# Stop the server, freeing up the port
self.server.shutdown()
def test_oauth_request(self):
# Send a grade request
header = {
'Content-Type': 'application/x-www-form-urlencoded',
u'Authorization': u'OAuth oauth_nonce="151177408427657509491377691584", \
oauth_timestamp="1377691584", oauth_version="1.0", \
oauth_signature_method="HMAC-SHA1", oauth_consumer_key="", \
oauth_signature="wc1unKXxsX5e4HXJu%2FuiQ1KbrVo%3D"',
'launch_presentation_return_url': '',
'user_id': 'default_user_id',
'lis_result_sourcedid': '',
'lti_version': 'LTI-1p0',
'lis_outcome_service_url': '',
'lti_message_type': 'basic-lti-launch-request',
'oauth_callback': 'about:blank'
}
body = {}
request = {
'header': json.dumps(header),
'body': json.dumps(body)}
response_handle = urllib.urlopen(
self.server_url + '/correct_lti_endpoint',
urllib.urlencode(request)
)
response_dict = json.loads(response_handle.read())
# Expect that the response is success
self.assertEqual(response_dict['return_code'], 0)
# self.assertEqual(response_dict['return_code'], 0)
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