Commit 0fd03cfb by Will Daly

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.
parent 8261f2b4
@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 "([^"]*)"$')
......
#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 *
"""
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()
"""
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
"""
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)
"""
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)
"""
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)
"""
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
"""
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
......
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()
"""
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$')
......
#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()
#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()
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
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/
......
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