Skip to content

Commit

Permalink
Move sensitive configuration options into separate files
Browse files Browse the repository at this point in the history
Read credentials for cloud testing services from configuration files in the working or home directory. Fixes #60
  • Loading branch information
davehunt committed Jan 25, 2017
1 parent 39cffc1 commit 2b3d913
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 223 deletions.
26 changes: 26 additions & 0 deletions docs/news.rst
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/pytest-dev/pytest-selenium/issues/60>`_ for
for original issue and related patch.

**1.7.0 (2016-11-29)**

* Introduced a ``firefox_options`` fixture.
Expand Down
57 changes: 30 additions & 27 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,19 +218,20 @@ Sauce Labs

To run your automated tests using `Sauce Labs <https://saucelabs.com/>`_, you
must provide a valid username and API key. This can be done either by using
a :ref:`configuration file <configuration-files>`, 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 <configuration-files>`:
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
~~~~~~~~~~~~~
Expand All @@ -252,20 +253,21 @@ BrowserStack

To run your automated tests using
`BrowserStack <https://www.browserstack.com/>`_, you must provide a valid
username and access key. This can be done either by using a
:ref:`configuration file <configuration-files>`, 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 <configuration-files>`:
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
~~~~~~~~~~~~~
Expand All @@ -285,20 +287,21 @@ TestingBot
----------

To run your automated tests using `TestingBot <http://testingbot.com/>`_, you
must provide a valid key and secret. This can be done either by using a
:ref:`configuration file <configuration-files>`, 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 <configuration-files>`:
Below is an example ``.testingbot`` configuration file:

.. code-block:: ini
[pytest]
testingbot_key = key
testingbot_secret = secret
[credentials]
key = key
secret = secret
Running tests
~~~~~~~~~~~~~
Expand All @@ -321,20 +324,20 @@ CrossBrowserTesting
To run your automated tests using
`CrossBrowserTesting <https://crossbrowsertesting.com/>`_, you must provide a
valid username and auth key. This can be done either by using
a :ref:`configuration file <configuration-files>`, 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 <configuration-files>`:
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
~~~~~~~~~~~~~
Expand Down
70 changes: 32 additions & 38 deletions pytest_selenium/drivers/browserstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
42 changes: 42 additions & 0 deletions pytest_selenium/drivers/cloud.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 2b3d913

Please sign in to comment.