diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index be0a6cc66..ab6778272 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -157,9 +157,11 @@ jobs: - uses: ./.github/actions/setup-project - name: Create requirements.txt run: poetry export --without-hashes --format=requirements.txt > requirements.txt - - uses: pypa/gh-action-pip-audit@v1.0.6 + - uses: pypa/gh-action-pip-audit@v1.0.8 with: inputs: requirements.txt + ignore-vulns: | + GHSA-w3h3-4rj7-4ph4 - name: Run npm audit run: make npm-audit diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 4d5aca14d..8453ef369 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -134,11 +134,17 @@ def template_usage(service_id): months=months, stats=stats, most_used_template_count=max( - max( - (template["requested_count"] for template in month["templates_used"]), - default=0, - ) - for month in months + ( + max( + ( + template["requested_count"] + for template in month["templates_used"] + ), + default=0, + ) + for month in months + ), + default=0, ), years=get_tuples_of_financial_years( partial(url_for, ".template_usage", service_id=service_id), @@ -155,31 +161,16 @@ def usage(service_id): year, current_financial_year = requested_and_current_financial_year(request) free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year( - service_id, year + service_id ) + units = billing_api_client.get_monthly_usage_for_service(service_id, year) + yearly_usage = billing_api_client.get_annual_usage_for_service(service_id, year) more_stats = format_monthly_stats_to_list( service_api_client.get_monthly_notification_stats(service_id, year)["data"] ) - if year == current_financial_year: - # This includes Oct, Nov, Dec - # but we don't need next year's data yet - more_stats = [ - month - for month in more_stats - if month["name"] in ["October", "November", "December"] - ] - elif year == (current_financial_year + 1): - # This is all the other months - # and we need last year's data - more_stats = [ - month - for month in more_stats - if month["name"] not in ["October", "November", "December"] - ] - return render_template( "views/usage.html", months=list(get_monthly_usage_breakdown(year, units, more_stats)), @@ -341,8 +332,15 @@ def get_dashboard_partials(service_id): dashboard_totals = (get_dashboard_totals(stats),) free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year( current_service.id, - get_current_financial_year(), ) + # These 2 calls will update the dashboard sms allowance count while in trial mode. + billing_api_client.get_monthly_usage_for_service( + service_id, get_current_financial_year() + ) + billing_api_client.create_or_update_free_sms_fragment_limit( + service_id, free_sms_fragment_limit=free_sms_allowance + ) + yearly_usage = billing_api_client.get_annual_usage_for_service( service_id, get_current_financial_year(), @@ -433,13 +431,7 @@ def aggregate_status_types(counts_dict): def get_months_for_financial_year(year, time_format="%B"): - return [ - month.strftime(time_format) - for month in ( - get_months_for_year(10, 13, year) + get_months_for_year(1, 10, year + 1) - ) - if month < datetime.now() - ] + return [month.strftime(time_format) for month in (get_months_for_year(1, 13, year))] def get_months_for_year(start, end, year): diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 402e61a08..ee427322e 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -6,6 +6,7 @@ import jwt import requests from flask import ( Markup, + Response, abort, current_app, flash, @@ -26,6 +27,7 @@ from app.main.views.verify import activate_user from app.models.user import InvitedUser, 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 def _reformat_keystring(orig): @@ -63,6 +65,10 @@ def _get_access_token(code, state): url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code" headers = {"Authorization": "Bearer %s" % token} response = requests.post(url, headers=headers) + if response.json().get("access_token") is None: + # Capture the response json here so it hopefully shows up in error reports + current_app.logger.error(f"Error when getting access token {response.json()}") + raise KeyError(f"'access_token' {response.json()}") access_token = response.json()["access_token"] return access_token @@ -84,31 +90,59 @@ def _do_login_dot_gov(): code = request.args.get("code") state = request.args.get("state") login_gov_error = request.args.get("error") - if code and state: - access_token = _get_access_token(code, state) - user_email, user_uuid = _get_user_email_and_uuid(access_token) - redirect_url = request.args.get("next") + + if 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}") + elif code and state: # activate the user try: + access_token = _get_access_token(code, state) + user_email, user_uuid = _get_user_email_and_uuid(access_token) + redirect_url = request.args.get("next") user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) - activate_user(user["id"]) + + # Check if the email needs to be revalidated + is_fresh_email = is_less_than_days_ago( + user["email_access_validated_at"], 90 + ) + if not is_fresh_email: + return verify_email(user, redirect_url) + + usr = User.from_email_address(user["email_address"]) + activate_user(usr.id) except BaseException as be: # noqa B036 current_app.logger.error(be) error(401) - return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url)) - 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 +def verify_email(user, redirect_url): + user_api_client.send_verify_code(user["id"], "email", None, redirect_url) + title = "Email resent" if request.args.get("email_resent") else "Check your email" + redirect_url = request.args.get("next") + return render_template( + "views/re-validate-email-sent.html", title=title, redirect_url=redirect_url + ) + + @main.route("/sign-in", methods=(["GET", "POST"])) @hide_from_search_engines def sign_in(): - _do_login_dot_gov() + # If we have to revalidated the email, send the message + # via email and redirect to the "verify your email page" + # and don't proceed further with login + email_verify_template = _do_login_dot_gov() + if ( + email_verify_template + and not isinstance(email_verify_template, Response) + and "Check your email" in email_verify_template + ): + return email_verify_template + redirect_url = request.args.get("next") if os.getenv("NOTIFY_E2E_TEST_EMAIL"): @@ -192,7 +226,6 @@ def sign_in(): form=form, again=bool(redirect_url), other_device=other_device, - login_gov_enabled=True, password_reset_url=password_reset_url, initial_signin_url=url, ) diff --git a/app/templates/error/500.html b/app/templates/error/500.html index af86388d7..aca8332df 100644 --- a/app/templates/error/500.html +++ b/app/templates/error/500.html @@ -7,7 +7,7 @@ Sorry, we can't deliver what you asked for right now.

- Please try again later or email us for more information.

+ Please try again later or email us for more information.

diff --git a/app/templates/new/components/main_nav.html b/app/templates/new/components/main_nav.html new file mode 100644 index 000000000..a3b02823e --- /dev/null +++ b/app/templates/new/components/main_nav.html @@ -0,0 +1,30 @@ +{% if help %} + {% include 'partials/tour.html' %} +{% else %} + +{% endif %} diff --git a/app/templates/new/components/org_nav.html b/app/templates/new/components/org_nav.html new file mode 100644 index 000000000..f7f7a5b93 --- /dev/null +++ b/app/templates/new/components/org_nav.html @@ -0,0 +1,11 @@ + diff --git a/app/templates/new/components/service_navigation.html b/app/templates/new/components/service_navigation.html new file mode 100644 index 000000000..e3826d279 --- /dev/null +++ b/app/templates/new/components/service_navigation.html @@ -0,0 +1,15 @@ + diff --git a/app/templates/new/components/settings_navigation.html b/app/templates/new/components/settings_navigation.html new file mode 100644 index 000000000..d94c417e7 --- /dev/null +++ b/app/templates/new/components/settings_navigation.html @@ -0,0 +1,16 @@ +{% if help %} +{% include 'partials/tour.html' %} +{% else %} + +{% endif %} diff --git a/app/templates/new/layouts/org_template.html b/app/templates/new/layouts/org_template.html index 5e04003fd..69f49b76a 100644 --- a/app/templates/new/layouts/org_template.html +++ b/app/templates/new/layouts/org_template.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "/new/base.html" %} {% block per_page_title %} {% block org_page_title %}{% endblock %} – {{ current_org.name }} @@ -17,7 +17,7 @@
- {% include "org_nav.html" %} + {% include "/new/components/org_nav.html" %}
{% block beforeContent %} diff --git a/app/templates/new/layouts/withnav_template.html b/app/templates/new/layouts/withnav_template.html index 412a004d0..d06d069e9 100644 --- a/app/templates/new/layouts/withnav_template.html +++ b/app/templates/new/layouts/withnav_template.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "/new/base.html" %} {% block per_page_title %} {% block service_page_title %}{% endblock %} – {{ current_service.name }} @@ -7,9 +7,9 @@ {% block main %}
{% block serviceNavigation %} - {% include "service_navigation.html" %} + {% include "new/components/service_navigation.html" %} {% endblock %} - +
{% if help %}
@@ -17,8 +17,8 @@
{% endif %} {% block sideNavigation %} - {% include "main_nav.html" %} - + {% include "/new/components/main_nav.html" %} + {% endblock %}
{% if help %} diff --git a/app/templates/new/templates_glossary.md b/app/templates/new/templates_glossary.md index 04f2c4151..1c52e75dc 100644 --- a/app/templates/new/templates_glossary.md +++ b/app/templates/new/templates_glossary.md @@ -14,6 +14,10 @@ This document serves as a glossary for the templates directory structure of the - `head.html`: Template for the site's , included in `base.html`. - `header.html`: Template for the site's header, included in `base.html`. - `footer.html`: Template for the site's footer, included in `base.html`. + - `settings_navigation.html`: The settings navigation used in `withnav_template.html` that previously extended `settings_template.html`. + - `org_nav.html`: The organization's navigation used solely in `org_template.html`. + - `main_nav.html`: The main navigation used in `withnav_template.html` + - `service_navigation.html`: The service navigation used in `withnav_template.html`. In withnav_template.html, the `serviceNavigation` block will be left empty in any child templates that previously extended `settings_template.html`. - **/views** (or **/pages**): Individual page templates that use the base layouts, components, and partials to present content. ### Best Practices @@ -30,9 +34,9 @@ This document serves as a glossary for the templates directory structure of the - withoutnav_template.html Delete - main_template.html Delete - settings_templates.html `withnav_template` can be used to replace `settings_template`. -- settings_nav.html (move to /new/navigation directory) -- main_nav.html (move to /new/navigation directory) -- service_navigation.html (move to /new/navigation directory) +- settings_nav.html (move to /components/ directory) +- main_nav.html (move to /components/ directory) +- service_navigation.html (move to /components/ directory) - org_template, could be under it's own directory called /layout/organization -- org_nav.html (move to /new/navigation directory) +- org_nav.html (move to /components/ directory) - content_template.html Delete diff --git a/app/templates/views/signin.html b/app/templates/views/signin.html index 6d6f578ee..5532eb7c1 100644 --- a/app/templates/views/signin.html +++ b/app/templates/views/signin.html @@ -12,18 +12,6 @@ {% block maincolumn_content %} -{% if login_gov_enabled %} -
-
-
-

Login.gov is required by April 16, 2024

-

- You have left to use Login.gov to sign in -

-
-
-
-{% endif %}
{% if again %} @@ -39,22 +27,12 @@ {% endif %} {% else %}

Sign in

- {% if login_gov_enabled %} -

You can access your account by signing in with one of the options below:

- Sign in with Login.gov -

Or:

- {% endif %} +

Access your Notify.gov account by signing in with Login.gov:

+ Sign in with Login.gov {% endif %} - - {% call form_wrapper(autocomplete=True) %} - {{ form.email_address(param_extensions={"autocomplete": "email"}) }} - {{ form.password(param_extensions={"autocomplete": "current-password"}) }} - {{ page_footer("Continue", secondary_link=password_reset_url, secondary_link_text="Forgot your password?") }} - {% endcall %}
- {% if login_gov_enabled %}
-

Notify.gov is changing the sign-in experience to Login.gov effective
April 16, 2024

+

Effective April 16, 2024 Notify.gov requires you sign-in through Login.gov

Why are we doing this?

  • Enhanced security: Login.gov is really secure and trustworthy
  • @@ -64,12 +42,11 @@

    What do I need to do?

    • If you have a Login.gov account, start using it to sign in to Notify today.
    • -
    • If you don’t have a Login.gov account, you must create one by April 16, 2024 to continue to access Notify.
    • +
    • If you don’t have a Login.gov account, you must create one to continue to access Notify.
    - Create Login.gov account + Create Login.gov account
-{% endif %} {% endblock %} diff --git a/app/utils/time.py b/app/utils/time.py index 9ae24b1bd..e2f4a8e36 100644 --- a/app/utils/time.py +++ b/app/utils/time.py @@ -11,7 +11,7 @@ def get_current_financial_year(): now = datetime.now(preferred_tz) current_month = int(now.strftime("%-m")) current_year = int(now.strftime("%Y")) - return current_year if current_month > 9 else current_year - 1 + return current_year if current_month < 10 else current_year + 1 def is_less_than_days_ago(date_from_db, number_of_days): diff --git a/poetry.lock b/poetry.lock index 49a2e0aad..8d6781821 100644 --- a/poetry.lock +++ b/poetry.lock @@ -87,33 +87,33 @@ lxml = ["lxml"] [[package]] name = "black" -version = "24.3.0" +version = "24.4.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, - {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, - {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, - {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, - {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, - {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, - {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, - {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, - {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, - {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, - {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, - {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, - {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, - {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, - {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, - {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, - {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, - {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, - {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, - {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, - {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, - {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, + {file = "black-24.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436"}, + {file = "black-24.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf"}, + {file = "black-24.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad"}, + {file = "black-24.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb"}, + {file = "black-24.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8"}, + {file = "black-24.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745"}, + {file = "black-24.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070"}, + {file = "black-24.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397"}, + {file = "black-24.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"}, + {file = "black-24.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33"}, + {file = "black-24.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965"}, + {file = "black-24.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd"}, + {file = "black-24.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1"}, + {file = "black-24.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8"}, + {file = "black-24.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d"}, + {file = "black-24.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3"}, + {file = "black-24.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665"}, + {file = "black-24.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6"}, + {file = "black-24.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e"}, + {file = "black-24.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702"}, + {file = "black-24.4.0-py3-none-any.whl", hash = "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e"}, + {file = "black-24.4.0.tar.gz", hash = "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641"}, ] [package.dependencies] @@ -171,17 +171,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.79" +version = "1.34.83" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.79-py3-none-any.whl", hash = "sha256:265b0b4865e8c07e27abb32a31d2bd9129bb009b1d89ca0783776ec084886123"}, - {file = "boto3-1.34.79.tar.gz", hash = "sha256:139dd2d94eaa0e3213ff37ba7cf4cb2e3823269178fe8f3e33c965f680a9ddde"}, + {file = "boto3-1.34.83-py3-none-any.whl", hash = "sha256:33cf93f6de5176f1188c923f4de1ae149ed723b89ed12e434f2b2f628491769e"}, + {file = "boto3-1.34.83.tar.gz", hash = "sha256:9733ce811bd82feab506ad9309e375a79cabe8c6149061971c17754ce8997551"}, ] [package.dependencies] -botocore = ">=1.34.79,<1.35.0" +botocore = ">=1.34.83,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -190,13 +190,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.79" +version = "1.34.83" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.79-py3-none-any.whl", hash = "sha256:a42a014d3dbaa9ef123810592af69f9e55b456c5be3ac9efc037325685519e83"}, - {file = "botocore-1.34.79.tar.gz", hash = "sha256:6b59b0f7de219d383a2a633f6718c2600642ebcb707749dc6c67a6a436474b7a"}, + {file = "botocore-1.34.83-py3-none-any.whl", hash = "sha256:0a3fbbe018416aeefa8978454fb0b8129adbaf556647b72269bf02e4bf1f4161"}, + {file = "botocore-1.34.83.tar.gz", hash = "sha256:0f302aa76283d4df62b4fbb6d3d20115c1a8957fc02171257fc93904d69d5636"}, ] [package.dependencies] @@ -1080,13 +1080,13 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -1603,7 +1603,7 @@ requests = ">=2.0.0" [[package]] name = "notifications-utils" -version = "0.4.5" +version = "0.4.6" description = "" optional = false python-versions = "^3.12.2" @@ -1614,8 +1614,8 @@ develop = false async-timeout = "^4.0.2" bleach = "^6.1.0" blinker = "^1.6.2" -boto3 = "^1.34.77" -botocore = "^1.34.79" +boto3 = "^1.34.83" +botocore = "^1.34.83" cachetools = "^5.3.0" certifi = "^2024.2.2" cffi = "^1.16.0" @@ -1626,7 +1626,7 @@ flask = "^2.3.2" flask-redis = "^0.4.0" geojson = "^3.0.1" govuk-bank-holidays = "^0.14" -idna = "^3.6" +idna = "^3.7" itsdangerous = "^2.1.2" jinja2 = "^3.1.3" jmespath = "^1.0.1" @@ -1655,7 +1655,7 @@ werkzeug = "^3.0.1" type = "git" url = "https://github.com/GSA/notifications-utils.git" reference = "HEAD" -resolved_reference = "7d1d2e9bb3791316231e97433c71da6a70c4d2ab" +resolved_reference = "d0db6073406bd160d2007edb9d00e41c9d5d44b7" [[package]] name = "numpy" diff --git a/tests/app/main/forms/test_placeholder_form.py b/tests/app/main/forms/test_placeholder_form.py index 40a12ae56..01075472f 100644 --- a/tests/app/main/forms/test_placeholder_form.py +++ b/tests/app/main/forms/test_placeholder_form.py @@ -48,7 +48,6 @@ def test_form_class_not_mutated(notify_admin): (False, "phone number", "sms", "2028675309", None), (False, "phone number", "sms", "+1 (202) 867-5309", None), (True, "phone number", "sms", "+123", "Not enough digits"), - (True, "phone number", "sms", "+44(0)7900 900-123", None), (True, "phone number", "sms", "+1-2345-678890", None), (False, "anything else", "sms", "", "Cannot be empty"), (False, "anything else", "email", "", "Cannot be empty"), diff --git a/tests/app/main/views/organizations/test_organizations.py b/tests/app/main/views/organizations/test_organizations.py index 7679e1429..27663ad22 100644 --- a/tests/app/main/views/organizations/test_organizations.py +++ b/tests/app/main/views/organizations/test_organizations.py @@ -388,7 +388,7 @@ def test_organization_services_shows_live_services_and_usage( client_request.login(active_user_with_permissions) page = client_request.get(".organization_dashboard", org_id=ORGANISATION_ID) - mock.assert_called_once_with(ORGANISATION_ID, 2019) + mock.assert_called_once_with(ORGANISATION_ID, 2020) services = page.select("main h3") usage_rows = page.select("main .grid-col-6") @@ -459,9 +459,9 @@ def test_organization_services_shows_live_services_and_usage_with_count_of_1( @pytest.mark.parametrize( ("financial_year", "expected_selected"), [ - (2017, "2017 to 2018 fiscal year"), (2018, "2018 to 2019 fiscal year"), (2019, "2019 to 2020 fiscal year"), + (2020, "2020 to 2021 fiscal year"), ], ) def test_organization_services_filters_by_financial_year( @@ -483,9 +483,9 @@ def test_organization_services_filters_by_financial_year( ) mock.assert_called_once_with(ORGANISATION_ID, financial_year) assert normalize_spaces(page.select_one(".pill").text) == ( + "2020 to 2021 fiscal year " "2019 to 2020 fiscal year " - "2018 to 2019 fiscal year " - "2017 to 2018 fiscal year" + "2018 to 2019 fiscal year" ) assert normalize_spaces(page.select_one(".pill-item--selected").text) == ( expected_selected @@ -610,7 +610,7 @@ def test_organization_services_links_to_downloadable_report( assert link_to_report.attrs["href"] == url_for( ".download_organization_usage_report", org_id=ORGANISATION_ID, - selected_year=2021, + selected_year=2022, ) diff --git a/tests/app/main/views/test_accept_invite.py b/tests/app/main/views/test_accept_invite.py index c677eeec2..d1eadc28f 100644 --- a/tests/app/main/views/test_accept_invite.py +++ b/tests/app/main/views/test_accept_invite.py @@ -222,8 +222,7 @@ def test_if_existing_user_accepts_twice_they_redirect_to_sign_in( ) == ( "You need to sign in again", # TODO: Improve this given Login.gov configuration. - # "We signed you out because you have not used Notify for a while.", - "You have left to use Login.gov to sign in", + "We signed you out because you have not used Notify for a while.", ) # We don’t let people update `email_access_validated_at` using an # already-accepted invite @@ -338,8 +337,7 @@ def test_existing_user_of_service_get_redirected_to_signin( ) == ( "You need to sign in again", # TODO: Improve this given Login.gov configuration. - # "We signed you out because you have not used Notify for a while.", - "You have left to use Login.gov to sign in", + "We signed you out because you have not used Notify for a while.", ) assert mock_accept_invite.call_count == 1 @@ -429,8 +427,7 @@ def test_existing_signed_out_user_accept_invite_redirects_to_sign_in( ) == ( "You need to sign in again", # TODO: Improve this given Login.gov configuration. - # "We signed you out because you have not used Notify for a while.", - "You have left to use Login.gov to sign in", + "We signed you out because you have not used Notify for a while.", ) diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 4c86fb328..926163dbd 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -354,10 +354,9 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages( "(202) 867-5300 message-2 1 hour ago", "(202) 867-5300 message-3 1 hour ago", "(202) 867-5302 message-4 3 hours ago", - "+33 1 12 34 56 78 message-5 5 hours ago", + "+33(0)1 12345678 message-5 5 hours ago", "(202) 555-0104 message-6 7 hours ago", "(202) 555-0104 message-7 9 hours ago", - "+682 12345 message-8 9 hours ago", ] ), ) @@ -519,10 +518,10 @@ def test_download_inbox( "(202) 867-5300,message-2,07-01-2016 10:59 US/Eastern\r\n" "(202) 867-5300,message-3,07-01-2016 10:59 US/Eastern\r\n" "(202) 867-5302,message-4,07-01-2016 08:59 US/Eastern\r\n" - "+33 1 12 34 56 78,message-5,07-01-2016 06:59 US/Eastern\r\n" + "+33(0)1 12345678,message-5,07-01-2016 06:59 US/Eastern\r\n" "(202) 555-0104,message-6,07-01-2016 04:59 US/Eastern\r\n" "(202) 555-0104,message-7,07-01-2016 02:59 US/Eastern\r\n" - "+682 12345,message-8,07-01-2016 02:59 US/Eastern\r\n" + "+68212345,message-8,07-01-2016 02:59 US/Eastern\r\n" ) @@ -679,12 +678,12 @@ def test_should_show_redirect_from_template_history( ) -@freeze_time("2017-01-01 12:00") # 4 months into 2016 financial year +@freeze_time("2017-01-01 12:00") @pytest.mark.parametrize( "extra_args", [ {}, - {"year": "2016"}, + {"year": "2017"}, ], ) def test_should_show_monthly_breakdown_of_template_usage( @@ -696,7 +695,7 @@ def test_should_show_monthly_breakdown_of_template_usage( "main.template_usage", service_id=SERVICE_ONE_ID, **extra_args ) - mock_get_monthly_template_usage.assert_called_once_with(SERVICE_ONE_ID, 2016) + mock_get_monthly_template_usage.assert_called_once_with(SERVICE_ONE_ID, 2017) table_rows = page.select("tbody tr") @@ -704,9 +703,22 @@ def test_should_show_monthly_breakdown_of_template_usage( "My first template " "Text message template " "2" ) - assert len(table_rows) == len(["October"]) + assert len(table_rows) == len(["January"]) + # October is the only month with data, thus it's not in the list. assert len(page.select(".table-no-data")) == len( - ["November", "December", "January"] + [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "November", + "December", + ] ) @@ -749,9 +761,9 @@ def test_stats_pages_show_last_3_years( ) assert normalize_spaces(page.select_one(".pill").text) == ( + "2015 to 2016 fiscal year " "2014 to 2015 fiscal year " - "2013 to 2014 fiscal year " - "2012 to 2013 fiscal year" + "2013 to 2014 fiscal year" ) @@ -966,18 +978,18 @@ def test_usage_page( service_id=SERVICE_ONE_ID, ) - mock_get_monthly_usage_for_service.assert_called_once_with(SERVICE_ONE_ID, 2011) - mock_get_annual_usage_for_service.assert_called_once_with(SERVICE_ONE_ID, 2011) - mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID, 2011) + mock_get_monthly_usage_for_service.assert_called_once_with(SERVICE_ONE_ID, 2012) + mock_get_annual_usage_for_service.assert_called_once_with(SERVICE_ONE_ID, 2012) + mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID) nav = page.find("ul", {"class": "pill"}) unselected_nav_links = nav.select("a:not(.pill-item--selected)") assert ( normalize_spaces(nav.find("a", {"aria-current": "page"}).text) - == "2011 to 2012 fiscal year" + == "2012 to 2013 fiscal year" ) - assert normalize_spaces(unselected_nav_links[0].text) == "2010 to 2011 fiscal year" - assert normalize_spaces(unselected_nav_links[1].text) == "2009 to 2010 fiscal year" + assert normalize_spaces(unselected_nav_links[0].text) == "2011 to 2012 fiscal year" + assert normalize_spaces(unselected_nav_links[1].text) == "2010 to 2011 fiscal year" annual_usage = page.find_all("div", {"class": "keyline-block"}) @@ -1044,6 +1056,7 @@ def test_usage_page_monthly_breakdown( page = client_request.get("main.usage", service_id=SERVICE_ONE_ID) monthly_breakdown = normalize_spaces(page.find("table").text) + assert "January" in monthly_breakdown assert "October" in monthly_breakdown assert "February" in monthly_breakdown assert "March" in monthly_breakdown @@ -1052,8 +1065,8 @@ def test_usage_page_monthly_breakdown( @pytest.mark.parametrize( ("now", "expected_number_of_months"), [ - (freeze_time("2017-03-31 11:09:00.061258"), 6), - (freeze_time("2017-01-01 11:09:00.061258"), 4), + (freeze_time("2017-03-31 11:09:00.061258"), 12), + (freeze_time("2017-01-01 11:09:00.061258"), 12), ], ) def test_usage_page_monthly_breakdown_shows_months_so_far( @@ -1113,7 +1126,7 @@ def test_usage_page_with_year_argument( ) mock_get_monthly_usage_for_service.assert_called_once_with(SERVICE_ONE_ID, 2000) mock_get_annual_usage_for_service.assert_called_once_with(SERVICE_ONE_ID, 2000) - mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID, 2000) + mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID) mock_get_monthly_notification_stats.assert_called_with(SERVICE_ONE_ID, 2000) @@ -1148,7 +1161,7 @@ def test_future_usage_page( mock_get_annual_usage_for_service_in_future.assert_called_once_with( SERVICE_ONE_ID, 2014 ) - mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID, 2014) + mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID) mock_get_monthly_notification_stats.assert_called_with(SERVICE_ONE_ID, 2014) @@ -1379,7 +1392,9 @@ def test_route_for_service_permissions( mock_has_no_jobs, mock_get_template_statistics, mock_get_service_statistics, + mock_get_monthly_usage_for_service, mock_get_annual_usage_for_service, + mock_create_or_update_free_sms_fragment_limit, mock_get_free_sms_fragment_limit, mock_get_inbound_sms_summary, ): diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py index ddb7a8b08..e89cb6e7b 100644 --- a/tests/app/main/views/test_sign_in.py +++ b/tests/app/main/views/test_sign_in.py @@ -12,40 +12,21 @@ def test_render_sign_in_template_for_new_user(client_request): client_request.logout() page = client_request.get("main.sign_in") assert normalize_spaces(page.select_one("h1").text) == "Sign in" - assert normalize_spaces(page.select("label")[0].text) == "Email address" - assert page.select_one("#email_address").get("value") is None - assert page.select_one("#email_address")["autocomplete"] == "email" - assert normalize_spaces(page.select("label")[1].text) == "Password" - assert page.select_one("#password").get("value") is None - assert page.select_one("#password")["autocomplete"] == "current-password" - # Removing for the pilot - # assert page.select('main a')[0].text == 'create one now' - # assert page.select('main a')[0]['href'] == url_for('main.register') + assert ( + page.select("main p")[0].text + == "Access your Notify.gov account by signing in with Login.gov:" + ) # TODO: Fix this test to be less brittle! If the Login.gov link is enabled, # then these indices need to be 1 instead of 0. # Currently it's not enabled for the test or production environments. - assert page.select("main a")[1].text == "Forgot your password?" - assert page.select("main a")[1]["href"] == url_for("main.forgot_password") + assert page.select("main a")[0].text == "Sign in with Login.gov" + assert page.select("main a")[1].text == "Create Login.gov account" # TODO: We'll have to adjust this depending on whether Login.gov is # enabled or not; fix this in the future. assert "Sign in again" not in normalize_spaces(page.text) -def test_render_sign_in_template_with_next_link_for_password_reset(client_request): - client_request.logout() - page = client_request.get( - "main.sign_in", - _optional_args=f"?next=/services/{SERVICE_ONE_ID}/templates", - _test_page_title=False, - ) - forgot_password_link = page.find("a", class_="usa-link") - assert forgot_password_link.text == "Forgot your password?" - assert forgot_password_link["href"] == url_for( - "main.forgot_password", next=f"/services/{SERVICE_ONE_ID}/templates" - ) - - def test_reformat_keystring(): orig = "-----BEGIN PRIVATE KEY----- blah blah blah -----END PRIVATE KEY-----" expected = """-----BEGIN PRIVATE KEY----- diff --git a/tests/app/utils/test_time.py b/tests/app/utils/test_time.py index 931fdb816..ae51edb97 100644 --- a/tests/app/utils/test_time.py +++ b/tests/app/utils/test_time.py @@ -20,10 +20,10 @@ def test_is_less_than_days_ago(date_from_db, expected_result): @pytest.mark.parametrize( ("datetime_string", "financial_year"), [ - ("2021-01-01T00:00:00+00:00", 2020), # Start of 2021 - ("2021-04-01T03:59:59+00:00", 2020), # One minute before midnight (BST) - ("2021-10-01T04:05:00+00:00", 2021), # Midnight (BST) - ("2021-12-12T12:12:12+01:00", 2021), # Later in the year + ("2021-01-01T00:00:00+00:00", 2021), # Start of 2021 + ("2021-04-01T03:59:59+00:00", 2021), # One minute before midnight (BST) + ("2021-10-01T04:05:00+00:00", 2022), # Midnight (BST) + ("2021-12-12T12:12:12+01:00", 2022), # Later in the year ], ) def test_get_financial_year(datetime_string, financial_year): diff --git a/tests/conftest.py b/tests/conftest.py index 4c2ceeea9..ab86e3052 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2330,6 +2330,8 @@ def client_request(logged_in_client, mocker, service_one): # noqa (C901 too com "app.billing_api_client.create_or_update_free_sms_fragment_limit", autospec=True ) + mocker.patch("app.billing_api_client.get_monthly_usage_for_service", autospec=True) + class ClientRequest: @staticmethod @contextmanager