diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index a3411dc7a8b2..526f404d70a4 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -49,3 +49,9 @@ # Unfortunately, we need to use debug mode to serve staticfiles DEBUG = True + +# Point the URL used to test YouTube availability to our stub YouTube server +YOUTUBE_PORT = 9080 +YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) +YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) +YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT) diff --git a/common/djangoapps/terrain/stubs/http.py b/common/djangoapps/terrain/stubs/http.py index 621b1eb0293b..def3450a6edd 100644 --- a/common/djangoapps/terrain/stubs/http.py +++ b/common/djangoapps/terrain/stubs/http.py @@ -139,6 +139,17 @@ def path_only(self): else: return path + def do_DELETE(self): # pylint: disable=C0103 + """ + Allow callers to delete all the server configurations using the /del_config URL. + """ + if self.path == "/del_config" or self.path == "/del_config/": + self.server.config = dict() + self.log_message("Reset Server Configuration.") + self.send_response(200) + else: + self.send_response(404) + def do_PUT(self): """ Allow callers to configure the stub server using the /set_config URL. diff --git a/common/djangoapps/terrain/stubs/start.py b/common/djangoapps/terrain/stubs/start.py index 0a60d425395f..2ee587a7e0a4 100644 --- a/common/djangoapps/terrain/stubs/start.py +++ b/common/djangoapps/terrain/stubs/start.py @@ -3,6 +3,7 @@ """ import sys import time +import requests import logging from .comments import StubCommentsService from .xqueue import StubXQueueService @@ -87,6 +88,9 @@ def main(): print "Starting stub service '{0}' on port {1}...".format(service_name, port_num) server = SERVICES[service_name](port_num=port_num) + if service_name == 'youtube': + config_dict['youtube_api_response'] = requests.get('http://www.youtube.com/iframe_api') + server.config.update(config_dict) try: diff --git a/common/djangoapps/terrain/stubs/tests/test_http.py b/common/djangoapps/terrain/stubs/tests/test_http.py index ba0769ac4527..01241120a156 100644 --- a/common/djangoapps/terrain/stubs/tests/test_http.py +++ b/common/djangoapps/terrain/stubs/tests/test_http.py @@ -14,6 +14,7 @@ def setUp(self): self.server = StubHttpService() self.addCleanup(self.server.shutdown) self.url = "http://127.0.0.1:{0}/set_config".format(self.server.port) + self.reset_config_url = "http://127.0.0.1:{0}/del_config".format(self.server.port) def test_configure(self): """ @@ -63,6 +64,25 @@ def test_unknown_path(self): ) self.assertEqual(response.status_code, 404) + def test_reset_configuration(self): + # JSON-encode parameter + post_params = {'test_reset': json.dumps('This is a reset config test')} + requests.put(self.url, data=post_params) + + # ensure that there is some data in server config dict + self.assertEqual(self.server.config.get('test_reset'), 'This is a reset config test') + + # reset server configuration + response = requests.delete(self.reset_config_url) + self.assertEqual(response.status_code, 200) + + # ensure that server config dict is empty after successful reset + self.assertEqual(self.server.config, {}) + + def test_reset_config_unknown_path(self): + response = requests.delete("http://127.0.0.1:{0}/invalid_url".format(self.server.port)) + self.assertEqual(response.status_code, 404) + class RequireRequestHandler(StubHttpRequestHandler): @require_params('GET', 'test_param') diff --git a/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py b/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py index e2603a82fe8b..e94e4626a393 100644 --- a/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py +++ b/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py @@ -60,3 +60,17 @@ def test_transcript_url_not_equal(self): def test_transcript_not_found(self): response = requests.get(self.url + 'test_transcripts_youtube/some_id') self.assertEqual(404, response.status_code) + + def test_reset_configuration(self): + + reset_config_url = self.url + 'del_config' + + # add some configuration data + self.server.config['test_reset'] = 'This is a reset config test' + + # reset server configuration + response = requests.delete(reset_config_url) + self.assertEqual(response.status_code, 200) + + # ensure that server config dict is set to initial values after successful reset + self.assertTrue('youtube_api_response' in self.server.config) diff --git a/common/djangoapps/terrain/stubs/youtube.py b/common/djangoapps/terrain/stubs/youtube.py index a5b5db864497..0d9d2a7bf2c7 100644 --- a/common/djangoapps/terrain/stubs/youtube.py +++ b/common/djangoapps/terrain/stubs/youtube.py @@ -33,6 +33,18 @@ class StubYouTubeHandler(StubHttpRequestHandler): # Default number of seconds to delay the response to simulate network latency. DEFAULT_DELAY_SEC = 0.5 + def do_DELETE(self): # pylint: disable=C0103 + """ + Allow callers to delete all the server configurations using the /del_config URL. + """ + if self.path == "/del_config" or self.path == "/del_config/": + self.server.config = dict() + self.server.config['youtube_api_response'] = requests.get('http://www.youtube.com/iframe_api') + self.log_message("Reset Server Configuration.") + self.send_response(200) + else: + self.send_response(404) + def do_GET(self): """ Handle a GET request from the client and sends response back. diff --git a/common/test/acceptance/tests/test_video_module.py b/common/test/acceptance/tests/test_video_module.py index c62c4c8f5db5..d7c40c3b91b7 100644 --- a/common/test/acceptance/tests/test_video_module.py +++ b/common/test/acceptance/tests/test_video_module.py @@ -4,6 +4,9 @@ Acceptance tests for Video. """ +import time +import json +import requests from .helpers import UniqueCourseTest from ..pages.lms.video import VideoPage from ..pages.lms.tab_nav import TabNavPage @@ -13,6 +16,8 @@ from ..fixtures.course import CourseFixture, XBlockFixtureDesc VIDEO_SOURCE_PORT = 8777 +YOUTUBE_STUB_PORT = 9080 +YOUTUBE_STUB_URL = 'http://127.0.0.1:{}/'.format(YOUTUBE_STUB_PORT) HTML5_SOURCES = [ 'http://localhost:{0}/gizmo.mp4'.format(VIDEO_SOURCE_PORT), @@ -25,6 +30,13 @@ ] +class YouTubeConfigError(Exception): + """ + Error occurred while configuring YouTube Stub Server. + """ + pass + + class VideoBaseTest(UniqueCourseTest): """ Base class for tests of the Video Player @@ -50,6 +62,10 @@ def setUp(self): self.metadata = None self.assets = [] self.verticals = None + self.youtube_configuraton = {} + + # reset youtube stub server + self.addCleanup(self._reset_youtube_stub_server) def navigate_to_video(self): """ Prepare the course and get to the video and render it """ @@ -76,6 +92,9 @@ def _install_course_fixture(self): self.course_fixture.add_children(chapter) self.course_fixture.install() + if self.youtube_configuraton: + self._configure_youtube_stub_server() + def _add_course_verticals(self): """ Create XBlockFixtureDesc verticals @@ -126,6 +145,38 @@ def _navigate_to_courseware_video_no_render(self): self._navigate_to_courseware_video() self.video.wait_for_video_class() + def _configure_youtube_stub_server(self): + """ + Allow callers to configure the stub server using the /set_config URL. + The request should have PUT data, such that: + Each PUT parameter is the configuration key. + Each PUT value is a JSON-encoded string value for the configuration. + :raise YouTubeConfigError: + """ + youtube_stub_config_url = YOUTUBE_STUB_URL + 'set_config' + + config_data = {param: json.dumps(value) for param, value in self.youtube_configuraton.items()} + response = requests.put(youtube_stub_config_url, data=config_data) + + if not response.ok: + raise YouTubeConfigError( + 'YouTube Server Configuration Failed. URL {0}, Configuration Data: {1}, Status was {2}'.format( + youtube_stub_config_url, self.youtube_configuraton, response.status_code)) + + def _reset_youtube_stub_server(self): + """ + Reset YouTube Stub Server Configurations using the /del_config URL. + :raise YouTubeConfigError: + """ + youtube_stub_config_url = YOUTUBE_STUB_URL + 'del_config' + + response = requests.delete(youtube_stub_config_url) + + if not response.ok: + raise YouTubeConfigError( + 'YouTube Server Configuration Failed. URL: {0} Status was {1}'.format( + youtube_stub_config_url, response.status_code)) + def metadata_for_mode(self, player_mode, additional_data=None): """ Create a dictionary for video player configuration according to `player_mode` @@ -337,6 +388,56 @@ def test_fullscreen_video_alignment_on_transcript_toggle(self): # check if video aligned correctly without enabled transcript self.assertTrue(self.video.is_aligned(False)) + def test_video_rendered_with_html5_sources(self): + """ + 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 + And the course has a Video component in "Youtube_HTML5" mode + Then the video has rendered in "Youtube" mode + """ + # configure youtube server + self.youtube_configuraton['time_to_response'] = 0.4 + self.metadata = self.metadata_for_mode('youtube_html5') + + self.navigate_to_video() + + self.assertTrue(self.video.is_video_rendered('youtube')) + + def test_video_not_rendered_with_html5_sources(self): + """ + Scenario: Video component is not rendered in the LMS in Youtube mode with HTML5 sources + Given youtube server is up and response time is 2 seconds + And the course has a Video component in "Youtube_HTML5" mode + Then the video has rendered in "HTML5" mode + """ + # configure youtube server + self.youtube_configuraton['time_to_response'] = 2.0 + self.metadata = self.metadata_for_mode('youtube_html5') + + self.navigate_to_video() + + self.assertTrue(self.video.is_video_rendered('html5')) + + def test_video_with_youtube_api_blocked(self): + """ + Scenario: Video is not rendered in the LMS in Youtube mode with HTML5 sources when YouTube API is blocked + Given youtube server is up and response time is 2 seconds + And youtube stub server blocks YouTube API + And the course has a Video component in "Youtube_HTML5" mode + And I wait "3" seconds so that video module completely switch to HTML5 + Then the video has rendered in "HTML5" mode + """ + # configure youtube server + self.youtube_configuraton['time_to_response'] = 2.0 + self.youtube_configuraton['youtube_api_blocked'] = True + self.metadata = self.metadata_for_mode('youtube_html5') + + self.navigate_to_video() + + time.sleep(3.0) + + self.assertTrue(self.video.is_video_rendered('html5')) + class YouTubeHtml5VideoTest(VideoBaseTest): """ Test YouTube HTML5 Video Player """ diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 2496b74e1bef..2754c2165dc6 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -182,6 +182,6 @@ def seed(): } # Point the URL used to test YouTube availability to our stub YouTube server -YOUTUBE['API'] = 'youtube.com/iframe_api' +YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT) diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py index 88f7f62c7bbd..07969726a704 100644 --- a/lms/envs/bok_choy.py +++ b/lms/envs/bok_choy.py @@ -5,6 +5,7 @@ import os from path import path + CONFIG_ROOT = path(__file__).abspath().dirname() # pylint: disable=E1120 TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root" @@ -60,3 +61,9 @@ # Unfortunately, we need to use debug mode to serve staticfiles DEBUG = True + +# Point the URL used to test YouTube availability to our stub YouTube server +YOUTUBE_PORT = 9080 +YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) +YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) +YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT) diff --git a/rakelib/bok_choy.rake b/rakelib/bok_choy.rake index 06bf342693af..d773cbbb7685 100644 --- a/rakelib/bok_choy.rake +++ b/rakelib/bok_choy.rake @@ -54,7 +54,14 @@ BOK_CHOY_STUBS = { :port => 8777, :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_video_sources.log"), :config => "root_dir=#{VIDEO_SOURCE_DIR}" + }, + + :youtube => { + + :port => 9080, + :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_youtube.log") } + } # For the time being, stubs are used by both the bok-choy and lettuce acceptance tests