From e71989022d234f2733badabbb31e0b152fdd9e68 Mon Sep 17 00:00:00 2001 From: samathad2023 Date: Thu, 4 Apr 2024 18:09:22 -0700 Subject: [PATCH 01/18] flask upgrade --- app/formatters.py | 3 +- app/main/forms.py | 3 +- app/main/views/api_keys.py | 3 +- app/main/views/jobs.py | 2 +- app/main/views/sign_in.py | 2 +- poetry.lock | 104 ++++++++++++++++-------------- pyproject.toml | 8 +-- tests/app/main/views/test_send.py | 6 +- 8 files changed, 70 insertions(+), 61 deletions(-) diff --git a/app/formatters.py b/app/formatters.py index 2bb8854f37..29cb9a4ed4 100644 --- a/app/formatters.py +++ b/app/formatters.py @@ -13,8 +13,9 @@ import markdown import pytz from bs4 import BeautifulSoup -from flask import Markup, render_template_string, url_for +from flask import render_template_string, url_for from flask.helpers import get_root_path +from markupsafe import Markup from notifications_utils.field import Field from notifications_utils.formatters import make_quotes_smart from notifications_utils.formatters import nl2br as utils_nl2br diff --git a/app/main/forms.py b/app/main/forms.py index 13a463a515..2d1820391c 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -4,12 +4,13 @@ from numbers import Number import pytz -from flask import Markup, render_template, request +from flask import render_template, request from flask_login import current_user from flask_wtf import FlaskForm as Form from flask_wtf.file import FileAllowed from flask_wtf.file import FileField as FileField_wtf from flask_wtf.file import FileSize +from markupsafe import Markup from notifications_utils.formatters import strip_all_whitespace from notifications_utils.insensitive_dict import InsensitiveDict from notifications_utils.recipients import InvalidPhoneError, validate_phone_number diff --git a/app/main/views/api_keys.py b/app/main/views/api_keys.py index 8cb28ba591..f93d2caa16 100644 --- a/app/main/views/api_keys.py +++ b/app/main/views/api_keys.py @@ -1,5 +1,6 @@ -from flask import Markup, abort, flash, redirect, render_template, request, url_for +from flask import abort, flash, redirect, render_template, request, url_for from flask_login import current_user +from markupsafe import Markup from app import ( api_key_api_client, diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index 0c0848e469..d914b10f01 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -3,7 +3,6 @@ from functools import partial from flask import ( - Markup, Response, abort, jsonify, @@ -15,6 +14,7 @@ url_for, ) from flask_login import current_user +from markupsafe import Markup from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTemplate from app import ( diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 402e61a080..9f070175c0 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -5,7 +5,6 @@ import jwt import requests from flask import ( - Markup, abort, current_app, flash, @@ -16,6 +15,7 @@ url_for, ) from flask_login import current_user +from markupsafe import Markup from notifications_utils.url_safe_token import generate_token from app import login_manager, user_api_client diff --git a/poetry.lock b/poetry.lock index 930c202327..4eca6b7c16 100644 --- a/poetry.lock +++ b/poetry.lock @@ -171,41 +171,41 @@ files = [ [[package]] name = "boto3" -version = "1.29.6" +version = "1.34.78" description = "The AWS SDK for Python" optional = false -python-versions = ">= 3.7" +python-versions = ">=3.8" files = [ - {file = "boto3-1.29.6-py3-none-any.whl", hash = "sha256:f4d19e01d176c3a5a05e4af733185ff1891b08a3c38d4a439800fa132aa6e9be"}, - {file = "boto3-1.29.6.tar.gz", hash = "sha256:d1d0d979a70bf9b0b13ae3b017f8523708ad953f62d16f39a602d67ee9b25554"}, + {file = "boto3-1.34.78-py3-none-any.whl", hash = "sha256:47a7899af97960493ed58754c838be658650c8fb231c658866f491965ddfc94f"}, + {file = "boto3-1.34.78.tar.gz", hash = "sha256:227487f9a40e7963aa108f4fabc81374d65e085891a2a442c190dfd976b86a9e"}, ] [package.dependencies] -botocore = ">=1.32.6,<1.33.0" +botocore = ">=1.34.78,<1.35.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.7.0,<0.8.0" +s3transfer = ">=0.10.0,<0.11.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.32.7" +version = "1.34.78" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">= 3.7" +python-versions = ">=3.8" files = [ - {file = "botocore-1.32.7-py3-none-any.whl", hash = "sha256:58b33d02cafa23461c8a9d211b30e8cded992380a84de409379fd02811fa3e11"}, - {file = "botocore-1.32.7.tar.gz", hash = "sha256:c6795c731b04c8e3635588c44cfd1a4462fc5987859195522c96812cf3eceff9"}, + {file = "botocore-1.34.78-py3-none-any.whl", hash = "sha256:bc10738826a4970a6d3a40ac40b9799c02b1b661c0c741a67b915b500562ab3c"}, + {file = "botocore-1.34.78.tar.gz", hash = "sha256:889fcfd1813fad225a5a70940c58cd4bd7a6f5ba6c9769a1d41d0c670272b75d"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.19.17)"] +crt = ["awscrt (==0.19.19)"] [[package]] name = "cachecontrol" @@ -801,13 +801,13 @@ flake8-plugin-utils = ">=1.3.2,<2.0.0" [[package]] name = "flask" -version = "2.3.3" +version = "3.0.2" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.8" files = [ - {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, - {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, + {file = "flask-3.0.2-py3-none-any.whl", hash = "sha256:3232e0e9c850d781933cf0207523d1ece087eb8d87b23777ae38456e2fbe7c6e"}, + {file = "flask-3.0.2.tar.gz", hash = "sha256:822c03f4b799204250a7ee84b1eddc40665395333973dfb9deebfe425fefcb7d"}, ] [package.dependencies] @@ -815,7 +815,7 @@ blinker = ">=1.6.2" click = ">=8.1.3" itsdangerous = ">=2.1.2" Jinja2 = ">=3.1.2" -Werkzeug = ">=2.3.7" +Werkzeug = ">=3.0.0" [package.extras] async = ["asgiref (>=3.2)"] @@ -925,13 +925,13 @@ files = [ [[package]] name = "govuk-bank-holidays" -version = "0.13" +version = "0.14" description = "Tool to load UK bank holidays from GOV.UK" optional = false python-versions = ">=3.6" files = [ - {file = "govuk-bank-holidays-0.13.tar.gz", hash = "sha256:ffb6ac050701cb850fd2f08eb9c7c91753090f00525e69cebec4f869f6f0de41"}, - {file = "govuk_bank_holidays-0.13-py3-none-any.whl", hash = "sha256:2b91901be492235c4160a64d56d4d1945f6ca7f11c1ea4277395981866c35bef"}, + {file = "govuk-bank-holidays-0.14.tar.gz", hash = "sha256:ce85102423b72908957d25981f616494729686515d5d66c09a1d35a354ce20a6"}, + {file = "govuk_bank_holidays-0.14-py3-none-any.whl", hash = "sha256:da485c4a40c6c874c925916e492e3f20b807cffba7eed5f07fb69327aef6b10b"}, ] [package.dependencies] @@ -1210,6 +1210,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li optional = false python-versions = ">=3.6" files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, @@ -1219,6 +1220,7 @@ files = [ {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, @@ -1228,6 +1230,7 @@ files = [ {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, @@ -1253,8 +1256,8 @@ files = [ {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cfbac9f6149174f76df7e08c2e28b19d74aed90cad60383ad8671d3af7d0502f"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, @@ -1262,6 +1265,7 @@ files = [ {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, @@ -1512,7 +1516,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, ] [[package]] @@ -1608,19 +1612,19 @@ develop = false [package.dependencies] async-timeout = "^4.0.2" bleach = "^6.1.0" -blinker = "^1.6.2" -boto3 = "^1.28.66" -botocore = "^1.31.66" +blinker = "^1.7.0" +boto3 = "^1.34.77" +botocore = "^1.34.77" cachetools = "^5.3.0" certifi = "^2024.2.2" cffi = "^1.16.0" charset-normalizer = "^3.1.0" -click = "^8.1.3" +click = "^8.1.7" cryptography = "^42.0.4" -flask = "^2.3.2" +flask = "^3.0.2" flask-redis = "^0.4.0" geojson = "^3.0.1" -govuk-bank-holidays = "^0.13" +govuk-bank-holidays = "^0.14" idna = "^3.6" itsdangerous = "^2.1.2" jinja2 = "^3.1.3" @@ -1633,24 +1637,24 @@ phonenumbers = "^8.13.32" pycparser = "^2.21" python-dateutil = "^2.8.2" python-json-logger = "^2.0.7" -pytz = "^2023.3" +pytz = "^2024.1" pyyaml = "^6.0" redis = "^5.0.3" regex = "^2023.12.25" requests = "^2.31.0" -s3transfer = "^0.7.0" +s3transfer = "^0.10.1" shapely = "^2.0.1" six = "^1.16.0" smartypants = "^2.0.1" -urllib3 = "^1.26.18" +urllib3 = "^2.0.7" webencodings = "^0.5.1" werkzeug = "^3.0.1" [package.source] type = "git" url = "https://github.com/GSA/notifications-utils.git" -reference = "HEAD" -resolved_reference = "4cf526bc1fd9532507936c174418dbd3be52c925" +reference = "c2ca77d" +resolved_reference = "c2ca77dd3fa877fd049147fb484790749a5157ad" [[package]] name = "numpy" @@ -2344,13 +2348,13 @@ unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "pytz" -version = "2023.4" +version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.4-py2.py3-none-any.whl", hash = "sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a"}, - {file = "pytz-2023.4.tar.gz", hash = "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] @@ -2378,6 +2382,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2619,20 +2624,20 @@ diagram = ["matplotlib (>=3.0.0)", "pydot (>=1.3.0)", "tqdm (>=v4.31.0)"] [[package]] name = "s3transfer" -version = "0.7.0" +version = "0.10.1" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">= 3.7" +python-versions = ">= 3.8" files = [ - {file = "s3transfer-0.7.0-py3-none-any.whl", hash = "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a"}, - {file = "s3transfer-0.7.0.tar.gz", hash = "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"}, + {file = "s3transfer-0.10.1-py3-none-any.whl", hash = "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d"}, + {file = "s3transfer-0.10.1.tar.gz", hash = "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19"}, ] [package.dependencies] -botocore = ">=1.12.36,<2.0a.0" +botocore = ">=1.33.2,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "setuptools" @@ -2810,19 +2815,20 @@ files = [ [[package]] name = "urllib3" -version = "1.26.18" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.8" files = [ - {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, - {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] -brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" @@ -2930,4 +2936,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "fb81af6421e07a1640bd784f2e1934237a064b30705a0c02e6f06415493e05ff" +content-hash = "6b6022385be06b9867620076b1a210ccac6d8c577b8b69003454fb586918e8e9" diff --git a/pyproject.toml b/pyproject.toml index 40a1aac24d..427d5298ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,19 +12,19 @@ ago = "~=0.0.95" beautifulsoup4 = "^4.12.3" blinker = "~=1.7" exceptiongroup = "==1.2.0" -flask = "~=2.3" +flask = "~=3.0" flask-basicauth = "~=0.2" flask-login = "^0.6" flask-talisman = "*" flask-wtf = "^1.2" -govuk-bank-holidays = "==0.13" +govuk-bank-holidays = "==0.14" gunicorn = {version = "==21.2.0", extras = ["eventlet"]} humanize = "~=4.9" itsdangerous = "~=2.1" jinja2 = "~=3.1" newrelic = "*" notifications-python-client = "==9.0.0" -notifications-utils = {git = "https://github.com/GSA/notifications-utils.git"} +notifications-utils = {git = "https://github.com/GSA/notifications-utils.git", rev = "c2ca77d"} pyexcel = "==0.7.0" pyexcel-io = "==0.6.6" pyexcel-ods3 = "==0.6.1" @@ -33,7 +33,7 @@ pyexcel-xlsx = "==0.6.0" openpyxl = "==3.0.10" pyproj = "==3.6.1" python-dotenv = "==1.0.1" -pytz = "~=2023.4" +pytz = "~=2024.1" rtreelib = "==0.2.0" werkzeug = "^3.0.1" wtforms = "~=3.1" diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index e157728334..ab35dd29be 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -979,7 +979,7 @@ def test_upload_valid_csv_shows_preview_and_table( '
A
', ( ' ' - '
' + "
" "
    " "
  • foo
  • foo
  • foo
  • " "
" @@ -992,7 +992,7 @@ def test_upload_valid_csv_shows_preview_and_table( '
B
', ( ' ' - '
' + "
" "
    " "
  • foo
  • foo
  • foo
  • " "
" @@ -1005,7 +1005,7 @@ def test_upload_valid_csv_shows_preview_and_table( '
C
', ( ' ' - '
' + "
" "
    " "
  • foo
  • foo
  • " "
" From 5dc23947e27ce5a9f38f1d856fc1e2d9239cfb4a Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Mon, 29 Apr 2024 15:44:33 -0400 Subject: [PATCH 02/18] Updated utils to 0.5.1 release in prep for Flask upgrade Signed-off-by: Carlo Costino --- poetry.lock | 14 +++++++------- pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3fa031aa11..69007e3b5a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1680,7 +1680,7 @@ requests = ">=2.0.0" [[package]] name = "notifications-utils" -version = "0.4.6" +version = "0.5.1" description = "" optional = false python-versions = "^3.12.2" @@ -1692,7 +1692,7 @@ async-timeout = "^4.0.2" bleach = "^6.1.0" blinker = "^1.7.0" boto3 = "^1.34.83" -botocore = "^1.34.83" +botocore = "^1.34.92" cachetools = "^5.3.0" certifi = "^2024.2.2" cffi = "^1.16.0" @@ -1711,7 +1711,7 @@ markupsafe = "^2.1.5" mistune = "==0.8.4" numpy = "^1.24.2" ordered-set = "^4.1.0" -phonenumbers = "^8.13.34" +phonenumbers = "^8.13.35" pycparser = "^2.21" python-dateutil = "^2.8.2" python-json-logger = "^2.0.7" @@ -1721,7 +1721,7 @@ redis = "^5.0.3" regex = "^2023.12.25" requests = "^2.31.0" s3transfer = "^0.10.1" -shapely = "^2.0.1" +shapely = "^2.0.4" six = "^1.16.0" smartypants = "^2.0.1" urllib3 = "^2.2.1" @@ -1731,8 +1731,8 @@ werkzeug = "^3.0.1" [package.source] type = "git" url = "https://github.com/GSA/notifications-utils.git" -reference = "d20efc2" -resolved_reference = "d20efc29d68ecbb55ef964db890d17426cf34a0f" +reference = "HEAD" +resolved_reference = "e047ba3e37f9fd885baafe6944a4910285a506fb" [[package]] name = "numpy" @@ -3014,4 +3014,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "0c8a8aec981bc31179f4ad0bafbc48ca36b228949c3bcfc533e7cc8e5c1f79b8" +content-hash = "50b9a0e68826deee9bc4a308db2ae39df0ef5108cfc760a3a497fdd114b40560" diff --git a/pyproject.toml b/pyproject.toml index edfad60937..9050cdea42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ itsdangerous = "~=2.2" jinja2 = "~=3.1" newrelic = "*" notifications-python-client = "==9.0.0" -notifications-utils = {git = "https://github.com/GSA/notifications-utils.git",rev = "d20efc2"} +notifications-utils = {git = "https://github.com/GSA/notifications-utils.git"} pyexcel = "==0.7.0" pyexcel-io = "==0.6.6" pyexcel-ods3 = "==0.6.1" From 83481a8b2869065c56d62940bf1f6bf3d64e08be Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 29 Apr 2024 14:25:13 -0700 Subject: [PATCH 03/18] cleaned up and removed from_job and notification_id from redirect, no longer needed --- app/main/views/send.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/main/views/send.py b/app/main/views/send.py index 02f7d21211..a0ef0b9054 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -1006,8 +1006,6 @@ def send_notification(service_id, template_id): ".view_job", service_id=service_id, job_id=upload_id, - from_job=upload_id, - notification_id=notifications["notifications"][0]["id"], # used to show the final step of the tour (help=3) or not show # a back link on a just sent one off notification (help=0) help=request.args.get("help"), From fb66531c442722cce86123a55f1b1fd5a55c36fc Mon Sep 17 00:00:00 2001 From: Jonathan Bobel Date: Fri, 3 May 2024 12:10:59 -0400 Subject: [PATCH 04/18] 1334 - Updating content for Eligible partners --- app/templates/views/signedout.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/templates/views/signedout.html b/app/templates/views/signedout.html index 3289dd121c..96ecb526d6 100644 --- a/app/templates/views/signedout.html +++ b/app/templates/views/signedout.html @@ -142,6 +142,20 @@

Notify.gov is a new shared service currently being piloted by the Public Benefits Studio, within Technology Transformation Services at the General Services Administration.

+

+ Who's Eligible? +

+

+ Eligible partners include federal agencies as well as state, local, territorial, tribal agencies + administering federally funded programs. As part of the onboarding process, non-federal + programs must specify the federal program that the messages they want to send are related + to. TTS will also seek concurrence from the federal funder. +

+

+ We prioritize partnerships focused on use cases related to increasing transparency and + reducing the burden of navigating government programs for low income individuals and + families. +

From 5fe35b4497f72402b0ffca0e35bb02da47630e9a Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Mon, 6 May 2024 13:02:02 -0400 Subject: [PATCH 05/18] Updating several dependencies related to Flask upgrade Signed-off-by: Carlo Costino --- poetry.lock | 120 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/poetry.lock b/poetry.lock index 69007e3b5a..f7962fa583 100644 --- a/poetry.lock +++ b/poetry.lock @@ -462,63 +462,63 @@ files = [ [[package]] name = "coverage" -version = "7.5.0" +version = "7.5.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, - {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, - {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, - {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, - {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, - {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, - {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, - {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, - {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, - {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, - {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, - {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, - {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, - {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, - {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, - {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, - {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, - {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, - {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, - {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, - {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, - {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, + {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, + {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, + {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, + {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, + {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, + {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, + {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, + {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, + {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, + {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, + {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, + {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, ] [package.extras] @@ -1128,13 +1128,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -2952,13 +2952,13 @@ files = [ [[package]] name = "werkzeug" -version = "3.0.2" +version = "3.0.3" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "werkzeug-3.0.2-py3-none-any.whl", hash = "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795"}, - {file = "werkzeug-3.0.2.tar.gz", hash = "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"}, + {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, + {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, ] [package.dependencies] @@ -3014,4 +3014,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "50b9a0e68826deee9bc4a308db2ae39df0ef5108cfc760a3a497fdd114b40560" +content-hash = "a511e7d8f022826d71a6a1bb47cc7587d81c7ad331f3a6c26a8538b1390929e3" diff --git a/pyproject.toml b/pyproject.toml index 9050cdea42..85d2fd4a20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ pyproj = "==3.6.1" python-dotenv = "==1.0.1" pytz = "^2024.1" rtreelib = "==0.2.0" -werkzeug = "^3.0.1" +werkzeug = "^3.0.3" wtforms = "~=3.1" markdown = "^3.5.2" From 9ecbfb7e05ce21f26c89056ff7138c6e16c2a7f5 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Mon, 6 May 2024 10:43:01 -0700 Subject: [PATCH 06/18] notify-admin-1459 --- app/main/views/sign_in.py | 133 ++++++++++++------------- poetry.lock | 2 + tests/app/main/views/test_sign_in.py | 144 +-------------------------- 3 files changed, 67 insertions(+), 212 deletions(-) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 532bd95006..7af9ce8ca6 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -4,27 +4,15 @@ import jwt import requests -from flask import ( - Response, - abort, - current_app, - flash, - redirect, - render_template, - request, - session, - url_for, -) +from flask import Response, current_app, redirect, render_template, request, url_for from flask_login import current_user -from markupsafe import Markup from notifications_utils.url_safe_token import generate_token from app import login_manager, user_api_client from app.main import main -from app.main.forms import LoginForm from app.main.views.index import error from app.main.views.verify import activate_user -from app.models.user import InvitedUser, User +from app.models.user import User from app.utils import hide_from_search_engines from app.utils.login import is_safe_redirect_url from app.utils.time import is_less_than_days_ago @@ -129,6 +117,16 @@ def verify_email(user, redirect_url): ) +def _handle_e2e_tests(redirect_url): + current_app.logger.warning("E2E TESTS ARE ENABLED.") + current_app.logger.warning( + "If you are getting a 404 on signin, comment out E2E vars in .env file!" + ) + user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL")) + activate_user(user["id"]) + return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url)) + + @main.route("/sign-in", methods=(["GET", "POST"])) @hide_from_search_engines def sign_in(): @@ -146,67 +144,60 @@ def sign_in(): redirect_url = request.args.get("next") if os.getenv("NOTIFY_E2E_TEST_EMAIL"): - current_app.logger.warning("E2E TESTS ARE ENABLED.") - current_app.logger.warning( - "If you are getting a 404 on signin, comment out E2E vars in .env file!" - ) - user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL")) - activate_user(user["id"]) - return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url)) + return _handle_e2e_tests(redirect_url) - current_app.logger.info(f"current user is {current_user}") if current_user and current_user.is_authenticated: if redirect_url and is_safe_redirect_url(redirect_url): return redirect(redirect_url) return redirect(url_for("main.show_accounts_or_dashboard")) - form = LoginForm() - current_app.logger.info("Got the login form") - password_reset_url = url_for(".forgot_password", next=request.args.get("next")) - - if form.validate_on_submit(): - user = User.from_email_address_and_password_or_none( - form.email_address.data, form.password.data - ) - - if user: - # add user to session to mark us as in the process of signing the user in - session["user_details"] = {"email": user.email_address, "id": user.id} - - if user.state == "pending": - return redirect( - url_for("main.resend_email_verification", next=redirect_url) - ) - - if user.is_active: - if session.get("invited_user_id"): - invited_user = InvitedUser.from_session() - if user.email_address.lower() != invited_user.email_address.lower(): - flash("You cannot accept an invite for another person.") - session.pop("invited_user_id", None) - abort(403) - else: - invited_user.accept_invite() - - user.send_login_code() - - if user.sms_auth: - return redirect(url_for(".two_factor_sms", next=redirect_url)) - - if user.email_auth: - return redirect( - url_for(".two_factor_email_sent", next=redirect_url) - ) - - # Vague error message for login in case of user not known, locked, inactive or password not verified - flash( - Markup( - ( - f"The email address or password you entered is incorrect." - f" Forgot your password?" - ) - ) - ) + # form = LoginForm() + # current_app.logger.info("Got the login form") + # password_reset_url = url_for(".forgot_password", next=request.args.get("next")) + + # if form.validate_on_submit(): + # user = User.from_email_address_and_password_or_none( + # form.email_address.data, form.password.data + # ) + + # if user: + # # add user to session to mark us as in the process of signing the user in + # session["user_details"] = {"email": user.email_address, "id": user.id} + + # if user.state == "pending": + # return redirect( + # url_for("main.resend_email_verification", next=redirect_url) + # ) + + # if user.is_active: + # if session.get("invited_user_id"): + # invited_user = InvitedUser.from_session() + # if user.email_address.lower() != invited_user.email_address.lower(): + # flash("You cannot accept an invite for another person.") + # session.pop("invited_user_id", None) + # abort(403) + # else: + # invited_user.accept_invite() + + # user.send_login_code() + + # if user.sms_auth: + # return redirect(url_for(".two_factor_sms", next=redirect_url)) + + # if user.email_auth: + # return redirect( + # url_for(".two_factor_email_sent", next=redirect_url) + # ) + + # # Vague error message for login in case of user not known, locked, inactive or password not verified + # flash( + # Markup( + # ( + # f"The email address or password you entered is incorrect." + # f" Forgot your password?" + # ) + # ) + # ) other_device = current_user.logged_in_elsewhere() @@ -222,10 +213,10 @@ def sign_in(): url = url.replace("STATE", token) return render_template( "views/signin.html", - form=form, + # form=form, again=bool(redirect_url), other_device=other_device, - password_reset_url=password_reset_url, + # password_reset_url=password_reset_url, initial_signin_url=url, ) diff --git a/poetry.lock b/poetry.lock index 69007e3b5a..d9cef6ee5a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1278,6 +1278,7 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1594,6 +1595,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index e89cb6e7b1..0b0638382a 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -134,116 +134,7 @@ def test_logged_in_user_doesnt_do_evil_redirect(client_request): ) -@pytest.mark.parametrize( - "redirect_url", - [ - None, - f"/services/{SERVICE_ONE_ID}/templates", - ], -) -@pytest.mark.parametrize( - ("email_address", "password"), - [ - ("valid@example.gsa.gov", "val1dPassw0rd!"), - (" valid@example.gsa.gov ", " val1dPassw0rd! "), - ], -) -def test_process_sms_auth_sign_in_return_2fa_template( - client_request, - api_user_active, - mock_send_verify_code, - mock_get_user, - mock_get_user_by_email, - mock_verify_password, - email_address, - password, - redirect_url, -): - client_request.logout() - client_request.post( - "main.sign_in", - next=redirect_url, - _data={ - "email_address": email_address, - "password": password, - }, - _expected_redirect=url_for(".two_factor_sms", next=redirect_url), - ) - mock_verify_password.assert_called_with(api_user_active["id"], password) - mock_get_user_by_email.assert_called_with("valid@example.gsa.gov") - - -@pytest.mark.parametrize( - "redirect_url", - [ - None, - f"/services/{SERVICE_ONE_ID}/templates", - ], -) -def test_process_email_auth_sign_in_return_2fa_template( - client_request, - api_user_active_email_auth, - mock_send_verify_code, - mock_verify_password, - mocker, - redirect_url, -): - client_request.logout() - mocker.patch( - "app.user_api_client.get_user", return_value=api_user_active_email_auth - ) - mocker.patch( - "app.user_api_client.get_user_by_email", return_value=api_user_active_email_auth - ) - - client_request.post( - "main.sign_in", - next=redirect_url, - _data={ - "email_address": "valid@example.gsa.gov", - "password": "val1dPassw0rd!", - }, - _expected_redirect=url_for(".two_factor_email_sent", next=redirect_url), - ) - - mock_send_verify_code.assert_called_with( - api_user_active_email_auth["id"], "email", None, redirect_url - ) - mock_verify_password.assert_called_with( - api_user_active_email_auth["id"], "val1dPassw0rd!" - ) - - -def test_should_return_locked_out_true_when_user_is_locked( - client_request, - mock_get_user_by_email_locked, -): - client_request.logout() - page = client_request.post( - "main.sign_in", - _data={ - "email_address": "valid@example.gsa.gov", - "password": "whatIsMyPassword!", - }, - _expected_status=200, - ) - assert "The email address or password you entered is incorrect" in page.text - - -def test_should_return_200_when_user_does_not_exist( - client_request, - mock_get_user_by_email_not_found, -): - client_request.logout() - page = client_request.post( - "main.sign_in", - _data={"email_address": "notfound@gsa.gov", "password": "doesNotExist!"}, - _expected_status=200, - ) - - assert "The email address or password you entered is incorrect" in page.text - - +@pytest.mark.skip("TODO is this still relevant post login.gov switch?") def test_should_return_redirect_when_user_is_pending( client_request, mock_get_user_by_email_pending, @@ -273,6 +164,7 @@ def test_should_return_redirect_when_user_is_pending( f"/services/{SERVICE_ONE_ID}/templates", ], ) +@pytest.mark.skip("TODO is this still relevant post login.gov switch?") def test_should_attempt_redirect_when_user_is_pending( client_request, mock_get_user_by_email_pending, mock_verify_password, redirect_url ): @@ -288,37 +180,7 @@ def test_should_attempt_redirect_when_user_is_pending( ) -def test_email_address_is_treated_case_insensitively_when_signing_in_as_invited_user( - client_request, - mocker, - mock_verify_password, - api_user_active, - sample_invite, - mock_accept_invite, - mock_send_verify_code, - mock_get_invited_user_by_id, -): - client_request.logout() - sample_invite["email_address"] = "TEST@user.gsa.gov" - - mocker.patch( - "app.models.user.User.from_email_address_and_password_or_none", - return_value=User(api_user_active), - ) - - with client_request.session_transaction() as session: - session["invited_user_id"] = sample_invite["id"] - - client_request.post( - "main.sign_in", - _data={"email_address": "test@user.gsa.gov", "password": "val1dPassw0rd!"}, - ) - - assert mock_accept_invite.called - assert mock_send_verify_code.called - mock_get_invited_user_by_id.assert_called_once_with(sample_invite["id"]) - - +@pytest.mark.skip("TODO move this to register and update with login.gov") def test_when_signing_in_as_invited_user_you_cannot_accept_an_invite_for_another_email_address( client_request, mocker, From 78e8dc95fe7c17c7936c9167e84462a8b8070161 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Mon, 6 May 2024 13:12:27 -0700 Subject: [PATCH 07/18] fix tests --- app/main/views/register.py | 70 +++++++++++---------------- tests/app/main/views/test_register.py | 35 ++++++++------ 2 files changed, 48 insertions(+), 57 deletions(-) diff --git a/app/main/views/register.py b/app/main/views/register.py index 90b340b1c9..5bd4d08c76 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -25,7 +25,6 @@ ) from app.main.views import sign_in from app.main.views.verify import activate_user -from app.models.service import Service from app.models.user import InvitedOrgUser, InvitedUser, User from app.utils import hide_from_search_engines, hilite @@ -156,41 +155,12 @@ def set_up_your_profile(): state = request.args.get("state") login_gov_error = request.args.get("error") if code and state: - _handle_login_dot_gov_invite(code, state) + _handle_login_dot_gov_invite(code, state, form) elif login_gov_error: current_app.logger.error(f"login.gov error: {login_gov_error}") raise Exception(f"Could not login with login.gov {login_gov_error}") # end login.gov - # create the user - # TODO we have to provide something for password until that column goes away - # TODO ideally we would set the user's preferred timezone here as well - - user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) - if user is None: - user = User.register( - name=form.name.data, - email_address=user_email, - mobile_number=form.mobile_number.data, - password=str(uuid.uuid4()), - auth_type="sms_auth", - ) - - # activate the user - user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) - activate_user(user["id"]) - usr = User.from_id(user["id"]) - usr.add_to_service( - invited_service.id, - invite_data["permissions"], - invite_data["folder_permissions"], - invite_data["from_user_id"], - ) - current_app.logger.debug( - hilite(f"Added user {usr.email_address} to service {invited_service.name}") - ) - return redirect(url_for("main.show_accounts_or_dashboard")) - return render_template("views/set-up-your-profile.html", form=form) @@ -208,26 +178,16 @@ def invited_user_accept_invite(invited_user_id): invited_user.accept_invite() -def _handle_login_dot_gov_invite(code, state): +def _handle_login_dot_gov_invite(code, state, form): access_token = sign_in._get_access_token(code, state) - print(f"access token {access_token}") user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token) - print(f"user_email {user_email} user_uuid {user_uuid}") - print(f"state = {state}") invite_data = state.encode("utf8") - print(f"encoded invite data {invite_data}") invite_data = base64.b64decode(invite_data) - print(f"decoded invite data = {invite_data}") invite_data = json.loads(invite_data) - print(f"final invite data {invite_data}") - invited_service = Service.from_id(invite_data["service_id"]) - print(f"invited service {invited_service}") invited_user_id = invite_data["invited_user_id"] invited_user_email_address = get_invited_user_email_address(invited_user_id) - print(f"invited_user_email_address = {invited_user_email_address}") if user_email.lower() != invited_user_email_address.lower(): - print(f"HITTING THE FLASH") flash("You cannot accept an invite for another person.") session.pop("invited_user_id", None) abort(403) @@ -239,3 +199,29 @@ def _handle_login_dot_gov_invite(code, state): ) ) current_app.logger.debug(hilite("ACCEPTED INVITE")) + user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) + if user is None: + user = User.register( + name=form.name.data, + email_address=user_email, + mobile_number=form.mobile_number.data, + password=str(uuid.uuid4()), + auth_type="sms_auth", + ) + + # activate the user + user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) + activate_user(user["id"]) + usr = User.from_id(user["id"]) + usr.add_to_service( + invite_data["service_id"], + invite_data["permissions"], + invite_data["folder_permissions"], + invite_data["from_user_id"], + ) + current_app.logger.debug( + hilite( + f"Added user {usr.email_address} to service {invite_data['service_id']}" + ) + ) + return redirect(url_for("main.show_accounts_or_dashboard")) diff --git a/tests/app/main/views/test_register.py b/tests/app/main/views/test_register.py index b82f7dde15..2a13ebb9d7 100644 --- a/tests/app/main/views/test_register.py +++ b/tests/app/main/views/test_register.py @@ -5,8 +5,9 @@ import pytest from flask import url_for +from app.main.forms import RegisterUserForm from app.main.views.register import _handle_login_dot_gov_invite -from app.models.user import InvitedUser, User +from app.models.user import User from tests.conftest import normalize_spaces @@ -549,11 +550,6 @@ def test_handle_login_dot_gov_invite_bad_email(client_request, mocker): return_value=["fake@fake.gov", "12345"], ) - mocker.patch( - "app.main.views.register.Service.from_id", - return_value={"service_from_id"}, - ) - mocker.patch( "app.main.views.register.get_invited_user_email_address", return_value="boo@fake.gov", @@ -570,8 +566,7 @@ def test_handle_login_dot_gov_invite_bad_email(client_request, mocker): invite_data = invite_data.encode("utf8") invite_data = base64.b64encode(invite_data) invite_data = invite_data.decode("utf8") - print(f"sending this as state {invite_data}") - _handle_login_dot_gov_invite("code", invite_data) + _handle_login_dot_gov_invite("code", invite_data, RegisterUserForm()) mock_flash.assert_called_once_with( "You cannot accept an invite for another person." ) @@ -591,22 +586,32 @@ def test_handle_login_dot_gov_invite_good_email(client_request, mocker): ) mocker.patch( - "app.main.views.register.Service.from_id", - return_value={"service_from_id"}, + "app.main.views.register.get_invited_user_email_address", + return_value="fake@fake.gov", ) mocker.patch( - "app.main.views.register.get_invited_user_email_address", - return_value="fake@fake.gov", + "app.main.views.register.user_api_client.get_user_by_uuid_or_email", + return_value={"id": "abc"}, + ) + + mock_user = mocker.patch( + "app.main.views.register.User.add_to_service", ) mock_accept = mocker.patch("app.main.views.register.invited_user_accept_invite") - invite_data = {"service_id": "service", "invited_user_id": "invited_user"} + invite_data = { + "service_id": "service", + "invited_user_id": "invited_user", + "permissions": ["manage_everything"], + "folder_permissions": [], + "from_user_id": "xyz", + } invite_data = json.dumps(invite_data) invite_data = invite_data.encode("utf8") invite_data = base64.b64encode(invite_data) invite_data = invite_data.decode("utf8") - print(f"sending this as state {invite_data}") - _handle_login_dot_gov_invite("code", invite_data) + _handle_login_dot_gov_invite("code", invite_data, RegisterUserForm()) mock_accept.assert_called_once() + mock_user.assert_called_once_with("service", ["manage_everything"], [], "xyz") From 39b0a4ff5226a0a262f459b43fb93f39df93a832 Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Tue, 7 May 2024 09:56:34 -0400 Subject: [PATCH 08/18] Update several dependencies from Dependabot This changeset updates a few dependencies that Dependabot flagged for updates. We cannot merge the Dependabot PRs at the moment due to E2E test compatability issues. Signed-off-by: Carlo Costino --- poetry.lock | 24 ++++++++++++------------ pyproject.toml | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index f7962fa583..bb92d1c041 100644 --- a/poetry.lock +++ b/poetry.lock @@ -149,13 +149,13 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "blinker" -version = "1.8.1" +version = "1.8.2" description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.8" files = [ - {file = "blinker-1.8.1-py3-none-any.whl", hash = "sha256:5f1cdeff423b77c31b89de0565cd03e5275a03028f44b2b15f912632a58cced6"}, - {file = "blinker-1.8.1.tar.gz", hash = "sha256:da44ec748222dcd0105ef975eed946da197d5bdf8bafb6aa92f5bc89da63fa25"}, + {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, + {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, ] [[package]] @@ -1895,18 +1895,18 @@ pip = "*" [[package]] name = "pip-audit" -version = "2.7.2" +version = "2.7.3" description = "A tool for scanning Python environments for known vulnerabilities" optional = false python-versions = ">=3.8" files = [ - {file = "pip_audit-2.7.2-py3-none-any.whl", hash = "sha256:49907430115baacb8bb7ffc1a2b689acfeac9d8534a43bffad3c73f8d8b32d52"}, - {file = "pip_audit-2.7.2.tar.gz", hash = "sha256:a12905e42dd452f43a2dbf895606d59c35348deed27b8cbaff8516423576fdfb"}, + {file = "pip_audit-2.7.3-py3-none-any.whl", hash = "sha256:46a11faee3323f76adf7987de8171daeb660e8f57d8088cc27fb1c1e5c7747b0"}, + {file = "pip_audit-2.7.3.tar.gz", hash = "sha256:08891bbf179bffe478521f150818112bae998424f58bf9285c0078965aef38bc"}, ] [package.dependencies] CacheControl = {version = ">=0.13.0", extras = ["filecache"]} -cyclonedx-python-lib = ">=5,<7" +cyclonedx-python-lib = ">=5,<8" html5lib = ">=1.1" packaging = ">=23.0.0" pip-api = ">=0.0.28" @@ -1918,7 +1918,7 @@ toml = ">=0.10" [package.extras] dev = ["build", "bump (>=1.3.2)", "pip-audit[doc,lint,test]"] doc = ["pdoc"] -lint = ["interrogate", "mypy", "ruff (<0.2.3)", "types-html5lib", "types-requests", "types-toml"] +lint = ["interrogate", "mypy", "ruff (<0.4.3)", "setuptools", "types-html5lib", "types-requests", "types-toml"] test = ["coverage[toml] (>=7.0,!=7.3.3,<8.0)", "pretend", "pytest", "pytest-cov"] [[package]] @@ -2334,13 +2334,13 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-playwright" -version = "0.4.4" +version = "0.5.0" description = "A pytest wrapper with fixtures for Playwright to automate web browsers" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-playwright-0.4.4.tar.gz", hash = "sha256:5488db4cc49028491c5130af0a2bb6b1d0b222a202217f6d14491d4c9aa67ff9"}, - {file = "pytest_playwright-0.4.4-py3-none-any.whl", hash = "sha256:df306f3a60a8631a3cfde1b95a2ed5a89203a3408dfa1154de049ca7de87c90b"}, + {file = "pytest-playwright-0.5.0.tar.gz", hash = "sha256:f9f5ae8ade2f773e6e2cd85ec6bfff2ab287f7943108b3956fe5971324151622"}, + {file = "pytest_playwright-0.5.0-py3-none-any.whl", hash = "sha256:b382c870384419c025d66aea14518bab71fb9e79917d4808692cde70d8c5216a"}, ] [package.dependencies] @@ -3014,4 +3014,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "a511e7d8f022826d71a6a1bb47cc7587d81c7ad331f3a6c26a8538b1390929e3" +content-hash = "794a77ce83a38682d5a7def4212fe212cbec48573b4fdd1e2dad3c4669fa98ac" diff --git a/pyproject.toml b/pyproject.toml index 85d2fd4a20..04102164e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ package-mode = false python = "^3.12.2" ago = "~=0.0.95" beautifulsoup4 = "^4.12.3" -blinker = "~=1.7" +blinker = "~=1.8" exceptiongroup = "==1.2.1" flask = "~=3.0" flask-basicauth = "~=0.2" @@ -56,7 +56,7 @@ pre-commit = "^3.6.0" pytest = "^8.1.1" pytest-env = "^1.1.3" pytest-mock = "^3.14.0" -pytest-playwright = "^0.4.4" +pytest-playwright = "^0.5.0" pytest-xdist = "^3.5.0" radon = "^6.0.1" requests-mock = "^1.11.0" From 125ad7a2f75fd31275afd6fc8743e0e7dd3182e4 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Tue, 7 May 2024 11:11:12 -0700 Subject: [PATCH 09/18] remove logged in elsewhere check --- app/models/user.py | 4 ++- tests/app/main/views/test_sign_in.py | 45 ---------------------------- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/app/models/user.py b/app/models/user.py index 7e9f10632d..3261dec012 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -141,7 +141,9 @@ def set_permissions(self, service_id, permissions, folder_permissions, set_by_id ) def logged_in_elsewhere(self): - return session.get("current_session_id") != self.current_session_id + # This check is deprecated due to the transition to using login.gov. + return False + # return session.get("current_session_id") != self.current_session_id def activate(self): if self.is_pending: diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index 0b0638382a..135f4a5baf 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -47,21 +47,6 @@ def test_sign_in_explains_session_timeout(client_request): ) -def test_sign_in_explains_other_browser(client_request, api_user_active, mocker): - api_user_active["current_session_id"] = str(uuid.UUID(int=1)) - mocker.patch("app.user_api_client.get_user", return_value=api_user_active) - - with client_request.session_transaction() as session: - session["current_session_id"] = str(uuid.UUID(int=2)) - - page = client_request.get("main.sign_in", next="/foo") - - assert ( - "We signed you out because you logged in to Notify on another device" - in page.text - ) - - def test_doesnt_redirect_to_sign_in_if_no_session_info( client_request, api_user_active, @@ -78,36 +63,6 @@ def test_doesnt_redirect_to_sign_in_if_no_session_info( client_request.get("main.add_service") -@pytest.mark.parametrize( - ("db_sess_id", "cookie_sess_id"), - [ - (None, None), - (None, uuid.UUID(int=1)), # BAD - cookie doesn't match db - ( - uuid.UUID(int=1), - None, - ), # BAD - has used other browsers before but this is a brand new browser with no cookie - ( - uuid.UUID(int=1), - uuid.UUID(int=2), - ), # BAD - this person has just signed in on a different browser - ], -) -def test_redirect_to_sign_in_if_logged_in_from_other_browser( - client_request, api_user_active, mocker, db_sess_id, cookie_sess_id -): - api_user_active["current_session_id"] = db_sess_id - mocker.patch("app.user_api_client.get_user", return_value=api_user_active) - with client_request.session_transaction() as session: - session["current_session_id"] = str(cookie_sess_id) - - client_request.get( - "main.choose_account", - _expected_status=302, - _expected_redirect=url_for("main.sign_in", next="/accounts"), - ) - - def test_logged_in_user_redirects_to_account(client_request): client_request.get( "main.sign_in", From 64aa45dcaa4d9ec189ff99794bf2c36f0dfa08b0 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Tue, 7 May 2024 12:37:04 -0700 Subject: [PATCH 10/18] remove commented out code --- app/main/views/sign_in.py | 50 --------------------------------------- 1 file changed, 50 deletions(-) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 7af9ce8ca6..f9873c656a 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -151,54 +151,6 @@ def sign_in(): return redirect(redirect_url) return redirect(url_for("main.show_accounts_or_dashboard")) - # form = LoginForm() - # current_app.logger.info("Got the login form") - # password_reset_url = url_for(".forgot_password", next=request.args.get("next")) - - # if form.validate_on_submit(): - # user = User.from_email_address_and_password_or_none( - # form.email_address.data, form.password.data - # ) - - # if user: - # # add user to session to mark us as in the process of signing the user in - # session["user_details"] = {"email": user.email_address, "id": user.id} - - # if user.state == "pending": - # return redirect( - # url_for("main.resend_email_verification", next=redirect_url) - # ) - - # if user.is_active: - # if session.get("invited_user_id"): - # invited_user = InvitedUser.from_session() - # if user.email_address.lower() != invited_user.email_address.lower(): - # flash("You cannot accept an invite for another person.") - # session.pop("invited_user_id", None) - # abort(403) - # else: - # invited_user.accept_invite() - - # user.send_login_code() - - # if user.sms_auth: - # return redirect(url_for(".two_factor_sms", next=redirect_url)) - - # if user.email_auth: - # return redirect( - # url_for(".two_factor_email_sent", next=redirect_url) - # ) - - # # Vague error message for login in case of user not known, locked, inactive or password not verified - # flash( - # Markup( - # ( - # f"The email address or password you entered is incorrect." - # f" Forgot your password?" - # ) - # ) - # ) - other_device = current_user.logged_in_elsewhere() token = generate_token( @@ -213,10 +165,8 @@ def sign_in(): url = url.replace("STATE", token) return render_template( "views/signin.html", - # form=form, again=bool(redirect_url), other_device=other_device, - # password_reset_url=password_reset_url, initial_signin_url=url, ) From 9c40a109003b43b3171f9f527859698127d73568 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Tue, 7 May 2024 13:58:59 -0700 Subject: [PATCH 11/18] cleanup --- app/main/views/register.py | 62 +++----- tests/app/main/views/test_accept_invite.py | 124 ---------------- tests/app/main/views/test_register.py | 156 --------------------- tests/app/test_navigation.py | 1 - 4 files changed, 20 insertions(+), 323 deletions(-) diff --git a/app/main/views/register.py b/app/main/views/register.py index 5bd4d08c76..1df9917339 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -17,9 +17,8 @@ from app import user_api_client from app.main import main -from app.main.forms import ( +from app.main.forms import ( # RegisterUserFromInviteForm, RegisterUserForm, - RegisterUserFromInviteForm, RegisterUserFromOrgInviteForm, SetupUserProfileForm, ) @@ -43,39 +42,10 @@ def register(): return render_template("views/register.html", form=form) -@main.route("/register-from-invite", methods=["GET", "POST"]) -# TODO This is deprecated, we are now handling invites in the -# login.gov workflow -def register_from_invite(): - invited_user = InvitedUser.from_session() - if not invited_user: - abort(404) - - form = RegisterUserFromInviteForm(invited_user) - - if form.validate_on_submit(): - if ( - form.service.data != invited_user.service - or form.email_address.data != invited_user.email_address - ): - abort(400) - _do_registration(form, send_email=False, send_sms=invited_user.sms_auth) - invited_user.accept_invite() - if invited_user.sms_auth: - return redirect(url_for("main.verify")) - else: - # we've already proven this user has email because they clicked the invite link, - # so just activate them straight away - return activate_user(session["user_details"]["id"]) - - return render_template( - "views/register-from-invite.html", invited_user=invited_user, form=form - ) - - @main.route("/register-from-org-invite", methods=["GET", "POST"]) # TODO This is deprecated, we are now handling invites in the -# login.gov workflow +# login.gov workflow. Leaving it here until we write the new +# org registration. def register_from_org_invite(): invited_org_user = InvitedOrgUser.from_session() if not invited_org_user: @@ -178,27 +148,35 @@ def invited_user_accept_invite(invited_user_id): invited_user.accept_invite() +def debug_msg(msg): + current_app.logger.debug(hilite(msg)) + + def _handle_login_dot_gov_invite(code, state, form): access_token = sign_in._get_access_token(code, state) + debug_msg("Got the access token for login.gov") user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token) + debug_msg( + f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov" + ) invite_data = state.encode("utf8") invite_data = base64.b64decode(invite_data) invite_data = json.loads(invite_data) invited_user_id = invite_data["invited_user_id"] invited_user_email_address = get_invited_user_email_address(invited_user_id) + debug_msg(f"email address from the invite_date is {invited_user_email_address}") if user_email.lower() != invited_user_email_address.lower(): + debug_msg("invited user email did not match expected email, abort(403)") flash("You cannot accept an invite for another person.") session.pop("invited_user_id", None) abort(403) else: invited_user_accept_invite() - current_app.logger.debug( - hilite( - f"INVITED USER {invited_user_email_address} to service {invite_data['service_id']}" - ) + debug_msg( + f"invited user {invited_user_email_address} to service {invite_data['service_id']}" ) - current_app.logger.debug(hilite("ACCEPTED INVITE")) + debug_msg("accepted invite") user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) if user is None: user = User.register( @@ -208,10 +186,12 @@ def _handle_login_dot_gov_invite(code, state, form): password=str(uuid.uuid4()), auth_type="sms_auth", ) + debug_msg(f"registered user {form.name.data} with email {user_email}") # activate the user user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) activate_user(user["id"]) + debug_msg("activated user") usr = User.from_id(user["id"]) usr.add_to_service( invite_data["service_id"], @@ -219,9 +199,7 @@ def _handle_login_dot_gov_invite(code, state, form): invite_data["folder_permissions"], invite_data["from_user_id"], ) - current_app.logger.debug( - hilite( - f"Added user {usr.email_address} to service {invite_data['service_id']}" - ) + debug_msg( + f"Added user {usr.email_address} to service {invite_data['service_id']}" ) return redirect(url_for("main.show_accounts_or_dashboard")) diff --git a/tests/app/main/views/test_accept_invite.py b/tests/app/main/views/test_accept_invite.py index 38202dfb24..a6d622c12e 100644 --- a/tests/app/main/views/test_accept_invite.py +++ b/tests/app/main/views/test_accept_invite.py @@ -431,71 +431,6 @@ def test_existing_signed_out_user_accept_invite_redirects_to_sign_in( ) -@pytest.mark.usefixtures("_mock_no_users_for_service") -def test_new_user_accept_invite_calls_api_and_redirects_to_registration( - client_request, - service_one, - mock_check_invite_token, - mock_dont_get_user_by_email, - mock_add_user_to_service, - mock_get_service, - mocker, -): - client_request.logout() - client_request.get( - "main.accept_invite", - token="thisisnotarealtoken", - _expected_redirect="/register-from-invite", - ) - - mock_check_invite_token.assert_called_with("thisisnotarealtoken") - mock_dont_get_user_by_email.assert_called_with("invited_user@test.gsa.gov") - - -@pytest.mark.usefixtures("_mock_no_users_for_service") -def test_new_user_accept_invite_calls_api_and_views_registration_page( - client_request, - service_one, - sample_invite, - mock_check_invite_token, - mock_dont_get_user_by_email, - mock_get_invited_user_by_id, - mock_add_user_to_service, - mock_get_service, - mocker, -): - client_request.logout() - page = client_request.get( - "main.accept_invite", - token="thisisnotarealtoken", - _follow_redirects=True, - ) - - mock_check_invite_token.assert_called_with("thisisnotarealtoken") - mock_dont_get_user_by_email.assert_called_with("invited_user@test.gsa.gov") - mock_get_invited_user_by_id.assert_called_once_with(sample_invite["id"]) - - assert page.h1.string.strip() == "Create an account" - - assert normalize_spaces(page.select_one("main p").text) == ( - "Your account will be created with this email address: " - "invited_user@test.gsa.gov" - ) - - form = page.find("form") - name = form.find("input", id="name") - password = form.find("input", id="password") - service = form.find("input", type="hidden", id="service") - email = form.find("input", type="hidden", id="email_address") - - assert email - assert email.attrs["value"] == "invited_user@test.gsa.gov" - assert name - assert password - assert service - assert service.attrs["value"] == service_one["id"] - - def test_cancelled_invited_user_accepts_invited_redirect_to_cancelled_invitation( client_request, mock_get_user, @@ -562,65 +497,6 @@ def test_new_user_accept_invite_with_malformed_token( ) -@pytest.mark.usefixtures("_mock_no_users_for_service") -def test_new_user_accept_invite_completes_new_registration_redirects_to_verify( - client_request, - service_one, - sample_invite, - api_user_active, - mock_check_invite_token, - mock_dont_get_user_by_email, - mock_email_is_not_already_in_use, - mock_register_user, - mock_send_verify_code, - mock_get_invited_user_by_id, - mock_accept_invite, - mock_add_user_to_service, - mock_get_service, - mocker, -): - client_request.logout() - expected_redirect_location = "/register-from-invite" - - client_request.get( - "main.accept_invite", - token="thisisnotarealtoken", - _expected_redirect=expected_redirect_location, - ) - with client_request.session_transaction() as session: - assert session.get("invited_user_id") == sample_invite["id"] - - data = { - "service": sample_invite["service"], - "email_address": sample_invite["email_address"], - "from_user": sample_invite["from_user"], - "password": "longpassword", - "mobile_number": "+12027890123", - "name": "Invited User", - "auth_type": "email_auth", - } - - expected_redirect_location = "/verify" - client_request.post( - "main.register_from_invite", - _data=data, - _expected_redirect=expected_redirect_location, - ) - - mock_send_verify_code.assert_called_once_with(ANY, "sms", data["mobile_number"]) - mock_get_invited_user_by_id.assert_called_once_with(sample_invite["id"]) - - mock_register_user.assert_called_with( - data["name"], - data["email_address"], - data["mobile_number"], - data["password"], - data["auth_type"], - ) - - assert mock_accept_invite.call_count == 1 - - def test_signed_in_existing_user_cannot_use_anothers_invite( client_request, mocker, diff --git a/tests/app/main/views/test_register.py b/tests/app/main/views/test_register.py index 2a13ebb9d7..b491a1b53c 100644 --- a/tests/app/main/views/test_register.py +++ b/tests/app/main/views/test_register.py @@ -219,144 +219,6 @@ def test_register_with_existing_email_sends_emails( ) -@pytest.mark.parametrize( - ("email_address", "expected_value"), - [ - ("first.last@example.com", "First Last"), - ("first.middle.last@example.com", "First Middle Last"), - ("first.m.last@example.com", "First Last"), - ("first.last-last@example.com", "First Last-Last"), - ("first.o'last@example.com", "First O’Last"), - ("first.last+testing@example.com", "First Last"), - ("first.last+testing+testing@example.com", "First Last"), - ("first.last6@example.com", "First Last"), - ("first.last.212@example.com", "First Last"), - ("first.2.last@example.com", "First Last"), - ("first.2b.last@example.com", "First Last"), - ("first.1.2.3.last@example.com", "First Last"), - ("first.last.1.2.3@example.com", "First Last"), - # Instances where we can’t make a good-enough guess: - ("example123@example.com", None), - ("f.last@example.com", None), - ("f.m.last@example.com", None), - ], -) -def test_shows_name_on_registration_page_from_invite( - client_request, - fake_uuid, - email_address, - expected_value, - sample_invite, - mock_get_invited_user_by_id, -): - sample_invite["email_address"] = email_address - with client_request.session_transaction() as session: - session["invited_user_id"] = sample_invite - - page = client_request.get("main.register_from_invite") - assert page.select_one("input[name=name]").get("value") == expected_value - - -def test_shows_hidden_email_address_on_registration_page_from_invite( - client_request, - fake_uuid, - sample_invite, - mock_get_invited_user_by_id, -): - with client_request.session_transaction() as session: - session["invited_user_id"] = sample_invite - - page = client_request.get("main.register_from_invite") - assert normalize_spaces(page.select_one("main p").text) == ( - "Your account will be created with this email address: invited_user@test.gsa.gov" - ) - hidden_input = page.select_one("form .usa-sr-only input") - for attr, value in ( - ("type", "email"), - ("name", "username"), - ("id", "username"), - ("value", "invited_user@test.gsa.gov"), - ("disabled", "disabled"), - ("tabindex", "-1"), - ("aria-hidden", "true"), - ("autocomplete", "username"), - ): - assert hidden_input[attr] == value - - -@pytest.mark.parametrize( - "extra_data", - [ - {}, - # The username field is present in the page but the POST request - # should ignore it - {"username": "invited@user.com"}, - {"username": "anythingelse@example.com"}, - ], -) -def test_register_from_invite( - client_request, - fake_uuid, - mock_email_is_not_already_in_use, - mock_register_user, - mock_send_verify_code, - mock_accept_invite, - mock_get_invited_user_by_id, - sample_invite, - extra_data, -): - client_request.logout() - with client_request.session_transaction() as session: - session["invited_user_id"] = sample_invite["id"] - client_request.post( - "main.register_from_invite", - _data=dict( - name="Registered in another Browser", - email_address=sample_invite["email_address"], - mobile_number="+12024900460", - service=sample_invite["service"], - password="somreallyhardthingtoguess", - auth_type="sms_auth", - **extra_data, - ), - _expected_redirect=url_for("main.verify"), - ) - mock_register_user.assert_called_once_with( - "Registered in another Browser", - sample_invite["email_address"], - "+12024900460", - "somreallyhardthingtoguess", - "sms_auth", - ) - mock_get_invited_user_by_id.assert_called_once_with(sample_invite["id"]) - - -def test_register_from_invite_when_user_registers_in_another_browser( - client_request, - api_user_active, - mock_get_user_by_email, - mock_accept_invite, - mock_get_invited_user_by_id, - sample_invite, -): - client_request.logout() - sample_invite["email_address"] = api_user_active["email_address"] - with client_request.session_transaction() as session: - session["invited_user_id"] = sample_invite["id"] - client_request.post( - "main.register_from_invite", - _data={ - "name": "Registered in another Browser", - "email_address": api_user_active["email_address"], - "mobile_number": api_user_active["mobile_number"], - "service": sample_invite["service"], - "password": "somreallyhardthingtoguess", - "auth_type": "sms_auth", - }, - _expected_redirect=url_for("main.verify"), - ) - - @pytest.mark.parametrize( "invite_email_address", ["gov-user@gsa.gov", "non-gov-user@example.com"] ) @@ -520,24 +382,6 @@ def test_cannot_register_with_sms_auth_and_missing_mobile_number( assert err.attrs["data-error-label"] == "mobile_number" -def test_register_from_invite_form_doesnt_show_mobile_number_field_if_email_auth( - client_request, - sample_invite, - mock_get_invited_user_by_id, -): - client_request.logout() - sample_invite["auth_type"] = "email_auth" - with client_request.session_transaction() as session: - session["invited_user_id"] = sample_invite["id"] - - page = client_request.get("main.register_from_invite") - - assert ( - page.find("input", attrs={"name": "auth_type"}).attrs["value"] == "email_auth" - ) - assert page.find("input", attrs={"name": "mobile_number"}) is None - - def test_handle_login_dot_gov_invite_bad_email(client_request, mocker): mocker.patch( diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 4634a65cf1..6f1cf58eba 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -146,7 +146,6 @@ "received_text_messages_callback", "redact_template", "register", - "register_from_invite", "register_from_org_invite", "registration_continue", "remove_user_from_organization", From 1d9457f9a2e978727f0c3ca61b3ee0f6fb1b7ac1 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Tue, 7 May 2024 14:36:21 -0700 Subject: [PATCH 12/18] add debug statements --- app/main/views/register.py | 10 +++++++--- tests/app/main/views/test_register.py | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app/main/views/register.py b/app/main/views/register.py index 1df9917339..fbcf42a736 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -125,7 +125,7 @@ def set_up_your_profile(): state = request.args.get("state") login_gov_error = request.args.get("error") if code and state: - _handle_login_dot_gov_invite(code, state, form) + return _handle_login_dot_gov_invite(code, state, form) elif login_gov_error: current_app.logger.error(f"login.gov error: {login_gov_error}") raise Exception(f"Could not login with login.gov {login_gov_error}") @@ -153,16 +153,20 @@ def debug_msg(msg): def _handle_login_dot_gov_invite(code, state, form): - + debug_msg(f"enter _handle_login_dot_gov_invite with code {code} state {state}") access_token = sign_in._get_access_token(code, state) debug_msg("Got the access token for login.gov") user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token) debug_msg( f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov" ) + debug_msg(f"raw state {state}") invite_data = state.encode("utf8") + debug_msg(f"utf8 encoded state {invite_data}") invite_data = base64.b64decode(invite_data) + debug_msg(f"b64 decoded state {invite_data}") invite_data = json.loads(invite_data) + debug_msg(f"final state {invite_data}") invited_user_id = invite_data["invited_user_id"] invited_user_email_address = get_invited_user_email_address(invited_user_id) debug_msg(f"email address from the invite_date is {invited_user_email_address}") @@ -172,7 +176,7 @@ def _handle_login_dot_gov_invite(code, state, form): session.pop("invited_user_id", None) abort(403) else: - invited_user_accept_invite() + invited_user_accept_invite(invited_user_id) debug_msg( f"invited user {invited_user_email_address} to service {invite_data['service_id']}" ) diff --git a/tests/app/main/views/test_register.py b/tests/app/main/views/test_register.py index b491a1b53c..f6a96d6b0e 100644 --- a/tests/app/main/views/test_register.py +++ b/tests/app/main/views/test_register.py @@ -459,3 +459,30 @@ def test_handle_login_dot_gov_invite_good_email(client_request, mocker): _handle_login_dot_gov_invite("code", invite_data, RegisterUserForm()) mock_accept.assert_called_once() mock_user.assert_called_once_with("service", ["manage_everything"], [], "xyz") + + +# Taken from the API project in service_invite/rest.py +def get_user_data_url_safe(data): + data = json.dumps(data) + data = base64.b64encode(data.encode("utf8")) + return data.decode("utf8") + + +def get_decoded(state): + state = state.encode("utf8") + state = base64.b64decode(state) + state = json.loads(state) + return state + + +# Test that we can successfully decode the invited user +# data that is sent in the state param +def test_decode_state(): + invite_data = { + "from_user_id": "abc", + "service_id": "bcd", + "permissions": ["manage_everything"], + "folder_permissions": [], + } + state = get_user_data_url_safe(invite_data) + assert invite_data == get_decoded(state) From 87a0204fe2f74e6c7ba67af8d7a17799f90e3ae8 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 8 May 2024 08:36:39 -0700 Subject: [PATCH 13/18] explain how to create a first user in the db --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index f907dca88b..ebbb5af882 100644 --- a/README.md +++ b/README.md @@ -351,6 +351,28 @@ This will run the local development web server and make the admin site available at http://localhost:6012; remember to make sure that the Notify.gov API is running as well! +## Creating a 'First User' in the database + +After you have completed all setup steps, you will be unable to log in, because there +will not be a user in the database to link to the login.gov account you are using. So +you will need to create that user in your database using the 'create-test-user' command. + +Open two terminals pointing to the api project and then run these commands in the +respective terminals. Make sure the email address is the same one you are using in +login.gov and make sure your phone number is in the format 5555555555. + +(Server 1) +env ALLOW_EXPIRED_API_TOKEN=1 make run-flask + +(Server 2) +poetry run flask command create-admin-jwt | tail -n 1 | pbcopy +poetry run flask command create-test-user --name="" --email="" --mobile_number="" --password="" --admin=True; + +If for any reason in the course of development it is necessary for your to delete your db +via the `dropdb` command, you will need to repeat these steps when you recreate your db. + + + ## Git Hooks We're using [`pre-commit`](https://pre-commit.com/) to manage hooks in order to From b79835dfd2cd2556a51c8b1e3f58e9abe151cd00 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 8 May 2024 09:10:41 -0700 Subject: [PATCH 14/18] code review feedback --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ebbb5af882..0d7ffc5bc3 100644 --- a/README.md +++ b/README.md @@ -358,15 +358,17 @@ will not be a user in the database to link to the login.gov account you are usin you will need to create that user in your database using the 'create-test-user' command. Open two terminals pointing to the api project and then run these commands in the -respective terminals. Make sure the email address is the same one you are using in -login.gov and make sure your phone number is in the format 5555555555. +respective terminals. (Server 1) env ALLOW_EXPIRED_API_TOKEN=1 make run-flask (Server 2) poetry run flask command create-admin-jwt | tail -n 1 | pbcopy -poetry run flask command create-test-user --name="" --email="" --mobile_number="" --password="" --admin=True; +poetry run flask command create-test-user --admin=True; + +Supply your name, email address, mobile number, and password when prompted. Make sure the email address +is the same one you are using in login.gov and make sure your phone number is in the format 5555555555. If for any reason in the course of development it is necessary for your to delete your db via the `dropdb` command, you will need to repeat these steps when you recreate your db. From e51517347ffa8b1656533d88d10895b28ae64150 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 8 May 2024 11:06:10 -0700 Subject: [PATCH 15/18] convert to test fixture --- tests/app/main/views/test_register.py | 22 +++++++--------------- tests/conftest.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/tests/app/main/views/test_register.py b/tests/app/main/views/test_register.py index f6a96d6b0e..cf4fba5a24 100644 --- a/tests/app/main/views/test_register.py +++ b/tests/app/main/views/test_register.py @@ -461,14 +461,7 @@ def test_handle_login_dot_gov_invite_good_email(client_request, mocker): mock_user.assert_called_once_with("service", ["manage_everything"], [], "xyz") -# Taken from the API project in service_invite/rest.py -def get_user_data_url_safe(data): - data = json.dumps(data) - data = base64.b64encode(data.encode("utf8")) - return data.decode("utf8") - - -def get_decoded(state): +def decode_invite_data(state): state = state.encode("utf8") state = base64.b64decode(state) state = json.loads(state) @@ -477,12 +470,11 @@ def get_decoded(state): # Test that we can successfully decode the invited user # data that is sent in the state param -def test_decode_state(): - invite_data = { - "from_user_id": "abc", - "service_id": "bcd", - "permissions": ["manage_everything"], +def test_decode_state(encoded_invite_data): + assert decode_invite_data(encoded_invite_data) == { "folder_permissions": [], + "from_user_id": "xyz", + "invited_user_id": "invited_user", + "permissions": ["manage_everything"], + "service_id": "service", } - state = get_user_data_url_safe(invite_data) - assert invite_data == get_decoded(state) diff --git a/tests/conftest.py b/tests/conftest.py index ab86e30523..fe1ee0c02a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import base64 import copy import json import os @@ -1901,6 +1902,25 @@ def sample_invite(mocker, service_one): ) +@pytest.fixture() +def encoded_invite_data(): + """ + This mimics what API does when it encodes invite data in + service_invite/rest.py + """ + invite_data = { + "service_id": "service", + "invited_user_id": "invited_user", + "permissions": ["manage_everything"], + "folder_permissions": [], + "from_user_id": "xyz", + } + invite_data = json.dumps(invite_data) + invite_data = invite_data.encode("utf8") + invite_data = base64.b64encode(invite_data) + return invite_data.decode("utf8") + + @pytest.fixture() def expired_invite(service_one): id_ = USER_ONE_ID From 852c0e57c8b0f2e2b6b327ee5dd7965a73f76356 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 8 May 2024 11:30:51 -0700 Subject: [PATCH 16/18] convert to test fixture --- app/main/views/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main/views/register.py b/app/main/views/register.py index fbcf42a736..e9e03a76a2 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -17,7 +17,7 @@ from app import user_api_client from app.main import main -from app.main.forms import ( # RegisterUserFromInviteForm, +from app.main.forms import ( RegisterUserForm, RegisterUserFromOrgInviteForm, SetupUserProfileForm, From 26988d255fad93a81e8279df26020479b6bc2dc8 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 8 May 2024 12:45:01 -0700 Subject: [PATCH 17/18] remove logged_in_elsewhere as per code review feedback --- app/main/views/sign_in.py | 3 --- app/models/user.py | 11 +---------- tests/app/models/test_user.py | 1 - 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index f9873c656a..618a356542 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -151,8 +151,6 @@ def sign_in(): return redirect(redirect_url) return redirect(url_for("main.show_accounts_or_dashboard")) - other_device = current_user.logged_in_elsewhere() - token = generate_token( str(request.remote_addr), current_app.config["SECRET_KEY"], @@ -166,7 +164,6 @@ def sign_in(): return render_template( "views/signin.html", again=bool(redirect_url), - other_device=other_device, initial_signin_url=url, ) diff --git a/app/models/user.py b/app/models/user.py index 3261dec012..932e754c8c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -140,11 +140,6 @@ def set_permissions(self, service_id, permissions, folder_permissions, set_by_id set_by_id=set_by_id, ) - def logged_in_elsewhere(self): - # This check is deprecated due to the transition to using login.gov. - return False - # return session.get("current_session_id") != self.current_session_id - def activate(self): if self.is_pending: user_data = user_api_client.activate_user(self.id) @@ -198,7 +193,7 @@ def is_gov_user(self): @property def is_authenticated(self): - return not self.logged_in_elsewhere() and super(User, self).is_authenticated + return super(User, self).is_authenticated @property def platform_admin(self): @@ -676,10 +671,6 @@ def accept_invite(self): class AnonymousUser(AnonymousUserMixin): - # set the anonymous user so that if a new browser hits us we don't error http://stackoverflow.com/a/19275188 - - def logged_in_elsewhere(self): - return False @property def default_organization(self): diff --git a/tests/app/models/test_user.py b/tests/app/models/test_user.py index 8c4c269072..1caf85f4a5 100644 --- a/tests/app/models/test_user.py +++ b/tests/app/models/test_user.py @@ -6,7 +6,6 @@ def test_anonymous_user(notify_admin): assert AnonymousUser().is_authenticated is False - assert AnonymousUser().logged_in_elsewhere() is False assert AnonymousUser().default_organization.name is None assert AnonymousUser().default_organization.domains == [] assert AnonymousUser().default_organization.organization_type is None From 85ddd494b35d728cbb4dbaaff6e130f7c2804c25 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Thu, 9 May 2024 07:33:26 -0700 Subject: [PATCH 18/18] remove debug statement --- app/utils/csv.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/utils/csv.py b/app/utils/csv.py index 04d5d19ed3..e6119b073a 100644 --- a/app/utils/csv.py +++ b/app/utils/csv.py @@ -1,7 +1,6 @@ import datetime import pytz -from flask import current_app from flask_login import current_user from notifications_utils.recipients import RecipientCSV @@ -67,7 +66,6 @@ def generate_notifications_csv(**kwargs): from app import notification_api_client from app.s3_client.s3_csv_client import s3download - current_app.logger.info("\n\n\n\nENTER generate_notifications_csv") if "page" not in kwargs: kwargs["page"] = 1