Moved stub servers to terrain
Refactored stub services for style and DRY Added unit tests for stub implementations Updated acceptance tests that depend on stubs. Updated Studio acceptance tests to use YouTube stub server; fixed failing tests in devstack.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
@shard_3
|
||||
Feature: Video Component Editor
|
||||
As a course author, I want to be able to create video components.
|
||||
|
||||
@@ -668,7 +669,7 @@ Feature: Video Component Editor
|
||||
And I edit the component
|
||||
|
||||
And I open tab "Advanced"
|
||||
And I revert the transcript field"HTML5 Transcript"
|
||||
And I revert the transcript field "HTML5 Transcript"
|
||||
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
@@ -49,25 +49,18 @@ TRANSCRIPTS_BUTTONS = {
|
||||
}
|
||||
|
||||
|
||||
def _clear_field(index):
|
||||
world.css_fill(SELECTORS['url_inputs'], '', index)
|
||||
# In some reason chromeDriver doesn't trigger 'input' event after filling
|
||||
# field by an empty value. That's why we trigger it manually via jQuery.
|
||||
world.trigger_event(SELECTORS['url_inputs'], event='input', index=index)
|
||||
|
||||
|
||||
@step('I clear fields$')
|
||||
def clear_fields(_step):
|
||||
js_str = '''
|
||||
|
||||
# Clear the input fields and trigger an 'input' event
|
||||
script = """
|
||||
$('{selector}')
|
||||
.eq({index})
|
||||
.prop('disabled', false)
|
||||
.removeClass('is-disabled');
|
||||
'''
|
||||
for index in range(1, 4):
|
||||
js = js_str.format(selector=SELECTORS['url_inputs'], index=index - 1)
|
||||
world.browser.execute_script(js)
|
||||
_clear_field(index)
|
||||
.removeClass('is-disabled')
|
||||
.val('')
|
||||
.trigger('input');
|
||||
""".format(selector=SELECTORS['url_inputs'])
|
||||
world.browser.execute_script(script)
|
||||
|
||||
world.wait(DELAY)
|
||||
world.wait_for_ajax_complete()
|
||||
@@ -76,7 +69,12 @@ def clear_fields(_step):
|
||||
@step('I clear field number (.+)$')
|
||||
def clear_field(_step, index):
|
||||
index = int(index) - 1
|
||||
_clear_field(index)
|
||||
world.css_fill(SELECTORS['url_inputs'], '', index)
|
||||
|
||||
# For some reason ChromeDriver doesn't trigger an 'input' event after filling
|
||||
# the field with an empty value. That's why we trigger it manually via jQuery.
|
||||
world.trigger_event(SELECTORS['url_inputs'], event='input', index=index)
|
||||
|
||||
world.wait(DELAY)
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def i_created_a_video_component(step):
|
||||
|
||||
world.wait_for_present('.is-initialized')
|
||||
world.wait(DELAY)
|
||||
assert not world.css_visible(SELECTORS['spinner'])
|
||||
world.wait_for_invisible(SELECTORS['spinner'])
|
||||
|
||||
|
||||
@step('I have created a Video component with subtitles$')
|
||||
@@ -59,8 +59,7 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
|
||||
world.disable_jquery_animations()
|
||||
|
||||
world.wait_for_present('.is-initialized')
|
||||
world.wait(DELAY)
|
||||
assert not world.css_visible(SELECTORS['spinner'])
|
||||
world.wait_for_invisible(SELECTORS['spinner'])
|
||||
|
||||
|
||||
@step('I have uploaded subtitles "([^"]*)"$')
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
from xmodule.util.mock_youtube_server.mock_youtube_server import MockYoutubeServer
|
||||
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_youtube_server():
|
||||
server_host = '127.0.0.1'
|
||||
|
||||
server_port = settings.VIDEO_PORT
|
||||
|
||||
address = (server_host, server_port)
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockYoutubeServer(address)
|
||||
logger.debug("Youtube server started at {} port".format(str(server_port)))
|
||||
|
||||
server.time_to_response = 0.1 # seconds
|
||||
|
||||
server.address = address
|
||||
|
||||
# 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.youtube_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_youtube_server(total):
|
||||
|
||||
# Stop the LTI server and free up the port
|
||||
world.youtube_server.shutdown()
|
||||
@@ -114,22 +114,6 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Because an override for where to run will affect which ports to use,
|
||||
# set this up after the local overrides.
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
LETTUCE_SERVER_PORT = choice(PORTS)
|
||||
else:
|
||||
LETTUCE_SERVER_PORT = randint(1024, 65535)
|
||||
|
||||
|
||||
# Set up Video information so that the cms will send
|
||||
# requests to a mock Youtube server running locally
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
VIDEO_PORT = choice(PORTS)
|
||||
PORTS.remove(VIDEO_PORT)
|
||||
else:
|
||||
VIDEO_PORT = randint(1024, 65535)
|
||||
|
||||
# for testing Youtube
|
||||
YOUTUBE_API['url'] = "http://127.0.0.1:" + str(VIDEO_PORT) + '/test_transcripts_youtube/'
|
||||
|
||||
# Point the URL used to test YouTube availability to our stub YouTube server
|
||||
YOUTUBE_TEST_URL = "http://127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT)
|
||||
YOUTUBE_API['url'] = "http://127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT)
|
||||
|
||||
@@ -359,6 +359,13 @@ CELERY_QUEUES = {
|
||||
DEFAULT_PRIORITY_QUEUE: {}
|
||||
}
|
||||
|
||||
|
||||
############################## Video ##########################################
|
||||
|
||||
# URL to test YouTube availability
|
||||
YOUTUBE_TEST_URL = 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
|
||||
|
||||
############################ APPS #####################################
|
||||
|
||||
INSTALLED_APPS = (
|
||||
|
||||
@@ -150,6 +150,17 @@ CELERY_ALWAYS_EAGER = True
|
||||
CELERY_RESULT_BACKEND = 'cache'
|
||||
BROKER_TRANSPORT = 'memory'
|
||||
|
||||
|
||||
########################### Server Ports ###################################
|
||||
|
||||
# These ports are carefully chosen so that if the browser needs to
|
||||
# access them, they will be available through the SauceLabs SSH tunnel
|
||||
LETTUCE_SERVER_PORT = 8003
|
||||
XQUEUE_PORT = 8040
|
||||
YOUTUBE_PORT = 8031
|
||||
LTI_PORT = 8765
|
||||
|
||||
|
||||
################### Make tests faster
|
||||
# http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
|
||||
PASSWORD_HASHERS = (
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from terrain.browser import *
|
||||
from terrain.steps import *
|
||||
from terrain.factories import *
|
||||
from terrain.start_stubs import *
|
||||
|
||||
37
common/djangoapps/terrain/start_stubs.py
Normal file
37
common/djangoapps/terrain/start_stubs.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Initialize and teardown fake HTTP services for use in acceptance tests.
|
||||
"""
|
||||
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
from terrain.stubs.youtube import StubYouTubeService
|
||||
from terrain.stubs.xqueue import StubXQueueService
|
||||
|
||||
|
||||
USAGE = "USAGE: python -m fakes.start SERVICE_NAME PORT_NUM"
|
||||
|
||||
SERVICES = {
|
||||
"youtube": {"port": settings.YOUTUBE_PORT, "class": StubYouTubeService},
|
||||
"xqueue": {"port": settings.XQUEUE_PORT, "class": StubXQueueService},
|
||||
}
|
||||
|
||||
|
||||
@before.all
|
||||
def start_stubs():
|
||||
"""
|
||||
Start each stub service running on a local port.
|
||||
"""
|
||||
for name, service in SERVICES.iteritems():
|
||||
fake_server = service['class'](port_num=service['port'])
|
||||
setattr(world, name, fake_server)
|
||||
|
||||
|
||||
@after.all
|
||||
def stop_stubs(_):
|
||||
"""
|
||||
Shut down each stub service.
|
||||
"""
|
||||
for name in SERVICES.keys():
|
||||
stub_server = getattr(world, name, None)
|
||||
if stub_server is not None:
|
||||
stub_server.shutdown()
|
||||
181
common/djangoapps/terrain/stubs/http.py
Normal file
181
common/djangoapps/terrain/stubs/http.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
Stub implementation of an HTTP service.
|
||||
"""
|
||||
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import urlparse
|
||||
import threading
|
||||
import json
|
||||
from lazy import lazy
|
||||
|
||||
from logging import getLogger
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
|
||||
"""
|
||||
Handler for the stub HTTP service.
|
||||
"""
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def log_message(self, format_str, *args):
|
||||
"""
|
||||
Redirect messages to keep the test console clean.
|
||||
"""
|
||||
|
||||
msg = "{0} - - [{1}] {2}\n".format(
|
||||
self.client_address[0],
|
||||
self.log_date_time_string(),
|
||||
format_str % args
|
||||
)
|
||||
|
||||
LOGGER.debug(msg)
|
||||
|
||||
@lazy
|
||||
def request_content(self):
|
||||
"""
|
||||
Retrieve the content of the request.
|
||||
"""
|
||||
try:
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
else:
|
||||
return self.rfile.read(length)
|
||||
|
||||
@lazy
|
||||
def post_dict(self):
|
||||
"""
|
||||
Retrieve the request POST parameters from the client as a dictionary.
|
||||
If no POST parameters can be interpreted, return an empty dict.
|
||||
"""
|
||||
contents = self.request_content
|
||||
|
||||
# The POST dict will contain a list of values for each key.
|
||||
# None of our parameters are lists, however, so we map [val] --> val
|
||||
# If the list contains multiple entries, we pick the first one
|
||||
try:
|
||||
post_dict = urlparse.parse_qs(contents, keep_blank_values=True)
|
||||
return {
|
||||
key: list_val[0]
|
||||
for key, list_val in post_dict.items()
|
||||
}
|
||||
|
||||
except:
|
||||
return dict()
|
||||
|
||||
@lazy
|
||||
def get_params(self):
|
||||
"""
|
||||
Return the GET parameters (querystring in the URL).
|
||||
"""
|
||||
return urlparse.parse_qs(self.path)
|
||||
|
||||
def do_PUT(self):
|
||||
"""
|
||||
Allow callers to configure the stub server using the /set_config URL.
|
||||
"""
|
||||
if self.path == "/set_config" or self.path == "/set_config/":
|
||||
|
||||
for key, value in self.post_dict.iteritems():
|
||||
self.log_message("Set config '{0}' to '{1}'".format(key, value))
|
||||
|
||||
try:
|
||||
value = json.loads(value)
|
||||
|
||||
except ValueError:
|
||||
self.log_message(u"Could not parse JSON: {0}".format(value))
|
||||
self.send_response(400)
|
||||
|
||||
else:
|
||||
self.server.set_config(unicode(key, 'utf-8'), value)
|
||||
self.send_response(200)
|
||||
|
||||
else:
|
||||
self.send_response(404)
|
||||
|
||||
def send_response(self, status_code, content=None, headers=None):
|
||||
"""
|
||||
Send a response back to the client with the HTTP `status_code` (int),
|
||||
`content` (str) and `headers` (dict).
|
||||
"""
|
||||
self.log_message(
|
||||
"Sent HTTP response: {0} with content '{1}' and headers {2}".format(status_code, content, headers)
|
||||
)
|
||||
|
||||
if headers is None:
|
||||
headers = dict()
|
||||
|
||||
BaseHTTPRequestHandler.send_response(self, status_code)
|
||||
|
||||
for (key, value) in headers.items():
|
||||
self.send_header(key, value)
|
||||
|
||||
if len(headers) > 0:
|
||||
self.end_headers()
|
||||
|
||||
if content is not None:
|
||||
self.wfile.write(content)
|
||||
|
||||
|
||||
class StubHttpService(HTTPServer, object):
|
||||
"""
|
||||
Stub HTTP service implementation.
|
||||
"""
|
||||
|
||||
# Subclasses override this to provide the handler class to use.
|
||||
# Should be a subclass of `StubHttpRequestHandler`
|
||||
HANDLER_CLASS = StubHttpRequestHandler
|
||||
|
||||
def __init__(self, port_num=0):
|
||||
"""
|
||||
Configure the server to listen on localhost.
|
||||
Default is to choose an arbitrary open port.
|
||||
"""
|
||||
address = ('127.0.0.1', port_num)
|
||||
HTTPServer.__init__(self, address, self.HANDLER_CLASS)
|
||||
|
||||
# Create a dict to store configuration values set by the client
|
||||
self._config = dict()
|
||||
|
||||
# Start the server in a separate thread
|
||||
server_thread = threading.Thread(target=self.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Log the port we're using to help identify port conflict errors
|
||||
LOGGER.debug('Starting service on port {0}'.format(self.port))
|
||||
|
||||
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()
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""
|
||||
Return the port that the service is listening on.
|
||||
"""
|
||||
_, port = self.server_address
|
||||
return port
|
||||
|
||||
def config(self, key, default=None):
|
||||
"""
|
||||
Return the configuration value for `key`. If this
|
||||
value has not been set, return `default` instead.
|
||||
"""
|
||||
return self._config.get(key, default)
|
||||
|
||||
def set_config(self, key, value):
|
||||
"""
|
||||
Set the configuration `value` for `key`.
|
||||
"""
|
||||
self._config[key] = value
|
||||
|
||||
58
common/djangoapps/terrain/stubs/tests/test_http.py
Normal file
58
common/djangoapps/terrain/stubs/tests/test_http.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Unit tests for stub HTTP server base class.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import requests
|
||||
import json
|
||||
from terrain.stubs.http import StubHttpService
|
||||
|
||||
|
||||
class StubHttpServiceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server = StubHttpService()
|
||||
self.addCleanup(self.server.shutdown)
|
||||
|
||||
def test_configure(self):
|
||||
"""
|
||||
All HTTP stub servers have an end-point that allows
|
||||
clients to configure how the server responds.
|
||||
"""
|
||||
params = {
|
||||
'test_str': 'This is only a test',
|
||||
'test_int': 12345,
|
||||
'test_float': 123.45,
|
||||
'test_unicode': u'\u2603 the snowman',
|
||||
'test_dict': { 'test_key': 'test_val' }
|
||||
}
|
||||
|
||||
for key, val in params.iteritems():
|
||||
post_params = {key: json.dumps(val)}
|
||||
response = requests.put(
|
||||
"http://127.0.0.1:{0}/set_config".format(self.server.port),
|
||||
data=post_params
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that the expected values were set in the configuration
|
||||
for key, val in params.iteritems():
|
||||
self.assertEqual(self.server.config(key), val)
|
||||
|
||||
def test_default_config(self):
|
||||
self.assertEqual(self.server.config('not_set', default=42), 42)
|
||||
|
||||
def test_bad_json(self):
|
||||
response = requests.put(
|
||||
"http://127.0.0.1:{0}/set_config".format(self.server.port),
|
||||
data="{,}"
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_unknown_path(self):
|
||||
response = requests.put(
|
||||
"http://127.0.0.1:{0}/invalid_url".format(self.server.port),
|
||||
data="{}"
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
74
common/djangoapps/terrain/stubs/tests/test_xqueue_stub.py
Normal file
74
common/djangoapps/terrain/stubs/tests/test_xqueue_stub.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Unit tests for stub XQueue implementation.
|
||||
"""
|
||||
|
||||
import mock
|
||||
import unittest
|
||||
import json
|
||||
import urllib
|
||||
import time
|
||||
from terrain.stubs.xqueue import StubXQueueService
|
||||
|
||||
|
||||
class StubXQueueServiceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server = StubXQueueService()
|
||||
self.url = "http://127.0.0.1:{0}".format(self.server.port)
|
||||
self.addCleanup(self.server.shutdown)
|
||||
|
||||
# For testing purposes, do not delay the grading response
|
||||
self.server.set_config('response_delay', 0)
|
||||
|
||||
@mock.patch('requests.post')
|
||||
def test_grade_request(self, post):
|
||||
|
||||
# Send a grade request
|
||||
callback_url = 'http://127.0.0.1:8000/test_callback'
|
||||
|
||||
grade_header = json.dumps({
|
||||
'lms_callback_url': callback_url,
|
||||
'lms_key': 'test_queuekey',
|
||||
'queue_name': 'test_queue'
|
||||
})
|
||||
|
||||
grade_body = json.dumps({
|
||||
'student_info': 'test',
|
||||
'grader_payload': 'test',
|
||||
'student_response': 'test'
|
||||
})
|
||||
|
||||
grade_request = {
|
||||
'xqueue_header': grade_header,
|
||||
'xqueue_body': grade_body
|
||||
}
|
||||
|
||||
response_handle = urllib.urlopen(
|
||||
self.url + '/xqueue/submit',
|
||||
urllib.urlencode(grade_request)
|
||||
)
|
||||
|
||||
response_dict = json.loads(response_handle.read())
|
||||
|
||||
# Expect that the response is success
|
||||
self.assertEqual(response_dict['return_code'], 0)
|
||||
|
||||
# Expect that the server tries to post back the grading info
|
||||
xqueue_body = json.dumps(
|
||||
{'correct': True, 'score': 1, 'msg': '<div></div>'}
|
||||
)
|
||||
|
||||
expected_callback_dict = {
|
||||
'xqueue_header': grade_header,
|
||||
'xqueue_body': xqueue_body
|
||||
}
|
||||
|
||||
# Wait for the server to POST back to the callback URL
|
||||
# Time out if it takes too long
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 5:
|
||||
if post.called:
|
||||
break
|
||||
|
||||
# Check that the POST request was made with the correct params
|
||||
post.assert_called_with(callback_url, data=expected_callback_dict)
|
||||
58
common/djangoapps/terrain/stubs/tests/test_youtube_stub.py
Normal file
58
common/djangoapps/terrain/stubs/tests/test_youtube_stub.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Unit test for stub YouTube implementation.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import requests
|
||||
from terrain.stubs.youtube import StubYouTubeService
|
||||
|
||||
|
||||
class StubYouTubeServiceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server = StubYouTubeService()
|
||||
self.url = "http://127.0.0.1:{0}/".format(self.server.port)
|
||||
self.server.set_config('time_to_response', 0.0)
|
||||
self.addCleanup(self.server.shutdown)
|
||||
|
||||
def test_unused_url(self):
|
||||
response = requests.get(self.url + 'unused_url')
|
||||
self.assertEqual("Unused url", response.content)
|
||||
|
||||
def test_video_url(self):
|
||||
response = requests.get(
|
||||
self.url + 'test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func'
|
||||
)
|
||||
|
||||
self.assertEqual('callback_func({"message": "I\'m youtube."})', response.content)
|
||||
|
||||
def test_transcript_url_equal(self):
|
||||
response = requests.get(
|
||||
self.url + 'test_transcripts_youtube/t__eq_exist'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"".join([
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
'<transcript><text start="1.0" dur="1.0">',
|
||||
'Equal transcripts</text></transcript>'
|
||||
]), response.content
|
||||
)
|
||||
|
||||
def test_transcript_url_not_equal(self):
|
||||
response = requests.get(
|
||||
self.url + 'test_transcripts_youtube/t_neq_exist',
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"".join([
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
'<transcript><text start="1.1" dur="5.5">',
|
||||
'Transcripts sample, different that on server',
|
||||
'</text></transcript>'
|
||||
]), response.content
|
||||
)
|
||||
|
||||
def test_transcript_not_found(self):
|
||||
response = requests.get(self.url + 'test_transcripts_youtube/some_id')
|
||||
self.assertEqual(404, response.status_code)
|
||||
122
common/djangoapps/terrain/stubs/xqueue.py
Normal file
122
common/djangoapps/terrain/stubs/xqueue.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Stub implementation of XQueue for acceptance tests.
|
||||
"""
|
||||
|
||||
from .http import StubHttpRequestHandler, StubHttpService
|
||||
import json
|
||||
import requests
|
||||
import threading
|
||||
|
||||
|
||||
class StubXQueueHandler(StubHttpRequestHandler):
|
||||
"""
|
||||
A handler for XQueue POST requests.
|
||||
"""
|
||||
|
||||
DEFAULT_RESPONSE_DELAY = 2
|
||||
DEFAULT_GRADE_RESPONSE = {'correct': True, 'score': 1, 'msg': ''}
|
||||
|
||||
def do_POST(self):
|
||||
"""
|
||||
Handle a POST request from the client
|
||||
|
||||
Sends back an immediate success/failure response.
|
||||
It then POSTS back to the client with grading results.
|
||||
"""
|
||||
msg = "XQueue received POST request {0} to path {1}".format(self.post_dict, self.path)
|
||||
self.log_message(msg)
|
||||
|
||||
# Respond only to grading requests
|
||||
if self._is_grade_request():
|
||||
try:
|
||||
xqueue_header = json.loads(self.post_dict['xqueue_header'])
|
||||
callback_url = xqueue_header['lms_callback_url']
|
||||
|
||||
except KeyError:
|
||||
# If the message doesn't have a header or body,
|
||||
# then it's malformed.
|
||||
# Respond with failure
|
||||
error_msg = "XQueue received invalid grade request"
|
||||
self._send_immediate_response(False, message=error_msg)
|
||||
|
||||
except ValueError:
|
||||
# If we could not decode the body or header,
|
||||
# respond with failure
|
||||
|
||||
error_msg = "XQueue could not decode grade request"
|
||||
self._send_immediate_response(False, message=error_msg)
|
||||
|
||||
else:
|
||||
# Send an immediate response of success
|
||||
# The grade request is formed correctly
|
||||
self._send_immediate_response(True)
|
||||
|
||||
# Wait a bit before POSTing back to the callback url with the
|
||||
# grade result configured by the server
|
||||
# Otherwise, the problem will not realize it's
|
||||
# queued and it will keep waiting for a response indefinitely
|
||||
delayed_grade_func = lambda: self._send_grade_response(
|
||||
callback_url, xqueue_header
|
||||
)
|
||||
|
||||
threading.Timer(
|
||||
self.server.config('response_delay', default=self.DEFAULT_RESPONSE_DELAY),
|
||||
delayed_grade_func
|
||||
).start()
|
||||
|
||||
# If we get a request that's not to the grading submission
|
||||
# URL, return an error
|
||||
else:
|
||||
error_message = "Invalid request URL"
|
||||
self._send_immediate_response(False, message=error_message)
|
||||
|
||||
def _send_immediate_response(self, success, message=""):
|
||||
"""
|
||||
Send an immediate success/failure message
|
||||
back to the client
|
||||
"""
|
||||
|
||||
# Send the response indicating success/failure
|
||||
response_str = json.dumps(
|
||||
{'return_code': 0 if success else 1, 'content': message}
|
||||
)
|
||||
|
||||
if self._is_grade_request():
|
||||
self.send_response(
|
||||
200, content=response_str, headers={'Content-type': 'text/plain'}
|
||||
)
|
||||
self.log_message("XQueue: sent response {0}".format(response_str))
|
||||
|
||||
else:
|
||||
self.send_response(500)
|
||||
|
||||
def _send_grade_response(self, postback_url, xqueue_header):
|
||||
"""
|
||||
POST the grade response back to the client
|
||||
using the response provided by the server configuration
|
||||
"""
|
||||
# Get the grade response from the server configuration
|
||||
grade_response = self.server.config('grade_response', default=self.DEFAULT_GRADE_RESPONSE)
|
||||
|
||||
# Wrap the message in <div> tags to ensure that it is valid XML
|
||||
if isinstance(grade_response, dict) and 'msg' in grade_response:
|
||||
grade_response['msg'] = "<div>{0}</div>".format(grade_response['msg'])
|
||||
|
||||
data = {
|
||||
'xqueue_header': json.dumps(xqueue_header),
|
||||
'xqueue_body': json.dumps(grade_response)
|
||||
}
|
||||
|
||||
requests.post(postback_url, data=data)
|
||||
self.log_message("XQueue: sent grading response {0}".format(data))
|
||||
|
||||
def _is_grade_request(self):
|
||||
return 'xqueue/submit' in self.path
|
||||
|
||||
|
||||
class StubXQueueService(StubHttpService):
|
||||
"""
|
||||
A stub XQueue grading server that responds to POST requests to localhost.
|
||||
"""
|
||||
|
||||
HANDLER_CLASS = StubXQueueHandler
|
||||
85
common/djangoapps/terrain/stubs/youtube.py
Normal file
85
common/djangoapps/terrain/stubs/youtube.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Stub implementation of YouTube for acceptance tests.
|
||||
"""
|
||||
|
||||
from .http import StubHttpRequestHandler, StubHttpService
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
|
||||
|
||||
class StubYouTubeHandler(StubHttpRequestHandler):
|
||||
"""
|
||||
A handler for Youtube GET requests.
|
||||
"""
|
||||
|
||||
# Default number of seconds to delay the response to simulate network latency.
|
||||
DEFAULT_DELAY_SEC = 0.5
|
||||
|
||||
def do_GET(self):
|
||||
"""
|
||||
Handle a GET request from the client and sends response back.
|
||||
"""
|
||||
|
||||
self.log_message(
|
||||
"Youtube provider received GET request to path {}".format(self.path)
|
||||
)
|
||||
|
||||
if 'test_transcripts_youtube' in self.path:
|
||||
|
||||
if 't__eq_exist' in self.path:
|
||||
status_message = "".join([
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
'<transcript><text start="1.0" dur="1.0">',
|
||||
'Equal transcripts</text></transcript>'
|
||||
])
|
||||
|
||||
self.send_response(
|
||||
200, content=status_message, headers={'Content-type': 'application/xml'}
|
||||
)
|
||||
|
||||
elif 't_neq_exist' in self.path:
|
||||
status_message = "".join([
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
'<transcript><text start="1.1" dur="5.5">',
|
||||
'Transcripts sample, different that on server',
|
||||
'</text></transcript>'
|
||||
])
|
||||
|
||||
self.send_response(
|
||||
200, content=status_message, headers={'Content-type': 'application/xml'}
|
||||
)
|
||||
|
||||
else:
|
||||
self.send_response(404)
|
||||
|
||||
elif 'test_youtube' in self.path:
|
||||
self._send_video_response("I'm youtube.")
|
||||
|
||||
else:
|
||||
self.send_response(
|
||||
404, content="Unused url", headers={'Content-type': 'text/plain'}
|
||||
)
|
||||
|
||||
def _send_video_response(self, message):
|
||||
"""
|
||||
Send message back to the client for video player requests.
|
||||
Requires sending back callback id.
|
||||
"""
|
||||
# Delay the response to simulate network latency
|
||||
time.sleep(self.server.config('time_to_response', self.DEFAULT_DELAY_SEC))
|
||||
|
||||
# Construct the response content
|
||||
callback = self.get_params['callback'][0]
|
||||
response = callback + '({})'.format(json.dumps({'message': message}))
|
||||
|
||||
self.send_response(200, content=response, headers={'Content-type': 'text/html'})
|
||||
self.log_message("Youtube: sent response {}".format(message))
|
||||
|
||||
|
||||
class StubYouTubeService(StubHttpService):
|
||||
"""
|
||||
A stub Youtube provider server that responds to GET requests to localhost.
|
||||
"""
|
||||
|
||||
HANDLER_CLASS = StubYouTubeHandler
|
||||
@@ -10,7 +10,7 @@ from textwrap import dedent
|
||||
from urllib import quote_plus
|
||||
from selenium.common.exceptions import (
|
||||
WebDriverException, TimeoutException,
|
||||
StaleElementReferenceException)
|
||||
StaleElementReferenceException, InvalidElementStateException)
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
@@ -581,7 +581,7 @@ def trigger_event(css_selector, event='change', index=0):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def retry_on_exception(func, max_attempts=5, ignored_exceptions=StaleElementReferenceException):
|
||||
def retry_on_exception(func, max_attempts=5, ignored_exceptions=(StaleElementReferenceException, InvalidElementStateException)):
|
||||
"""
|
||||
Retry the interaction, ignoring the passed exceptions.
|
||||
By default ignore StaleElementReferenceException, which happens often in our application
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import json
|
||||
import mock
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urlparse
|
||||
from logging import getLogger
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
class MockYoutubeRequestHandler(BaseHTTPRequestHandler):
|
||||
'''
|
||||
A handler for Youtube GET requests.
|
||||
'''
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Log an arbitrary message."""
|
||||
# Code copied from BaseHTTPServer.py. Changed to write to sys.stdout
|
||||
# so that messages won't pollute test output.
|
||||
sys.stdout.write("%s - - [%s] %s\n" %
|
||||
(self.client_address[0],
|
||||
self.log_date_time_string(),
|
||||
format % args))
|
||||
|
||||
def do_HEAD(self):
|
||||
code = 200
|
||||
if 'test_transcripts_youtube' in self.path:
|
||||
if not 'trans_exist' in self.path:
|
||||
code = 404
|
||||
self._send_head(code)
|
||||
|
||||
def do_GET(self):
|
||||
'''
|
||||
Handle a GET request from the client and sends response back.
|
||||
'''
|
||||
logger.debug("Youtube provider received GET request to path {}".format(
|
||||
self.path)
|
||||
) # Log the request
|
||||
|
||||
if 'test_transcripts_youtube' in self.path:
|
||||
if 't__eq_exist' in self.path:
|
||||
status_message = """<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.0" dur="1.0">Equal transcripts</text></transcript>"""
|
||||
self._send_head()
|
||||
self._send_transcripts_response(status_message)
|
||||
elif 't_neq_exist' in self.path:
|
||||
status_message = """<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.1" dur="5.5">Transcripts sample, different that on server</text></transcript>"""
|
||||
self._send_head()
|
||||
self._send_transcripts_response(status_message)
|
||||
else:
|
||||
self._send_head(404)
|
||||
elif 'test_youtube' in self.path:
|
||||
self._send_head()
|
||||
#testing videoplayers
|
||||
status_message = "I'm youtube."
|
||||
response_timeout = float(self.server.time_to_response)
|
||||
|
||||
# threading timer produces TypeError: 'NoneType' object is not callable here
|
||||
# so we use time.sleep, as we already in separate thread.
|
||||
time.sleep(response_timeout)
|
||||
self._send_video_response(status_message)
|
||||
else:
|
||||
# unused url
|
||||
self._send_head()
|
||||
self._send_transcripts_response('Unused url')
|
||||
logger.debug("Request to unused url.")
|
||||
|
||||
def _send_head(self, code=200):
|
||||
'''
|
||||
Send the response code and MIME headers
|
||||
'''
|
||||
|
||||
self.send_response(code)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
|
||||
def _send_transcripts_response(self, message):
|
||||
'''
|
||||
Send message back to the client for transcripts ajax requests.
|
||||
'''
|
||||
response = message
|
||||
# Log the response
|
||||
logger.debug("Youtube: sent response {}".format(message))
|
||||
|
||||
self.wfile.write(response)
|
||||
|
||||
def _send_video_response(self, message):
|
||||
'''
|
||||
Send message back to the client for video player requests.
|
||||
Requires sending back callback id.
|
||||
'''
|
||||
callback = urlparse.parse_qs(self.path)['callback'][0]
|
||||
response = callback + '({})'.format(json.dumps({'message': message}))
|
||||
# Log the response
|
||||
logger.debug("Youtube: sent response {}".format(message))
|
||||
|
||||
self.wfile.write(response)
|
||||
|
||||
|
||||
class MockYoutubeServer(HTTPServer):
|
||||
'''
|
||||
A mock Youtube provider server that responds
|
||||
to GET requests to localhost.
|
||||
'''
|
||||
|
||||
def __init__(self, address):
|
||||
'''
|
||||
Initialize the mock XQueue server instance.
|
||||
|
||||
*address* is the (host, host's port to listen to) tuple.
|
||||
'''
|
||||
handler = MockYoutubeRequestHandler
|
||||
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()
|
||||
@@ -1,77 +0,0 @@
|
||||
"""
|
||||
Test for Mock_Youtube_Server
|
||||
"""
|
||||
import unittest
|
||||
import threading
|
||||
import requests
|
||||
from mock_youtube_server import MockYoutubeServer
|
||||
|
||||
|
||||
class MockYoutubeServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the YouTube provider server that listens on a local
|
||||
port and responds with jsonp.
|
||||
|
||||
Used for lettuce BDD tests in lms/courseware/features/video.feature
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Create the server
|
||||
server_port = 8034
|
||||
server_host = '127.0.0.1'
|
||||
address = (server_host, server_port)
|
||||
self.server = MockYoutubeServer(address, )
|
||||
self.server.time_to_response = 0.5
|
||||
# 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_request(self):
|
||||
"""
|
||||
Tests that Youtube server processes request with right program
|
||||
path, and responses with incorrect signature.
|
||||
"""
|
||||
# GET request
|
||||
|
||||
# unused url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/some url',
|
||||
)
|
||||
self.assertEqual("Unused url", response.content)
|
||||
|
||||
# video player test url, callback shoud be presented in url params
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func',
|
||||
)
|
||||
self.assertEqual("""callback_func({"message": "I\'m youtube."})""", response.content)
|
||||
|
||||
# transcripts test url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/t__eq_exist',
|
||||
)
|
||||
self.assertEqual(
|
||||
'<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.0" dur="1.0">Equal transcripts</text></transcript>',
|
||||
response.content
|
||||
)
|
||||
|
||||
# transcripts test url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/t_neq_exist',
|
||||
)
|
||||
self.assertEqual(
|
||||
'<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.1" dur="5.5">Transcripts sample, different that on server</text></transcript>',
|
||||
response.content
|
||||
)
|
||||
|
||||
# transcripts test url, not trans_exist youtube_id, so 404 should be returned
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/some_id',
|
||||
)
|
||||
self.assertEqual(404, response.status_code)
|
||||
@@ -167,12 +167,6 @@ class VideoModule(VideoFields, XModule):
|
||||
sources = {get_ext(src): src for src in self.html5_sources}
|
||||
sources['main'] = self.source
|
||||
|
||||
# for testing Youtube timeout in acceptance tests
|
||||
if getattr(settings, 'VIDEO_PORT', None):
|
||||
yt_test_url = "http://127.0.0.1:" + str(settings.VIDEO_PORT) + '/test_youtube/'
|
||||
else:
|
||||
yt_test_url = 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
|
||||
return self.system.render_template('video.html', {
|
||||
'youtube_streams': _create_youtube_string(self),
|
||||
'id': self.location.html_id(),
|
||||
@@ -191,7 +185,7 @@ class VideoModule(VideoFields, XModule):
|
||||
# TODO: Later on the value 1500 should be taken from some global
|
||||
# configuration setting field.
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': yt_test_url
|
||||
'yt_test_url': settings.YOUTUBE_TEST_URL
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ logger = getLogger(__name__)
|
||||
def setup_mock_lti_server():
|
||||
|
||||
server_host = '127.0.0.1'
|
||||
|
||||
# Add +1 to XQUEUE random port number
|
||||
server_port = settings.XQUEUE_PORT + 1
|
||||
server_port = settings.LTI_PORT
|
||||
|
||||
address = (server_host, server_port)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ def set_external_grader_response(step, correctness):
|
||||
|
||||
# Set the fake xqueue server to always respond
|
||||
# correct/incorrect when asked to grade a problem
|
||||
world.xqueue_server.set_grade_response(response_dict)
|
||||
world.xqueue.set_config('grade_response', response_dict)
|
||||
|
||||
|
||||
@step(u'I answer a "([^"]*)" problem "([^"]*)ly"')
|
||||
|
||||
@@ -18,7 +18,7 @@ Feature: LMS.Video component
|
||||
# 3
|
||||
# Youtube testing
|
||||
Scenario: Video component is fully rendered in the LMS in Youtube mode with HTML5 sources
|
||||
Given youtube server is up and response time is 0.4 seconds
|
||||
Given youtube server is up and response time is 0.4 seconds
|
||||
And the course has a Video component in Youtube_HTML5 mode
|
||||
Then when I view the video it has rendered in Youtube mode
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ def add_video_to_course(course, player_mode):
|
||||
|
||||
@step('youtube server is up and response time is (.*) seconds$')
|
||||
def set_youtube_response_timeout(_step, time):
|
||||
world.youtube_server.time_to_response = time
|
||||
world.youtube.set_config('time_to_response', float(time))
|
||||
|
||||
|
||||
@step('when I view the video it has rendered in (.*) mode$')
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
import threading
|
||||
|
||||
@before.all
|
||||
def setup_mock_xqueue_server():
|
||||
|
||||
# Retrieve the local port from settings
|
||||
server_port = settings.XQUEUE_PORT
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockXQueueServer(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.xqueue_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_xqueue_server(total):
|
||||
|
||||
# Stop the xqueue server and free up the port
|
||||
world.xqueue_server.shutdown()
|
||||
@@ -1,46 +0,0 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
from xmodule.util.mock_youtube_server.mock_youtube_server import MockYoutubeServer
|
||||
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_youtube_server():
|
||||
# import ipdb; ipdb.set_trace()
|
||||
server_host = '127.0.0.1'
|
||||
|
||||
server_port = settings.VIDEO_PORT
|
||||
|
||||
address = (server_host, server_port)
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockYoutubeServer(address)
|
||||
logger.debug("Youtube server started at {} port".format(str(server_port)))
|
||||
|
||||
server.time_to_response = 1 # seconds
|
||||
|
||||
server.address = address
|
||||
|
||||
# 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.youtube_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_youtube_server(total):
|
||||
|
||||
# Stop the LTI server and free up the port
|
||||
world.youtube_server.shutdown()
|
||||
@@ -1,211 +0,0 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import json
|
||||
import urllib
|
||||
import urlparse
|
||||
import threading
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class MockXQueueRequestHandler(BaseHTTPRequestHandler):
|
||||
'''
|
||||
A handler for XQueue POST requests.
|
||||
'''
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def do_HEAD(self):
|
||||
self._send_head()
|
||||
|
||||
def do_POST(self):
|
||||
'''
|
||||
Handle a POST request from the client
|
||||
|
||||
Sends back an immediate success/failure response.
|
||||
It then POSTS back to the client
|
||||
with grading results, as configured in MockXQueueServer.
|
||||
'''
|
||||
self._send_head()
|
||||
|
||||
# Retrieve the POST data
|
||||
post_dict = self._post_dict()
|
||||
|
||||
# Log the request
|
||||
logger.debug("XQueue received POST request %s to path %s" %
|
||||
(str(post_dict), self.path))
|
||||
|
||||
# Respond only to grading requests
|
||||
if self._is_grade_request():
|
||||
try:
|
||||
xqueue_header = json.loads(post_dict['xqueue_header'])
|
||||
xqueue_body = json.loads(post_dict['xqueue_body'])
|
||||
|
||||
callback_url = xqueue_header['lms_callback_url']
|
||||
|
||||
except KeyError:
|
||||
# If the message doesn't have a header or body,
|
||||
# then it's malformed.
|
||||
# Respond with failure
|
||||
error_msg = "XQueue received invalid grade request"
|
||||
self._send_immediate_response(False, message=error_msg)
|
||||
|
||||
except ValueError:
|
||||
# If we could not decode the body or header,
|
||||
# respond with failure
|
||||
|
||||
error_msg = "XQueue could not decode grade request"
|
||||
self._send_immediate_response(False, message=error_msg)
|
||||
|
||||
else:
|
||||
# Send an immediate response of success
|
||||
# The grade request is formed correctly
|
||||
self._send_immediate_response(True)
|
||||
|
||||
# Wait a bit before POSTing back to the callback url with the
|
||||
# grade result configured by the server
|
||||
# Otherwise, the problem will not realize it's
|
||||
# queued and it will keep waiting for a response
|
||||
# indefinitely
|
||||
delayed_grade_func = lambda: self._send_grade_response(callback_url,
|
||||
xqueue_header)
|
||||
|
||||
timer = threading.Timer(2, delayed_grade_func)
|
||||
timer.start()
|
||||
|
||||
# If we get a request that's not to the grading submission
|
||||
# URL, return an error
|
||||
else:
|
||||
error_message = "Invalid request URL"
|
||||
self._send_immediate_response(False, message=error_message)
|
||||
|
||||
|
||||
def _send_head(self):
|
||||
'''
|
||||
Send the response code and MIME headers
|
||||
'''
|
||||
if self._is_grade_request():
|
||||
self.send_response(200)
|
||||
else:
|
||||
self.send_response(500)
|
||||
|
||||
self.send_header('Content-type', 'text/plain')
|
||||
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
|
||||
# If 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_immediate_response(self, success, message=""):
|
||||
'''
|
||||
Send an immediate success/failure message
|
||||
back to the client
|
||||
'''
|
||||
|
||||
# Send the response indicating success/failure
|
||||
response_str = json.dumps({'return_code': 0 if success else 1,
|
||||
'content': message})
|
||||
|
||||
# Log the response
|
||||
logger.debug("XQueue: sent response %s" % response_str)
|
||||
|
||||
self.wfile.write(response_str)
|
||||
|
||||
def _send_grade_response(self, postback_url, xqueue_header):
|
||||
'''
|
||||
POST the grade response back to the client
|
||||
using the response provided by the server configuration
|
||||
'''
|
||||
response_dict = {'xqueue_header': json.dumps(xqueue_header),
|
||||
'xqueue_body': json.dumps(self.server.grade_response())}
|
||||
|
||||
# Log the response
|
||||
logger.debug("XQueue: sent grading response %s" % str(response_dict))
|
||||
|
||||
MockXQueueRequestHandler.post_to_url(postback_url, response_dict)
|
||||
|
||||
def _is_grade_request(self):
|
||||
return 'xqueue/submit' in self.path
|
||||
|
||||
@staticmethod
|
||||
def post_to_url(url, param_dict):
|
||||
'''
|
||||
POST *param_dict* to *url*
|
||||
We make this a separate function so we can easily patch
|
||||
it during testing.
|
||||
'''
|
||||
urllib.urlopen(url, urllib.urlencode(param_dict))
|
||||
|
||||
|
||||
class MockXQueueServer(HTTPServer):
|
||||
'''
|
||||
A mock XQueue grading server that responds
|
||||
to POST requests to localhost.
|
||||
'''
|
||||
|
||||
def __init__(self, port_num,
|
||||
grade_response_dict={'correct': True, 'score': 1, 'msg': ''}):
|
||||
'''
|
||||
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.set_grade_response(grade_response_dict)
|
||||
|
||||
handler = MockXQueueRequestHandler
|
||||
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 grade_response(self):
|
||||
return self._grade_response
|
||||
|
||||
def set_grade_response(self, grade_response_dict):
|
||||
|
||||
# Check that the grade response has the right keys
|
||||
assert('correct' in grade_response_dict and
|
||||
'score' in grade_response_dict and
|
||||
'msg' in grade_response_dict)
|
||||
|
||||
# Wrap the message in <div> tags to ensure that it is valid XML
|
||||
grade_response_dict['msg'] = "<div>%s</div>" % grade_response_dict['msg']
|
||||
|
||||
# Save the response dictionary
|
||||
self._grade_response = grade_response_dict
|
||||
@@ -1,84 +0,0 @@
|
||||
import mock
|
||||
import unittest
|
||||
import threading
|
||||
import json
|
||||
import urllib
|
||||
import time
|
||||
from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class MockXQueueServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the XQueue server that listens on a local
|
||||
port and responds with pre-defined grade messages.
|
||||
|
||||
Used for lettuce BDD tests in lms/courseware/features/problems.feature
|
||||
and lms/courseware/features/problems.py
|
||||
|
||||
This is temporary and will be removed when XQueue is
|
||||
rewritten using celery.
|
||||
'''
|
||||
|
||||
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 = MockXQueueServer(server_port,
|
||||
{'correct': True, 'score': 1, 'msg': ''})
|
||||
|
||||
# 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_grade_request(self):
|
||||
|
||||
# Patch post_to_url() so we can intercept
|
||||
# outgoing POST requests from the server
|
||||
MockXQueueRequestHandler.post_to_url = mock.Mock()
|
||||
|
||||
# Send a grade request
|
||||
callback_url = 'http://127.0.0.1:8000/test_callback'
|
||||
|
||||
grade_header = json.dumps({'lms_callback_url': callback_url,
|
||||
'lms_key': 'test_queuekey',
|
||||
'queue_name': 'test_queue'})
|
||||
|
||||
grade_body = json.dumps({'student_info': 'test',
|
||||
'grader_payload': 'test',
|
||||
'student_response': 'test'})
|
||||
|
||||
grade_request = {'xqueue_header': grade_header,
|
||||
'xqueue_body': grade_body}
|
||||
|
||||
response_handle = urllib.urlopen(self.server_url + '/xqueue/submit',
|
||||
urllib.urlencode(grade_request))
|
||||
|
||||
response_dict = json.loads(response_handle.read())
|
||||
|
||||
# Expect that the response is success
|
||||
self.assertEqual(response_dict['return_code'], 0)
|
||||
|
||||
# Wait a bit before checking that the server posted back
|
||||
time.sleep(3)
|
||||
|
||||
# Expect that the server tries to post back the grading info
|
||||
xqueue_body = json.dumps({'correct': True, 'score': 1,
|
||||
'msg': '<div></div>'})
|
||||
expected_callback_dict = {'xqueue_header': grade_header,
|
||||
'xqueue_body': xqueue_body}
|
||||
MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url,
|
||||
expected_callback_dict)
|
||||
@@ -152,6 +152,7 @@ SELENIUM_GRID = {
|
||||
'BROWSER': LETTUCE_BROWSER,
|
||||
}
|
||||
|
||||
|
||||
#####################################################################
|
||||
# See if the developer has any local overrides.
|
||||
try:
|
||||
@@ -161,22 +162,9 @@ except ImportError:
|
||||
|
||||
# Because an override for where to run will affect which ports to use,
|
||||
# set these up after the local overrides.
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
LETTUCE_SERVER_PORT = choice(PORTS)
|
||||
PORTS.remove(LETTUCE_SERVER_PORT)
|
||||
else:
|
||||
LETTUCE_SERVER_PORT = randint(1024, 65535)
|
||||
|
||||
# Set up XQueue information so that the lms will send
|
||||
# requests to a mock XQueue server running locally
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
XQUEUE_PORT = choice(PORTS)
|
||||
PORTS.remove(XQUEUE_PORT)
|
||||
else:
|
||||
XQUEUE_PORT = randint(1024, 65535)
|
||||
|
||||
# Configure XQueue interface to use our stub XQueue server
|
||||
XQUEUE_INTERFACE = {
|
||||
"url": "http://127.0.0.1:%d" % XQUEUE_PORT,
|
||||
"url": "http://127.0.0.1:{0:d}".format(XQUEUE_PORT),
|
||||
"django_auth": {
|
||||
"username": "lms",
|
||||
"password": "***REMOVED***"
|
||||
@@ -184,10 +172,5 @@ XQUEUE_INTERFACE = {
|
||||
"basic_auth": ('anant', 'agarwal'),
|
||||
}
|
||||
|
||||
# Set up Video information so that the lms will send
|
||||
# requests to a mock Youtube server running locally
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
VIDEO_PORT = choice(PORTS)
|
||||
PORTS.remove(VIDEO_PORT)
|
||||
else:
|
||||
VIDEO_PORT = randint(1024, 65535)
|
||||
# Point the URL used to test YouTube availability to our stub YouTube server
|
||||
YOUTUBE_TEST_URL = "http://127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT)
|
||||
|
||||
@@ -911,6 +911,11 @@ BULK_EMAIL_LOG_SENT_EMAILS = False
|
||||
BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS = 0.02
|
||||
|
||||
|
||||
############################## Video ##########################################
|
||||
|
||||
# URL to test YouTube availability
|
||||
YOUTUBE_TEST_URL = 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
|
||||
|
||||
################################### APPS ######################################
|
||||
INSTALLED_APPS = (
|
||||
|
||||
@@ -239,6 +239,16 @@ FILE_UPLOAD_HANDLERS = (
|
||||
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
|
||||
)
|
||||
|
||||
########################### Server Ports ###################################
|
||||
|
||||
# These ports are carefully chosen so that if the browser needs to
|
||||
# access them, they will be available through the SauceLabs SSH tunnel
|
||||
LETTUCE_SERVER_PORT = 8003
|
||||
XQUEUE_PORT = 8040
|
||||
YOUTUBE_PORT = 8031
|
||||
LTI_PORT = 8765
|
||||
|
||||
|
||||
################### Make tests faster
|
||||
|
||||
#http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
|
||||
|
||||
Reference in New Issue
Block a user