diff --git a/.gitignore b/.gitignore
index 6dd547343daa34feb66dd0003aeb75b9d4a82875..dd88ea04014aac1c41f616ecd5d375c0e8d128eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 *~
+*.pyc
 \#*\#
 .\#*
 /node_modules
@@ -11,4 +12,4 @@
 .DS_Store
 Thumbs.db
 /locale
-/resources/email_templates/email-test.html
\ No newline at end of file
+/resources/email_templates/email-test.html
diff --git a/automation-tests/123done/conftest.py b/automation-tests/123done/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8062b47659d0ebf81145c7aacd9b3aa83d3ed57
--- /dev/null
+++ b/automation-tests/123done/conftest.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+# 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 py
+
+def pytest_runtest_setup(item):
+    pytest_mozwebqa = py.test.config.pluginmanager.getplugin("mozwebqa")
+    pytest_mozwebqa.TestSetup.server_base_url = item.config.option.server_base_url
+
+
+def pytest_addoption(parser):
+    parser.addoption("--serverbaseurl",
+                     action="store",
+                     dest='server_base_url',
+                     metavar='str',
+                     default="https://login.dev.anosrep.org",
+                     help="specify the server base url")
+
+
+def pytest_funcarg__mozwebqa(request):
+    pytest_mozwebqa = py.test.config.pluginmanager.getplugin("mozwebqa")
+    return pytest_mozwebqa.TestSetup(request)
diff --git a/automation-tests/123done/mocks/__init__.py b/automation-tests/123done/mocks/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/123done/mocks/mock_user.py b/automation-tests/123done/mocks/mock_user.py
new file mode 100644
index 0000000000000000000000000000000000000000..f5e4d2a55532c002a59ba52e0f0c20f8241c78f9
--- /dev/null
+++ b/automation-tests/123done/mocks/mock_user.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+# 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/.
+
+
+class MockUser(dict):
+
+    def __init__(self, **kwargs):
+        # set your default values
+        import time
+
+        self['email'] = '123donetest_%s@restmail.net' % repr(time.time())
+        self['password'] = 'Password12345'
+
+        # update with any keyword arguments passed
+        self.update(**kwargs)
+
+    # allow getting items as if they were attributes
+    def __getattr__(self, attr):
+        return self[attr]
diff --git a/automation-tests/123done/mozwebqa.cfg b/automation-tests/123done/mozwebqa.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..2faf04e06135edab7c0455218824a7cee5111c18
--- /dev/null
+++ b/automation-tests/123done/mozwebqa.cfg
@@ -0,0 +1,4 @@
+[DEFAULT]
+api = webdriver
+baseurl = http://dev.123done.org
+tags = 123done
diff --git a/automation-tests/123done/page.py b/automation-tests/123done/page.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ca4b63b2a6f06a8313f58138ba2362e8d8487ea
--- /dev/null
+++ b/automation-tests/123done/page.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from unittestzero import Assert
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.common.exceptions import NoSuchElementException
+from selenium.common.exceptions import ElementNotVisibleException
+
+
+class Page(object):
+
+    def __init__(self, testsetup):
+        self.testsetup = testsetup
+        self.base_url = testsetup.base_url
+        self.selenium = testsetup.selenium
+        self.timeout = testsetup.timeout
+
+    @property
+    def is_the_current_page(self):
+        if self._page_title:
+            WebDriverWait(self.selenium, self.timeout).until(lambda s: s.title)
+
+        Assert.equal(self.selenium.title, self._page_title)
+        return True
+
+    def is_element_present(self, *locator):
+        self.selenium.implicitly_wait(0)
+        try:
+            self.selenium.find_element(*locator)
+            return True
+        except NoSuchElementException:
+            return False
+        finally:
+            # set back to where you once belonged
+            self.selenium.implicitly_wait(self.testsetup.default_implicit_wait)
+
+    def is_element_visible(self, *locator):
+        try:
+            return self.selenium.find_element(*locator).is_displayed()
+        except NoSuchElementException, ElementNotVisibleException:
+            return False
diff --git a/automation-tests/123done/pages/__init__.py b/automation-tests/123done/pages/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/123done/pages/home.py b/automation-tests/123done/pages/home.py
new file mode 100644
index 0000000000000000000000000000000000000000..59ff30b71c590f90aec84a39575832840f3a1d51
--- /dev/null
+++ b/automation-tests/123done/pages/home.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+from page import Page
+
+
+class HomePage(Page):
+
+    _page_title = '123done - your tasks, simplified'
+
+    _sign_in_locator = (By.CSS_SELECTOR, '#loggedout > button')
+    _logout_locator = (By.CSS_SELECTOR, '#loggedin > a')
+    _logged_in_user_email_locator = (By.CSS_SELECTOR, '#loggedin > span')
+    _loading_spinner_locator = (By.CSS_SELECTOR, "li.loading img")
+
+    def go_to_home_page(self):
+        self.selenium.get(self.base_url + '/')
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: not self.is_element_visible(*self._loading_spinner_locator),
+            'Timeout waiting for sign-in button to appear.')
+        self.is_the_current_page
+
+    def sign_in(self, user='default'):
+        credentials = self.testsetup.credentials[user]
+        browserid = self.click_sign_in()
+        browserid.sign_in(credentials['email'], credentials['password'])
+        self.wait_for_user_login()
+
+    def logout(self):
+        self.click_logout()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: self.is_element_visible(*self._sign_in_locator) and not \
+                      self.is_element_visible(*self._loading_spinner_locator),
+            'Timeout waiting for user to log out.')
+
+    def click_sign_in(self, expect='new'):
+        """Click the 'sign in' button.
+
+        Keyword arguments:
+        expect -- the expected resulting page
+                  'new' for user that is not currently signed in (default)
+                  'returning' for users already signed in or recently verified
+
+        """
+        self.selenium.find_element(*self._sign_in_locator).click()
+        from browserid.pages.sign_in import SignIn
+        return SignIn(self.selenium, self.timeout, expect=expect)
+
+    def click_logout(self):
+        self.selenium.find_element(*self._logout_locator).click()
+
+    @property
+    def is_logged_in(self):
+        return self.is_element_visible(*self._logout_locator)
+
+    @property
+    def logged_in_user_email(self):
+        return self.selenium.find_element(*self._logged_in_user_email_locator).text
+
+    def wait_for_user_login(self):
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: self.is_element_visible(*self._logout_locator) and not \
+                      self.is_element_visible(*self._loading_spinner_locator),
+            'Timeout waiting for user to login.')
diff --git a/automation-tests/123done/restmail/__init__.py b/automation-tests/123done/restmail/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/123done/restmail/restmail.py b/automation-tests/123done/restmail/restmail.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3a96034d931b780f0d0f79af560a4d98b872af0
--- /dev/null
+++ b/automation-tests/123done/restmail/restmail.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+
+# 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 requests
+import json
+import re
+from time import sleep
+
+
+class RestmailInbox(object):
+    """
+    This wrapper loads restmail for the given email address.
+    It will loop and wait for an email to arrive if there is not one present.
+    find_by_* methods can be used to find an email and return it as Email() class.
+    """
+
+    _restmail_mail_server = "https://restmail.net/mail/"
+
+    def __init__(self, email):
+        self.email = email
+        self.username = email.split('@')[0]
+        self.json = self._wait_and_return_json_response(self.username)
+
+    def _wait_and_return_json_response(self, username, timeout=60):
+        # Loop for 60 attempts until the restmail json returned is not empty
+
+        timer = 0
+        response_json = []
+
+        while timer < timeout:
+            sleep(1)
+            timer += 1
+
+            response = requests.get(self._restmail_mail_server + self.username, verify=False)
+            response_json = json.loads(response.content)
+            if response_json != []:
+                return response_json
+
+        raise Exception("Failed to find an email before timeout")
+
+    def delete_all_mail(self):
+        # Delete all of the mail in the inbox
+
+        requests.delete(self._restmail_mail_server + self.username, verify=False)
+
+    def find_by_index(self, index):
+        return Email(self.json[index])
+
+    def find_by_sender(self, sender):
+        # Loop through the address and name objects for each sender and match at least one
+
+        for json_object in self.json:
+            for from_source in json_object['from']:
+                if from_source['address'] == sender or from_source['name'] == sender:
+                    return Email(json_object)
+        else:
+            raise Exception("Sender not found")
+
+
+class Email():
+    """
+    This returns a class representation of an email from restmail inbox
+    """
+
+    def __init__(self, json):
+        self.json = json
+
+    @property
+    def body(self):
+        return(self.json['text'])
+
+    @property
+    def verify_user_link(self):
+        # This returns the link for verifying the email address of a new account
+        regex = 'https:\/\/.*verify_email_address\?token=.{48}'
+
+        verify_link = re.search(regex, self.body).group(0)
+        return verify_link
+
+    @property
+    def add_email_address_link(self):
+        # This returns the link for adding the email address of a new account
+        regex = 'https:\/\/.*confirm\?token=.{48}'
+
+        add_email_link = re.search(regex, self.body).group(0)
+        return add_email_link
diff --git a/automation-tests/123done/tests/__init__.py b/automation-tests/123done/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/123done/tests/test_change_password.py b/automation-tests/123done/tests/test_change_password.py
new file mode 100644
index 0000000000000000000000000000000000000000..621e694b57ded8647fffcd31647ff45044973afa
--- /dev/null
+++ b/automation-tests/123done/tests/test_change_password.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from pages.home import HomePage
+from restmail.restmail import RestmailInbox
+from mocks.mock_user import MockUser
+from unittestzero import Assert
+
+import pytest
+
+
+class TestChangePassword:
+
+    def test_can_change_user_password(self, mozwebqa):
+        user = MockUser()
+        home_pg = HomePage(mozwebqa)
+
+        home_pg.go_to_home_page()
+        bid_login = home_pg.click_sign_in()
+        bid_login.sign_in_new_user(user['email'], user['password'])
+
+        # Open restmail inbox, find the email
+        inbox = RestmailInbox(user['email'])
+        email = inbox.find_by_index(0)
+
+        # Load the BrowserID link from the email in the browser
+        mozwebqa.selenium.get(email.verify_user_link)
+        from browserid.pages.complete_registration import CompleteRegistration
+        CompleteRegistration(mozwebqa.selenium, mozwebqa.timeout)
+
+        mozwebqa.selenium.get(mozwebqa.server_base_url)
+        from browserid.pages.account_manager import AccountManager
+        account_manager = AccountManager(mozwebqa.selenium, mozwebqa.timeout)
+
+        Assert.contains(user['email'], account_manager.emails)
+
+        account_manager.click_edit_password()
+        account_manager.old_password = user['password']
+        new_password = "newpass12345"
+        account_manager.new_password = new_password
+        account_manager.click_password_done()
+
+        account_manager.click_sign_out()
+
+        home_pg.go_to_home_page()
+
+        bid_login = home_pg.click_sign_in()
+        bid_login.sign_in(user['email'], new_password)
+
+        home_pg.wait_for_user_login()
+        Assert.true(home_pg.is_logged_in)
diff --git a/automation-tests/123done/tests/test_logout.py b/automation-tests/123done/tests/test_logout.py
new file mode 100644
index 0000000000000000000000000000000000000000..1267b890e3d0bd142112ac772d582d2352dd6b98
--- /dev/null
+++ b/automation-tests/123done/tests/test_logout.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from pages.home import HomePage
+from unittestzero import Assert
+
+import pytest
+
+
+class TestLogout:
+
+    @pytest.mark.nondestructive
+    def test_that_user_can_logout(self, mozwebqa):
+        home_pg = HomePage(mozwebqa)
+        home_pg.go_to_home_page()
+        home_pg.sign_in()
+
+        home_pg.logout()
+        Assert.false(home_pg.is_logged_in)
diff --git a/automation-tests/123done/tests/test_new_user.py b/automation-tests/123done/tests/test_new_user.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed04e9c7e0be50d9f901a48fb50b212db948feab
--- /dev/null
+++ b/automation-tests/123done/tests/test_new_user.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from pages.home import HomePage
+from restmail.restmail import RestmailInbox
+from mocks.mock_user import MockUser
+from unittestzero import Assert
+
+import pytest
+
+
+class TestNewAccount:
+
+    def test_can_create_new_user_account(self, mozwebqa):
+        user = MockUser()
+        home_pg = HomePage(mozwebqa)
+
+        home_pg.go_to_home_page()
+        bid_login = home_pg.click_sign_in()
+        bid_login.sign_in_new_user(user['email'], user['password'])
+
+        # Open restmail inbox, find the email
+        inbox = RestmailInbox(user['email'])
+        email = inbox.find_by_index(0)
+
+        # Load the BrowserID link from the email in the browser
+        mozwebqa.selenium.get(email.verify_user_link)
+        from browserid.pages.complete_registration import CompleteRegistration
+        complete_registration = CompleteRegistration(mozwebqa.selenium, mozwebqa.timeout)
+
+        # Check the message on the registration page reflects a successful registration!
+        Assert.contains("Thank you for signing up with Persona.", complete_registration.thank_you)
+
+        home_pg.go_to_home_page()
+
+        Assert.equal(home_pg.logged_in_user_email, user['email'])
diff --git a/automation-tests/123done/tests/test_sign_in.py b/automation-tests/123done/tests/test_sign_in.py
new file mode 100644
index 0000000000000000000000000000000000000000..caac0ed4fd57bfe1f978a97c87e2a2b985c36786
--- /dev/null
+++ b/automation-tests/123done/tests/test_sign_in.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from pages.home import HomePage
+from unittestzero import Assert
+
+import pytest
+
+
+class TestSignIn:
+
+    @pytest.mark.nondestructive
+    def test_that_user_can_sign_in(self, mozwebqa):
+        home_pg = HomePage(mozwebqa)
+        home_pg.go_to_home_page()
+        home_pg.sign_in()
+        Assert.true(home_pg.is_logged_in)
diff --git a/automation-tests/README.md b/automation-tests/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a7440ac3e27f9d99a054a028490b30981c53fe87
--- /dev/null
+++ b/automation-tests/README.md
@@ -0,0 +1,114 @@
+getting started
+===============
+
+# I'm super impatient. Let's get going in 10 sec or less.
+
+TL;DR: just execute ```./run.py``` from inside the automation-tests directory.
+
+If you're missing pip or virtualenv, it'll tell you what to do.
+
+If you're missing test dependencies, it'll install them for you.
+
+If all that is OK, it'll connect to dev.123done.org and try to create a fake user, login, and logout.
+
+If you want to run that single test against your ephemeral instance called 'foo', just do ```run.py --target=foo```.
+
+If you want to run all the tests, create a dummy user, put its info in credentials.yaml, then do ```run.py --all``` to run all the tests, including 123done and myfavoritebeer tests.
+
+# I've got time. Tell me more!
+
+OK, sure...
+
+## how to run selenium tests inside the automation-tests directory against ephemeral, stage, or prod environments
+
+Node bindings don't exist for Selenium 2 API (webdriver), so we're using python bindings instead. This requires some python-centric setup, but it shouldn't take more than 15 minutes or so to get up and running.
+
+These tests currently only hit myfavoritebeers and 123done domains. For example, to test an ephemeral install named foo.personatest.org, you can pass 'foo.123done.org' into the py.test baseurl parameter (this is covered again in the examples section).
+
+### check system-wide python requirements
+
+You should have python 2.7 on your system (check python --version).
+
+We have to install a bunch of python libraries. pip fetches packages; virtualenv sandboxes them. If pip and virtualenv aren't on your system already, you'll need to do this once (once per computer, not once per repo):
+
+    # only do this if pip and virtualenv aren't on your computer already
+    # might need to use sudo
+    easy_install pip
+    pip install virtualenv
+
+### build a sandboxed python test environment
+
+From the automated-tests directory, create a sandboxed python environment to install python dependencies (only need to do this once per clone):
+
+    # only do this once per clone
+    virtualenv bid_selenium 
+
+Be sure you do not accidentally add the virtualenv directory (here, bid_selenium) to git.
+
+You can activate the sandbox, meaning link installed programs, via:
+
+    . bid_selenium/bin/activate
+
+And when you want to stop using the sandbox, you can exit via ```deactivate```. Deactivating the virtualenv doesn't destroy it.
+
+In order to install python dependencies into the sandbox, activate the virtualenv, then install the python requirements in requirements.txt:
+
+    pip install -Ur requirements.txt
+
+Sweet. Your environment is now ready.
+
+### create a test user in credentials.yaml
+
+Some of the automation tests verify that existing accounts work, so create a test account, and put the info into credentials.yaml.
+
+### run the tests
+
+When you want to run the tests, make sure the virtualenv is active:
+
+    . bid_selenium/bin/activate
+
+Then, run the tests by calling py.test on the command line with some options. [Here](https://github.com/davehunt/pytest-mozwebqa) is the most relevant documentation: command-line options added to py.test by the mozwebqa plugin, which is awesome. [Here](http://pytest.org/latest/usage.html) is the documentation for the upstream pytest project.
+
+#### examples
+
+Use local Firefox to run the 123done tests (in the 123done directory) against dev.123done.org:
+
+    python -m py.test --destructive --credentials=credentials.yaml \
+        --baseurl=http://dev.123done.org \
+        --driver=firefox \
+        -q 123done
+
+Use local Chrome (assuming you've downloaded [Chromedriver](http://code.google.com/p/selenium/wiki/ChromeDriver) to /usr/local/bin/chromedriver) to run just one of the the myfavoritebeer tests against myfavoritebeer.org:
+
+    python -m py.test --destructive --credentials=credentials.yaml \
+        --baseurl=http://www.myfavoritebeer.org \
+        --driver=chrome --chromepath=/usr/local/bin/chromedriver \
+        -q myfavoritebeer/tests/test_logout.py
+
+Use Sauce Labs (assuming you've got credentials in saucelabs.yaml) to run IE 8 against an ephemeral instance called 'foo':
+
+    python -m py.test --destructive --credentials=credentials.yaml \
+        --baseurl=http://foo.123done.org \
+        --platform=XP --browsername="internet explorer" --browserver=8 \
+        --saucelabs=saucelabs.yaml \
+        -q 123done
+
+note, your saucelabs.yaml file should be of the form:
+
+        # example sauce_labs.yaml config file
+        username: <username>
+        password: <password>
+        api-key: <api-key>
+
+#### Check out your results
+    
+The tests create a /results directory, which contains an index.html file with test results, screenshots, and videos if you used sauce labs. In case of a failure, you'll also see the backtrace. Totally sweet.
+
+## writing automation tests
+
+TODO: some idioms from the existing test code to help people quickly express "find this" and "click this" idiomatically.
+
+Refer to [mozilla's pytest_mozwebqa](https://github.com/davehunt/pytest-mozwebqa#writing-tests-for-pytest_mozwebqa) documentation on writing tests for the time being.
+
+A note about upstreaming bidpom changes: this codebase contains [mozilla's bidpom](https://github.com/mozilla/bidpom) as [git-subtree](https://github.com/apenwarr/git-subtree/blob/master/git-subtree.txt). This allows us to pull in changes from upstream, while easily tracking the bidpom code to branches. It's unlikely that we'll need to push or pull to upstream frequently, but for details on doing so, see also apenwarr's [blog post](http://apenwarr.ca/log/?m=200904#30).
+
diff --git a/automation-tests/browserid/.gitignore b/automation-tests/browserid/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c1c3e1d2badf37f9f939493b88913eee6e521e18
--- /dev/null
+++ b/automation-tests/browserid/.gitignore
@@ -0,0 +1,4 @@
+*.pyc
+*.komodoproject
+build
+results
diff --git a/automation-tests/browserid/.travis.yml b/automation-tests/browserid/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..89d46cb5070cca8418940f3cf71b558ba484e945
--- /dev/null
+++ b/automation-tests/browserid/.travis.yml
@@ -0,0 +1,19 @@
+before_script:
+  - sh -e /etc/init.d/xvfb start
+
+language: python
+python:
+  - 2.6
+  - 2.7
+
+script: py.test --baseurl=http://dev.123done.org --driver=firefox -m travis tests
+
+env:
+  - DISPLAY=':99.0'
+
+notifications:
+  email:
+    - dave.hunt@gmail.com
+  irc:
+    - "irc.mozilla.org#automation"
+    - "irc.mozilla.org#identity"
diff --git a/automation-tests/browserid/README.md b/automation-tests/browserid/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..df702c27ced028d9a83ae20ef4494914525b6b7a
--- /dev/null
+++ b/automation-tests/browserid/README.md
@@ -0,0 +1,15 @@
+**B**rowser**ID** **P**age **O**bject **M**odel
+===============================================
+Selenium compatible page object model for Mozilla's BrowserID.
+
+Documentation
+-------------
+See the project's [wiki](https://github.com/mozilla/bidpom/wiki).
+
+License
+-------
+This software is licensed under the [MPL](http://www.mozilla.org/MPL/2.0/) 2.0:
+
+    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/.
diff --git a/automation-tests/browserid/__init__.py b/automation-tests/browserid/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..087f0766badd9866e8596230707814a76a96ac1f
--- /dev/null
+++ b/automation-tests/browserid/__init__.py
@@ -0,0 +1 @@
+from browser_id import BrowserID
diff --git a/automation-tests/browserid/browser_id.py b/automation-tests/browserid/browser_id.py
new file mode 100644
index 0000000000000000000000000000000000000000..21aea5ebce650d688cc8c32e64b05373c47c9e87
--- /dev/null
+++ b/automation-tests/browserid/browser_id.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+# 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 selenium
+
+
+class BrowserID(object):
+
+    VERIFY_URL_REGEX = 'https?:\/\/(\S+)\/verify_email_address\?token=(.{48})'
+    CONFIRM_URL_REGEX = 'https?:\/\/(\S+)\/confirm\?token=(.{48})'
+    RESET_URL_REGEX = 'https?:\/\/(\S+)\/reset_password\?token=(.{48})'
+    INCLUDE_URL_REGEX = '(https?:\/\/(\S+))\/include\.js'
+
+    def __init__(self, selenium, timeout=60):
+        self.selenium = selenium
+        self.timeout = timeout
+
+    def sign_in(self, email, password):
+        """Signs in using the specified email address and password."""
+        from pages.sign_in import SignIn
+        sign_in = SignIn(self.selenium, timeout=self.timeout, expect='new')
+        sign_in.sign_in(email, password)
diff --git a/automation-tests/browserid/conftest.py b/automation-tests/browserid/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..048e3efc767ae0bf352ece75fefde1ebc9d8a265
--- /dev/null
+++ b/automation-tests/browserid/conftest.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+# 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 py
+
+
+def pytest_runtest_setup(item):
+    pytest_mozwebqa = py.test.config.pluginmanager.getplugin('mozwebqa')
+    pytest_mozwebqa.TestSetup.email = item.config.option.email
+    pytest_mozwebqa.TestSetup.password = item.config.option.password
+
+
+def pytest_addoption(parser):
+    group = parser.getgroup('persona', 'persona')
+    group._addoption('--email',
+                     action='store',
+                     metavar='str',
+                     help='email address for persona account')
+    group._addoption('--password',
+                     action='store',
+                     metavar='str',
+                     help='password for persona account')
+
+
+def pytest_funcarg__mozwebqa(request):
+    return request.getfuncargvalue('mozwebqa')
diff --git a/automation-tests/browserid/mocks/__init__.py b/automation-tests/browserid/mocks/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/browserid/mocks/user.py b/automation-tests/browserid/mocks/user.py
new file mode 100644
index 0000000000000000000000000000000000000000..8be1e3bfd9a2b390989be5cd4cec9552e27e4e2b
--- /dev/null
+++ b/automation-tests/browserid/mocks/user.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# 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 uuid
+
+
+class MockUser(dict):
+
+    def __init__(self, **kwargs):
+        self['id'] = 'bidpom_%s' % uuid.uuid1()
+        self['primary_email'] = '%s@restmail.net' % self.id
+        self['password'] = 'password'
+        self['additional_emails'] = []
+
+        self.update(**kwargs)
+
+    def __getattr__(self, attr):
+        return self[attr]
diff --git a/automation-tests/browserid/mozwebqa.cfg b/automation-tests/browserid/mozwebqa.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..8b6ad1effcda45be9e2f84987c804c5d56f21eec
--- /dev/null
+++ b/automation-tests/browserid/mozwebqa.cfg
@@ -0,0 +1,3 @@
+[DEFAULT]
+baseurl = http://123done.org
+tags = bidpom
diff --git a/automation-tests/browserid/pages/__init__.py b/automation-tests/browserid/pages/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/browserid/pages/account_manager.py b/automation-tests/browserid/pages/account_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..0428acd806d1a3d5efe2a357da802e53654085b3
--- /dev/null
+++ b/automation-tests/browserid/pages/account_manager.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from base import Base
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+class AccountManager(Base):
+
+    _emails_locator = (By.CSS_SELECTOR, '#emailList .email')
+    _edit_password_button_locator = (By.CSS_SELECTOR, '#edit_password button.edit')
+    _old_password_field_locator = (By.ID, 'old_password')
+    _new_password_field_locator = (By.ID, 'new_password')
+    _change_password_done_locator = (By.ID, 'changePassword')
+    _sign_in_locator = (By.CSS_SELECTOR, 'a.signIn')
+    _sign_out_locator = (By.CSS_SELECTOR, 'a.signOut')
+
+    def __init__(self, selenium, timeout):
+        Base.__init__(self, selenium, timeout)
+
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(*self._emails_locator).is_displayed())
+
+    @property
+    def signed_in(self):
+        return 'not_authenticated' not in self.selenium.find_element(By.TAG_NAME, 'body').get_attribute('class')
+
+    @property
+    def emails(self):
+        return [element.text for element in self.selenium.find_elements(*self._emails_locator)]
+
+    def click_edit_password(self):
+        """Click edit password to show the new/old password fields"""
+        self.selenium.find_element(*self._edit_password_button_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(*self._old_password_field_locator).is_displayed())
+
+    @property
+    def old_password(self):
+        """Get the value of the old password field."""
+        return self.selenium.find_element(*self._old_password_field_locator).text
+
+    @old_password.setter
+    def old_password(self, value):
+        """Set the value of the old password field."""
+        password = self.selenium.find_element(*self._old_password_field_locator)
+        password.clear()
+        password.send_keys(value)
+
+    @property
+    def new_password(self):
+        """Get the value of the new password field."""
+        return self.selenium.find_element(*self._new_password_field_locator).text
+
+    @new_password.setter
+    def new_password(self, value):
+        """Set the value of the new password field."""
+        password = self.selenium.find_element(*self._new_password_field_locator)
+        password.clear()
+        password.send_keys(value)
+
+    def click_password_done(self):
+        """Click password done to save the new password."""
+        self.selenium.find_element(*self._change_password_done_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(*self._edit_password_button_locator).is_displayed())
+
+    def click_sign_out(self):
+        self.selenium.find_element(*self._sign_out_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: not self.signed_in)
diff --git a/automation-tests/browserid/pages/base.py b/automation-tests/browserid/pages/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8cd99b7bf7ec042cfafafe60fc043dbfbcf5edd
--- /dev/null
+++ b/automation-tests/browserid/pages/base.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+
+# 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/.
+
+
+class Base(object):
+
+    _page_title = 'Mozilla Persona: A Better Way to Sign In'
+
+    def __init__(self, selenium, timeout=60):
+        self.selenium = selenium
+        self.timeout = timeout
+        self._main_window_handle = self.selenium.current_window_handle
+
+    def switch_to_main_window(self):
+        self.selenium.switch_to_window(self._main_window_handle)
diff --git a/automation-tests/browserid/pages/complete_registration.py b/automation-tests/browserid/pages/complete_registration.py
new file mode 100644
index 0000000000000000000000000000000000000000..141c1ea1fe33a2e89594cc6d625ab34d85423d83
--- /dev/null
+++ b/automation-tests/browserid/pages/complete_registration.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from base import Base
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+class CompleteRegistration(Base):
+
+    _email_locator = (By.ID, 'email')
+    _password_locator = (By.ID, 'password')
+    _finish_locator = (By.CSS_SELECTOR, 'div.submit > button')
+    _thank_you_locator = (By.ID, 'congrats')
+
+    def __init__(self, selenium, timeout, expect='success'):
+        Base.__init__(self, selenium, timeout)
+
+        if expect == 'success':
+            WebDriverWait(self.selenium, self.timeout).until(
+                lambda s: s.find_element(*self._thank_you_locator).is_displayed())
+        elif expect == 'verify':
+            WebDriverWait(self.selenium, self.timeout).until(
+                lambda s: s.find_element(*self._password_locator).is_displayed())
+        else:
+            raise Exception('Unknown expect value: %s' % expect)
+
+    @property
+    def email(self):
+        """Get the value of the email field."""
+        return self.selenium.find_element(*self._email_locator).text
+
+    @property
+    def password(self):
+        """Get the value of the password field."""
+        return self.selenium.find_element(*self._password_locator).text
+
+    @password.setter
+    def password(self, value):
+        """Set the value of the password field."""
+        password = self.selenium.find_element(*self._password_locator)
+        password.clear()
+        password.send_keys(value)
+
+    def click_finish(self):
+        """Clicks the 'finish' button."""
+        self.selenium.find_element(*self._finish_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(*self._thank_you_locator).is_displayed())
+
+    @property
+    def thank_you(self):
+        """Returns the 'thank you' message."""
+        return self.selenium.find_element(*self._thank_you_locator).text
diff --git a/automation-tests/browserid/pages/sign_in.py b/automation-tests/browserid/pages/sign_in.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a22a4a435b8b26d8d2b625bb449130253917475
--- /dev/null
+++ b/automation-tests/browserid/pages/sign_in.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from base import Base
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+class SignIn(Base):
+
+    _this_is_not_me_locator = (By.ID, 'thisIsNotMe')
+    _signed_in_email_locator = (By.CSS_SELECTOR, 'label[for=email_0]')
+    _emails_locator = (By.CSS_SELECTOR, 'label[for^=email_]')
+    _email_locator = (By.ID, 'email')
+    _password_locator = (By.ID, 'password')
+    _verify_password_locator = (By.ID, 'vpassword')
+    _next_locator = (By.CSS_SELECTOR, 'button.start')
+    _sign_in_locator = (By.CSS_SELECTOR, 'button.returning')
+    _sign_in_returning_user_locator = (By.ID, 'signInButton')
+    _verify_email_locator = (By.ID, 'verify_user')
+    _forgot_password_locator = (By.ID, 'forgotPassword')
+    _reset_password_locator = (By.ID, 'password_reset')
+    _check_email_at_locator = (By.CSS_SELECTOR, '#wait .contents h2 + p strong')
+    _add_another_email_locator = (By.ID, 'useNewEmail')
+    _new_email_locator = (By.ID, 'newEmail')
+    _add_new_email_locator = (By.ID, 'addNewEmail')
+
+    def __init__(self, selenium, timeout, expect='new'):
+        Base.__init__(self, selenium, timeout)
+
+        if self.selenium.title != self._page_title:
+            for handle in self.selenium.window_handles:
+                self.selenium.switch_to_window(handle)
+                WebDriverWait(self.selenium, self.timeout).until(lambda s: s.title)
+                if self.selenium.title == self._page_title:
+                    break
+            else:
+                raise Exception('Popup has not loaded')
+
+        if expect == 'new':
+            WebDriverWait(self.selenium, self.timeout).until(
+                lambda s: s.find_element(*self._email_locator).is_displayed())
+        elif expect == 'returning':
+            WebDriverWait(self.selenium, self.timeout).until(
+                lambda s: s.find_element(
+                    *self._sign_in_returning_user_locator).is_displayed())
+            import time
+            time.sleep(2) # TODO: Remove this sleep
+        else:
+            raise Exception('Unknown expect value: %s' % expect)
+
+    def close_window(self):
+        self.selenium.close()
+
+    @property
+    def signed_in_email(self):
+        """Get the value of the email that is currently signed in."""
+        return self.selenium.find_element(*self._signed_in_email_locator).text
+
+    def click_this_is_not_me(self):
+        """Clicks the 'This is not me' button."""
+        self.selenium.find_element(*self._this_is_not_me_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(*self._email_locator).is_displayed())
+
+    @property
+    def emails(self):
+        """Get the emails for the returning user."""
+        return [element.text for element in
+                self.selenium.find_elements(*self._emails_locator)]
+
+    @property
+    def email(self):
+        """Get the value of the email field."""
+        return self.selenium.find_element(*self._email_locator).text
+
+    @email.setter
+    def email(self, value):
+        """Set the value of the email field."""
+        email = self.selenium.find_element(*self._email_locator)
+        email.clear()
+        email.send_keys(value)
+
+    @property
+    def new_email(self):
+        """Get the value of the new email field."""
+        return self.selenium.find_element(*self._new_email_locator).text
+
+    @new_email.setter
+    def new_email(self, value):
+        """Set the value of the new email field."""
+        email = self.selenium.find_element(*self._new_email_locator)
+        email.clear()
+        email.send_keys(value)
+
+    @property
+    def selected_email(self):
+        """Return the value of the selected email of returning user's multiple emails"""
+        for email in self.selenium.find_elements(*self._emails_locator):
+            if email.find_element(By.TAG_NAME, 'input').is_selected():
+                return email.text
+
+    def select_email(self, value):
+        """Select email from the returning user's multiple emails."""
+        for email in self.selenium.find_elements(*self._emails_locator):
+            if email.text == value:
+                email.click()
+                break
+        else:
+            raise Exception('Email not found: %s' % value)
+
+    @property
+    def password(self):
+        """Get the value of the password field."""
+        return self.selenium.find_element(*self._password_locator).text
+
+    @password.setter
+    def password(self, value):
+        """Set the value of the password field."""
+        password = self.selenium.find_element(*self._password_locator)
+        password.clear()
+        password.send_keys(value)
+
+    @property
+    def verify_password(self):
+        """Get the value of the verify password field."""
+        return self.selenium.find_element(*self._verify_password_locator).text
+
+    @verify_password.setter
+    def verify_password(self, value):
+        """Set the value of the verify password field."""
+        password = self.selenium.find_element(*self._verify_password_locator)
+        password.clear()
+        password.send_keys(value)
+
+    @property
+    def check_email_at_address(self):
+        """Get the value of the email address for confirmation."""
+        return self.selenium.find_element(*self._check_email_at_locator).text
+
+    def click_next(self, expect='password'):
+        """Clicks the 'next' button."""
+        self.selenium.find_element(*self._next_locator).click()
+        if expect == 'password':
+            WebDriverWait(self.selenium, self.timeout).until(
+                lambda s: s.find_element(
+                    *self._password_locator).is_displayed())
+        elif expect == 'verify':
+            WebDriverWait(self.selenium, self.timeout).until(
+                lambda s: s.find_element(
+                    *self._verify_email_locator).is_displayed())
+        else:
+            raise Exception('Unknown expect value: %s' % expect)
+
+    def click_sign_in(self):
+        """Clicks the 'sign in' button."""
+        self.selenium.find_element(*self._sign_in_locator).click()
+        self.switch_to_main_window()
+
+    def click_sign_in_returning_user(self):
+        """Clicks the 'sign in' button."""
+        self.selenium.find_element(
+            *self._sign_in_returning_user_locator).click()
+        self.switch_to_main_window()
+
+    def click_verify_email(self):
+        """Clicks 'verify email' button."""
+        self.selenium.find_element(*self._verify_email_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(
+                *self._check_email_at_locator).is_displayed())
+
+    def click_forgot_password(self):
+        """Clicks 'forgot password' link (visible after entering a valid email)"""
+        self.selenium.find_element(*self._forgot_password_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(
+                *self._reset_password_locator).is_displayed())
+
+    def click_reset_password(self):
+        """Clicks 'reset password' after forgot password and new passwords entered"""
+        self.selenium.find_element(*self._reset_password_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(
+                *self._check_email_at_locator).is_displayed())
+
+    def click_add_another_email_address(self):
+        """Clicks 'add another email' button."""
+        self.selenium.find_element(*self._add_another_email_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(
+                *self._add_new_email_locator).is_displayed())
+
+    def click_add_new_email(self):
+        """Clicks 'Add' button to insert new email address."""
+        self.selenium.find_element(*self._add_new_email_locator).click()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: s.find_element(
+                *self._check_email_at_locator).is_displayed())
+
+    def sign_in(self, email, password):
+        """Signs in using the specified email address and password."""
+        self.email = email
+        self.click_next(expect='password')
+        self.password = password
+        self.click_sign_in()
+
+    def sign_in_new_user(self, email, password):
+        """Requests verification email using the specified email address."""
+        self.email = email
+        self.click_next(expect='verify')
+        self.password = password
+        self.verify_password = password
+        self.click_verify_email()
+        self.close_window()
+        self.switch_to_main_window()
+
+    def sign_in_returning_user(self):
+        """Signs in with the stored user."""
+        self.click_sign_in_returning_user()
diff --git a/automation-tests/browserid/requirements.txt b/automation-tests/browserid/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..94fd2ed89d1ebf52c93c3bb428e02bb6c1d98812
--- /dev/null
+++ b/automation-tests/browserid/requirements.txt
@@ -0,0 +1,2 @@
+pytest-mozwebqa==1.0
+requests==0.13.3
diff --git a/automation-tests/browserid/setup.cfg b/automation-tests/browserid/setup.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..5ca77d16be0357283249ae1e9f1456a748bccee1
--- /dev/null
+++ b/automation-tests/browserid/setup.cfg
@@ -0,0 +1,2 @@
+[pytest]
+python_files=check_*.py
diff --git a/automation-tests/browserid/tests/__init__.py b/automation-tests/browserid/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/browserid/tests/base.py b/automation-tests/browserid/tests/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..8adf6357ce14e817412fbad8532374c0df8309fa
--- /dev/null
+++ b/automation-tests/browserid/tests/base.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+# 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 re
+
+import requests
+from selenium.webdriver.support.ui import WebDriverWait
+
+from .. import BrowserID
+from .. mocks.user import MockUser
+import restmail
+
+
+class BaseTest(object):
+
+    def browserid_url(self, base_url):
+        response = requests.get('%s/' % base_url, verify=False)
+        match = re.search(BrowserID.INCLUDE_URL_REGEX, response.content)
+        if match:
+            return match.group(1)
+        else:
+            raise Exception('Unable to determine BrowserID URL from %s.' % base_url)
+
+    def log_out(self, selenium, timeout):
+        WebDriverWait(selenium, timeout).until(
+            lambda s: s.find_element_by_id('loggedin').is_displayed())
+        selenium.find_element_by_css_selector('#loggedin a').click()
+        WebDriverWait(selenium, timeout).until(
+            lambda s: s.find_element_by_css_selector('#loggedout button').is_displayed())
+
+    def create_verified_user(self, selenium, timeout):
+        user = MockUser()
+        from .. pages.sign_in import SignIn
+        signin = SignIn(selenium, timeout, expect='new')
+        signin.sign_in_new_user(user.primary_email, user.password)
+        mail = restmail.get_mail(user.primary_email, timeout=timeout)
+        verify_url = re.search(BrowserID.VERIFY_URL_REGEX,
+                               mail[0]['text']).group(0)
+
+        selenium.get(verify_url)
+        from .. pages.complete_registration import CompleteRegistration
+        complete_registration = CompleteRegistration(selenium,
+                                                     timeout,
+                                                     expect='success')
+        assert 'Thank you' in complete_registration.thank_you
+        return user
diff --git a/automation-tests/browserid/tests/check_add_email.py b/automation-tests/browserid/tests/check_add_email.py
new file mode 100644
index 0000000000000000000000000000000000000000..79e1e00f9be379298ef5a7654125f8a9736c8a92
--- /dev/null
+++ b/automation-tests/browserid/tests/check_add_email.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+
+# 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 re
+
+import pytest
+
+from .. import BrowserID
+from base import BaseTest
+import restmail
+
+
+@pytest.mark.nondestructive
+class TestSignIn(BaseTest):
+
+    @pytest.mark.travis
+    def test_add_email(self, mozwebqa):
+        user = self.create_verified_user(mozwebqa.selenium, mozwebqa.timeout)
+        user.additional_emails.append('%s_1@restmail.net' % user.id)
+
+        mozwebqa.selenium.get('%s/' % mozwebqa.base_url)
+        self.log_out(mozwebqa.selenium, mozwebqa.timeout)
+        mozwebqa.selenium.find_element_by_css_selector('#loggedout button').click()
+
+        from .. pages.sign_in import SignIn
+        signin = SignIn(mozwebqa.selenium, mozwebqa.timeout, expect='returning')
+        signin.click_add_another_email_address()
+        signin.new_email = user.additional_emails[0]
+        signin.click_add_new_email()
+        signin.close_window()
+        signin.switch_to_main_window()
+
+        mail = restmail.get_mail(user.additional_emails[0],
+                                 timeout=mozwebqa.timeout)
+        assert 'Click to confirm this email address' in mail[0]['text']
+        confirm_url = re.search(BrowserID.CONFIRM_URL_REGEX,
+            mail[0]['text']).group(0)
+
+        mozwebqa.selenium.get(confirm_url)
+        from .. pages.complete_registration import CompleteRegistration
+        complete_registration = CompleteRegistration(mozwebqa.selenium,
+            mozwebqa.timeout,
+            expect='success')
+        assert 'Your address has been verified' in complete_registration.thank_you
+
+        mozwebqa.selenium.get('%s/' % mozwebqa.base_url)
+        self.log_out(mozwebqa.selenium, mozwebqa.timeout)
+        mozwebqa.selenium.find_element_by_css_selector('#loggedout button').click()
+
+        signin = SignIn(mozwebqa.selenium, mozwebqa.timeout, expect='returning')
+        assert user.additional_emails[0] in signin.emails
+        assert signin.selected_email == user.additional_emails[0]
diff --git a/automation-tests/browserid/tests/check_change_password.py b/automation-tests/browserid/tests/check_change_password.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb9e0501ae30a8c8b2b509e0b65c78caa649e586
--- /dev/null
+++ b/automation-tests/browserid/tests/check_change_password.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+# 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
+from selenium.webdriver.support.ui import WebDriverWait
+
+from .. import BrowserID
+from base import BaseTest
+
+
+@pytest.mark.nondestructive
+class TestSignIn(BaseTest):
+
+    @pytest.mark.travis
+    def test_change_password(self, mozwebqa):
+        user = self.create_verified_user(mozwebqa.selenium, mozwebqa.timeout)
+
+        mozwebqa.selenium.get(self.browserid_url(mozwebqa.base_url))
+        from .. pages.account_manager import AccountManager
+        account_manager = AccountManager(mozwebqa.selenium, mozwebqa.timeout)
+
+        assert user.primary_email in account_manager.emails
+
+        account_manager.click_edit_password()
+        account_manager.old_password = user.password
+        user.password += '_new'
+        account_manager.new_password = user.password
+        account_manager.click_password_done()
+        account_manager.click_sign_out()
+
+        mozwebqa.selenium.get('%s/' % mozwebqa.base_url)
+
+        login_locator = '#loggedout button'
+        WebDriverWait(mozwebqa.selenium, mozwebqa.timeout).until(
+            lambda s: s.find_element_by_css_selector(login_locator).is_displayed())
+        mozwebqa.selenium.find_element_by_css_selector(login_locator).click()
+
+        browser_id = BrowserID(mozwebqa.selenium, mozwebqa.timeout)
+        browser_id.sign_in(user.primary_email, user.password)
+
+        WebDriverWait(mozwebqa.selenium, mozwebqa.timeout).until(
+            lambda s: s.find_element_by_id('loggedin').is_displayed())
diff --git a/automation-tests/browserid/tests/check_reset_password.py b/automation-tests/browserid/tests/check_reset_password.py
new file mode 100644
index 0000000000000000000000000000000000000000..6dced83572eded7d8ac17a8b7cdd173f22cd9f8e
--- /dev/null
+++ b/automation-tests/browserid/tests/check_reset_password.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+
+# 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 re
+
+import pytest
+
+from .. import BrowserID
+from base import BaseTest
+import restmail
+
+
+@pytest.mark.nondestructive
+class TestSignIn(BaseTest):
+
+    @pytest.mark.travis
+    def test_reset_password(self, mozwebqa):
+        user = self.create_verified_user(mozwebqa.selenium, mozwebqa.timeout)
+        mozwebqa.selenium.get('%s/' % mozwebqa.base_url)
+        self.log_out(mozwebqa.selenium, mozwebqa.timeout)
+        mozwebqa.selenium.find_element_by_css_selector('#loggedout button').click()
+
+        from .. pages.sign_in import SignIn
+        signin = SignIn(mozwebqa.selenium, mozwebqa.timeout, expect='returning')
+        signin.click_this_is_not_me()
+        signin.email = user.primary_email
+        signin.click_next()
+        signin.click_forgot_password()
+        user.password += '_new'
+        signin.password = user.password
+        signin.verify_password = user.password
+        signin.click_reset_password()
+        assert signin.check_email_at_address == user.primary_email
+
+        signin.close_window()
+        signin.switch_to_main_window()
+        mail = restmail.get_mail(user.primary_email,
+                                 message_count=2,
+                                 timeout=mozwebqa.timeout)
+        assert 'Click to reset your password' in mail[1]['text']
+
+        reset_url = re.search(BrowserID.RESET_URL_REGEX,
+            mail[1]['text']).group(0)
+        mozwebqa.selenium.get(reset_url)
+
+        from .. pages.complete_registration import CompleteRegistration
+        complete_registration = CompleteRegistration(mozwebqa.selenium,
+            mozwebqa.timeout,
+            expect='success')
+        assert 'Your address has been verified!' in complete_registration.thank_you
diff --git a/automation-tests/browserid/tests/check_sign_in.py b/automation-tests/browserid/tests/check_sign_in.py
new file mode 100644
index 0000000000000000000000000000000000000000..81a5ab4765fa28c215c23e07a473a9cfe90832cd
--- /dev/null
+++ b/automation-tests/browserid/tests/check_sign_in.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+
+# 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
+from selenium.webdriver.support.ui import WebDriverWait
+
+from .. import BrowserID
+from .. mocks.user import MockUser
+from base import BaseTest
+import restmail
+
+
+@pytest.mark.nondestructive
+class TestSignIn(BaseTest):
+
+    def test_sign_in_helper(self, mozwebqa):
+        browser_id = BrowserID(mozwebqa.selenium, mozwebqa.timeout)
+        browser_id.sign_in(mozwebqa.email, mozwebqa.password)
+
+        WebDriverWait(mozwebqa.selenium, mozwebqa.timeout).until(
+            lambda s: s.find_element_by_id('loggedin').is_displayed())
+
+    def test_sign_in(self, mozwebqa):
+        from .. pages.sign_in import SignIn
+        signin = SignIn(mozwebqa.selenium, mozwebqa.timeout, expect='new')
+        signin.email = mozwebqa.email
+        signin.click_next(expect='password')
+        signin.password = mozwebqa.password
+        signin.click_sign_in()
+
+        WebDriverWait(mozwebqa.selenium, mozwebqa.timeout).until(
+            lambda s: s.find_element_by_id('loggedin').is_displayed())
+
+    @pytest.mark.travis
+    def test_sign_in_new_user_helper(self, mozwebqa):
+        user = MockUser()
+        from .. pages.sign_in import SignIn
+        signin = SignIn(mozwebqa.selenium, mozwebqa.timeout, expect='new')
+        print 'signing in as %s' % user.primary_email
+        signin.sign_in_new_user(user.primary_email, 'password')
+        mail = restmail.get_mail(user.primary_email, timeout=mozwebqa.timeout)
+        assert 'Click to confirm this email address' in mail[0]['text']
+
+    @pytest.mark.travis
+    def test_sign_in_new_user(self, mozwebqa):
+        user = MockUser()
+        from .. pages.sign_in import SignIn
+        signin = SignIn(mozwebqa.selenium, mozwebqa.timeout, expect='new')
+        print 'signing in as %s' % user.primary_email
+        signin.email = user.primary_email
+        signin.click_next(expect='verify')
+        signin.password = user.password
+        signin.verify_password = user.password
+        signin.click_verify_email()
+        assert signin.check_email_at_address == user.primary_email
+
+        signin.close_window()
+        signin.switch_to_main_window()
+        mail = restmail.get_mail(user.primary_email, timeout=mozwebqa.timeout)
+        assert 'Click to confirm this email address' in mail[0]['text']
+
+    @pytest.mark.travis
+    def test_sign_in_returning_user(self, mozwebqa):
+        self.create_verified_user(mozwebqa.selenium, mozwebqa.timeout)
+        mozwebqa.selenium.get('%s/' % mozwebqa.base_url)
+        WebDriverWait(mozwebqa.selenium, mozwebqa.timeout).until(
+            lambda s: s.find_element_by_id('loggedin').is_displayed())
diff --git a/automation-tests/browserid/tests/conftest.py b/automation-tests/browserid/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..172c531145870ee29862ddc22e3c33bcb00d3716
--- /dev/null
+++ b/automation-tests/browserid/tests/conftest.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def pytest_runtest_setup(item):
+    item.config.option.api = 'webdriver'
+
+
+def pytest_funcarg__mozwebqa(request):
+    mozwebqa = request.getfuncargvalue('mozwebqa')
+    mozwebqa.selenium.get('%s/' % mozwebqa.base_url)
+    WebDriverWait(mozwebqa.selenium, mozwebqa.timeout).until(
+        lambda s: s.find_element_by_css_selector('#loggedout button').is_displayed())
+    mozwebqa.selenium.find_element_by_css_selector('#loggedout button').click()
+    return mozwebqa
diff --git a/automation-tests/browserid/tests/restmail.py b/automation-tests/browserid/tests/restmail.py
new file mode 100644
index 0000000000000000000000000000000000000000..910b549fcdf2a0118c8c1eefbf2798eb91b7f72d
--- /dev/null
+++ b/automation-tests/browserid/tests/restmail.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+# 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 json
+import time
+
+import requests
+
+
+def get_mail(username, message_count=1, timeout=60):
+    username = username.partition('@restmail.net')[0]
+    end_time = time.time() + timeout
+    while(True):
+        response = requests.get(
+            'https://restmail.net/mail/%s' % username,
+            verify=False)
+        restmail = json.loads(response.content)
+        if len(restmail) == message_count:
+            return restmail
+        time.sleep(0.5)
+        if(time.time() > end_time):
+            break
+    raise Exception('Timeout after %(TIMEOUT)s seconds getting restmail for '
+                    '%(USERNAME)s. Expected %(EXPECTED_MESSAGE_COUNT)s '
+                    'messages but there were %(ACTUAL_MESSAGE_COUNT)s.' % {
+                        'TIMEOUT': timeout,
+                        'USERNAME': username,
+                        'EXPECTED_MESSAGE_COUNT': message_count,
+                        'ACTUAL_MESSAGE_COUNT': len(restmail)})
diff --git a/automation-tests/credentials.yaml b/automation-tests/credentials.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..2ce53331519a6d23f8f9eb276f5f2dd69c8d3c23
--- /dev/null
+++ b/automation-tests/credentials.yaml
@@ -0,0 +1,37 @@
+# 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/.
+
+# File contains users data.
+#
+# Each user is a section named with its role
+# and any number of values. At least email,
+# password and name should be present.
+#
+# Example:
+#     admin:
+#         email: email@site.com
+#         password: password
+#         name: Test User
+#
+# Still, you are free to add any more data you wish. It will be kept
+# in the same dictionary.
+#
+# Example:
+#     admin:
+#         email: email@site.com
+#         password: password
+#         name: Test User
+#         username: testuser
+#         some_user_data: data
+#
+# The contents of this file are accessible via the pytest-mozwebqa plugin:
+#
+# Example:
+#   credentials = mozwebqa.credentials['default']
+#   credentials['email']
+
+default:
+    email: <value>
+    password: <value>
+    name: <value>
diff --git a/automation-tests/myfavoritebeer/mozwebqa.cfg b/automation-tests/myfavoritebeer/mozwebqa.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..8e79b8254cfcdbdd08f4e635b54b17981fc409d5
--- /dev/null
+++ b/automation-tests/myfavoritebeer/mozwebqa.cfg
@@ -0,0 +1,4 @@
+[DEFAULT]
+api = webdriver
+baseurl = http://myfavoritebeer.org
+tags = browserid
diff --git a/automation-tests/myfavoritebeer/page.py b/automation-tests/myfavoritebeer/page.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ca4b63b2a6f06a8313f58138ba2362e8d8487ea
--- /dev/null
+++ b/automation-tests/myfavoritebeer/page.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from unittestzero import Assert
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.common.exceptions import NoSuchElementException
+from selenium.common.exceptions import ElementNotVisibleException
+
+
+class Page(object):
+
+    def __init__(self, testsetup):
+        self.testsetup = testsetup
+        self.base_url = testsetup.base_url
+        self.selenium = testsetup.selenium
+        self.timeout = testsetup.timeout
+
+    @property
+    def is_the_current_page(self):
+        if self._page_title:
+            WebDriverWait(self.selenium, self.timeout).until(lambda s: s.title)
+
+        Assert.equal(self.selenium.title, self._page_title)
+        return True
+
+    def is_element_present(self, *locator):
+        self.selenium.implicitly_wait(0)
+        try:
+            self.selenium.find_element(*locator)
+            return True
+        except NoSuchElementException:
+            return False
+        finally:
+            # set back to where you once belonged
+            self.selenium.implicitly_wait(self.testsetup.default_implicit_wait)
+
+    def is_element_visible(self, *locator):
+        try:
+            return self.selenium.find_element(*locator).is_displayed()
+        except NoSuchElementException, ElementNotVisibleException:
+            return False
diff --git a/automation-tests/myfavoritebeer/pages/__init__.py b/automation-tests/myfavoritebeer/pages/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/myfavoritebeer/pages/home.py b/automation-tests/myfavoritebeer/pages/home.py
new file mode 100644
index 0000000000000000000000000000000000000000..c71e36f4dcd1314bc356af3e57729dad9ed42be3
--- /dev/null
+++ b/automation-tests/myfavoritebeer/pages/home.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+
+from page import Page
+
+
+class HomePage(Page):
+
+    _page_title = 'My Favorite Beer, a BrowserID example'
+
+    _sign_in_locator = (By.CSS_SELECTOR, '#loginInfo .login')
+    _logout_locator = (By.ID, 'logout')
+
+    def go_to_home_page(self):
+        self.selenium.get(self.base_url + '/')
+        self.is_the_current_page
+
+    def sign_in(self, user='default'):
+        credentials = self.testsetup.credentials[user]
+        self.click_sign_in()
+        from browserid import BrowserID
+        browserid = BrowserID(self.selenium, self.timeout)
+        browserid.sign_in(credentials['email'], credentials['password'])
+
+    def logout(self):
+        self.click_logout()
+        WebDriverWait(self.selenium, self.timeout).until(
+            lambda s: not self.is_element_present(*self._logout_locator))
+
+    def click_sign_in(self):
+        self.selenium.find_element(*self._sign_in_locator).click()
+
+    def click_logout(self):
+        self.selenium.find_element(*self._logout_locator).click()
+
+    @property
+    def is_logged_in(self):
+        return self.is_element_visible(*self._logout_locator)
diff --git a/automation-tests/myfavoritebeer/tests/__init__.py b/automation-tests/myfavoritebeer/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/automation-tests/myfavoritebeer/tests/test_logout.py b/automation-tests/myfavoritebeer/tests/test_logout.py
new file mode 100644
index 0000000000000000000000000000000000000000..94304f9ef2199891640f0c4b9e77880e9375d448
--- /dev/null
+++ b/automation-tests/myfavoritebeer/tests/test_logout.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from pages.home import HomePage
+from unittestzero import Assert
+
+import pytest
+
+
+class TestLogout:
+
+    @pytest.mark.nondestructive
+    def test_that_user_can_logout(self, mozwebqa):
+        home_pg = HomePage(mozwebqa)
+        home_pg.go_to_home_page()
+        home_pg.sign_in()
+        home_pg.logout()
+        Assert.false(home_pg.is_logged_in)
diff --git a/automation-tests/myfavoritebeer/tests/test_sign_in.py b/automation-tests/myfavoritebeer/tests/test_sign_in.py
new file mode 100644
index 0000000000000000000000000000000000000000..caac0ed4fd57bfe1f978a97c87e2a2b985c36786
--- /dev/null
+++ b/automation-tests/myfavoritebeer/tests/test_sign_in.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+# 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/.
+
+from pages.home import HomePage
+from unittestzero import Assert
+
+import pytest
+
+
+class TestSignIn:
+
+    @pytest.mark.nondestructive
+    def test_that_user_can_sign_in(self, mozwebqa):
+        home_pg = HomePage(mozwebqa)
+        home_pg.go_to_home_page()
+        home_pg.sign_in()
+        Assert.true(home_pg.is_logged_in)
diff --git a/automation-tests/requirements.txt b/automation-tests/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..00b26b5d068cf14b5a69ce16f482f85509afe62c
--- /dev/null
+++ b/automation-tests/requirements.txt
@@ -0,0 +1,11 @@
+PyYAML==3.10
+UnittestZero
+certifi==0.0.8
+chardet==1.0.1
+execnet==1.1
+py==1.4.9
+pytest==2.2.4
+pytest-mozwebqa==1.0
+pytest-xdist==1.8
+requests==0.13.2
+selenium
diff --git a/automation-tests/run.py b/automation-tests/run.py
new file mode 100755
index 0000000000000000000000000000000000000000..7cb1d55a64dd6cf77f1b74c4ecd666282e34f191
--- /dev/null
+++ b/automation-tests/run.py
@@ -0,0 +1,98 @@
+#!/usr/bin/python
+
+import optparse
+import os
+import platform
+import subprocess
+import sys
+
+
+# used to check for existence of virtualenv and pip.
+# lifted from: http://stackoverflow.com/questions/377017
+def which(program):
+    def is_exe(fpath):
+        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
+    fpath, fname = os.path.split(program)
+    if fpath:
+        if is_exe(program):
+            return program
+    else:
+        for path in os.environ["PATH"].split(os.pathsep):
+            exe_file = os.path.join(path, program)
+            if is_exe(exe_file):
+                return exe_file
+    return None
+
+
+def main():
+    # virtualenv location differs on windows
+    # TODO platform detection is brittle. is there a better way?
+    if platform.system() == 'Windows':
+        env_path = 'bid_selenium\Scripts\\'
+    else:
+        env_path = 'bid_selenium/bin/'
+    env_py = env_path + 'python'
+
+    # parse command line options
+    parser = optparse.OptionParser()
+    parser.add_option('--install', '-i', dest='install', action="store_true",
+                      help='install python dependencies inside a virtualenv')
+    parser.add_option('--all', '-a', dest='run_all', action="store_true",
+                      help='run all tests. requires test account credentials' +
+                           ' to be created and added to credentials.yaml')
+    parser.add_option('--target', '-t', dest='target_hostname', 
+                      default="dev", help='run tests against an ephemeral' +
+                      ' instance. Specify your instance\'s hostname ("foo"),' +
+                      ' not the full domain name ("foo.123done.org")')
+    # TODO add other options
+    options, arguments = parser.parse_args()
+
+    # 1. check that python is the right version TODO: would 2.6 actually work?
+    if sys.version_info < (2,7,0):
+        sys.stderr.write('python 2.7 or later is required to run the tests\n')
+        exit(1)
+    # 2. TODO check that virtualenv and pip exist. if not, bail.
+    if not which('pip'):
+        sys.stderr.write('pip must be installed; do "easy_install pip", ' +
+                         ' then try again\n')
+        exit(1)
+    if not which('virtualenv'):
+        sys.stderr.write('virtualenv must be installed; do "pip install ' +
+                         'virtualenv", then try again\n')
+        exit(1)
+    # 3. create the virtualenv if they asked you to install it or it's missing
+    if options.install or not os.path.exists(env_py):
+        subprocess.call('virtualenv bid_selenium', shell=True)
+        # 4. pip install requirements (or verify they're installed).
+        subprocess.call(env_path + 'pip install -Ur requirements.txt', 
+                        shell=True)
+
+    # 4 1/2. check the ephemeral instance to hit.
+    host = options.target_hostname
+
+    # 5. run the tests
+    # TODO parse arguments to know which tests to run
+    # TODO right now we only run one 123done test in the default case
+    if options.run_all:
+        subprocess.call(env_py + ' -m py.test --destructive ' +
+                        '--credentials=credentials.yaml ' +
+                        '--baseurl=http://' + host + '.123done.org ' +
+                        '--driver=firefox -q browserid', shell=True);
+        subprocess.call(env_py + ' -m py.test --destructive ' +
+                        '--credentials=credentials.yaml ' +
+                        '--baseurl=http://' + host + '.myfavoritebeer.org ' +
+                        '--driver=firefox -q myfavoritebeer', shell=True);
+        tests_123 = '123done'
+    else:
+        tests_123 = '123done/tests/test_new_user.py'
+    # the 123done tests always run
+    subprocess.call(env_py + ' -m py.test --destructive ' + 
+                    '--credentials=credentials.yaml ' +
+                    '--baseurl=http://' + host + '.123done.org ' +
+                    '--driver=firefox -q ' + tests_123, shell=True);
+
+    # 6. TODO deactivate/destroy virtualenv?? maybe '--cleanup' argument?
+
+
+if __name__ == '__main__':
+    main()