mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-02 12:49:01 -04:00
Merge branch 'main' into notify-admin-638
This commit is contained in:
8
.github/workflows/checks.yml
vendored
8
.github/workflows/checks.yml
vendored
@@ -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
|
||||
|
||||
|
||||
8
Makefile
8
Makefile
@@ -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=96
|
||||
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
|
||||
|
||||
12
Pipfile
12
Pipfile
@@ -6,6 +6,7 @@ name = "pypi"
|
||||
[packages]
|
||||
ago = "~=0.0.95"
|
||||
blinker = "~=1.4"
|
||||
exceptiongroup = "==1.1.2"
|
||||
fido2 = "~=0.9"
|
||||
flask = "~=2.3"
|
||||
flask-basicauth = "~=0.2"
|
||||
@@ -19,7 +20,7 @@ humanize = "~=4.1"
|
||||
itsdangerous = "~=2.1"
|
||||
jinja2 = "~=3.1"
|
||||
notifications-python-client = "==8.0.1"
|
||||
prometheus-client = "==0.14.1"
|
||||
prometheus-client = "==0.17.1"
|
||||
pyexcel = "==0.7.0"
|
||||
pyexcel-io = "==0.6.6"
|
||||
pyexcel-ods3 = "==0.6.1"
|
||||
@@ -36,17 +37,18 @@ 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"
|
||||
pytest = "==7.4.0"
|
||||
pytest-env = "==0.6.2"
|
||||
pytest-env = "==0.8.2"
|
||||
pytest-mock = "==3.11.1"
|
||||
pytest-xdist = "==2.5.0"
|
||||
pytest-xdist = "==3.3.1"
|
||||
beautifulsoup4 = "==4.12.2"
|
||||
freezegun = "==1.2.2"
|
||||
flake8 = "==4.0.1"
|
||||
flake8-bugbear = "==22.4.25"
|
||||
flake8 = "==6.0.0"
|
||||
flake8-bugbear = "==23.7.10"
|
||||
flake8-print = "==5.0.0"
|
||||
moto = "~=4.1"
|
||||
requests-mock = "==1.11.0"
|
||||
|
||||
277
Pipfile.lock
generated
277
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "3732c4192a2ac5a4af64f7648528ac061c4cf60c0a9bdaa95b15650f6a0afd65"
|
||||
"sha256": "e6e49305bdee7cdd4605b8f6f7d0135cfe91e0c038abe6efdfa1784194ce3cdd"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -24,14 +24,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.0.95"
|
||||
},
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780",
|
||||
"sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.7.1"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15",
|
||||
@@ -58,19 +50,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:1f4b9c23dfcad910b6f8e74aac9fe507c1e75fcdd832e25ed2ff1e6d7a99cddf",
|
||||
"sha256:92c0631ab91b4c5aa0e18a90b4d12df361723c6df1ef7e346db71f2ad0803ab3"
|
||||
"sha256:b2d178c8a56fe3e4c9b123dccdff20e9555d12a597b72627fa659aa6295e238a",
|
||||
"sha256:db6443fd2c65d9f35f671b03bacb0592b62d06884395ed65d75922ccddc34c2e"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.28.4"
|
||||
"version": "==1.28.13"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:1c14ac4521af707a7a407cee0e22695ce3e95c0f1a0c974e21cb25a3ce78a538",
|
||||
"sha256:f9738a23b03c55c2958ebdee65273afeda80deaeefebe595887fc3251e48293a"
|
||||
"sha256:78b96afbd88b8bd4c0967611a4cedddd9ea33d8601309dc351f81cbb5479d976",
|
||||
"sha256:9a5080ea2a444f0447a7a1a79f64252ae2a1417b6c13a54656ee991cb610dd4e"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.31.4"
|
||||
"version": "==1.31.13"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@@ -82,11 +74,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
|
||||
"sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
|
||||
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2023.5.7"
|
||||
"version": "==2023.7.22"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@@ -248,11 +240,11 @@
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:4be4b1af8d665c6d942909916d31a213a106800c47d0eeba73d34da3cbc11367",
|
||||
"sha256:e576aa487d679441d7d30abb87e1b43d24fc53bffb8758443b1a9e1cee504548"
|
||||
"sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd",
|
||||
"sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.5"
|
||||
"version": "==8.1.6"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
@@ -351,11 +343,11 @@
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:46b4052a55b56beea3a3bdd7b30295c292bd6827dd442348bc116f2d35b17f0a",
|
||||
"sha256:758e691dbb454d5ccf4e1b154a19e52847f79e21a42fef17b969144af29a4e6c"
|
||||
"sha256:5b7488477388b8c0b70a8ce93b227c5603bc7b77f1565afe8e729c36c51447d7",
|
||||
"sha256:c33971c79af5be968bb897e95c2448e11a645ee84d93b265ce0b7aabe5dfdca8"
|
||||
],
|
||||
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
||||
"version": "==2.4.0"
|
||||
"version": "==2.4.1"
|
||||
},
|
||||
"docopt": {
|
||||
"hashes": [
|
||||
@@ -383,7 +375,7 @@
|
||||
"sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
|
||||
"sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"index": "pypi",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"fido2": {
|
||||
@@ -540,22 +532,6 @@
|
||||
"ref": "1299ea9e967a61ae2edebe191082fd169b864c64",
|
||||
"version": "==20.1.0"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
|
||||
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.14.0"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888",
|
||||
"sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"humanize": {
|
||||
"hashes": [
|
||||
"sha256:7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a",
|
||||
@@ -580,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",
|
||||
@@ -850,20 +834,49 @@
|
||||
],
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
|
||||
"sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.1"
|
||||
},
|
||||
"phonenumbers": {
|
||||
"hashes": [
|
||||
"sha256:92772b142eac9f3aeb0e87958c436996ac16d468d96646483997c8a57517933b",
|
||||
"sha256:d7ca6b392eac44b50b923c6dc661525c73b21fa506723d2540c72de3db023442"
|
||||
"sha256:89671217c706cbaa3ced101deefafa779836feac3e059434d886ac31f09f32c0",
|
||||
"sha256:e8ffd86b2e0b844fd6189fdb0927dbe8707cb03b59102cba5532b3ea305cc1bd"
|
||||
],
|
||||
"version": "==8.13.16"
|
||||
"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:522fded625282822a89e2773452f42df14b5a8e84a86433e3f8a189c1d54dc01",
|
||||
"sha256:5459c427624961076277fdc6dc50540e2bacb98eebde99886e59ec55ed92093a"
|
||||
"sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091",
|
||||
"sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.14.1"
|
||||
"version": "==0.17.1"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
@@ -872,6 +885,13 @@
|
||||
],
|
||||
"version": "==2.21"
|
||||
},
|
||||
"pyee": {
|
||||
"hashes": [
|
||||
"sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32",
|
||||
"sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5"
|
||||
],
|
||||
"version": "==9.0.4"
|
||||
},
|
||||
"pyexcel": {
|
||||
"hashes": [
|
||||
"sha256:ddc6904512bfa2ecda509fb3b58229bb30db14498632fd9e7a5ba7bbfb02ed1b",
|
||||
@@ -921,11 +941,11 @@
|
||||
},
|
||||
"pyjwt": {
|
||||
"hashes": [
|
||||
"sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1",
|
||||
"sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074"
|
||||
"sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de",
|
||||
"sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.7.0"
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"pypdf2": {
|
||||
"hashes": [
|
||||
@@ -966,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",
|
||||
@@ -990,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",
|
||||
@@ -1142,13 +1194,12 @@
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"sniffio": {
|
||||
"text-unidecode": {
|
||||
"hashes": [
|
||||
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
|
||||
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
|
||||
"sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
|
||||
"sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.3"
|
||||
},
|
||||
"texttable": {
|
||||
"hashes": [
|
||||
@@ -1157,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",
|
||||
@@ -1247,19 +1306,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:1f4b9c23dfcad910b6f8e74aac9fe507c1e75fcdd832e25ed2ff1e6d7a99cddf",
|
||||
"sha256:92c0631ab91b4c5aa0e18a90b4d12df361723c6df1ef7e346db71f2ad0803ab3"
|
||||
"sha256:b2d178c8a56fe3e4c9b123dccdff20e9555d12a597b72627fa659aa6295e238a",
|
||||
"sha256:db6443fd2c65d9f35f671b03bacb0592b62d06884395ed65d75922ccddc34c2e"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.28.4"
|
||||
"version": "==1.28.13"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:1c14ac4521af707a7a407cee0e22695ce3e95c0f1a0c974e21cb25a3ce78a538",
|
||||
"sha256:f9738a23b03c55c2958ebdee65273afeda80deaeefebe595887fc3251e48293a"
|
||||
"sha256:78b96afbd88b8bd4c0967611a4cedddd9ea33d8601309dc351f81cbb5479d976",
|
||||
"sha256:9a5080ea2a444f0447a7a1a79f64252ae2a1417b6c13a54656ee991cb610dd4e"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.31.4"
|
||||
"version": "==1.31.13"
|
||||
},
|
||||
"cachecontrol": {
|
||||
"extras": [
|
||||
@@ -1274,11 +1333,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
|
||||
"sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
|
||||
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2023.5.7"
|
||||
"version": "==2023.7.22"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@@ -1480,7 +1539,7 @@
|
||||
"sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
|
||||
"sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"index": "pypi",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"execnet": {
|
||||
@@ -1500,19 +1559,19 @@
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d",
|
||||
"sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"
|
||||
"sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7",
|
||||
"sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.1"
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"flake8-bugbear": {
|
||||
"hashes": [
|
||||
"sha256:ec374101cddf65bd7a96d393847d74e58d3b98669dbf9768344c39b6290e8bd6",
|
||||
"sha256:f7c080563fca75ee6b205d06b181ecba22b802babb96b0b084cc7743d6908a55"
|
||||
"sha256:0ebdc7d8ec1ca8bd49347694562381f099f4de2f8ec6bda7a7dca65555d9e0d4",
|
||||
"sha256:d99d005114020fbef47ed5e4aebafd22f167f9a0fbd0d8bf3c9e90612cb25c34"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==22.4.25"
|
||||
"version": "==23.7.10"
|
||||
},
|
||||
"flake8-print": {
|
||||
"hashes": [
|
||||
@@ -1671,10 +1730,11 @@
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"mdurl": {
|
||||
"hashes": [
|
||||
@@ -1762,11 +1822,11 @@
|
||||
},
|
||||
"packageurl-python": {
|
||||
"hashes": [
|
||||
"sha256:4bad1d3ea4feb5e7a1db5ca8fb690ac9c82ab18e08d500755947b853df68817d",
|
||||
"sha256:bbcc53d2cb5920c815c1626c75992f319bfc450b73893fa7bd8aac5869aa49fe"
|
||||
"sha256:01fbf74a41ef85cf413f1ede529a1411f658bda66ed22d45d27280ad9ceba471",
|
||||
"sha256:799acfe8d9e6e3534bbc19660be97d5b66754bc033e62c39f1e2f16323fcfa84"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.11.1"
|
||||
"version": "==0.11.2"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
@@ -1786,11 +1846,11 @@
|
||||
},
|
||||
"pip": {
|
||||
"hashes": [
|
||||
"sha256:78e5353a9dda374b462f2054f83a7b63f3f065c98236a68361845c1b0ee7e35f",
|
||||
"sha256:a160a170f3331d9ca1a0247eb1cd79c758879f1f81158f9cd05bbb5df80bea5c"
|
||||
"sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be",
|
||||
"sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.2"
|
||||
"version": "==23.2.1"
|
||||
},
|
||||
"pip-api": {
|
||||
"hashes": [
|
||||
@@ -1802,11 +1862,11 @@
|
||||
},
|
||||
"pip-audit": {
|
||||
"hashes": [
|
||||
"sha256:49e97e3d6663d2ed0c00b7a7c468afcb816beb3988f32f8496d3fe3927cfd627",
|
||||
"sha256:6431c363efa80ef52c2599197c5b8a39ff8708ce316624b97fa35b5cdf493118"
|
||||
"sha256:55c9bd18b0fe3959f73397db08d257c6012ad1826825e3d74cb6c3f79e95c245",
|
||||
"sha256:8a32bb67dca6a76c244bbccebed562c0f6957b1fc9d34d59a9ec0fbff0672ae0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.6.0"
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"pip-requirements-parser": {
|
||||
"hashes": [
|
||||
@@ -1824,14 +1884,6 @@
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
|
||||
"sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"py-serializable": {
|
||||
"hashes": [
|
||||
"sha256:79e21f0672822e6200b15f45ce9f636e8126466f62dbd7d488c67313c72b5c3e",
|
||||
@@ -1842,11 +1894,11 @@
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20",
|
||||
"sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"
|
||||
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
|
||||
"sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.8.0"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.10.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
@@ -1857,11 +1909,11 @@
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c",
|
||||
"sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"
|
||||
"sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf",
|
||||
"sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.0"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
@@ -1884,23 +1936,16 @@
|
||||
"sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32",
|
||||
"sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==7.4.0"
|
||||
},
|
||||
"pytest-env": {
|
||||
"hashes": [
|
||||
"sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"
|
||||
"sha256:5e533273f4d9e6a41c3a3120e0c7944aae5674fa773b329f00a5eb1f23c53a38",
|
||||
"sha256:baed9b3b6bae77bd75b9238e0ed1ee6903a42806ae9d6aeffb8754cd5584d4ff"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.2"
|
||||
},
|
||||
"pytest-forked": {
|
||||
"hashes": [
|
||||
"sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f",
|
||||
"sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.6.0"
|
||||
"version": "==0.8.2"
|
||||
},
|
||||
"pytest-mock": {
|
||||
"hashes": [
|
||||
@@ -1912,11 +1957,11 @@
|
||||
},
|
||||
"pytest-xdist": {
|
||||
"hashes": [
|
||||
"sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf",
|
||||
"sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"
|
||||
"sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93",
|
||||
"sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.5.0"
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
@@ -2069,10 +2114,10 @@
|
||||
},
|
||||
"types-pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f",
|
||||
"sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97"
|
||||
"sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b",
|
||||
"sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"
|
||||
],
|
||||
"version": "==6.0.12.10"
|
||||
"version": "==6.0.12.11"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
|
||||
21
README.md
21
README.md
@@ -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`
|
||||
|
||||
@@ -34,14 +34,14 @@ def convert_to_boolean(value):
|
||||
|
||||
|
||||
def format_datetime(date):
|
||||
return '{} at {}'.format(
|
||||
return '{} at {} UTC'.format(
|
||||
format_date(date),
|
||||
format_time_24h(date)
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_24h(date):
|
||||
return '{} at {}'.format(
|
||||
return '{} at {} UTC'.format(
|
||||
format_date(date),
|
||||
format_time_24h(date),
|
||||
)
|
||||
@@ -52,28 +52,28 @@ def format_time(date):
|
||||
|
||||
|
||||
def format_datetime_normal(date):
|
||||
return '{} at {}'.format(
|
||||
return '{} at {} UTC'.format(
|
||||
format_date_normal(date),
|
||||
format_time_24h(date)
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_short(date):
|
||||
return '{} at {}'.format(
|
||||
return '{} at {} UTC'.format(
|
||||
format_date_short(date),
|
||||
format_time_24h(date)
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_relative(date):
|
||||
return '{} at {}'.format(
|
||||
return '{} at {} UTC'.format(
|
||||
get_human_day(date),
|
||||
format_time_24h(date)
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_numeric(date):
|
||||
return '{} {}'.format(
|
||||
return '{} {} UTC'.format(
|
||||
format_date_numeric(date),
|
||||
format_time_24h(date),
|
||||
)
|
||||
@@ -134,7 +134,7 @@ def format_date_human(date):
|
||||
|
||||
|
||||
def format_datetime_human(date, date_prefix=''):
|
||||
return '{} at {}'.format(
|
||||
return '{} at {} UTC'.format(
|
||||
get_human_day(date, date_prefix='on'),
|
||||
format_time_24h(date),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import itertools
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
@@ -12,6 +13,7 @@ from app import (
|
||||
notification_api_client,
|
||||
platform_stats_api_client,
|
||||
service_api_client,
|
||||
user_api_client,
|
||||
)
|
||||
from app.extensions import redis_client
|
||||
from app.main import main
|
||||
@@ -292,6 +294,32 @@ def get_billing_report():
|
||||
return render_template('views/platform-admin/get-billing-report.html', form=form)
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/get-users-report", methods=['GET', 'POST'])
|
||||
@user_is_platform_admin
|
||||
def get_users_report():
|
||||
headers = [
|
||||
"name", "services", "platform admin", "permissions", "password changed at",
|
||||
"state"
|
||||
]
|
||||
try:
|
||||
result = user_api_client.get_all_users()
|
||||
|
||||
except HTTPError as e:
|
||||
raise e
|
||||
|
||||
rows = []
|
||||
for r in result:
|
||||
rows.append(_get_user_row(r))
|
||||
if rows:
|
||||
return Spreadsheet.from_rows([headers] + rows).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': f'attachment; filename="User Report {datetime.utcnow()}.csv"'
|
||||
}
|
||||
else:
|
||||
flash('No results')
|
||||
return render_template('views/platform-admin/get-users-report.html')
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/volumes-by-service", methods=['GET', 'POST'])
|
||||
@user_is_platform_admin
|
||||
def get_volumes_by_service():
|
||||
@@ -540,3 +568,40 @@ def format_stats_by_service(services):
|
||||
'created_at': service['created_at'],
|
||||
'active': service['active']
|
||||
}
|
||||
|
||||
|
||||
def _get_user_row(r):
|
||||
|
||||
# [{
|
||||
# 'name': 'Kenneth Kehl',
|
||||
# 'organizations': [],
|
||||
# 'password_changed_at': '2023-07-21 14:12:54.832850', 'permissions': {
|
||||
# '672b8a66-e22e-40f6-b1e5-39cc1c6bf857': ['manage_users', 'manage_templates', 'manage_settings', 'send_texts',
|
||||
# 'send_emails', 'manage_api_keys', 'view_activity']},
|
||||
# 'platform_admin': True, 'services': ['672b8a66-e22e-40f6-b1e5-39cc1c6bf857'], 'state': 'active'}]
|
||||
|
||||
row = []
|
||||
row.append(r['name'])
|
||||
|
||||
service_id_name_lookup = {}
|
||||
services = []
|
||||
for s in r['services']:
|
||||
my_service = service_api_client.get_service(s)
|
||||
service_id_name_lookup[my_service['data']['id']] = my_service['data']['name']
|
||||
services.append(my_service['data']['name'])
|
||||
services = str(services)
|
||||
services = services.replace("[", "")
|
||||
services = services.replace("]", "")
|
||||
row.append(services)
|
||||
row.append(r['platform_admin'])
|
||||
permissions = r['permissions']
|
||||
for k, v in service_id_name_lookup.items():
|
||||
if permissions.get(k):
|
||||
permissions[v] = permissions[k]
|
||||
del permissions[k]
|
||||
|
||||
permissions = json.dumps(permissions, indent=4)
|
||||
row.append(permissions)
|
||||
row.append(r['password_changed_at'])
|
||||
row.append(r['state'])
|
||||
return row
|
||||
|
||||
@@ -95,6 +95,7 @@ class HeaderNavigation(Navigation):
|
||||
'live_services_csv',
|
||||
'notifications_sent_by_service',
|
||||
'get_billing_report',
|
||||
'get_users_report',
|
||||
'get_daily_volumes',
|
||||
'get_daily_sms_provider_volumes',
|
||||
'get_volumes_by_service',
|
||||
@@ -266,7 +267,6 @@ class OrgNavigation(Navigation):
|
||||
'edit_organization_billing_details',
|
||||
'edit_organization_domains',
|
||||
'edit_organization_email_branding',
|
||||
'edit_organization_domains',
|
||||
'edit_organization_go_live_notes',
|
||||
'edit_organization_name',
|
||||
'edit_organization_notes',
|
||||
|
||||
@@ -154,6 +154,10 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
endpoint = '/organizations/{}/users'.format(org_id)
|
||||
return self.get(endpoint)['data']
|
||||
|
||||
def get_all_users(self):
|
||||
endpoint = '/user'
|
||||
return self.get(endpoint)['data']
|
||||
|
||||
@cache.delete('service-{service_id}')
|
||||
@cache.delete('service-{service_id}-template-folders')
|
||||
@cache.delete('user-{user_id}')
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="ajax-block-container">
|
||||
<div class="grid-row grid-gap">
|
||||
<div id="total-sms" class="grid-col-6">
|
||||
<div id="total-sms" class="grid-col-12">
|
||||
{{ big_number_with_status(
|
||||
statistics['sms']['requested'],
|
||||
statistics['sms']['requested']|message_count_label('sms', suffix='sent'),
|
||||
@@ -14,7 +14,7 @@
|
||||
smaller=True,
|
||||
) }}
|
||||
</div>
|
||||
<div id="total-email" class="grid-col-6">
|
||||
<!-- <div id="total-email" class="grid-col-6">
|
||||
<span class="big-number-with-status pilot-disabled">
|
||||
<a class="usa-link govuk-link--no-visited-state display-block margin-bottom-1">
|
||||
<span class="big-number-smaller">
|
||||
@@ -24,6 +24,6 @@
|
||||
</a>
|
||||
<span class="big-number-status">No failures</span>
|
||||
</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% from "components/big-number.html" import big_number %}
|
||||
|
||||
<div class='grid-row grid-gap ajax-block-container'>
|
||||
<div class='grid-col-6'>
|
||||
<div class='grid-col-12'>
|
||||
<div class="keyline-block">
|
||||
{% if sms_cost %}
|
||||
{{ big_number(
|
||||
@@ -15,9 +15,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class='grid-col-6 pilot-disabled'>
|
||||
<!-- <div class='grid-col-6 pilot-disabled'>
|
||||
<div class="keyline-block">
|
||||
{{ big_number("0", 'email disabled during SMS pilot', smaller=True) }}
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
{% elif created_by %}
|
||||
by {{ created_by.name }}
|
||||
{% endif %}
|
||||
{{ created_at|format_datetime_human }} UTC
|
||||
{{ created_at|format_datetime_human }}
|
||||
</p>
|
||||
|
||||
<div class="">
|
||||
|
||||
44
app/templates/views/platform-admin/get-users-report.html
Normal file
44
app/templates/views/platform-admin/get-users-report.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "views/platform-admin/_base_template.html" %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/table.html" import mapping_table, row, text_field %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Billing Report
|
||||
{% endblock %}
|
||||
|
||||
{% block platform_admin_content %}
|
||||
|
||||
<h1 class="font-body-2xl margin-bottom-3">
|
||||
Users Report
|
||||
</h1>
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{{ form.start_date(param_extensions={"hint": {"text": "Use the format YYYY-MM-DD"}}) }}
|
||||
{{ form.end_date(param_extensions={"hint": {"text": "Use the format YYYY-MM-DD"}}) }}
|
||||
{{ page_footer('Download report (CSV)') }}
|
||||
{% endcall %}
|
||||
|
||||
<h2 class="font-body-lg">
|
||||
Data included in the report
|
||||
</h2>
|
||||
<div class="bottom-gutter-3-2">
|
||||
{% call mapping_table(
|
||||
caption='Descriptions of billing report data',
|
||||
field_headings=['Name', 'Description'],
|
||||
field_headings_visible=True,
|
||||
caption_visible=False
|
||||
) %}
|
||||
{% for column_heading, description in [
|
||||
('sms cost', 'The total cost of text messages sent after a service has used its free allowance.'),
|
||||
('sms chargeable units', 'The number of fragments sent after a service has used its free allowance. This number takes into account the cost multiplier for <a class="govuk-link govuk-link--no-visited-state" href="https://www.notifications.service.gov.uk/pricing#international-numbers">sending international text messages</a>.' | safe),
|
||||
('purchase order number, contact names, contact email addresses and billing reference', 'We add this data manually based on the information we get from services. You can help by adding it to the service settings page.'),
|
||||
] %}
|
||||
{% call row() %}
|
||||
{{ text_field(column_heading) }}
|
||||
{{ text_field(description) }}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -28,4 +28,7 @@
|
||||
<p>
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.get_daily_sms_provider_volumes') }}">Daily SMS provider volumes Report</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.get_users_report') }}">Users Report</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -39,16 +39,16 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class='govuk-grid-column-one-half pilot-disabled'>
|
||||
{# <div class='govuk-grid-column-one-half'>
|
||||
<h2 class='heading-small'>Emails</h2>
|
||||
<div class="keyline-block">
|
||||
{{ big_number(emails_sent, 'email disabled during SMS pilot', smaller=True) }}
|
||||
{{ big_number("", '', smaller=True) }}
|
||||
</div>
|
||||
</div>
|
||||
</div> #}
|
||||
</div>
|
||||
|
||||
<!-- <div class='govuk-grid-row'>
|
||||
{# <div class='govuk-grid-row'>
|
||||
<div class='govuk-grid-column-one-half'>
|
||||
<div class="keyline-block">
|
||||
{{ big_number(
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div> #}
|
||||
|
||||
{% if months %}
|
||||
<div class="dashboard-table usage-table body-copy-table margin-top-4">
|
||||
|
||||
120
docs/end_to_end_tests.md
Normal file
120
docs/end_to_end_tests.md
Normal 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.
|
||||
@@ -1,5 +1,14 @@
|
||||
# Notify Sprint Goals Log
|
||||
|
||||
## Sprint: Osprey (7/19/23)
|
||||
|
||||
| | Goals | Impact |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Engineering | Reduce [technical debt](https://github.com/GSA/notifications-api/issues/361) and stabilize application; carry out rigorous [testing](https://github.com/GSA/notifications-api/issues/312) of features and [infrastructure](https://github.com/GSA/notifications-api/issues/87); solve known [bugs](https://github.com/GSA/notifications-api/issues/360) | Increase app reliability prior to first partner use |
|
||||
| UX | Continue [user testing interviews](https://github.com/GSA/notifications-admin/issues/592) and feedback; Kick off internal message recipient [opt-out test](https://github.com/GSA/notifications-admin/issues/395) | Gain feedback on usability and functionality in order to find bugs, streamline confusing flows, and adjust processes |
|
||||
| Security | (Assessment has begun!) Respond to any assessment questions and/or needs | Keep the LATO award timeline as short as possible|
|
||||
| Content | Perform an application and documentation audit to [replace instances](https://github.com/GSA/notifications-admin/issues/631) of `U.S. Notify` with `Notify.gov` | Reflect a more streamlined brand
|
||||
|
||||
## Sprint: Northern Screamer (7/5/23)
|
||||
|
||||
| | Goals | Impact |
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -15716,9 +15716,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -28109,9 +28109,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true
|
||||
},
|
||||
"wrap-ansi": {
|
||||
|
||||
@@ -57,38 +57,38 @@ def test_format_number_in_pounds_as_currency(input_number, formatted_number):
|
||||
|
||||
@pytest.mark.parametrize('time, human_readable_datetime', [
|
||||
# incoming in UTC, outgoing in "human formatted" UTC
|
||||
('2018-03-14 09:00', '14 March at 09:00'),
|
||||
('2018-03-14 19:00', '14 March at 19:00'),
|
||||
('2018-03-14 09:00', '14 March at 09:00 UTC'),
|
||||
('2018-03-14 19:00', '14 March at 19:00 UTC'),
|
||||
|
||||
('2018-03-15 09:00', '15 March at 09:00'),
|
||||
('2018-03-15 19:00', '15 March at 19:00'),
|
||||
('2018-03-15 09:00', '15 March at 09:00 UTC'),
|
||||
('2018-03-15 19:00', '15 March at 19:00 UTC'),
|
||||
|
||||
('2018-03-19 09:00', '19 March at 09:00'),
|
||||
('2018-03-19 19:00', '19 March at 19:00'),
|
||||
('2018-03-19 23:59', '19 March at 23:59'),
|
||||
('2018-03-19 09:00', '19 March at 09:00 UTC'),
|
||||
('2018-03-19 19:00', '19 March at 19:00 UTC'),
|
||||
('2018-03-19 23:59', '19 March at 23:59 UTC'),
|
||||
|
||||
('2018-03-20 00:00', '19 March at 00:00'), # we specifically refer to 00:00 as belonging to the day before.
|
||||
('2018-03-20 04:01', 'yesterday at 04:01'),
|
||||
('2018-03-20 09:00', 'yesterday at 09:00'),
|
||||
('2018-03-20 19:00', 'yesterday at 19:00'),
|
||||
('2018-03-20 23:59', 'yesterday at 23:59'),
|
||||
('2018-03-20 00:00', '19 March at 00:00 UTC'), # we specifically refer to 00:00 as belonging to the day before.
|
||||
('2018-03-20 04:01', 'yesterday at 04:01 UTC'),
|
||||
('2018-03-20 09:00', 'yesterday at 09:00 UTC'),
|
||||
('2018-03-20 19:00', 'yesterday at 19:00 UTC'),
|
||||
('2018-03-20 23:59', 'yesterday at 23:59 UTC'),
|
||||
|
||||
('2018-03-21 00:00', 'yesterday at 00:00'), # we specifically refer to 00:00 as belonging to the day before.
|
||||
('2018-03-21 04:01', 'today at 04:01'),
|
||||
('2018-03-21 09:00', 'today at 09:00'),
|
||||
('2018-03-21 12:00', 'today at 12:00'),
|
||||
('2018-03-21 19:00', 'today at 19:00'),
|
||||
('2018-03-21 23:59', 'today at 23:59'),
|
||||
('2018-03-21 00:00', 'yesterday at 00:00 UTC'), # we specifically refer to 00:00 as belonging to the day before.
|
||||
('2018-03-21 04:01', 'today at 04:01 UTC'),
|
||||
('2018-03-21 09:00', 'today at 09:00 UTC'),
|
||||
('2018-03-21 12:00', 'today at 12:00 UTC'),
|
||||
('2018-03-21 19:00', 'today at 19:00 UTC'),
|
||||
('2018-03-21 23:59', 'today at 23:59 UTC'),
|
||||
|
||||
('2018-03-22 00:00', 'today at 00:00'),
|
||||
('2018-03-22 04:01', 'tomorrow at 04:01'),
|
||||
('2018-03-22 09:00', 'tomorrow at 09:00'),
|
||||
('2018-03-22 19:00', 'tomorrow at 19:00'),
|
||||
('2018-03-22 23:59', 'tomorrow at 23:59'),
|
||||
('2018-03-22 00:00', 'today at 00:00 UTC'),
|
||||
('2018-03-22 04:01', 'tomorrow at 04:01 UTC'),
|
||||
('2018-03-22 09:00', 'tomorrow at 09:00 UTC'),
|
||||
('2018-03-22 19:00', 'tomorrow at 19:00 UTC'),
|
||||
('2018-03-22 23:59', 'tomorrow at 23:59 UTC'),
|
||||
|
||||
('2018-03-23 04:01', '23 March at 04:01'),
|
||||
('2018-03-23 09:00', '23 March at 09:00'),
|
||||
('2018-03-23 19:00', '23 March at 19:00'),
|
||||
('2018-03-23 04:01', '23 March at 04:01 UTC'),
|
||||
('2018-03-23 09:00', '23 March at 09:00 UTC'),
|
||||
('2018-03-23 19:00', '23 March at 19:00 UTC'),
|
||||
|
||||
])
|
||||
def test_format_datetime_relative(time, human_readable_datetime):
|
||||
|
||||
@@ -153,7 +153,7 @@ def test_can_show_notifications(
|
||||
assert normalize_spaces(
|
||||
first_row.select_one('.table-field-right-aligned .align-with-message-body').text
|
||||
) == (
|
||||
'Delivered 1 January at 06:01'
|
||||
'Delivered 1 January at 06:01 UTC'
|
||||
)
|
||||
|
||||
assert page_title in page.h1.text.strip()
|
||||
@@ -377,7 +377,7 @@ def test_search_recipient_form(
|
||||
assert page.select_one('label[for=to]').text.strip() == expected_search_box_label
|
||||
|
||||
recipient_inputs = page.select("input[name=to]")
|
||||
assert(len(recipient_inputs) == 2)
|
||||
assert (len(recipient_inputs) == 2)
|
||||
|
||||
for field in recipient_inputs:
|
||||
assert field.get("value") == expected_search_box_contents
|
||||
@@ -626,16 +626,16 @@ def test_redacts_templates_that_should_be_redacted(
|
||||
@freeze_time("2017-09-27 12:30:00.000000")
|
||||
@pytest.mark.parametrize(
|
||||
"message_type, status, expected_hint_status, single_line", [
|
||||
('email', 'created', 'Sending since 27 September at 12:30', True),
|
||||
('email', 'sending', 'Sending since 27 September at 12:30', True),
|
||||
('email', 'temporary-failure', 'Inbox not accepting messages right now 27 September at 12:31', False),
|
||||
('email', 'permanent-failure', 'Email address does not exist 27 September at 12:31', False),
|
||||
('email', 'delivered', 'Delivered 27 September at 12:31', True),
|
||||
('sms', 'created', 'Sending since 27 September at 12:30', True),
|
||||
('sms', 'sending', 'Sending since 27 September at 12:30', True),
|
||||
('sms', 'temporary-failure', 'Phone not accepting messages right now 27 September at 12:31', False),
|
||||
('sms', 'permanent-failure', 'Not delivered 27 September at 12:31', False),
|
||||
('sms', 'delivered', 'Delivered 27 September at 12:31', True),
|
||||
('email', 'created', 'Sending since 27 September at 12:30 UTC', True),
|
||||
('email', 'sending', 'Sending since 27 September at 12:30 UTC', True),
|
||||
('email', 'temporary-failure', 'Inbox not accepting messages right now 27 September at 12:31 UTC', False),
|
||||
('email', 'permanent-failure', 'Email address does not exist 27 September at 12:31 UTC', False),
|
||||
('email', 'delivered', 'Delivered 27 September at 12:31 UTC', True),
|
||||
('sms', 'created', 'Sending since 27 September at 12:30 UTC', True),
|
||||
('sms', 'sending', 'Sending since 27 September at 12:30 UTC', True),
|
||||
('sms', 'temporary-failure', 'Phone not accepting messages right now 27 September at 12:31 UTC', False),
|
||||
('sms', 'permanent-failure', 'Not delivered 27 September at 12:31 UTC', False),
|
||||
('sms', 'delivered', 'Delivered 27 September at 12:31 UTC', True),
|
||||
]
|
||||
)
|
||||
def test_sending_status_hint_displays_correctly_on_notifications_page(
|
||||
|
||||
@@ -114,55 +114,55 @@ def test_view_conversation(
|
||||
for index, expected in enumerate([
|
||||
(
|
||||
'message-8',
|
||||
'yesterday at 14:59',
|
||||
'yesterday at 14:59 UTC',
|
||||
),
|
||||
(
|
||||
'message-7',
|
||||
'yesterday at 14:59',
|
||||
'yesterday at 14:59 UTC',
|
||||
),
|
||||
(
|
||||
'message-6',
|
||||
'yesterday at 16:59',
|
||||
'yesterday at 16:59 UTC',
|
||||
),
|
||||
(
|
||||
'message-5',
|
||||
'yesterday at 18:59',
|
||||
'yesterday at 18:59 UTC',
|
||||
),
|
||||
(
|
||||
'message-4',
|
||||
'yesterday at 20:59',
|
||||
'yesterday at 20:59 UTC',
|
||||
),
|
||||
(
|
||||
'message-3',
|
||||
'yesterday at 22:59',
|
||||
'yesterday at 22:59 UTC',
|
||||
),
|
||||
(
|
||||
'message-2',
|
||||
'yesterday at 22:59',
|
||||
'yesterday at 22:59 UTC',
|
||||
),
|
||||
(
|
||||
'message-1',
|
||||
'yesterday at 23:00',
|
||||
'yesterday at 23:00 UTC',
|
||||
),
|
||||
(
|
||||
expected_outbound_content,
|
||||
'yesterday at 00:00',
|
||||
'yesterday at 00:00 UTC',
|
||||
),
|
||||
(
|
||||
expected_outbound_content,
|
||||
'yesterday at 00:00',
|
||||
'yesterday at 00:00 UTC',
|
||||
),
|
||||
(
|
||||
expected_outbound_content,
|
||||
'yesterday at 00:00',
|
||||
'yesterday at 00:00 UTC',
|
||||
),
|
||||
(
|
||||
expected_outbound_content,
|
||||
'yesterday at 00:00',
|
||||
'yesterday at 00:00 UTC',
|
||||
),
|
||||
(
|
||||
expected_outbound_content,
|
||||
'yesterday at 00:00',
|
||||
'yesterday at 00:00 UTC',
|
||||
),
|
||||
]):
|
||||
assert (
|
||||
|
||||
@@ -389,14 +389,14 @@ def test_download_inbox(
|
||||
)
|
||||
assert response.get_data(as_text=True) == (
|
||||
'Phone number,Message,Received\r\n'
|
||||
'(202) 867-5300,message-1,2016-07-01 11:00\r\n'
|
||||
'(202) 867-5300,message-2,2016-07-01 10:59\r\n'
|
||||
'(202) 867-5300,message-3,2016-07-01 10:59\r\n'
|
||||
'(202) 867-5302,message-4,2016-07-01 08:59\r\n'
|
||||
'+33 1 12 34 56 78,message-5,2016-07-01 06:59\r\n'
|
||||
'(202) 555-0104,message-6,2016-07-01 04:59\r\n'
|
||||
'(202) 555-0104,message-7,2016-07-01 02:59\r\n'
|
||||
'+682 12345,message-8,2016-07-01 02:59\r\n'
|
||||
'(202) 867-5300,message-1,2016-07-01 11:00 UTC\r\n'
|
||||
'(202) 867-5300,message-2,2016-07-01 10:59 UTC\r\n'
|
||||
'(202) 867-5300,message-3,2016-07-01 10:59 UTC\r\n'
|
||||
'(202) 867-5302,message-4,2016-07-01 08:59 UTC\r\n'
|
||||
'+33 1 12 34 56 78,message-5,2016-07-01 06:59 UTC\r\n'
|
||||
'(202) 555-0104,message-6,2016-07-01 04:59 UTC\r\n'
|
||||
'(202) 555-0104,message-7,2016-07-01 02:59 UTC\r\n'
|
||||
'+682 12345,message-8,2016-07-01 02:59 UTC\r\n'
|
||||
)
|
||||
|
||||
|
||||
@@ -648,7 +648,7 @@ def test_should_show_upcoming_jobs_on_dashboard(
|
||||
page.select_one('a.banner-dashboard').text
|
||||
) == (
|
||||
'2 files waiting to send '
|
||||
'sending starts today at 11:09'
|
||||
'sending starts today at 11:09 UTC'
|
||||
)
|
||||
|
||||
assert page.select_one('a.banner-dashboard')['href'] == url_for(
|
||||
@@ -746,12 +746,12 @@ def test_correct_font_size_for_big_numbers(
|
||||
)
|
||||
|
||||
assert (
|
||||
len(page.select_one('[data-key=totals]').select('.grid-col-6'))
|
||||
) == (
|
||||
len(page.select_one('[data-key=usage]').select('.grid-col-6'))
|
||||
len(page.select_one('[data-key=totals]').select('.grid-col-12'))
|
||||
) == (
|
||||
# len(page.select_one('[data-key=usage]').select('.grid-col-6'))
|
||||
# ) == (
|
||||
len(page.select('.big-number-with-status .big-number-smaller'))
|
||||
) == 2
|
||||
) == 1
|
||||
|
||||
|
||||
def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
|
||||
@@ -804,14 +804,13 @@ def test_usage_page(
|
||||
assert normalize_spaces(unselected_nav_links[1].text) == '2009 to 2010 fiscal year'
|
||||
|
||||
annual_usage = page.find_all('div', {'class': 'govuk-grid-column-one-half'})
|
||||
# print(annual_usage)
|
||||
|
||||
# annual stats are shown in two rows, each with three column; email is col 1
|
||||
# email_column = normalize_spaces(annual_usage[0].text + annual_usage[2].text)
|
||||
# assert 'Emails' in email_column
|
||||
# assert '1,000 sent' in email_column
|
||||
|
||||
sms_column = normalize_spaces(annual_usage[0].text + annual_usage[1].text)
|
||||
sms_column = normalize_spaces(annual_usage[0].text)
|
||||
assert 'Text messages' in sms_column
|
||||
assert '251,800 sent' in sms_column
|
||||
assert '250,000 free allowance' in sms_column
|
||||
@@ -845,8 +844,9 @@ def test_usage_page_no_sms_spend(
|
||||
)
|
||||
|
||||
annual_usage = page.find_all('div', {'class': 'govuk-grid-column-one-half'})
|
||||
sms_column = normalize_spaces(annual_usage[0].text + annual_usage[1].text)
|
||||
sms_column = normalize_spaces(annual_usage[0].text)
|
||||
assert 'Text messages' in sms_column
|
||||
assert '1,000 sent' in sms_column
|
||||
assert '250,000 free allowance' in sms_column
|
||||
assert '249,000 free allowance remaining' in sms_column
|
||||
assert '$0.00 spent' not in sms_column
|
||||
@@ -879,7 +879,7 @@ def test_usage_page_monthly_breakdown(
|
||||
assert '1,230 text messages at 1.70p' in monthly_breakdown
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ pytest.mark.parametrize(
|
||||
'now, expected_number_of_months', [
|
||||
(freeze_time("2017-03-31 11:09:00.061258"), 6),
|
||||
(freeze_time("2017-01-01 11:09:00.061258"), 4)
|
||||
@@ -954,7 +954,7 @@ def test_usage_page_for_invalid_year(
|
||||
)
|
||||
|
||||
|
||||
@freeze_time("2012-03-31 12:12:12")
|
||||
@ freeze_time("2012-03-31 12:12:12")
|
||||
def test_future_usage_page(
|
||||
client_request,
|
||||
mock_get_annual_usage_for_service_in_future,
|
||||
@@ -1199,7 +1199,7 @@ def test_get_dashboard_totals_adds_percentages():
|
||||
assert get_dashboard_totals(stats)['email']['failed_percentage'] == '0'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ pytest.mark.parametrize(
|
||||
'failures,expected', [
|
||||
(2, False),
|
||||
(3, False),
|
||||
@@ -1253,7 +1253,7 @@ def _stats(requested, delivered, failed):
|
||||
return {'requested': requested, 'delivered': delivered, 'failed': failed}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dict_in, expected_failed, expected_requested', [
|
||||
@ pytest.mark.parametrize('dict_in, expected_failed, expected_requested', [
|
||||
(
|
||||
{},
|
||||
0,
|
||||
@@ -1433,7 +1433,7 @@ def test_breadcrumb_shows_if_service_is_suspended(
|
||||
assert 'Suspended' in page.select_one('.navigation-service-name').text
|
||||
|
||||
|
||||
@pytest.mark.parametrize('permissions', (
|
||||
@ pytest.mark.parametrize('permissions', (
|
||||
['email', 'sms'],
|
||||
))
|
||||
def test_service_dashboard_shows_usage(
|
||||
@@ -1453,9 +1453,10 @@ def test_service_dashboard_shows_usage(
|
||||
page.select_one('[data-key=usage]').text
|
||||
) == (
|
||||
'$29.85 '
|
||||
'spent on text messages '
|
||||
'0 '
|
||||
'email disabled during SMS pilot'
|
||||
'spent on text messages'
|
||||
# Disabled for pilot
|
||||
# '0 '
|
||||
# 'email disabled during SMS pilot'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -376,10 +376,14 @@ def test_archive_user_does_not_create_event_if_user_client_raises_unexpected_exc
|
||||
mocker,
|
||||
mock_events,
|
||||
):
|
||||
with pytest.raises(Exception):
|
||||
# flake8 doesn't like this
|
||||
# with pytest.raises(Exception):
|
||||
try:
|
||||
client_request.login(platform_admin_user)
|
||||
client_request.post(
|
||||
'main.archive_user', user_id=api_user_active.id,
|
||||
)
|
||||
|
||||
assert 1 == 0
|
||||
except Exception:
|
||||
assert 1 == 1
|
||||
assert not mock_events.called
|
||||
|
||||
@@ -83,7 +83,7 @@ def test_should_show_page_for_one_job(
|
||||
|
||||
assert page.h1.text.strip() == 'thisisatest.csv'
|
||||
assert ' '.join(page.find('tbody').find('tr').text.split()) == (
|
||||
'2021234567 template content Delivered 1 January at 11:10'
|
||||
'2021234567 template content Delivered 1 January at 11:10 UTC'
|
||||
)
|
||||
assert page.find('div', {'data-key': 'notifications'})['data-resource'] == url_for(
|
||||
'main.view_job_updates',
|
||||
@@ -104,7 +104,7 @@ def test_should_show_page_for_one_job(
|
||||
assert normalize_spaces(page.select_one('tbody tr').text) == normalize_spaces(
|
||||
'2021234567 '
|
||||
'template content '
|
||||
'Delivered 1 January at 11:10'
|
||||
'Delivered 1 January at 11:10 UTC'
|
||||
)
|
||||
assert page.select_one('tbody tr a')['href'] == url_for(
|
||||
'main.view_notification',
|
||||
@@ -329,7 +329,7 @@ def test_should_show_scheduled_job(
|
||||
)
|
||||
|
||||
assert normalize_spaces(page.select('main p')[1].text) == (
|
||||
'Sending Two week reminder tomorrow at 05:00'
|
||||
'Sending Two week reminder tomorrow at 05:00 UTC'
|
||||
)
|
||||
assert page.select('main p a')[0]['href'] == url_for(
|
||||
'main.view_template_version',
|
||||
|
||||
@@ -1008,6 +1008,46 @@ def test_get_daily_volumes_report_calls_api_and_download_data(
|
||||
)
|
||||
|
||||
|
||||
def test_get_users_report(
|
||||
client_request,
|
||||
platform_admin_user,
|
||||
mocker
|
||||
):
|
||||
mocker.patch(
|
||||
"app.main.views.platform_admin.user_api_client.get_all_users",
|
||||
return_value=[{
|
||||
'name': 'Johnny Sokko',
|
||||
'organizations': [],
|
||||
'password_changed_at': '2023-07-21 14:12:54.832850', 'permissions': {
|
||||
'test service': [
|
||||
'manage_users', 'manage_templates', 'manage_settings', 'send_texts',
|
||||
'send_emails', 'manage_api_keys', 'view_activity']},
|
||||
'platform_admin': True, 'services': ['test service'], 'state': 'active'}
|
||||
]
|
||||
|
||||
|
||||
)
|
||||
|
||||
client_request.login(platform_admin_user)
|
||||
response = client_request.post_response(
|
||||
'main.get_users_report',
|
||||
_data={},
|
||||
_expected_status=200,
|
||||
)
|
||||
|
||||
assert response.content_type == 'text/csv; charset=utf-8'
|
||||
assert 'attachment' in response.headers['Content-Disposition']
|
||||
assert 'filename' in response.headers['Content-Disposition']
|
||||
assert 'User Report' in response.headers['Content-Disposition']
|
||||
|
||||
my_response = response.get_data(as_text=True)
|
||||
|
||||
assert 'Johnny Sokko' in my_response
|
||||
assert 'manage_users' in my_response
|
||||
assert 'test service' in my_response
|
||||
assert 'active' in my_response
|
||||
|
||||
|
||||
def test_get_daily_sms_provider_volumes_report_calls_api_and_download_data(
|
||||
client_request,
|
||||
platform_admin_user,
|
||||
|
||||
@@ -188,7 +188,7 @@ def test_view_providers_shows_all_providers(
|
||||
assert table_data[1].text.strip() == "20"
|
||||
assert table_data[2].text.strip() == "42"
|
||||
assert table_data[3].text.strip() == "True"
|
||||
assert table_data[4].text.strip() == "16 January at 15:20"
|
||||
assert table_data[4].text.strip() == "16 January at 15:20 UTC"
|
||||
assert table_data[5].text.strip() == "Test User"
|
||||
|
||||
domestic_sms_second_row = domestic_sms_table.tbody.find_all('tr')[1]
|
||||
|
||||
@@ -472,7 +472,7 @@ def test_caseworker_sees_template_page_if_template_is_deleted(
|
||||
|
||||
content = str(page)
|
||||
assert url_for("main.send_one_off", service_id=SERVICE_ONE_ID, template_id=fake_uuid) not in content
|
||||
assert page.select('p.hint')[0].text.strip() == 'This template was deleted today at 15:00.'
|
||||
assert page.select('p.hint')[0].text.strip() == 'This template was deleted today at 15:00 UTC.'
|
||||
|
||||
mock_get_deleted_template.assert_called_with(SERVICE_ONE_ID, template_id, None)
|
||||
|
||||
@@ -1571,7 +1571,7 @@ def test_should_show_page_for_a_deleted_template(
|
||||
content = str(page)
|
||||
assert url_for("main.edit_service_template", service_id=SERVICE_ONE_ID, template_id=fake_uuid) not in content
|
||||
assert url_for("main.send_one_off", service_id=SERVICE_ONE_ID, template_id=fake_uuid) not in content
|
||||
assert page.select('p.hint')[0].text.strip() == 'This template was deleted today at 15:00.'
|
||||
assert page.select('p.hint')[0].text.strip() == 'This template was deleted today at 15:00 UTC.'
|
||||
assert 'Delete this template' not in page.select_one('main').text
|
||||
|
||||
mock_get_deleted_template.assert_called_with(SERVICE_ONE_ID, template_id, None)
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_get_upload_hub_page(
|
||||
|
||||
assert normalize_spaces(uploads[0].text.strip()) == (
|
||||
'some.csv '
|
||||
'Sent 1 January 2016 at 11:09 '
|
||||
'Sent 1 January 2016 at 11:09 UTC '
|
||||
'0 pending 8 delivered 2 failed'
|
||||
)
|
||||
assert uploads[0].select_one('a.file-list-filename-large')['href'] == (
|
||||
@@ -79,12 +79,12 @@ def test_uploads_page_shows_scheduled_jobs(
|
||||
),
|
||||
(
|
||||
'even_later.csv '
|
||||
'Sending 1 January 2016 at 23:09 '
|
||||
'Sending 1 January 2016 at 23:09 UTC '
|
||||
'1 text message waiting to send'
|
||||
),
|
||||
(
|
||||
'send_me_later.csv '
|
||||
'Sending 1 January 2016 at 11:09 '
|
||||
'Sending 1 January 2016 at 11:09 UTC '
|
||||
'1 text message waiting to send'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -103,6 +103,7 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
|
||||
'find_users_by_email',
|
||||
'forgot_password',
|
||||
'get_billing_report',
|
||||
'get_users_report',
|
||||
'get_daily_volumes',
|
||||
'get_daily_sms_provider_volumes',
|
||||
'get_volumes_by_service',
|
||||
|
||||
@@ -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
|
||||
|
||||
109
tests/end_to_end/test_landing_and_sign_in_pages.py
Normal file
109
tests/end_to_end/test_landing_and_sign_in_pages.py
Normal 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...
|
||||
Reference in New Issue
Block a user