diff --git a/docs/news.rst b/docs/news.rst index c9ac71b7..211f7a69 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -1,6 +1,32 @@ Release Notes ============= +**1.8.0 (unreleased)** + +* **BREAKING CHANGE:** Moved cloud testing provider credentials into separate + files for improved security. + + * If you are using the environment variables for specifying cloud testing + provider credentials, then you will not be affected. + * If you are storing credentials from any of the cloud testing providers in + one of the default configuration files then they will no longer be used. + These files are often checked into source code repositories, so it was + previously very easy to accidentally expose your credentials. + * Each cloud provider now has their own configuration file, such as + ``.browserstack``, ``.crossbrowsertesting``, ``.saucelabs``, + ``.testingbot`` and these can be located in the working directory or in the + user's home directory. This provides a convenient way to set up these files + globally, and override them for individual projects. + * To migrate, check ``pytest.ini``, ``tox.ini``, and ``setup.cfg`` for any + keys starting with ``browserstack_``, ``crossbrowsertesting_``, + ``saucelabs_``, or ``testingbot_``. If you find any, create a new + configuration file for the appropriate cloud testing provider with your + credentials, and remove the entries from the original file. + * The configuration keys can differ between cloud testing providers, so + please check the :doc:`user_guide` for details. + * See `#60 `_ for + for original issue and related patch. + **1.7.0 (2016-11-29)** * Introduced a ``firefox_options`` fixture. diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 988c6a7d..de7455e3 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -218,19 +218,20 @@ Sauce Labs To run your automated tests using `Sauce Labs `_, you must provide a valid username and API key. This can be done either by using -a :ref:`configuration file `, or by setting the -``SAUCELABS_USERNAME`` and ``SAUCELABS_API_KEY`` environment variables. +a ``.saucelabs`` configuration file in the working directory or your home +directory, or by setting the ``SAUCELABS_USERNAME`` and ``SAUCELABS_API_KEY`` +environment variables. Configuration ~~~~~~~~~~~~~ -Below is an example :ref:`configuration file `: +Below is an example ``.saucelabs`` configuration file: .. code-block:: ini - [pytest] - sauce_labs_username = username - sauce_labs_api_key = secret + [credentials] + username = username + key = secret Running tests ~~~~~~~~~~~~~ @@ -252,20 +253,21 @@ BrowserStack To run your automated tests using `BrowserStack `_, you must provide a valid -username and access key. This can be done either by using a -:ref:`configuration file `, or by setting the -``BROWSERSTACK_USERNAME`` and ``BROWSERSTACK_ACCESS_KEY`` environment variables. +username and access key. This can be done either by using +a ``.browserstack`` configuration file in the working directory or your home +directory, or by setting the ``BROWSERSTACK_USERNAME`` and +``BROWSERSTACK_ACCESS_KEY`` environment variables. Configuration ~~~~~~~~~~~~~ -Below is an example :ref:`configuration file `: +Below is an example ``.browserstack`` configuration file: .. code-block:: ini - [pytest] - browserstack_username = username - browserstack_access_key = secret + [credentials] + username = username + key = secret Running tests ~~~~~~~~~~~~~ @@ -285,20 +287,21 @@ TestingBot ---------- To run your automated tests using `TestingBot `_, you -must provide a valid key and secret. This can be done either by using a -:ref:`configuration file `, or by setting the -``TESTINGBOT_KEY`` and ``TESTINGBOT_SECRET`` environment variables. +must provide a valid key and secret. This can be done either by using +a ``.testingbot`` configuration file in the working directory or your home +directory, or by setting the ``TESTINGBOT_KEY`` and ``TESTINGBOT_SECRET`` +environment variables. Configuration ~~~~~~~~~~~~~ -Below is an example :ref:`configuration file `: +Below is an example ``.testingbot`` configuration file: .. code-block:: ini - [pytest] - testingbot_key = key - testingbot_secret = secret + [credentials] + key = key + secret = secret Running tests ~~~~~~~~~~~~~ @@ -321,20 +324,20 @@ CrossBrowserTesting To run your automated tests using `CrossBrowserTesting `_, you must provide a valid username and auth key. This can be done either by using -a :ref:`configuration file `, or by setting the -``CROSSBROWSERTESTING_USERNAME`` and ``CROSSBROWSERTESTING_AUTH_KEY`` -environment variables. +a ``.crossbrowsertesting`` configuration file in the working directory or your +home directory, or by setting the ``CROSSBROWSERTESTING_USERNAME`` and +``CROSSBROWSERTESTING_AUTH_KEY`` environment variables. Configuration ~~~~~~~~~~~~~ -Below is an example :ref:`configuration file `: +Below is an example ``.crossbrowsertesting`` configuration file: .. code-block:: ini - [pytest] - crossbrowsertesting_username = username - crossbrowsertesting_auth_key = secret + [credentials] + username = username + key = secret Running tests ~~~~~~~~~~~~~ diff --git a/pytest_selenium/drivers/browserstack.py b/pytest_selenium/drivers/browserstack.py index 1724806c..ca098243 100644 --- a/pytest_selenium/drivers/browserstack.py +++ b/pytest_selenium/drivers/browserstack.py @@ -2,46 +2,56 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -import os - import pytest import requests -DRIVER = 'BrowserStack' -API_JOB_URL = 'https://www.browserstack.com/automate/sessions/{session}.json' -EXECUTOR_URL = 'http://{username}:{key}@hub.browserstack.com:80/wd/hub' +from pytest_selenium.drivers.cloud import Provider + + +class BrowserStack(Provider): + + API = 'https://www.browserstack.com/automate/sessions/{session}.json' + + @property + def auth(self): + return (self.username, self.key) + @property + def executor(self): + return 'http://{0}:{1}@hub.browserstack.com:80/wd/hub'.format( + self.username, self.key) -def pytest_addoption(parser): - parser.addini('browserstack_username', - help='browserstack username', - default=os.getenv('BROWSERSTACK_USERNAME')) - parser.addini('browserstack_access_key', - help='browserstack access key', - default=os.getenv('BROWSERSTACK_ACCESS_KEY')) + @property + def username(self): + return self.get_credential('username', 'BROWSERSTACK_USERNAME') + + @property + def key(self): + return self.get_credential('key', 'BROWSERSTACK_ACCESS_KEY') @pytest.mark.optionalhook def pytest_selenium_runtest_makereport(item, report, summary, extra): - if item.config.getoption('driver') != DRIVER: + provider = BrowserStack() + if item.config.getoption('driver') != provider.driver: return passed = report.passed or (report.failed and hasattr(report, 'wasxfail')) session_id = item._driver.session_id - auth = (_username(item.config), _access_key(item.config)) - api_url = API_JOB_URL.format(session=session_id) + api_url = provider.API.format(session=session_id) try: - job_info = requests.get(api_url, auth=auth, timeout=10).json() + job_info = requests.get(api_url, auth=provider.auth, timeout=10).json() job_url = job_info['automation_session']['browser_url'] # Add the job URL to the summary - summary.append('{0} Job: {1}'.format(DRIVER, job_url)) + summary.append('{0} Job: {1}'.format(provider.name, job_url)) pytest_html = item.config.pluginmanager.getplugin('html') # Add the job URL to the HTML report - extra.append(pytest_html.extras.url(job_url, '{0} Job'.format(DRIVER))) + extra.append(pytest_html.extras.url(job_url, '{0} Job'.format( + provider.name))) except Exception as e: summary.append('WARNING: Failed to determine {0} job URL: {1}'.format( - DRIVER, e)) + provider.name, e)) try: # Update the job result @@ -55,32 +65,16 @@ def pytest_selenium_runtest_makereport(item, report, summary, extra): api_url, headers={'Content-Type': 'application/json'}, params={'status': status}, - auth=auth, + auth=provider.auth, timeout=10) except Exception as e: summary.append('WARNING: Failed to update job status: {0}'.format(e)) def driver_kwargs(request, test, capabilities, **kwargs): + provider = BrowserStack() capabilities.setdefault('name', test) - executor = EXECUTOR_URL.format( - username=_username(request.config), - key=_access_key(request.config)) kwargs = { - 'command_executor': executor, + 'command_executor': provider.executor, 'desired_capabilities': capabilities} return kwargs - - -def _access_key(config): - access_key = config.getini('browserstack_access_key') - if not access_key: - raise pytest.UsageError('BrowserStack access key must be set') - return access_key - - -def _username(config): - username = config.getini('browserstack_username') - if not username: - raise pytest.UsageError('BrowserStack username must be set') - return username diff --git a/pytest_selenium/drivers/cloud.py b/pytest_selenium/drivers/cloud.py new file mode 100644 index 00000000..15f528a1 --- /dev/null +++ b/pytest_selenium/drivers/cloud.py @@ -0,0 +1,42 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +from pytest_selenium.exceptions import MissingCloudCredentialError + +if sys.version_info[0] == 2: + import ConfigParser as configparser +else: + import configparser + + +class Provider(object): + + @property + def driver(self): + return type(self).__name__ + + @property + def name(self): + return self.driver + + @property + def config(self): + name = '.{0}'.format(self.driver.lower()) + config = configparser.ConfigParser() + config.read([name, os.path.join(os.path.expanduser('~'), name)]) + return config + + def get_credential(self, key, env): + try: + value = self.config.get('credentials', key) + except (configparser.NoSectionError, + configparser.NoOptionError, + KeyError): + value = os.getenv(env) + if not value: + raise MissingCloudCredentialError(self.name, key, env) + return value diff --git a/pytest_selenium/drivers/crossbrowsertesting.py b/pytest_selenium/drivers/crossbrowsertesting.py index a4d5097e..25ea130f 100644 --- a/pytest_selenium/drivers/crossbrowsertesting.py +++ b/pytest_selenium/drivers/crossbrowsertesting.py @@ -2,34 +2,44 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -import os - from py.xml import html import pytest import requests -DRIVER = 'CrossBrowserTesting' -API_URL = 'https://crossbrowsertesting.com/api/v3/selenium/{session}' -EXECUTOR_URL = 'http://{username}:{key}@hub.crossbrowsertesting.com:80/wd/hub' +from pytest_selenium.drivers.cloud import Provider + + +class CrossBrowserTesting(Provider): + + API = 'https://crossbrowsertesting.com/api/v3/selenium/{session}' + + @property + def auth(self): + return (self.username, self.key) + + @property + def executor(self): + return 'http://{0}:{1}@hub.crossbrowsertesting.com:80/wd/hub'.format( + self.username, self.key) + @property + def username(self): + return self.get_credential('username', 'CROSSBROWSERTESTING_USERNAME') -def pytest_addoption(parser): - parser.addini('crossbrowsertesting_username', - help='crossbrowsertesting username', - default=os.getenv('CROSSBROWSERTESTING_USERNAME')) - parser.addini('crossbrowsertesting_auth_key', - help='crossbrowsertesting auth key', - default=os.getenv('CROSSBROWSERTESTING_AUTH_KEY')) + @property + def key(self): + return self.get_credential('key', 'CROSSBROWSERTESTING_AUTH_KEY') @pytest.mark.optionalhook def pytest_selenium_capture_debug(item, report, extra): - if item.config.getoption('driver') != DRIVER: + provider = CrossBrowserTesting() + if item.config.getoption('driver') != provider.driver: return videos = requests.get( - API_URL.format(session=item._driver.session_id), - auth=_auth(item), + provider.API.format(session=item._driver.session_id), + auth=provider.auth, timeout=10).json().get('videos') if videos and len(videos) > 0: @@ -39,22 +49,23 @@ def pytest_selenium_capture_debug(item, report, extra): @pytest.mark.optionalhook def pytest_selenium_runtest_makereport(item, report, summary, extra): - if item.config.getoption('driver') != DRIVER: + provider = CrossBrowserTesting() + if item.config.getoption('driver') != provider.driver: return passed = report.passed or (report.failed and hasattr(report, 'wasxfail')) # Add the test URL to the summary info = requests.get( - API_URL.format(session=item._driver.session_id), - auth=_auth(item), + provider.API.format(session=item._driver.session_id), + auth=provider.auth, timeout=10).json() url = info.get('show_result_public_url') - summary.append('{0}: {1}'.format(DRIVER, url)) + summary.append('{0}: {1}'.format(provider.name, url)) pytest_html = item.config.pluginmanager.getplugin('html') # Add the job URL to the HTML report - extra.append(pytest_html.extras.url(url, DRIVER)) + extra.append(pytest_html.extras.url(url, provider.name)) try: # Update the test result @@ -63,46 +74,25 @@ def pytest_selenium_runtest_makereport(item, report, summary, extra): score = 'pass' if passed else 'fail' data = {'action': 'set_score', 'score': score} r = requests.put( - API_URL.format(session=info.get('selenium_test_id')), + provider.API.format(session=info.get('selenium_test_id')), data=data, - auth=_auth(item), + auth=provider.auth, timeout=10) r.raise_for_status() except Exception as e: summary.append('WARNING: Failed to update {0} job status: {1}'.format( - DRIVER, e)) + provider.name, e)) def driver_kwargs(request, test, capabilities, **kwargs): + provider = CrossBrowserTesting() capabilities.setdefault('name', test) - executor = EXECUTOR_URL.format( - username=_username(request.config), - key=_auth_key(request.config)) kwargs = { - 'command_executor': executor, + 'command_executor': provider.executor, 'desired_capabilities': capabilities} return kwargs -def _auth_key(config): - auth_key = config.getini('crossbrowsertesting_auth_key') - if not auth_key: - raise pytest.UsageError('CrossBrowserTesting auth key must be set') - return auth_key - - -def _username(config): - username = config.getini('crossbrowsertesting_username') - if not username: - raise pytest.UsageError('CrossBrowserTesting username must be set') - return username - - -def _auth(item): - username = _username(item.config) - return (username, _auth_key(item.config)) - - def _video_html(video): html.__tagspec__.update(dict([(x, 1) for x in ('video', 'source')])) video_attrs = { diff --git a/pytest_selenium/drivers/saucelabs.py b/pytest_selenium/drivers/saucelabs.py index de24099f..7ceb547f 100644 --- a/pytest_selenium/drivers/saucelabs.py +++ b/pytest_selenium/drivers/saucelabs.py @@ -3,31 +3,46 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import json -import os from _pytest.mark import MarkInfo from py.xml import html import pytest import requests -DRIVER = 'SauceLabs' -API_JOB_URL = 'http://saucelabs.com/rest/v1/{username}/jobs/{session}' -EXECUTOR_URL = 'http://{username}:{key}@ondemand.saucelabs.com:80/wd/hub' -JOB_URL = 'http://saucelabs.com/jobs/{session}' +from pytest_selenium.drivers.cloud import Provider -def pytest_addoption(parser): - parser.addini('sauce_labs_username', - help='sauce labs username', - default=os.getenv('SAUCELABS_USERNAME')) - parser.addini('sauce_labs_api_key', - help='sauce labs api key', - default=os.getenv('SAUCELABS_API_KEY')) +class SauceLabs(Provider): + + API = 'http://saucelabs.com/rest/v1/{username}/jobs/{session}' + JOB = 'http://saucelabs.com/jobs/{session}' + + @property + def auth(self): + return (self.username, self.key) + + @property + def executor(self): + return 'http://{0}:{1}@ondemand.saucelabs.com:80/wd/hub'.format( + self.username, self.key) + + @property + def name(self): + return 'Sauce Labs' + + @property + def username(self): + return self.get_credential('username', 'SAUCELABS_USERNAME') + + @property + def key(self): + return self.get_credential('key', 'SAUCELABS_API_KEY') @pytest.mark.optionalhook def pytest_selenium_capture_debug(item, report, extra): - if item.config.getoption('driver') != DRIVER: + provider = SauceLabs() + if item.config.getoption('driver') != provider.driver: return pytest_html = item.config.pluginmanager.getplugin('html') @@ -36,64 +51,51 @@ def pytest_selenium_capture_debug(item, report, extra): @pytest.mark.optionalhook def pytest_selenium_runtest_makereport(item, report, summary, extra): - if item.config.getoption('driver') != DRIVER: + provider = SauceLabs() + if item.config.getoption('driver') != provider.driver: return passed = report.passed or (report.failed and hasattr(report, 'wasxfail')) session_id = item._driver.session_id # Add the job URL to the summary - job_url = JOB_URL.format(session=session_id) - summary.append('{0} Job: {1}'.format(DRIVER, job_url)) + provider = SauceLabs() + job_url = provider.JOB.format(session=session_id) + summary.append('{0} Job: {1}'.format(provider.name, job_url)) pytest_html = item.config.pluginmanager.getplugin('html') # Add the job URL to the HTML report - extra.append(pytest_html.extras.url(job_url, '{0} Job'.format(DRIVER))) + extra.append(pytest_html.extras.url(job_url, '{0} Job'.format( + provider.name))) try: # Update the job result - username = _username(item.config) - auth = (username, _api_key(item.config)) - api_url = API_JOB_URL.format(username=username, session=session_id) - job_info = requests.get(api_url, auth=auth, timeout=10).json() + api_url = provider.API.format( + session=session_id, + username=provider.username) + job_info = requests.get(api_url, auth=provider.auth, timeout=10).json() if report.when == 'setup' or job_info.get('passed') is not False: # Only update the result if it's not already marked as failed data = json.dumps({'passed': passed}) - requests.put(api_url, data=data, auth=auth, timeout=10) + requests.put(api_url, data=data, auth=provider.auth, timeout=10) except Exception as e: summary.append('WARNING: Failed to update {0} job status: {1}'.format( - DRIVER, e)) + provider.name, e)) def driver_kwargs(request, test, capabilities, **kwargs): + provider = SauceLabs() keywords = request.node.keywords capabilities.setdefault('name', test) markers = [m for m in keywords.keys() if isinstance(keywords[m], MarkInfo)] tags = capabilities.get('tags', []) + markers if tags: capabilities['tags'] = tags - executor = EXECUTOR_URL.format( - username=_username(request.config), - key=_api_key(request.config)) kwargs = { - 'command_executor': executor, + 'command_executor': provider.executor, 'desired_capabilities': capabilities} return kwargs -def _api_key(config): - api_key = config.getini('sauce_labs_api_key') - if not api_key: - raise pytest.UsageError('Sauce Labs API key must be set') - return api_key - - -def _username(config): - username = config.getini('sauce_labs_username') - if not username: - raise pytest.UsageError('Sauce Labs username must be set') - return username - - def _video_html(session): flash_vars = 'config={{\ "clip":{{\ diff --git a/pytest_selenium/drivers/testingbot.py b/pytest_selenium/drivers/testingbot.py index 3f1b31b6..06b964bf 100644 --- a/pytest_selenium/drivers/testingbot.py +++ b/pytest_selenium/drivers/testingbot.py @@ -2,31 +2,41 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -import os - import pytest from _pytest.mark import MarkInfo from py.xml import html import requests -DRIVER = 'TestingBot' -API_JOB_URL = 'https://api.testingbot.com/v1/tests/{session}' -EXECUTOR_URL = 'http://{key}:{secret}@hub.testingbot.com/wd/hub' -JOB_URL = 'http://testingbot.com/members/tests/{session}' +from pytest_selenium.drivers.cloud import Provider + + +class TestingBot(Provider): + + API = 'https://api.testingbot.com/v1/tests/{session}' + JOB = 'http://testingbot.com/members/tests/{session}' + + @property + def auth(self): + return (self.key, self.secret) + @property + def executor(self): + return 'http://{0}:{1}@hub.testingbot.com/wd/hub'.format( + self.key, self.secret) -def pytest_addoption(parser): - parser.addini('testingbot_key', - help='testingbot key', - default=os.getenv('TESTINGBOT_KEY')) - parser.addini('testingbot_secret', - help='testingbot secret', - default=os.getenv('TESTINGBOT_SECRET')) + @property + def key(self): + return self.get_credential('key', 'TESTINGBOT_KEY') + + @property + def secret(self): + return self.get_credential('secret', 'TESTINGBOT_SECRET') @pytest.mark.optionalhook def pytest_selenium_capture_debug(item, report, extra): - if item.config.getoption('driver') != DRIVER: + provider = TestingBot() + if item.config.getoption('driver') != provider.driver: return pytest_html = item.config.pluginmanager.getplugin('html') @@ -35,63 +45,48 @@ def pytest_selenium_capture_debug(item, report, extra): @pytest.mark.optionalhook def pytest_selenium_runtest_makereport(item, report, summary, extra): - if item.config.getoption('driver') != DRIVER: + provider = TestingBot() + if item.config.getoption('driver') != provider.driver: return passed = report.passed or (report.failed and hasattr(report, 'wasxfail')) session_id = item._driver.session_id # Add the job URL to the summary - job_url = JOB_URL.format(session=session_id) - summary.append('{0} Job: {1}'.format(DRIVER, job_url)) + job_url = provider.JOB.format(session=session_id) + summary.append('{0} Job: {1}'.format(provider.name, job_url)) pytest_html = item.config.pluginmanager.getplugin('html') # Add the job URL to the HTML report - extra.append(pytest_html.extras.url(job_url, '{0} Job'.format(DRIVER))) + extra.append(pytest_html.extras.url(job_url, '{0} Job'.format( + provider.name))) try: # Update the job result - auth = (_key(item.config), _secret(item.config)) - api_url = API_JOB_URL.format(session=session_id) - job_info = requests.get(api_url, auth=auth, timeout=10).json() + api_url = provider.API.format(session=session_id) + job_info = requests.get(api_url, auth=provider.auth, timeout=10).json() if report.when == 'setup' or job_info.get('success') is not False: # Only update the result if it's not already marked as failed data = {'test[success]': '1' if passed else '0'} - requests.put(api_url, data=data, auth=auth, timeout=10) + requests.put(api_url, data=data, auth=provider.auth, timeout=10) except Exception as e: summary.append('WARNING: Failed to update {0} job status: {1}'.format( - DRIVER, e)) + provider.name, e)) def driver_kwargs(request, test, capabilities, **kwargs): + provider = TestingBot() keywords = request.node.keywords capabilities.setdefault('name', test) markers = [m for m in keywords.keys() if isinstance(keywords[m], MarkInfo)] groups = capabilities.get('groups', []) + markers if groups: capabilities['groups'] = groups - executor = EXECUTOR_URL.format( - key=_key(request.config), - secret=_secret(request.config)) kwargs = { - 'command_executor': executor, + 'command_executor': provider.executor, 'desired_capabilities': capabilities} return kwargs -def _key(config): - key = config.getini('testingbot_key') - if not key: - raise pytest.UsageError('TestingBot key must be set') - return key - - -def _secret(config): - secret = config.getini('testingbot_secret') - if not secret: - raise pytest.UsageError('TestingBot secret must be set') - return secret - - def _video_html(session): flash_vars = 'config={{\ "clip":{{\ diff --git a/pytest_selenium/exceptions.py b/pytest_selenium/exceptions.py new file mode 100644 index 00000000..7e63bd02 --- /dev/null +++ b/pytest_selenium/exceptions.py @@ -0,0 +1,14 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import pytest + + +class MissingCloudCredentialError(pytest.UsageError): + + def __init__(self, driver, key, env): + super(MissingCloudCredentialError, self).__init__( + '{0} {1} must be set. Try setting the {2} environment ' + 'variable, or see the documentation for how to use a ' + 'configuration file.'.format(driver, key, env)) diff --git a/testing/test_browserstack.py b/testing/test_browserstack.py index 5bb0017a..0db76feb 100644 --- a/testing/test_browserstack.py +++ b/testing/test_browserstack.py @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from functools import partial +import os import pytest @@ -32,20 +33,35 @@ def failure(testdir, testfile, httpserver_base_url): '--driver', 'BrowserStack') -def test_missing_username(failure): - out = failure() - assert 'UsageError: BrowserStack username must be set' in out +def test_missing_username(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + assert 'BrowserStack username must be set' in failure() -def test_missing_access_key(failure, monkeypatch): +def test_missing_access_key_env(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) monkeypatch.setenv('BROWSERSTACK_USERNAME', 'foo') - out = failure() - assert 'UsageError: BrowserStack access key must be set' in out + assert 'BrowserStack key must be set' in failure() + + +def test_missing_access_key_file(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + tmpdir.join('.browserstack').write('[credentials]\nusername=foo') + assert 'BrowserStack key must be set' in failure() -@pytest.mark.skipif(reason='Frequent timeouts occurring with BrowserStack') -def test_invalid_credentials(failure, monkeypatch): +def test_invalid_credentials_env(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) monkeypatch.setenv('BROWSERSTACK_USERNAME', 'foo') monkeypatch.setenv('BROWSERSTACK_ACCESS_KEY', 'bar') out = failure() - assert 'Invalid username or password' in out + messages = ['Invalid username or password', 'basic auth failed'] + assert any(message in out for message in messages) + + +def test_invalid_credentials_file(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + tmpdir.join('.browserstack').write('[credentials]\nusername=foo\nkey=bar') + out = failure() + messages = ['Invalid username or password', 'basic auth failed'] + assert any(message in out for message in messages) diff --git a/testing/test_crossbrowsertesting.py b/testing/test_crossbrowsertesting.py index ae63f9bf..1f8da568 100644 --- a/testing/test_crossbrowsertesting.py +++ b/testing/test_crossbrowsertesting.py @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from functools import partial +import os import pytest @@ -33,12 +34,36 @@ def failure(testdir, testfile, httpserver_base_url): '--driver', 'CrossBrowserTesting') -def test_missing_username(failure): - out = failure() - assert 'UsageError: CrossBrowserTesting username must be set' in out +def test_missing_username(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + assert 'CrossBrowserTesting username must be set' in failure() -def test_missing_api_key(failure, monkeypatch): +def test_missing_access_key_env(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) monkeypatch.setenv('CROSSBROWSERTESTING_USERNAME', 'foo') + assert 'CrossBrowserTesting key must be set' in failure() + + +def test_missing_access_key_file(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + tmpdir.join('.crossbrowsertesting').write('[credentials]\nusername=foo') + assert 'CrossBrowserTesting key must be set' in failure() + + +def test_invalid_credentials_env(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + monkeypatch.setenv('CROSSBROWSERTESTING_USERNAME', 'foo') + monkeypatch.setenv('CROSSBROWSERTESTING_AUTH_KEY', 'bar') + out = failure() + messages = ['You are not authorized to view this', 'basic auth failed'] + assert any(message in out for message in messages) + + +def test_invalid_credentials_file(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + config = tmpdir.join('.crossbrowsertesting') + config.write('[credentials]\nusername=foo\nkey=bar') out = failure() - assert 'UsageError: CrossBrowserTesting auth key must be set' in out + messages = ['You are not authorized to view this', 'basic auth failed'] + assert any(message in out for message in messages) diff --git a/testing/test_saucelabs.py b/testing/test_saucelabs.py index 559b5e77..fb58ab06 100644 --- a/testing/test_saucelabs.py +++ b/testing/test_saucelabs.py @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from functools import partial +import os import pytest @@ -33,18 +34,35 @@ def failure(testdir, testfile, httpserver_base_url): '--driver', 'SauceLabs') -def test_missing_username(failure): - out = failure() - assert 'UsageError: Sauce Labs username must be set' in out +def test_missing_username(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + assert 'Sauce Labs username must be set' in failure() -def test_missing_api_key(failure, monkeypatch): +def test_missing_api_key_env(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) monkeypatch.setenv('SAUCELABS_USERNAME', 'foo') - out = failure() - assert 'UsageError: Sauce Labs API key must be set' in out + assert 'Sauce Labs key must be set' in failure() -def test_invalid_credentials(failure, monkeypatch): +def test_missing_api_key_file(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + tmpdir.join('.saucelabs').write('[credentials]\nusername=foo') + assert 'Sauce Labs key must be set' in failure() + + +def test_invalid_credentials_env(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) monkeypatch.setenv('SAUCELABS_USERNAME', 'foo') monkeypatch.setenv('SAUCELABS_API_KEY', 'bar') - failure('--capability', 'browserName', 'Firefox') + out = failure('--capability', 'browserName', 'Firefox') + messages = ['Sauce Labs Authentication Error', 'basic auth failed'] + assert any(message in out for message in messages) + + +def test_invalid_credentials_file(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + tmpdir.join('.saucelabs').write('[credentials]\nusername=foo\nkey=bar') + out = failure('--capability', 'browserName', 'Firefox') + messages = ['Sauce Labs Authentication Error', 'basic auth failed'] + assert any(message in out for message in messages) diff --git a/testing/test_testingbot.py b/testing/test_testingbot.py index a9d79b8b..180162ff 100644 --- a/testing/test_testingbot.py +++ b/testing/test_testingbot.py @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from functools import partial +import os import pytest @@ -33,18 +34,35 @@ def failure(testdir, testfile, httpserver_base_url): '--driver', 'TestingBot') -def test_missing_key(failure): - out = failure() - assert 'UsageError: TestingBot key must be set' in out +def test_missing_key(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + assert 'TestingBot key must be set' in failure() -def test_missing_secret(failure, monkeypatch): +def test_missing_secret_env(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) monkeypatch.setenv('TESTINGBOT_KEY', 'foo') - out = failure() - assert 'UsageError: TestingBot secret must be set' in out + assert 'TestingBot secret must be set' in failure() -def test_invalid_credentials(failure, monkeypatch): +def test_missing_secret_file(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + tmpdir.join('.testingbot').write('[credentials]\nkey=foo') + assert 'TestingBot secret must be set' in failure() + + +def test_invalid_credentials_env(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) monkeypatch.setenv('TESTINGBOT_KEY', 'foo') monkeypatch.setenv('TESTINGBOT_SECRET', 'bar') - failure('--capability', 'browserName', 'firefox') + out = failure('--capability', 'browserName', 'firefox') + messages = ['incorrect TestingBot credentials', 'basic auth failed'] + assert any(message in out for message in messages) + + +def test_invalid_credentials_file(failure, monkeypatch, tmpdir): + monkeypatch.setattr(os.path, 'expanduser', lambda p: str(tmpdir)) + tmpdir.join('.testingbot').write('[credentials]\nkey=foo\nsecret=bar') + out = failure('--capability', 'browserName', 'firefox') + messages = ['incorrect TestingBot credentials', 'basic auth failed'] + assert any(message in out for message in messages)