From 7949c64b9b6e6f9c494b13a002f58971604c9c41 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 10 Oct 2019 09:22:10 -0400 Subject: [PATCH] Initial set up for Ghost Inspector integration tests. Adds a CircleCI integration for Ghost Inspector (https://ghostinspector.com), a headless browser testing SaaS. The README is updated with details about how to run GI locally. Removes the bootstrap setup for Selenium testing with BrowserStack. --- .circleci/config.yml | 68 +++++++++++++++++++++++++++++++++- .gitignore | 6 ++- .secrets.baseline | 29 +++++---------- README.md | 37 ++++++++++-------- tests/acceptance/__init__.py | 0 tests/acceptance/browsers.py | 18 --------- tests/acceptance/conftest.py | 65 -------------------------------- tests/acceptance/test_basic.py | 50 ------------------------- 8 files changed, 101 insertions(+), 172 deletions(-) delete mode 100644 tests/acceptance/__init__.py delete mode 100644 tests/acceptance/browsers.py delete mode 100644 tests/acceptance/conftest.py delete mode 100644 tests/acceptance/test_basic.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 2851bce1..1b472716 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -135,6 +135,67 @@ jobs: - run: "docker tag ${AZURE_SERVER_NAME}/atat:atat-${CIRCLE_SHA1} ${AZURE_SERVER_NAME}/atat:latest" - run: "docker push ${AZURE_SERVER_NAME}/atat:latest" + integration-tests: + docker: + - image: docker:17.05.0-ce-git + steps: + - setup_remote_docker: + version: 18.06.0-ce + - checkout + - run: + name: Set up temporary docker network + command: docker network create atat + - run: + name: Build image + command: docker build . -t atat:latest + - run: + name: Get storage containers + command: docker pull postgres:latest && docker pull redis:latest + - run: + name: Start redis + command: docker run -d --network atat --link redis:redis -p 6379:6379 --name redis redis:latest + - run: + name: Start postgres + command: docker run -d --network atat --link postgres:postgres -p 5432:5432 --name postgres postgres:latest + - run: + name: Start application container + command: | + docker run -d \ + -e DISABLE_CRL_CHECK=true \ + -e PGHOST=postgres \ + -e REDIS_URI=redis://redis:6379 \ + -p 8000:8000 \ + --network atat \ + --name test-atat \ + atat:latest \ + uwsgi \ + --callable app \ + --module app \ + --plugin python3 \ + --virtualenv /opt/atat/atst/.venv \ + --http-socket :8000 + - run: + name: Wait for containers + command: sleep 3 + - run: + name: Create database + command: docker exec postgres createdb -U postgres atat + - run: + name: Apply migrations + command: docker exec test-atat .venv/bin/python .venv/bin/alembic upgrade head + - run: + name: Execute Ghost Inspector test suite + command: | + docker pull ghostinspector/test-runner-standalone:latest + docker run \ + -e NGROK_TOKEN=$NGROK_TOKEN \ + -e GI_API_KEY=$GI_API_KEY \ + -e GI_SUITE=$GI_SUITE \ + -e GI_PARAMS_JSON='{}' \ + -e APP_PORT="test-atat:8000" \ + --network atat \ + ghostinspector/test-runner-standalone:latest + workflows: version: 2 run-tests: @@ -143,9 +204,12 @@ workflows: - test: requires: - app_setup - - azure-build-and-push-image: + - integration-tests: requires: - test + - azure-build-and-push-image: + requires: + - integration-tests filters: branches: only: @@ -204,7 +268,7 @@ workflows: repo: atat tag: "atat-${CIRCLE_SHA1},latest" requires: - - test + - integration-tests filters: branches: only: diff --git a/.gitignore b/.gitignore index 36a8261e..b50dc9ba 100644 --- a/.gitignore +++ b/.gitignore @@ -52,10 +52,12 @@ ssl/client-certs/*.srl # je coverage output coverage - -# selenium testing +# BrowserStack browserstacklocal +# decompiled CircleCI yaml +local-ci.yml + # python config .python-version diff --git a/.secrets.baseline b/.secrets.baseline index 87e6414b..a71319f1 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2019-10-03T14:34:50Z", + "generated_at": "2019-10-14T19:14:26Z", "plugins_used": [ { "base64_limit": 4.5, @@ -40,6 +40,13 @@ "is_verified": false, "line_number": 156, "type": "Secret Keyword" + }, + { + "hashed_secret": "81b127e2222d9bfc4609053faec85300f7525463", + "is_secret": false, + "is_verified": false, + "line_number": 244, + "type": "Secret Keyword" } ], "alembic.ini": [ @@ -153,24 +160,6 @@ "type": "Private Key" } ], - "tests/acceptance/conftest.py": [ - { - "hashed_secret": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Basic Auth Credentials" - } - ], - "tests/fixtures/chain/make-chain.sh": [ - { - "hashed_secret": "bad2e396920ce37fe53fc291f90b130d915375fb", - "is_secret": false, - "is_verified": false, - "line_number": 35, - "type": "Secret Keyword" - } - ], "tests/forms/test_validators.py": [ { "hashed_secret": "260408f687da9094705a841acda9b029563053ee", @@ -194,7 +183,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 651, + "line_number": 656, "type": "Hex High Entropy String" } ] diff --git a/README.md b/README.md index bfb1a016..15a4f102 100644 --- a/README.md +++ b/README.md @@ -220,30 +220,37 @@ To generate coverage reports for the Javascript tests: yarn test:coverage -### Selenium Tests +### Ghost Inspector Tests -Selenium tests rely on BrowserStack. In order to run the Selenium tests -locally, you need BrowserStack credentials. The user email and key can -be found on the account settings page. To run the selenium tests: +AT-AT uses [Ghost Inpsector](https://app.ghostinspector.com/) for +integration testing. These tests do not run locally as part of the +regular test suite but do run in CI. To run them locally, you will +need the following: + +- [docker](https://docs.docker.com/v17.12/install/) +- [circleci CLI tool](https://circleci.com/docs/2.0/local-cli/#installation) +- the prerequisite variable information listed [here](https://ghostinspector.com/docs/integration/circle-ci/) + +The version of our CircleCI config (2.1) is incompatible with the +`circleci` tool. First run: ``` -BROWSERSTACK_TOKEN= BROWSERSTACK_EMAIL= ./script/selenium_test +circleci config process .circleci/config.yml > local-ci.yml ``` -The selenium tests are in `tests/acceptance`. This directory is ignored by -pytest for normal test runs. - -The `selenium_test` script manages the setup of a separate database and -launching the BrowserStackLocal client. If you already have the client running -locally, you can run the selenium tests with: +Then run the job: ``` -BROWSERSTACK_TOKEN= BROWSERSTACK_EMAIL= pipenv run pytest tests/acceptance +circleci local execute -e GI_SUITE= -e GI_API_KEY= -e NGROK_TOKEN= --job integration-tests -c local-ci.yml ``` -The BrowserStack email is the one associated with the account. The token is -available in the BrowserStack profile information page. Go to the dashboard, -then "Account" > "Settings", then the token is under "Local Testing". +If the job fails and you want to re-run it, you may receive errors +about running docker containers or the network already existing. +Some version of the following should reset your local docker state: + +``` +docker container stop redis postgres test-atat; docker container rm redis postgres test-atat ; docker network rm atat +``` ## Notes diff --git a/tests/acceptance/__init__.py b/tests/acceptance/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/acceptance/browsers.py b/tests/acceptance/browsers.py deleted file mode 100644 index f9c30fcd..00000000 --- a/tests/acceptance/browsers.py +++ /dev/null @@ -1,18 +0,0 @@ -BROWSERSTACK_CONFIG = { - "win7_ie10": { - "browser": "IE", - "browser_version": "10.0", - "os": "Windows", - "os_version": "7", - "resolution": "1024x768", - "browserstack.local": True, - }, - "win10_chrome62": { - "browser": "Chrome", - "browser_version": "62.0", - "os": "Windows", - "os_version": "10", - "resolution": "1024x768", - "browserstack.local": True, - }, -} diff --git a/tests/acceptance/conftest.py b/tests/acceptance/conftest.py deleted file mode 100644 index 29aa2953..00000000 --- a/tests/acceptance/conftest.py +++ /dev/null @@ -1,65 +0,0 @@ -import os -import pytest -import logging -from collections import Mapping -from selenium import webdriver -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities - -from .browsers import BROWSERSTACK_CONFIG - - -@pytest.fixture(scope="function", autouse=True) -def session(db, request): - """ - Override base test session - """ - pass - - -class DriverCollection(Mapping): - """ - Allows access to drivers with dictionary syntax. Keeps track of which ones - have already been initialized. Allows teardown of all existing drivers. - """ - - def __init__(self): - self._drivers = {} - - def __iter__(self): - return iter(self._drivers) - - def __len__(self): - return len(self._drivers) - - def __getitem__(self, name): - if name in self._drivers: - return self._drivers[name] - - elif name in BROWSERSTACK_CONFIG: - self._drivers[name] = self._build_driver(name) - return self._drivers[name] - - else: - raise AttributeError("Driver {} not found".format(name)) - - def _build_driver(self, config_key): - return webdriver.Remote( - command_executor="http://{}:{}@hub.browserstack.com:80/wd/hub".format( - os.getenv("BROWSERSTACK_EMAIL"), os.getenv("BROWSERSTACK_TOKEN") - ), - desired_capabilities=BROWSERSTACK_CONFIG.get(config_key), - ) - - def teardown(self): - for driver in self._drivers.values(): - driver.quit() - - -@pytest.fixture(scope="session") -def drivers(): - driver_collection = DriverCollection() - - yield driver_collection - - driver_collection.teardown() diff --git a/tests/acceptance/test_basic.py b/tests/acceptance/test_basic.py deleted file mode 100644 index a130d1fc..00000000 --- a/tests/acceptance/test_basic.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest -import requests -from flask import url_for -from urllib.parse import urljoin -from .browsers import BROWSERSTACK_CONFIG -from atst.domain.users import Users -import atst.domain.exceptions as exceptions -from atst.routes.dev import _DEV_USERS as DEV_USERS -from tests.test_auth import _login - -import cryptography.x509 as x509 -from cryptography.hazmat.backends import default_backend - - -USER_CERT = "ssl/client-certs/atat.mil.crt" - - -@pytest.mark.parametrize("browser_type", BROWSERSTACK_CONFIG.keys()) -@pytest.mark.usefixtures("live_server") -def test_can_get_title(browser_type, app, drivers): - driver = drivers[browser_type] - driver.get(url_for("atst.root", _external=True)) - assert "JEDI" in driver.title - - -def _get_common_name(cert_path): - with open(USER_CERT, "rb") as cert_file: - cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend()) - common_names = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME) - return common_names[0].value - - -@pytest.fixture(scope="module") -def valid_user_from_cert(): - cn = _get_common_name(USER_CERT) - cn_parts = cn.split(".") - user_info = { - "last_name": cn_parts[0], - "first_name": cn_parts[1], - "dod_id": cn_parts[-1], - "atat_role_name": "developer", - } - return Users.get_or_create_by_dod_id(**user_info) - - -@pytest.mark.usefixtures("live_server") -def test_login(drivers, client, app, valid_user_from_cert): - driver = drivers["win7_ie10"] - driver.get(url_for("dev.login_dev", _external=True)) - assert "Sign in" not in driver.title