Add support for E2E (end-to-end) tests (#625)

This changeset lays the foundation for supporting E2E (end-to-end) integration tests for US Notify.  It brings in the Playwright testing framework along with the Playwright pytest plugin to make this possible, and includes the following adjustments:

- A new test session fixture for ensuring that Playwright authenticates with the sites that are currently behind HTTP Auth (requies env-var config)
- A new end_to_end test directory specifically for E2E tests
- Updates to the Makefile that make sure E2E tests are not run as a part of the normal test routine but can be run separately
- A new command in the Makefile to run E2E tests that will run in Chromium, Firefox, and Webkit headless browsers

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
This commit is contained in:
Carlo Costino
2023-07-28 09:31:45 -04:00
committed by GitHub
parent e3e11327da
commit b5664c3d20
8 changed files with 385 additions and 21 deletions

View File

@@ -41,7 +41,13 @@ jobs:
- name: Run js tests
run: npm test
- name: Run py tests with coverage
run: pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10
run: pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
- name: Run E2E tests
run: pipenv run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
env:
NOTIFY_STAGING_HTTP_AUTH_PASSWORD: ${{ secrets.NOTIFY_STAGING_HTTP_AUTH_PASSWORD }}
NOTIFY_STAGING_HTTP_AUTH_USER: ${{ secrets.NOTIFY_STAGING_HTTP_AUTH_USER }}
NOTIFY_STAGING_URI: ${{ secrets.NOTIFY_STAGING_URI }}
- name: Check coverage threshold
run: pipenv run coverage report --fail-under=90

View File

@@ -16,6 +16,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
.PHONY: bootstrap
bootstrap: generate-version-file ## Set up everything to run the app
pipenv install --dev
pipenv run playwright install --with-deps
source $(NVMSH) --no-use && nvm install && npm ci --no-audit
source $(NVMSH) && npm run build
@@ -54,10 +55,15 @@ py-lint: ## Run python linting scanners
.PHONY: py-test
py-test: export NEW_RELIC_ENVIRONMENT=test
py-test: ## Run python unit tests
pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 tests/
pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
pipenv run coverage report --fail-under=90
pipenv run coverage html -d .coverage_cache
.PHONY: e2e-test
e2e-test: export NEW_RELIC_ENVIRONMENT=test
e2e-test: ## Run end-to-end integration tests
pipenv run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
.PHONY: js-lint
js-lint: ## Run javascript linting scanners
source $(NVMSH) && npm run lint

View File

@@ -37,6 +37,7 @@ newrelic = "*"
flask-talisman = "*"
notifications-utils = {editable = true, ref = "main", git = "https://github.com/GSA/notifications-utils.git"}
coverage = "*"
pytest-playwright = "*"
[dev-packages]
isort = "==5.12.0"

127
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "7d90c44fa5dd863a7ebab1ec4fa02b8d5dc11050235d7bad87ee6cfde791720e"
"sha256": "e6e49305bdee7cdd4605b8f6f7d0135cfe91e0c038abe6efdfa1784194ce3cdd"
},
"pipfile-spec": 6,
"requires": {
@@ -50,19 +50,19 @@
},
"boto3": {
"hashes": [
"sha256:cfcb20d5784428f31d89889e68b26efeda90f231c3119eef4af8b25ad405c55f",
"sha256:d5ac6599951fdd519ed26c6fe15c41a7aa4021cb9adce33167344f8ce5cdb07b"
"sha256:b2d178c8a56fe3e4c9b123dccdff20e9555d12a597b72627fa659aa6295e238a",
"sha256:db6443fd2c65d9f35f671b03bacb0592b62d06884395ed65d75922ccddc34c2e"
],
"markers": "python_version >= '3.7'",
"version": "==1.28.12"
"version": "==1.28.13"
},
"botocore": {
"hashes": [
"sha256:7e5db466c762a071bb58c9a39d070f1333ce4f4ba6fdf9820ba21e87bd4c7e29",
"sha256:86380672151866b5e425636e3ebad74f2b83e7163e36ef5d38d11a04b9cba33b"
"sha256:78b96afbd88b8bd4c0967611a4cedddd9ea33d8601309dc351f81cbb5479d976",
"sha256:9a5080ea2a444f0447a7a1a79f64252ae2a1417b6c13a54656ee991cb610dd4e"
],
"markers": "python_version >= '3.7'",
"version": "==1.31.12"
"version": "==1.31.13"
},
"cachetools": {
"hashes": [
@@ -235,7 +235,7 @@
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
],
"markers": "python_version >= '3.7'",
"markers": "python_full_version >= '3.7.0'",
"version": "==3.2.0"
},
"click": {
@@ -556,6 +556,14 @@
"markers": "python_version < '3.10'",
"version": "==6.8.0"
},
"iniconfig": {
"hashes": [
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.0"
},
"itsdangerous": {
"hashes": [
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
@@ -826,6 +834,14 @@
],
"version": "==2.0.3"
},
"packaging": {
"hashes": [
"sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
"sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
],
"markers": "python_version >= '3.7'",
"version": "==23.1"
},
"phonenumbers": {
"hashes": [
"sha256:89671217c706cbaa3ced101deefafa779836feac3e059434d886ac31f09f32c0",
@@ -833,6 +849,27 @@
],
"version": "==8.13.17"
},
"playwright": {
"hashes": [
"sha256:428a719a6c7e40781c19860ed813840ac2d63678f7587abe12e800ea030d4b7e",
"sha256:4e396853034742b76654cdab27422155d238f46e4dc6369ea75854fafb935586",
"sha256:72e80076e595f5fcd8ebd89bf6635ad78e4bafa633119faed8b2568d17dbd398",
"sha256:84213339f179fd2a70f77ea7faea0616d74871349d556c53a1ecb7dd5097973c",
"sha256:89ca2261bb00b67d3dff97691cf18f4347ee0529a11e431e47df67b703d4d8fa",
"sha256:b7c6ddfca2b141b0385387cc56c125b14ea867902c39e3fc650ddd6c429b17da",
"sha256:ffbb927679b62fad5071439d5fe0840af46ad1844bc44bf80e1a0ad706140c98"
],
"markers": "python_version >= '3.8'",
"version": "==1.36.0"
},
"pluggy": {
"hashes": [
"sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
"sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
],
"markers": "python_version >= '3.7'",
"version": "==1.2.0"
},
"prometheus-client": {
"hashes": [
"sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091",
@@ -848,6 +885,13 @@
],
"version": "==2.21"
},
"pyee": {
"hashes": [
"sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32",
"sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5"
],
"version": "==9.0.4"
},
"pyexcel": {
"hashes": [
"sha256:ddc6904512bfa2ecda509fb3b58229bb30db14498632fd9e7a5ba7bbfb02ed1b",
@@ -942,6 +986,30 @@
"index": "pypi",
"version": "==3.6.0"
},
"pytest": {
"hashes": [
"sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32",
"sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"
],
"markers": "python_version >= '3.7'",
"version": "==7.4.0"
},
"pytest-base-url": {
"hashes": [
"sha256:e1e88a4fd221941572ccdcf3bf6c051392d2f8b6cef3e0bc7da95abec4b5346e",
"sha256:ed36fd632c32af9f1c08f2c2835dcf42ca8fcd097d6ed44a09f253d365ad8297"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==2.0.0"
},
"pytest-playwright": {
"hashes": [
"sha256:83a896b1b28bfaa081ca9ea27229a06a114e106e2e62fb3d5f06544748fbc1fe",
"sha256:9bf79c633c97dd1405308b8d3600e6c8c2a200a733e2f36c5a150ba4701936f8"
],
"index": "pypi",
"version": "==0.3.3"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
@@ -966,6 +1034,14 @@
"markers": "python_version >= '3.6'",
"version": "==2.0.7"
},
"python-slugify": {
"hashes": [
"sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395",
"sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"
],
"markers": "python_version >= '3.7'",
"version": "==8.0.1"
},
"pytz": {
"hashes": [
"sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588",
@@ -1118,6 +1194,13 @@
],
"version": "==2.0.1"
},
"text-unidecode": {
"hashes": [
"sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
"sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
],
"version": "==1.3"
},
"texttable": {
"hashes": [
"sha256:290348fb67f7746931bcdfd55ac7584ecd4e5b0846ab164333f0794b121760f2",
@@ -1125,6 +1208,14 @@
],
"version": "==1.6.7"
},
"tomli": {
"hashes": [
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
"markers": "python_version < '3.11'",
"version": "==2.0.1"
},
"typing-extensions": {
"hashes": [
"sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36",
@@ -1215,19 +1306,19 @@
},
"boto3": {
"hashes": [
"sha256:cfcb20d5784428f31d89889e68b26efeda90f231c3119eef4af8b25ad405c55f",
"sha256:d5ac6599951fdd519ed26c6fe15c41a7aa4021cb9adce33167344f8ce5cdb07b"
"sha256:b2d178c8a56fe3e4c9b123dccdff20e9555d12a597b72627fa659aa6295e238a",
"sha256:db6443fd2c65d9f35f671b03bacb0592b62d06884395ed65d75922ccddc34c2e"
],
"markers": "python_version >= '3.7'",
"version": "==1.28.12"
"version": "==1.28.13"
},
"botocore": {
"hashes": [
"sha256:7e5db466c762a071bb58c9a39d070f1333ce4f4ba6fdf9820ba21e87bd4c7e29",
"sha256:86380672151866b5e425636e3ebad74f2b83e7163e36ef5d38d11a04b9cba33b"
"sha256:78b96afbd88b8bd4c0967611a4cedddd9ea33d8601309dc351f81cbb5479d976",
"sha256:9a5080ea2a444f0447a7a1a79f64252ae2a1417b6c13a54656ee991cb610dd4e"
],
"markers": "python_version >= '3.7'",
"version": "==1.31.12"
"version": "==1.31.13"
},
"cachecontrol": {
"extras": [
@@ -1395,7 +1486,7 @@
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
],
"markers": "python_version >= '3.7'",
"markers": "python_full_version >= '3.7.0'",
"version": "==3.2.0"
},
"cryptography": {
@@ -1782,7 +1873,7 @@
"sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526",
"sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3"
],
"markers": "python_version >= '3.6'",
"markers": "python_full_version >= '3.6.0'",
"version": "==32.0.1"
},
"pluggy": {
@@ -1845,7 +1936,7 @@
"sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32",
"sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==7.4.0"
},
"pytest-env": {
@@ -1955,7 +2046,7 @@
"sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec",
"sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898"
],
"markers": "python_version >= '3.7'",
"markers": "python_full_version >= '3.7.0'",
"version": "==13.4.2"
},
"s3transfer": {

View File

@@ -15,7 +15,9 @@ The [Notify API](https://github.com/GSA/notifications-api) provides the UI's bac
### Common steps
1. Install pre-requisites for setup:
1. Install pre-requisites for setup (on a Mac):
- Install XCode or at least the XCode Command Line Tools
- [Homebrew](https://brew.sh/) (follow instructions on page)
- [jq](https://stedolan.github.io/jq/): `brew install jq`
- [terraform](https://www.terraform.io/): `brew install terraform` or `brew install tfenv` and use `tfenv` to install `terraform ~> 1.4.0`
- [cf-cli@8](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html): `brew install cloudfoundry/tap/cf-cli@8`
@@ -44,6 +46,23 @@ The [Notify API](https://github.com/GSA/notifications-api) provides the UI's bac
`make bootstrap`
If you run into certificate errors at the `playwright install` step, try doing this:
1. Run `brew --prefix` to see Homebrew's root directory
1. Create or modify the local `.env` file in the project and add this line:
`NODE_EXTRA_CA_CERTS=/CHANGE-TO-HOMEBREW-INSTALL-PATH/etc/ca-certificates/cert.pem`
Make sure to change `CHANGE-TO-HOMEBREW-INSTALL-PATH` to the path given by `brew --prefix` in the step above.
For example, if `brew --prefix` gave `/opt/homebrew` as output, then the line would look like this:
`NODE_EXTRA_CA_CERTS=/opt/homebrew/etc/ca-certificates/cert.pem`
1. Save the changes to the `.env` file
1. Run `make bootstrap` again
1. Run the Flask server
`make run-flask`

120
docs/end_to_end_tests.md Normal file
View File

@@ -0,0 +1,120 @@
# Working with End-to-End Tests
End-to-End (E2E) tests are an important part of a assessing the overall
integrity and stability of a system. They are a part of the overall
test suite and serve the function of simulating a user working through
the application. By having comprehensive E2E tests in place, we can
instill higher confidence that future changes and refactorings won't
negatively impact any user experience or break existing functionality.
The US Notify project leverages [`pytest`](https://pytest.org/) for its
existing test suite (at least on the Python side of things) and is now
leveraging [Playwright for Python](https://playwright.dev/python/)
along with its `pytest` plugin for the E2E tests.
## Getting Started
To work with the E2E tests in US Notify, you need to make sure you have
all of the necessary components installed. The quick and easy way to do
this is to use the Makefile as you did for the initial project setup. In
fact, if you've already done this, you are already set to go! If not,
then run the bootstrap command in your shell:
```sh
make bootstrap
```
This takes care of installing all of your dependencies, including those
now needed for Playwright.
If you run into certificate errors at the `playwright install` step, try
doing this:
1. Run `brew --prefix` to see Homebrew's root directory
1. Create or modify the local `.env` file in the project and add this line:
`NODE_EXTRA_CA_CERTS=/CHANGE-TO-HOMEBREW-INSTALL-PATH/etc/ca-certificates/cert.pem`
Make sure to change `CHANGE-TO-HOMEBREW-INSTALL-PATH` to the path
given by `brew --prefix` in the step above. For example, if `brew --prefix`
gave `/opt/homebrew` as output, then the line would look like this:
`NODE_EXTRA_CA_CERTS=/opt/homebrew/etc/ca-certificates/cert.pem`
1. Save the changes to the `.env` file
1. Run `make bootstrap` again
### Manual Installation
If you need to install things separately, you'll still need to make sure
your environment is set up and configured as outlined in the README.
At your shell in the project root folder, run the following commands:
```sh
pipenv install pytest-playwright
pipenv run playwright install --with-deps
```
This will install Playwright and its `pytest` plugin, then the
additional dependencies that Playwright requires.
See more details on the [Playwright for Python Installation page](https://playwright.dev/python/docs/intro).
## Local Configuration
In order to run the E2E tests successfully on your local machine, you'll also
need to make sure you have a `.env` file in the root project folder, and that it
has at least these environment variables set in it:
```
NOTIFY_STAGING_URI
NOTIFY_STAGING_HTTP_AUTH_USER
NOTIFY_STAGING_HTTP_AUTH_PASSWORD
```
This file is **not** checked into source control and is configured to be
ignored in the project's `.gitignore` file; please be careful that it is
not committed to the repo and pushed!
## Running E2E Tests Locally
To run the E2E tests on your local machine, type this command in your
shell at the project root directory:
```sh
make e2e-test
```
You should see `pytest` start producing output and the existing E2E
tests run in multiple headless browsers.
## How to Create and Maintain E2E Tests
All of the E2E tests are found in the `tests/end_to_end` folder and are
written as `pytest` scripts using [Playwright's Python Framework](https://playwright.dev/python/docs/writing-tests).
## Maintaining E2E Tests with GitHub
The E2E tests are configured to run as a separate GitHub action as a
part of our other checks found in `.github/workflows/checks.yml`.
The E2E tests are not run as a part of the regular unit test suite; if
you look at the `Makefile` you'll see that the tests are two separate
commands, with the E2E tests configured separately.
This is done for a couple of reasons:
- Keeps unit tests isolated from the E2E tests
- Allows us to configure E2E tests separately
The environment variables are managed as a part of the GitHub
repository settings.

View File

@@ -3384,3 +3384,15 @@ def webauthn_credential_2():
'registration_response': 'stuff',
'created_at': '2021-05-14T16:57:14.154185Z',
}
@pytest.fixture(scope='session')
def end_to_end_auth_context(browser):
# Create a context with HTTP Authentication credentials for Playwright E2E
# tests.
context = browser.new_context(http_credentials={
'username': os.environ.get('NOTIFY_STAGING_HTTP_AUTH_USER'),
'password': os.environ.get('NOTIFY_STAGING_HTTP_AUTH_PASSWORD'),
})
yield context

View File

@@ -0,0 +1,109 @@
import os
import re
from playwright.sync_api import expect
def test_landing_page(end_to_end_auth_context):
# Open a new page and go to the staging site.
page = end_to_end_auth_context.new_page()
page.goto(os.environ.get('NOTIFY_STAGING_URI'))
# Check the page title exists and matches what we expect.
expect(page).to_have_title(re.compile('U.S. Notify'))
# Retrieve some prominent elements on the page for testing.
main_header = page.get_by_role(
'heading',
name='Send text messages to your participants'
)
sign_in_button = page.get_by_role('link', name='Sign in')
benefits_studio_email = page.get_by_role(
'link',
name='tts-benefits-studio@gsa.gov'
)
# Check to make sure the elements are visible.
expect(main_header).to_be_visible()
expect(sign_in_button).to_be_visible()
expect(benefits_studio_email).to_be_visible()
# Check to make sure the sign-in button and email links are correct.
expect(sign_in_button).to_have_attribute('href', '/sign-in')
expect(benefits_studio_email).to_have_attribute(
'href',
'mailto:tts-benefits-studio@gsa.gov'
)
# Retrieve all other main content headers and check that they're
# visible.
content_headers = [
'Control your content',
'See how your messages perform',
'No technical integration needed',
'About the product',
]
for content_header in content_headers:
expect(
page.get_by_role('heading', name=re.compile(content_header))
).to_be_visible()
def test_sign_in_page(end_to_end_auth_context):
# Open a new page and go to the staging site.
page = end_to_end_auth_context.new_page()
page.goto(os.environ.get('NOTIFY_STAGING_URI'))
sign_in_button = page.get_by_role('link', name='Sign in')
# Test trying to sign in.
sign_in_button.click()
# Check the page title exists and matches what we expect.
# NOTE: The dash is a special character! It had to be copied from
# the template itself.
# TODO: Improve this check, or change it so no special character is
# needed. Better yet, fix the template(s) character too.
expect(page).to_have_title(re.compile('Sign in U.S. Notify'))
# Check for the sign in heading.
sign_in_heading = page.get_by_role('heading', name='Sign in')
expect(sign_in_heading).to_be_visible()
# Check for the sign in form elements.
# NOTE: Playwright cannot find input elements by role and recommends using
# get_by_label() instead; however, hidden form elements do not have
# labels associated with them, hence the XPath!
# See https://playwright.dev/python/docs/api/class-page#page-get-by-label
# and https://playwright.dev/python/docs/locators#locate-by-css-or-xpath
# for more information.
email_address_input = page.get_by_label('Email address')
password_input = page.get_by_label('Password')
csrf_token = page.locator('xpath=//input[@name="csrf_token"]')
continue_button = page.get_by_role('button', name=re.compile('Continue'))
forgot_password_link = page.get_by_role(
'link',
name='Forgot your password?'
)
# Make sure form elements are visible and not visible as expected.
expect(email_address_input).to_be_visible()
expect(password_input).to_be_visible()
expect(continue_button).to_be_visible()
expect(forgot_password_link).to_be_visible()
expect(csrf_token).to_be_hidden()
# Make sure form elements are configured correctly with the right
# attributes.
expect(email_address_input).to_have_attribute('type', 'email')
expect(password_input).to_have_attribute('type', 'password')
expect(csrf_token).to_have_attribute('type', 'hidden')
expect(continue_button).to_have_attribute('type', 'submit')
expect(forgot_password_link).to_have_attribute(
'href',
'/forgot-password'
)
# TODO: Figure out how to actually sign in...