diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c30ef6c57..a12624dcf 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -47,9 +47,12 @@ jobs: - 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 }} + NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }} + NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }} + NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD }} + NOTIFY_E2E_TEST_HTTP_AUTH_USER: ${{ secrets.NOTIFY_E2E_TEST_HTTP_AUTH_USER }} + NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }} + NOTIFY_E2E_TEST_URI: ${{ secrets.NOTIFY_E2E_TEST_URI }} - name: Check coverage threshold run: pipenv run coverage report --fail-under=90 diff --git a/.gitignore b/.gitignore index f01baa32a..77e846db7 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,6 @@ app/templates/vendor secrets.auto.tfvars terraform.tfstate terraform.tfstate.backup + +# Playwright +playwright/ diff --git a/Makefile b/Makefile index 3bd0b715e..aa981c8e9 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,9 @@ dead-code: .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 + rm -rf playwright + mkdir -p playwright/.auth + pipenv run pytest -vv --browser chromium --browser firefox --browser webkit tests/end_to_end .PHONY: js-lint js-lint: ## Run javascript linting scanners diff --git a/Pipfile b/Pipfile index 5da402ebe..c6e2e2846 100644 --- a/Pipfile +++ b/Pipfile @@ -36,7 +36,6 @@ newrelic = "*" flask-talisman = "*" notifications-utils = {editable = true, ref = "main", git = "https://github.com/GSA/notifications-utils.git"} coverage = "*" -pytest-playwright = "*" vulture = "==2.7" radon = "==6.0.1" diff --git a/Pipfile.lock b/Pipfile.lock index 819f0bfb8..bc5edb7bd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9fc8c5c0307fc5824eaa56ed470c0495229b902957ceeb1a1ac41fbb2d020b4e" + "sha256": "199a6207203e5f15979e65959f2521226ce0d817f9530cc1dacf688e864ded4f" }, "pipfile-spec": 6, "requires": { @@ -50,19 +50,19 @@ }, "boto3": { "hashes": [ - "sha256:b505faa126db84e226f6f8d242a798fae30a725f0cac8a76c6aca9ace4e8eb28", - "sha256:ed787f250ce2562c7744395bdf32b5a7bc9184126ef50a75e97bcb66043dccf3" + "sha256:07997e299e7b87afbbb25dc9de677017eafbd96b4f1b81e931d5127716dc6dd1", + "sha256:fafc0eda7ebe7878be2ab934558ea1776cbd1bd624ce9e9b827e304d301ccd00" ], "markers": "python_version >= '3.7'", - "version": "==1.28.32" + "version": "==1.28.33" }, "botocore": { "hashes": [ - "sha256:7a07d8dc8cc47bf23af39409ada81f388eb78233e1bb2cde0c415756da753664", - "sha256:8992ac186988c4b4cc168e8e479e9472da1442b193c1bf7c9dcd1877ec62d23c" + "sha256:1b76549c45f712ca9734888e60a2ab9c857e6e6025b156b36c344162a7e9d0dc", + "sha256:3fd7cb89cf834b28bc7e8427cb29bb861b10652a3bebe9d0d18d9a2c1e4f3f67" ], "markers": "python_version >= '3.7'", - "version": "==1.31.32" + "version": "==1.31.33" }, "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": { @@ -251,7 +251,7 @@ "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], - "markers": "python_version >= '3.5'", + "markers": "python_version > '3.4'", "version": "==0.4.6" }, "coverage": { @@ -549,14 +549,6 @@ "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", @@ -758,24 +750,24 @@ }, "newrelic": { "hashes": [ - "sha256:39a699f88042255634c88beff92c2fc10d9d522b6c989f52a6ae098823b51a02", - "sha256:56161cbaa97f93a807db4bd61436ff7757c8a896758d325b708567cca48bfd13", - "sha256:62c558a5dbfa8728cbb0addd199038c0e75feb79282a9e07c272a2acaf250094", - "sha256:6bd507fa91175cc558c810cefe2b7ee20d1143f88a7153e255d49d7ce12cd287", - "sha256:74a7b07153aaac65cefe21183ebbedc5dee7d0cd681ec27c5258499b7a4319b1", - "sha256:7555f4bd91bea441cd2a5dc94848034e4ab2ab068d30905288a789d3214b1a03", - "sha256:885c852dc88d9612fe3de1940246e9200a510a289b0cf56459494782096cdba4", - "sha256:930457ffd0cd5696f75cd0d761e550cecbba2eadc37c0db617c25b7c4a5d19e1", - "sha256:962ac353b66f2827b337af941b86dff2d61d1c7638e5cab950e995e53e52665c", - "sha256:9eb93901fd9caebc7f965812a7dea349d43b44d45d693ffb6ee6c4c40b5de78d", - "sha256:ae9b96bad4f6b92a45fe28d7303b747d36aa2f9ad545fd3fc51f7eac2a45f011", - "sha256:c4d87e6d517f7bc8bf5f982b6260ce1efc642b4fe5b83f23d11a69a9351d83c3", - "sha256:d0c15312c6cd73559f5b01fe018feaf018a4566210338fdc5616dfb4f1ce24f0", - "sha256:e7789b0340d04bbd31c76d42f2d5d064b47f90c09c74978e6175d77bcdd9e226", - "sha256:f9361fc81911e42c272eab640569cbce1849873f5c5c9d74c9a58a3b8bd23d2f" + "sha256:16761522dde9c05146078f7729aefbf05cea7fc42c703c0b756618168939f872", + "sha256:18dae1606f79a92c21967d11cbd4f42c03eb5b04b2f6b86a954b5e6a736113d7", + "sha256:1c9463dd1b1407ade27a4c7ebb2137b77c51e23498cec7ee2671fff3fd33fd73", + "sha256:2e70371c8b5fcfca6e7f57395f205ed6b38228b4a5594f49f7aae4d978aa9eef", + "sha256:313aa30918e54bfc226229d3b4fa7f8879fa5beab98ed46218ef36265b7205e5", + "sha256:41cf36017b1b54187c5b422fff32528abc4f2043227397f7a242864c31939c37", + "sha256:59cb3da7e5dd526e7f8696a5ed704d06c43bc2d4da840897534e9a51da7266eb", + "sha256:5d5a3921c73913ea91407a6b2cda4e68aa3e52d7ceba77365d9b090b184f7e15", + "sha256:5e32b7c92ed8922df599769270dbc5a322ca78e29b5c89083b2b39af80a44361", + "sha256:68fec20006b312a3c93de82e91cb39be44ac73ba08d15552fff5206dfabda227", + "sha256:7a649068a0cf5c4a7ba0f9739e52f1ad9d2882aafcfe3361fd264cbb975c5318", + "sha256:b8954dcfa78cb5d0398490938f3a84f29d53554bb498bb50bf232715a3577a68", + "sha256:cc169ad19cb3b44ec028df08d172327b72d0137d62935c87cbbef173f710c2ed", + "sha256:d95eade883fb3aa0a2a2d04f1dd85f0df04d31bfc32f78139fe91c0362c1987e", + "sha256:fc4e0fe04ad4f1fc19cb86844d73e834d99dd613dd8a4f2e7be204193d5b8dc8" ], "index": "pypi", - "version": "==8.10.1" + "version": "==8.11.0" }, "notifications-python-client": { "hashes": [ @@ -834,14 +826,6 @@ ], "version": "==2.0.3" }, - "packaging": { - "hashes": [ - "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" - ], - "markers": "python_version >= '3.7'", - "version": "==23.1" - }, "phonenumbers": { "hashes": [ "sha256:38180247697240ccedd74dec4bfbdbc22bb108b9c5f991f270ca3e41395e6f96", @@ -849,27 +833,6 @@ ], "version": "==8.13.19" }, - "playwright": { - "hashes": [ - "sha256:41f0280472af94c426e941f6a969ff6a7ea156dc15fd01d09ac4b8f092e2346e", - "sha256:428fdf9bfff586b73f96df53692d50d422afb93ca4650624f61e8181f548fed2", - "sha256:678b9926be2df06321d11a525d4bf08d9f4a5b151354a3b82fe2ac14476322d5", - "sha256:68d56efe5ce916bab349177e90726837a6f0cae77ebd6a5200f5333b787b25fb", - "sha256:8b5d96aae54289129ab19d3d0e2e431171ae3e5d88d49a10900dcbe569a27d43", - "sha256:b476f63251876f1625f490af8d58ec0db90b555c623b7f54105f91d33878c06d", - "sha256:b574889ef97b7f44a633aa10d72b8966a850a4354d915fd0bc7e8658e825dd63" - ], - "markers": "python_version >= '3.8'", - "version": "==1.37.0" - }, - "pluggy": { - "hashes": [ - "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", - "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" - ], - "markers": "python_version >= '3.7'", - "version": "==1.2.0" - }, "prometheus-client": { "hashes": [ "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091", @@ -885,13 +848,6 @@ ], "version": "==2.21" }, - "pyee": { - "hashes": [ - "sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32", - "sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5" - ], - "version": "==9.0.4" - }, "pyexcel": { "hashes": [ "sha256:ddc6904512bfa2ecda509fb3b58229bb30db14498632fd9e7a5ba7bbfb02ed1b", @@ -978,36 +934,12 @@ "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:68dd0069e2dbf8e6024c6237d51d0277626687d94ba0dd387cea2a94ae4005b9", - "sha256:9e9622e5507f8d27a3c7fd07aadcf43122f0086b69d1b3e6728995c8dbc0e44f" - ], - "index": "pypi", - "version": "==0.4.2" - }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "python-dotenv": { @@ -1026,14 +958,6 @@ "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", @@ -1185,7 +1109,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "smartypants": { @@ -1194,13 +1118,6 @@ ], "version": "==2.0.1" }, - "text-unidecode": { - "hashes": [ - "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", - "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" - ], - "version": "==1.3" - }, "texttable": { "hashes": [ "sha256:290348fb67f7746931bcdfd55ac7584ecd4e5b0846ab164333f0794b121760f2", @@ -1208,21 +1125,13 @@ ], "version": "==1.6.7" }, - "tomli": { + "toml": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", - "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" - ], - "markers": "python_version >= '3.7'", - "version": "==4.7.1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" }, "urllib3": { "hashes": [ @@ -1232,6 +1141,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.26.16" }, + "vulture": { + "hashes": [ + "sha256:67fb80a014ed9fdb599dd44bb96cb54311032a104106fc2e706ef7a6dad88032", + "sha256:bccc51064ed76db15a6b58277cea8885936af047f53d2655fb5de575e93d0bca" + ], + "index": "pypi", + "version": "==2.7" + }, "webencodings": { "hashes": [ "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", @@ -1306,19 +1223,19 @@ }, "boto3": { "hashes": [ - "sha256:b505faa126db84e226f6f8d242a798fae30a725f0cac8a76c6aca9ace4e8eb28", - "sha256:ed787f250ce2562c7744395bdf32b5a7bc9184126ef50a75e97bcb66043dccf3" + "sha256:07997e299e7b87afbbb25dc9de677017eafbd96b4f1b81e931d5127716dc6dd1", + "sha256:fafc0eda7ebe7878be2ab934558ea1776cbd1bd624ce9e9b827e304d301ccd00" ], "markers": "python_version >= '3.7'", - "version": "==1.28.32" + "version": "==1.28.33" }, "botocore": { "hashes": [ - "sha256:7a07d8dc8cc47bf23af39409ada81f388eb78233e1bb2cde0c415756da753664", - "sha256:8992ac186988c4b4cc168e8e479e9472da1442b193c1bf7c9dcd1877ec62d23c" + "sha256:1b76549c45f712ca9734888e60a2ab9c857e6e6025b156b36c344162a7e9d0dc", + "sha256:3fd7cb89cf834b28bc7e8427cb29bb861b10652a3bebe9d0d18d9a2c1e4f3f67" ], "markers": "python_version >= '3.7'", - "version": "==1.31.32" + "version": "==1.31.33" }, "cachecontrol": { "extras": [ @@ -1486,7 +1403,7 @@ "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.2.0" }, "cryptography": { @@ -1799,7 +1716,7 @@ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==0.7.0" }, "mdurl": { @@ -1812,11 +1729,11 @@ }, "moto": { "hashes": [ - "sha256:545afeb4df94dfa730e2d7e87366dc26b4a33c2891f462cbb049f040c80ed1ec", - "sha256:7d3bd748a34641715ba469c761f72fb8ec18f349987c98f5a0f9be85a07a9911" + "sha256:00fbae396fc48c3596e47b4e3267c1a41ca01c968de023beb68e774c63910b58", + "sha256:e4835912f05627b6a53b938562b717122230fb038d023819133f8526f60ed0a7" ], "index": "pypi", - "version": "==4.1.14" + "version": "==4.2.0" }, "msgpack": { "hashes": [ @@ -2022,7 +1939,7 @@ "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" ], - "markers": "python_version >= '3.7'", + "index": "pypi", "version": "==7.4.0" }, "pytest-base-url": { @@ -2070,7 +1987,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "python-slugify": { @@ -2156,7 +2073,7 @@ "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808", "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==13.5.2" }, "s3transfer": { @@ -2172,7 +2089,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "smmap": { @@ -2180,7 +2097,7 @@ "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==5.0.0" }, "sortedcontainers": { @@ -2218,7 +2135,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==0.10.2" }, "tomli": { diff --git a/docs/end_to_end_tests.md b/docs/end_to_end_tests.md index 906200a1c..0f4947133 100644 --- a/docs/end_to_end_tests.md +++ b/docs/end_to_end_tests.md @@ -33,7 +33,8 @@ 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: +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` @@ -56,26 +57,31 @@ 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 install pytest-playwright --dev 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). +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: +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 +# E2E Test Configuration - only set for the Admin site. +NOTIFY_E2E_TEST_URI +NOTIFY_E2E_TEST_HTTP_AUTH_USER # This is optional +NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD # This is optional +NOTIFY_E2E_TEST_EMAIL +NOTIFY_E2E_TEST_PASSWORD +NOTIFY_E2E_AUTH_STATE_PATH ``` This file is **not** checked into source control and is configured to be @@ -99,7 +105,8 @@ 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). +written as `pytest` scripts using +[Playwright's Python Framework](https://playwright.dev/python/docs/writing-tests). ## Maintaining E2E Tests with GitHub @@ -117,4 +124,22 @@ This is done for a couple of reasons: - Allows us to configure E2E tests separately The environment variables are managed as a part of the GitHub -repository settings. +environment and repository settings. + + +### E2E Environment Variable Management + +These are the E2E test environment variables that must be set: + +``` +NOTIFY_E2E_TEST_URI +NOTIFY_E2E_TEST_HTTP_AUTH_USER # This is optional +NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD # This is optional +NOTIFY_E2E_TEST_EMAIL +NOTIFY_E2E_TEST_PASSWORD +NOTIFY_E2E_AUTH_STATE_PATH +``` + +These are only set for the Admin site in GitHub, but must be set +for both GitHub Actions and Dependabot for the same reason as +the MFA environment variables. diff --git a/sample.env b/sample.env index 78956aa09..cb886c5d5 100644 --- a/sample.env +++ b/sample.env @@ -14,6 +14,17 @@ NODE_VERSION=16.15.1 ############################################################# +# E2E Testing + +NOTIFY_E2E_TEST_URI=http://localhost:6012/ +#NOTIFY_E2E_TEST_HTTP_AUTH_USER="this is optional" +#NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD="this is optional - don't write secrets to the sample file" +NOTIFY_E2E_TEST_EMAIL=fake.user@example.com +NOTIFY_E2E_TEST_PASSWORD="don't write secrets to the sample file" +NOTIFY_E2E_AUTH_STATE_PATH=playwright/.auth/ + +############################################################# + # Local Docker setup # API_HOST_NAME=http://dev:6011 # REDIS_URL=redis://adminredis:6379/0 diff --git a/tests/conftest.py b/tests/conftest.py index 946cf4984..866b26f42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import copy import json import os +import re from contextlib import contextmanager from datetime import date, datetime, timedelta from unittest.mock import Mock, PropertyMock @@ -3347,13 +3348,94 @@ def mock_get_invited_org_user_by_id(mocker, sample_org_invite): ) +def login_for_end_to_end_testing(browser): + # Open a new page and go to the staging site. + context = browser.new_context() + page = context.new_page() + page.goto(os.getenv('NOTIFY_E2E_TEST_URI')) + + sign_in_button = page.get_by_role('link', name='Sign in') + + # Test trying to sign in. + sign_in_button.click() + + # Wait for the next page to fully load. + page.wait_for_load_state('domcontentloaded') + + # 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') + continue_button = page.get_by_role('button', name=re.compile('Continue')) + + # Sign in to the site. + email_address_input.fill(os.getenv('NOTIFY_E2E_TEST_EMAIL')) + password_input.fill(os.getenv('NOTIFY_E2E_TEST_PASSWORD')) + continue_button.click() + + # Wait for the next page to fully load. + page.wait_for_load_state('domcontentloaded') + + # 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. + # mfa_input = page.get_by_label('Text message code') + # continue_button = page.get_by_role('button', name=re.compile('Continue')) + + # # Enter MFA code and continue. + # TODO: Revisit this at a later point in time. + # totp = pyotp.TOTP( + # os.getenv('MFA_TOTP_SECRET'), + # digits=int(os.getenv('MFA_TOTP_LENGTH')) + # ) + + # mfa_input.fill(totp.now()) + # continue_button.click() + + # page.wait_for_load_state('domcontentloaded') + + # # Save storage state into the file. + # auth_state_path = os.path.join( + # os.getenv('NOTIFY_E2E_AUTH_STATE_PATH'), + # 'state.json' + # ) + # context.storage_state(path=auth_state_path) + + @pytest.fixture(scope='session') -def end_to_end_auth_context(browser): +def end_to_end_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'), - }) + # tests, if the environment variables exist. + if os.getenv('NOTIFY_E2E_TEST_HTTP_AUTH_USER'): + context = browser.new_context(http_credentials={ + 'username': os.getenv('NOTIFY_E2E_TEST_HTTP_AUTH_USER'), + 'password': os.getenv('NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD'), + }) + else: + context = browser.new_context() + + yield context + + +@pytest.fixture(scope='session') +def end_to_end_authenticated_context(browser): + # Create and load a previously authenticated context for Playwright E2E + # tests. + login_for_end_to_end_testing(browser) + + auth_state_path = os.path.join( + os.getenv('NOTIFY_E2E_AUTH_STATE_PATH'), + 'state.json' + ) + context = browser.new_context(storage_state=auth_state_path) yield context diff --git a/tests/end_to_end/test_accounts_page.py b/tests/end_to_end/test_accounts_page.py new file mode 100644 index 000000000..2073d203b --- /dev/null +++ b/tests/end_to_end/test_accounts_page.py @@ -0,0 +1,122 @@ +import datetime +import os +import re + +import pytest +from playwright.sync_api import expect + + +@pytest.mark.skip(reason='Not authenticating test users.') +def test_accounts_page(end_to_end_authenticated_context): + # Open a new page and go to the staging site. + page = end_to_end_authenticated_context.new_page() + + accounts_uri = '{}accounts'.format(os.getenv('NOTIFY_E2E_TEST_URI')) + + page.goto(accounts_uri) + + # Check to make sure that we've arrived at the next page. + page.wait_for_load_state('domcontentloaded') + + # Check to make sure that we've arrived at the next page. + # Check the page title exists and matches what we expect. + expect(page).to_have_title(re.compile('Choose service')) + + # Check for the sign in heading. + sign_in_heading = page.get_by_role('heading', name='Choose service') + expect(sign_in_heading).to_be_visible() + + # Retrieve some prominent elements on the page for testing. + add_service_button = page.get_by_role( + 'button', + name=re.compile('Add a new service') + ) + + expect(add_service_button).to_be_visible() + + +@pytest.mark.skip(reason='Not authenticating test users.') +def test_add_new_service_workflow(end_to_end_authenticated_context): + # Prepare for adding a new service later in the test. + current_date_time = datetime.datetime.now() + new_service_name = 'E2E Federal Test Service {now} - {browser_type}'.format( + now=current_date_time.strftime('%m/%d/%Y %H:%M:%S'), + browser_type=end_to_end_authenticated_context.browser.browser_type.name + ) + + # Open a new page and go to the staging site. + page = end_to_end_authenticated_context.new_page() + + accounts_uri = '{}accounts'.format(os.getenv('NOTIFY_E2E_TEST_URI')) + + page.goto(accounts_uri) + + # Check to make sure that we've arrived at the next page. + page.wait_for_load_state('domcontentloaded') + + # Check to make sure that we've arrived at the next page. + # Check the page title exists and matches what we expect. + expect(page).to_have_title(re.compile('Choose service')) + + # Check for the sign in heading. + sign_in_heading = page.get_by_role('heading', name='Choose service') + expect(sign_in_heading).to_be_visible() + + # Retrieve some prominent elements on the page for testing. + add_service_button = page.get_by_role( + 'button', + name=re.compile('Add a new service') + ) + + expect(add_service_button).to_be_visible() + + existing_service_link = page.get_by_role( + 'link', + name=new_service_name + ) + + # Check to see if the service was already created - if so, we should fail. + # TODO: Figure out how to make this truly isolated, and/or work in a + # delete service workflow. + expect(existing_service_link).to_have_count(0) + + # Click on add a new service. + add_service_button.click() + + # Check to make sure that we've arrived at the next page. + page.wait_for_load_state('domcontentloaded') + + # Check for the sign in heading. + about_heading = page.get_by_role('heading', name='About your service') + expect(about_heading).to_be_visible() + + # Retrieve some prominent elements on the page for testing. + service_name_input = page.locator('xpath=//input[@name="name"]') + federal_radio_button = page.locator('xpath=//input[@value="federal"]') + state_radio_button = page.locator('xpath=//input[@value="state"]') + other_radio_button = page.locator('xpath=//input[@value="other"]') + add_service_button = page.get_by_role( + 'button', + name=re.compile('Add service') + ) + + expect(service_name_input).to_be_visible() + expect(federal_radio_button).to_be_visible() + expect(state_radio_button).to_be_visible() + expect(other_radio_button).to_be_visible() + expect(add_service_button).to_be_visible() + + # Fill in the form. + service_name_input.fill(new_service_name) + federal_radio_button.click() + + # Click on add service. + add_service_button.click() + + # Check to make sure that we've arrived at the next page. + page.wait_for_load_state('domcontentloaded') + + # Check for the service name title and heading. + service_heading = page.get_by_text(new_service_name) + expect(service_heading).to_be_visible() + expect(page).to_have_title(re.compile(new_service_name)) diff --git a/tests/end_to_end/test_landing_and_sign_in_pages.py b/tests/end_to_end/test_landing_and_sign_in_pages.py index cd3c81d2e..d8b0008f7 100644 --- a/tests/end_to_end/test_landing_and_sign_in_pages.py +++ b/tests/end_to_end/test_landing_and_sign_in_pages.py @@ -1,13 +1,17 @@ import os import re +import pytest from playwright.sync_api import expect -def test_landing_page(end_to_end_auth_context): +def test_landing_page(end_to_end_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')) + page = end_to_end_context.new_page() + page.goto(os.getenv('NOTIFY_E2E_TEST_URI')) + + # Check to make sure that we've arrived at the next page. + page.wait_for_load_state('domcontentloaded') # Check the page title exists and matches what we expect. expect(page).to_have_title(re.compile('Notify.gov')) @@ -50,22 +54,22 @@ def test_landing_page(end_to_end_auth_context): ).to_be_visible() -def test_sign_in_page(end_to_end_auth_context): +@pytest.mark.skip(reason='Not authenticating test users.') +def test_sign_in_and_mfa_pages(end_to_end_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')) + page = end_to_end_context.new_page() + page.goto(os.getenv('NOTIFY_E2E_TEST_URI')) sign_in_button = page.get_by_role('link', name='Sign in') # Test trying to sign in. sign_in_button.click() + # Check to make sure that we've arrived at the next page. + page.wait_for_load_state('domcontentloaded') + # 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 – Notify.gov')) + expect(page).to_have_title(re.compile('Sign in')) # Check for the sign in heading. sign_in_heading = page.get_by_role('heading', name='Sign in') @@ -81,7 +85,10 @@ def test_sign_in_page(end_to_end_auth_context): 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')) + continue_button = page.get_by_role( + 'button', + name=re.compile('Continue') + ) forgot_password_link = page.get_by_role( 'link', name='Forgot your password?' @@ -106,4 +113,80 @@ def test_sign_in_page(end_to_end_auth_context): '/forgot-password' ) - # TODO: Figure out how to actually sign in... + # Sign in to the site. + email_address_input.fill( + os.getenv('NOTIFY_E2E_TEST_EMAIL') + ) + password_input.fill(os.getenv('NOTIFY_E2E_TEST_PASSWORD')) + continue_button.click() + + # Wait for the next page to fully load. + page.wait_for_load_state('domcontentloaded') + + # Check the page title exists and matches what we expect. + expect(page).to_have_title(re.compile('Check your phone')) + + # Check for the sign in heading. + sign_in_heading = page.get_by_role( + 'heading', + name='Check your phone' + ) + 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. + mfa_input = page.get_by_label('Text message code') + csrf_token = page.locator('xpath=//input[@name="csrf_token"]') + continue_button = page.get_by_role( + 'button', + name=re.compile('Continue') + ) + not_received_message_link = page.get_by_role( + 'link', + name='Not received a text message?' + ) + + # Make sure form elements are visible and not visible as expected. + expect(mfa_input).to_be_visible() + expect(continue_button).to_be_visible() + expect(not_received_message_link).to_be_visible() + + expect(csrf_token).to_be_hidden() + + # Make sure form elements are configured correctly with the right + # attributes. + expect(mfa_input).to_have_attribute('type', 'tel') + expect(mfa_input).to_have_attribute('pattern', '[0-9]*') + expect(csrf_token).to_have_attribute('type', 'hidden') + expect(continue_button).to_have_attribute('type', 'submit') + expect(not_received_message_link).to_have_attribute( + 'href', + '/text-not-received' + ) + + # Enter MFA code and continue. + # TODO: Revisit this at a later point in time. + # totp = pyotp.TOTP( + # os.getenv('MFA_TOTP_SECRET'), + # digits=int(os.getenv('MFA_TOTP_LENGTH')) + # ) + + # mfa_input.fill('totp.now()') + # continue_button.click() + + # # Check to make sure that we've arrived at the next page. + # page.wait_for_load_state('domcontentloaded') + + # # Check that no MFA code error happened. + # code_not_found_error = page.get_by_text('Code not found') + # expect(code_not_found_error).to_have_count(0) + + # # Check the page title exists and matches what we expect. + # # This could be either the Dashboard of a service if there is only + # # one, or choosing a service if there are multiple. + # expect(page).to_have_title(re.compile('Dashboard|Choose service'))