diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 6ceb592f4..7a11fe3b2 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -40,6 +40,7 @@ import sys import urllib.request import urllib.error import urllib.parse +from random import randint from PyQt4 import QtGui, QtCore @@ -61,10 +62,29 @@ APPLICATION_VERSION = {} IMAGES_FILTER = None ICU_COLLATOR = None UNO_CONNECTION_TYPE = 'pipe' -#UNO_CONNECTION_TYPE = u'socket' CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE) INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE) DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE) +USER_AGENTS = { + 'win32': [ + 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36' + ], + 'darwin': [ + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.43 Safari/537.31', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.47 Safari/536.11', + ], + 'linux2': [ + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 Chrome/25.0.1364.160 Safari/537.22', + 'Mozilla/5.0 (X11; CrOS armv7l 2913.260.0) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.99 Safari/537.11', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/26.0.1389.0 Safari/537.27' + ], + 'default': [ + 'Mozilla/5.0 (X11; NetBSD amd64; rv:18.0) Gecko/20130120 Firefox/18.0' + ] +} class VersionThread(QtCore.QThread): @@ -298,6 +318,17 @@ def delete_file(file_path_name): return False +def _get_user_agent(): + """ + Return a user agent customised for the platform the user is on. + """ + browser_list = USER_AGENTS.get(sys.platform, None) + if not browser_list: + browser_list = USER_AGENTS['default'] + random_index = randint(0, len(browser_list) - 1) + return browser_list[random_index] + + def get_web_page(url, header=None, update_openlp=False): """ Attempts to download the webpage at url and returns that page or None. @@ -318,6 +349,9 @@ def get_web_page(url, header=None, update_openlp=False): if not url: return None req = urllib.request.Request(url) + user_agent = _get_user_agent() + log.debug('Using user agent: %s', user_agent) + req.add_header('User-Agent', user_agent) if header: req.add_header(header[0], header[1]) page = None diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index dfc31b245..d5760ccb0 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -32,14 +32,74 @@ Functional tests to test the AppLocation class and related methods. from unittest import TestCase from openlp.core.utils import clean_filename, get_filesystem_encoding, get_locale_key, \ - get_natural_key, split_filename -from tests.functional import patch + get_natural_key, split_filename, _get_user_agent, get_web_page, get_uno_instance, add_actions +from tests.functional import MagicMock, patch class TestUtils(TestCase): """ A test suite to test out various methods around the AppLocation class. """ + def add_actions_empty_list_test(self): + """ + Test that no actions are added when the list is empty + """ + # GIVEN: a mocked action list, and an empty list + mocked_target = MagicMock() + empty_list = [] + + # WHEN: The empty list is added to the mocked target + add_actions(mocked_target, empty_list) + + # THEN: The add method on the mocked target is never called + self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called') + self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called') + + def add_actions_none_action_test(self): + """ + Test that a separator is added when a None action is in the list + """ + # GIVEN: a mocked action list, and a list with None in it + mocked_target = MagicMock() + separator_list = [None] + + # WHEN: The list is added to the mocked target + add_actions(mocked_target, separator_list) + + # THEN: The addSeparator method is called, but the addAction method is never called + mocked_target.addSeparator.assert_called_with() + self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called') + + def add_actions_add_action_test(self): + """ + Test that an action is added when a valid action is in the list + """ + # GIVEN: a mocked action list, and a list with an action in it + mocked_target = MagicMock() + action_list = ['action'] + + # WHEN: The list is added to the mocked target + add_actions(mocked_target, action_list) + + # THEN: The addSeparator method is not called, and the addAction method is called + self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called') + mocked_target.addAction.assert_called_with('action') + + def add_actions_action_and_none_test(self): + """ + Test that an action and a separator are added when a valid action and None are in the list + """ + # GIVEN: a mocked action list, and a list with an action and None in it + mocked_target = MagicMock() + action_list = ['action', None] + + # WHEN: The list is added to the mocked target + add_actions(mocked_target, action_list) + + # THEN: The addSeparator method is called, and the addAction method is called + mocked_target.addSeparator.assert_called_with() + mocked_target.addAction.assert_called_with('action') + def get_filesystem_encoding_sys_function_not_called_test(self): """ Test the get_filesystem_encoding() function does not call the sys.getdefaultencoding() function @@ -153,3 +213,181 @@ class TestUtils(TestCase): # THEN: We get a properly sorted list self.assertEqual(['1st item', 'item 3b', 'item 10a'], sorted_list, 'Numbers should be sorted naturally') + + def get_uno_instance_pipe_test(self): + """ + Test that when the UNO connection type is "pipe" the resolver is given the "pipe" URI + """ + # GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "pipe" + mock_resolver = MagicMock() + + # WHEN: get_uno_instance() is called + get_uno_instance(mock_resolver) + + # THEN: the resolve method is called with the correct argument + mock_resolver.resolve.assert_called_with('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') + + def get_user_agent_linux_test(self): + """ + Test that getting a user agent on Linux returns a user agent suitable for Linux + """ + with patch('openlp.core.utils.sys') as mocked_sys: + + # GIVEN: The system is Linux + mocked_sys.platform = 'linux2' + + # WHEN: We call _get_user_agent() + user_agent = _get_user_agent() + + # THEN: The user agent is a Linux (or ChromeOS) user agent + result = 'Linux' in user_agent or 'CrOS' in user_agent + self.assertTrue(result, u'The user agent should be a valid Linux user agent') + + def get_user_agent_windows_test(self): + """ + Test that getting a user agent on Windows returns a user agent suitable for Windows + """ + with patch('openlp.core.utils.sys') as mocked_sys: + + # GIVEN: The system is Linux + mocked_sys.platform = 'win32' + + # WHEN: We call _get_user_agent() + user_agent = _get_user_agent() + + # THEN: The user agent is a Linux (or ChromeOS) user agent + self.assertIn('Windows', user_agent, u'The user agent should be a valid Windows user agent') + + def get_user_agent_macos_test(self): + """ + Test that getting a user agent on OS X returns a user agent suitable for OS X + """ + with patch('openlp.core.utils.sys') as mocked_sys: + + # GIVEN: The system is Linux + mocked_sys.platform = 'darwin' + + # WHEN: We call _get_user_agent() + user_agent = _get_user_agent() + + # THEN: The user agent is a Linux (or ChromeOS) user agent + self.assertIn('Mac OS X', user_agent, u'The user agent should be a valid OS X user agent') + + def get_user_agent_default_test(self): + """ + Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent + """ + with patch('openlp.core.utils.sys') as mocked_sys: + + # GIVEN: The system is Linux + mocked_sys.platform = 'freebsd' + + # WHEN: We call _get_user_agent() + user_agent = _get_user_agent() + + # THEN: The user agent is a Linux (or ChromeOS) user agent + self.assertIn('NetBSD', user_agent, u'The user agent should be the default user agent') + + def get_web_page_no_url_test(self): + """ + Test that sending a URL of None to the get_web_page method returns None + """ + # GIVEN: A None url + test_url = None + + # WHEN: We try to get the test URL + result = get_web_page(test_url) + + # THEN: None should be returned + self.assertIsNone(result, 'The return value of get_web_page should be None') + + def get_web_page_test(self): + """ + Test that the get_web_page method works correctly + """ + with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ + patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \ + patch('openlp.core.utils.Registry') as MockRegistry: + # GIVEN: Mocked out objects and a fake URL + mocked_request_object = MagicMock() + MockRequest.return_value = mocked_request_object + mocked_page_object = MagicMock() + mock_urlopen.return_value = mocked_page_object + mock_get_user_agent.return_value = 'user_agent' + fake_url = 'this://is.a.fake/url' + + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url) + + # THEN: The correct methods are called with the correct arguments and a web page is returned + MockRequest.assert_called_with(fake_url) + mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') + self.assertEqual(1, mocked_request_object.add_header.call_count, + 'There should only be 1 call to add_header') + mock_urlopen.assert_called_with(mocked_request_object) + mocked_page_object.geturl.assert_called_with() + self.assertEqual(0, MockRegistry.call_count, 'The Registry() object should have never been called') + self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + + def get_web_page_with_header_test(self): + """ + Test that adding a header to the call to get_web_page() adds the header to the request + """ + with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ + patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.utils._get_user_agent') as mock_get_user_agent: + # GIVEN: Mocked out objects, a fake URL and a fake header + mocked_request_object = MagicMock() + MockRequest.return_value = mocked_request_object + mocked_page_object = MagicMock() + mock_urlopen.return_value = mocked_page_object + mock_get_user_agent.return_value = 'user_agent' + fake_url = 'this://is.a.fake/url' + fake_header = ('Fake-Header', 'fake value') + + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url, header=fake_header) + + # THEN: The correct methods are called with the correct arguments and a web page is returned + MockRequest.assert_called_with(fake_url) + mocked_request_object.add_header.assert_called_with('Fake-Header', 'fake value') + self.assertEqual(2, mocked_request_object.add_header.call_count, + 'There should only be 2 calls to add_header') + mock_urlopen.assert_called_with(mocked_request_object) + mocked_page_object.geturl.assert_called_with() + self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + + def get_web_page_update_openlp_test(self): + """ + Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() + """ + with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ + patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \ + patch('openlp.core.utils.Registry') as MockRegistry: + # GIVEN: Mocked out objects, a fake URL + mocked_request_object = MagicMock() + MockRequest.return_value = mocked_request_object + mocked_page_object = MagicMock() + mock_urlopen.return_value = mocked_page_object + mock_get_user_agent.return_value = 'user_agent' + mocked_registry_object = MagicMock() + mocked_application_object = MagicMock() + mocked_registry_object.get.return_value = mocked_application_object + MockRegistry.return_value = mocked_registry_object + fake_url = 'this://is.a.fake/url' + + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url, update_openlp=True) + + # THEN: The correct methods are called with the correct arguments and a web page is returned + MockRequest.assert_called_with(fake_url) + mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') + self.assertEqual(1, mocked_request_object.add_header.call_count, + 'There should only be 1 call to add_header') + mock_urlopen.assert_called_with(mocked_request_object) + mocked_page_object.geturl.assert_called_with() + mocked_registry_object.get.assert_called_with('application') + mocked_application_object.process_events.assert_called_with() + self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')