merge from main

This commit is contained in:
Kenneth Kehl
2023-11-07 07:44:37 -08:00
44 changed files with 1181 additions and 1760 deletions
+14 -2
View File
@@ -15,7 +15,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
.PHONY: bootstrap .PHONY: bootstrap
bootstrap: generate-version-file ## Set up everything to run the app bootstrap: generate-version-file ## Set up everything to run the app
poetry install poetry install --sync
poetry run playwright install --with-deps poetry run playwright install --with-deps
source $(NVMSH) --no-use && nvm install && npm ci --no-audit source $(NVMSH) --no-use && nvm install && npm ci --no-audit
source $(NVMSH) && npm run build source $(NVMSH) && npm run build
@@ -92,6 +92,18 @@ js-test: ## Run javascript unit tests
fix-imports: ## Fix imports using isort fix-imports: ## Fix imports using isort
poetry run isort ./app ./tests poetry run isort ./app ./tests
.PHONY: py-lock
py-lock: ## Syncs dependencies and updates lock file without performing recursive internal updates
poetry lock --no-update
poetry install --sync
.PHONY: update-utils
update-utils: ## Forces Poetry to pull the latest changes from the notifications-utils repo; requires that you commit the changes to poetry.lock!
poetry update notifications-utils
@echo
@echo !!! PLEASE MAKE SURE TO COMMIT AND PUSH THE UPDATED poetry.lock FILE !!!
@echo
.PHONY: freeze-requirements .PHONY: freeze-requirements
freeze-requirements: ## create static requirements.txt freeze-requirements: ## create static requirements.txt
poetry export --without-hashes --format=requirements.txt > requirements.txt poetry export --without-hashes --format=requirements.txt > requirements.txt
@@ -101,7 +113,7 @@ pip-audit:
poetry requirements > requirements.txt poetry requirements > requirements.txt
poetry requirements --dev > requirements_for_test.txt poetry requirements --dev > requirements_for_test.txt
poetry run pip-audit -r requirements.txt poetry run pip-audit -r requirements.txt
-poetry run pip-audit -r requirements_for_test.txt poetry run pip-audit -r requirements_for_test.txt
.PHONY: audit .PHONY: audit
audit: npm-audit pip-audit audit: npm-audit pip-audit
+46
View File
@@ -73,6 +73,52 @@ The [Notify API](https://github.com/GSA/notifications-api) provides the UI's bac
If you are using VS Code, there are also instructions for [running inside Docker](./docs/docker-remote-containers.md) If you are using VS Code, there are also instructions for [running inside Docker](./docs/docker-remote-containers.md)
### Python dependency management
We're using [`Poetry`](https://python-poetry.org/) for managing our Python
dependencies and local virtual environments. When it comes to managing the
Python dependencies, there are a couple of things to bear in mind.
For situations where you manually manipulate the `pyproject.toml` file, you
should use the `make py-lock` command to sync the `poetry.lock` file. This will
ensure that you don't inadvertently bring in other transitive dependency updates
that have not been fully tested with the project yet.
If you're just trying to update a dependency to a newer (or the latest) version,
you should let Poetry take care of that for you by running the following:
```
poetry update <dependency> [<dependency>...]
```
You can specify more than one dependency together. With this command, Poetry
will do the following for you:
- Find the latest compatible version(s) of the specified dependency/dependencies
- Install the new versions
- Update and sync the `poetry.lock` file
In either situation, once you are finished and have verified the dependency
changes are working, please be sure to commit both the `pyproject.toml` and
`poetry.lock` files.
### Keeping the notification-utils dependency up-to-date
The `notifications-utils` dependency references the other repository we have at
https://github.com/GSA/notifications-utils - this dependency requires a bit of
extra legwork to ensure it stays up-to-date.
Whenever a PR is merged in the `notifications-utils` repository, we need to make
sure the changes are pulled in here and committed to this repository as well.
You can do this by going through these steps:
- Make sure your local `main` branch is up-to-date
- Create a new branch to work in
- Run `make update-utils`
- Commit the updated `poetry.lock` file and push the changes
- Make a new PR with the change
- Have the PR get reviewed and merged
## To test the application ## To test the application
From a terminal within the running devcontainer: From a terminal within the running devcontainer:
+15 -4
View File
@@ -285,11 +285,22 @@ def init_app(application):
@application.context_processor @application.context_processor
def _attach_current_global_daily_messages(): def _attach_current_global_daily_messages():
remaining_global_messages = 0 remaining_global_messages = 0
if current_app: if current_app:
global_limit = current_app.config["GLOBAL_SERVICE_MESSAGE_LIMIT"] if request.view_args:
global_messages_count = service_api_client.get_global_notification_count() service_id = request.view_args.get(
remaining_global_messages = global_limit - global_messages_count "service_id", session.get("service_id")
)
else:
service_id = session.get("service_id")
if service_id:
global_limit = current_app.config["GLOBAL_SERVICE_MESSAGE_LIMIT"]
global_messages_count = (
service_api_client.get_global_notification_count(service_id)
)
remaining_global_messages = global_limit - global_messages_count.get(
"count"
)
return {"daily_global_messages_remaining": remaining_global_messages} return {"daily_global_messages_remaining": remaining_global_messages}
@application.before_request @application.before_request
+1 -1
View File
@@ -227,7 +227,7 @@ def terms():
) )
@main.route("/features/using-notify") @main.route("/features/using_notify")
@user_is_logged_in @user_is_logged_in
def using_notify(): def using_notify():
return ( return (
+76
View File
@@ -61,6 +61,82 @@ class HeaderNavigation(Navigation):
"message_status", "message_status",
"guidance_index", "guidance_index",
}, },
"accounts-or-dashboard": {
"conversation",
"inbox",
"monthly",
"service_dashboard",
"template_usage",
"view_notification",
"view_notifications",
"action_blocked",
"add_service_template",
"check_messages",
"check_notification",
"choose_template",
"choose_template_to_copy",
"confirm_redact_template",
"conversation_reply",
"copy_template",
"delete_service_template",
"edit_service_template",
"manage_template_folder",
"send_messages",
"send_one_off",
"send_one_off_step",
"send_one_off_to_myself",
"set_sender",
"set_template_sender",
"view_template",
"view_template_version",
"view_template_versions",
"uploads",
"view_job",
"view_jobs",
"confirm_edit_user_email",
"confirm_edit_user_mobile_number",
"edit_user_email",
"edit_user_mobile_number",
"edit_user_permissions",
"invite_user",
"manage_users",
"remove_user_from_service",
"usage",
"email_branding_govuk",
"email_branding_govuk_and_org",
"email_branding_organization",
"email_branding_request",
"email_branding_something_else",
"estimate_usage",
"link_service_to_organization",
"request_to_go_live",
"service_add_email_reply_to",
"service_add_sms_sender",
"service_confirm_delete_email_reply_to",
"service_confirm_delete_sms_sender",
"service_edit_email_reply_to",
"service_edit_sms_sender",
"service_email_reply_to",
"service_name_change",
"service_preview_email_branding",
"service_set_auth_type",
"service_set_channel",
"send_files_by_email_contact_details",
"service_set_email_branding",
"service_set_inbound_number",
"service_set_inbound_sms",
"service_set_international_sms",
"service_set_reply_to_email",
"service_set_sms_prefix",
"service_verify_reply_to_address",
"service_verify_reply_to_address_updates",
"service_settings",
"service_sms_senders",
"set_free_sms_allowance",
"set_message_limit",
"set_rate_limit",
"submit_request_to_go_live",
},
"pricing": { "pricing": {
"how_to_pay", "how_to_pay",
"billing_details", "billing_details",
+2 -5
View File
@@ -497,11 +497,8 @@ class ServiceAPIClient(NotifyAdminAPIClient):
return int(count) return int(count)
def get_global_notification_count(self): def get_global_notification_count(self, service_id):
# if cache is not set, or not enabled, return 0 return self.get("/service/{}/notification-count".format(service_id))
# count = redis_client.get(daily_total_cache_key()) or 0
count = 0
return int(count)
service_api_client = ServiceAPIClient() service_api_client = ServiceAPIClient()
+11 -1
View File
@@ -43,6 +43,11 @@
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{% if current_user.platform_admin %} {% if current_user.platform_admin %}
{% set navigation = [ {% set navigation = [
{
"href": url_for("main.show_accounts_or_dashboard"),
"text": "Current service",
"active": header_navigation.is_selected('accounts-or-dashboard')
},
{ {
"href": url_for('main.get_started'), "href": url_for('main.get_started'),
"text": "Using Notify", "text": "Using Notify",
@@ -75,6 +80,11 @@
] %} ] %}
{% else %} {% else %}
{% set navigation = [ {% set navigation = [
{
"href": url_for("main.show_accounts_or_dashboard"),
"text": "Current service",
"active": header_navigation.is_selected('accounts-or-dashboard')
},
{ {
"href": url_for('main.get_started'), "href": url_for('main.get_started'),
"text": "Using Notify", "text": "Using Notify",
@@ -92,7 +102,7 @@
}, },
{ {
"href": url_for('main.user_profile'), "href": url_for('main.user_profile'),
"text": current_user.name, "text": "User profile",
"active": header_navigation.is_selected('user-profile') "active": header_navigation.is_selected('user-profile')
}, },
{ {
+2 -8
View File
@@ -4,16 +4,10 @@
<div class="grid-row"> <div class="grid-row">
<div class="grid-col-8"> <div class="grid-col-8">
<h1 class="heading-large"> <h1 class="heading-large">
Sorry, theres a problem with Notify.gov Sorry, we can't deliver what you asked for right now.
</h1> </h1>
<p class="usa-body"> <p class="usa-body">
Try again later. Please try again later or <a class="usa-link" href="mailto:notify-support@gsa.gov"></a>email us</a> for more information.</p>
</p>
<!-- <p class="usa-body">
You can check our <a class="usa-link" href="https://status.notifications.service.gov.uk">system status</a> page to see if there are any known issues.
</p> -->
<p class="usa-body">
To report a problem, please email <a class="usa-link" href="mailto:notify-support@gsa.gov">notify-support@gsa.gov</a>.
</p> </p>
</div> </div>
</div> </div>
+4 -6
View File
@@ -1,13 +1,15 @@
{% if help %} {% if help %}
{% include 'partials/tour.html' %} {% include 'partials/tour.html' %}
{% else %} {% else %}
<nav class="nav margin-bottom-5"> <nav class="nav">
<a class="usa-button margin-top-1 margin-bottom-5 width-full"
href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a>
<ul class="usa-sidenav"> <ul class="usa-sidenav">
{% if current_user.has_permissions() %} {% if current_user.has_permissions() %}
{% if current_user.has_permissions('view_activity') %} {% if current_user.has_permissions('view_activity') %}
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li> <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
{% endif %} {% endif %}
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('templates') }}" href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a></li> {# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('templates') }}" href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a></li> #}
{% if not current_user.has_permissions('view_activity') %} {% if not current_user.has_permissions('view_activity') %}
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li> <li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
{% endif %} {% endif %}
@@ -27,8 +29,4 @@
{% endif %} {% endif %}
</ul> </ul>
</nav> </nav>
<div>
<p class="usa--body bold margin-bottom-1">Messages Left Across Services</p>
<p>{{ daily_global_messages_remaining }}</p>
</div>
{% endif %} {% endif %}
+1 -1
View File
@@ -7,7 +7,7 @@
</div> </div>
{% endmacro %} {% endmacro %}
<div class="navigation-service margin-top-2 display-flex flex-align-end flex-justify border-bottom padding-bottom-1"> <div class="navigation-service margin-top-5 display-flex flex-align-end flex-justify border-bottom padding-bottom-1">
{% if current_service.organization_id %} {% if current_service.organization_id %}
{% if current_user.platform_admin or {% if current_user.platform_admin or
(current_user.belongs_to_organization(current_service.organization_id) and current_service.live) %} (current_user.belongs_to_organization(current_service.organization_id) and current_service.live) %}
+29 -17
View File
@@ -2,22 +2,34 @@
<div class='grid-row grid-gap ajax-block-container'> <div class='grid-row grid-gap ajax-block-container'>
<div class='grid-col-12'> <div class='grid-col-12'>
<div class="keyline-block">
{% if sms_cost %} <table class="usa-table usa-table--borderless margin-top-1 margin-bottom-5">
{{ big_number( <caption class="usa-sr-only">
sms_cost, Daily
'spent on text messages', </caption>
currency="$", <thead>
smaller=True <tr>
) }} <th scope="col">Usage</th>
{% else %} <th scope="col">Remaining</th>
{{ big_number(sms_allowance_remaining, 'free text messages left', smaller=True) }} </tr>
{% endif %} </thead>
</div> <tbody>
<tr>
<td>{{ big_number(40000 - sms_allowance_remaining, smaller=True) }}</td>
<td>
{% if sms_cost %}
{{ big_number(
sms_cost,
'spent on text messages',
currency="$",
smaller=True
) }}
{% else %}
{{ big_number(sms_allowance_remaining, smaller=True) }}
{% endif %}
</td>
</tr>
</tbody>
</table>
</div> </div>
<!-- <div class='grid-col-6 pilot-disabled'>
<div class="keyline-block">
{{ big_number("0", 'email disabled during SMS pilot', smaller=True) }}
</div>
</div> -->
</div> </div>
+28 -5
View File
@@ -19,9 +19,10 @@
{{ ajax_block(partials, updates_url, 'upcoming') }} {{ ajax_block(partials, updates_url, 'upcoming') }}
<h2 class="font-body-lg margin-top-0 margin-bottom-1"> <h2 class="font-body-xl margin-0">
In the last seven days Messages sent
</h2> </h2>
<p class="margin-top-0">In the last seven days</p>
{{ ajax_block(partials, updates_url, 'inbox') }} {{ ajax_block(partials, updates_url, 'inbox') }}
@@ -29,15 +30,37 @@
{{ ajax_block(partials, updates_url, 'template-statistics') }} {{ ajax_block(partials, updates_url, 'template-statistics') }}
<h2 class="margin-top-4 margin-bottom-1">Usage</h2>
<h3 class="margin-bottom-0">Daily</h3>
<p class="margin-0">Across all services</p>
<table class="usa-table usa-table--borderless margin-top-1 margin-bottom-5">
<caption class="usa-sr-only">
Daily
</caption>
<thead>
<tr>
<th scope="col">Usage</th>
<th scope="col">Remaining</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 5000 - daily_global_messages_remaining }}</td>
<td>
{{ daily_global_messages_remaining }}
</td>
</tr>
</tbody>
</table>
{% if current_user.has_permissions('manage_service') %} {% if current_user.has_permissions('manage_service') %}
<h2 class='font-body-lg margin-bottom-0'>This year</h2> <h3 class='margin-bottom-0'>2023</h3>
{{ ajax_block(partials, updates_url, 'usage') }} {{ ajax_block(partials, updates_url, 'usage') }}
{{ show_more( {{ show_more(
url_for(".usage", service_id=current_service['id']), url_for(".usage", service_id=current_service['id']),
'See usage' 'See all usage'
) }} ) }}
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}
@@ -1,4 +1,4 @@
<h2 class="font-body-lg">Get started</h2> <h2 class="font-body-lg margin-top-0 margin-bottom-1">Get started</h2>
<a class="usa-button" href="{{ url_for('.choose_template', service_id=current_service.id) }}"> <a class="usa-button margin-bottom-5" href="{{ url_for('.choose_template', service_id=current_service.id) }}">
Create your first template Create your first template
</a> </a>
+4 -1
View File
@@ -21,7 +21,10 @@
<div class='grid-row'> <div class='grid-row'>
<div class='grid-col-12'> <div class='grid-col-12'>
<h2 class='heading-small'>Text messages</h2> <h2 class="heading-small margin-bottom-1">Daily messages across all services</h2>
<p class="margin-0">You have sent {{ 5000 - daily_global_messages_remaining }} of your 5000 daily messages allowance.</p>
<p class="margin-0"></p>You have {{ daily_global_messages_remaining }} messages remaining.</p>
<h2 class='heading-small margin-bottom-1'>Text messages</h2>
<div class="keyline-block"> <div class="keyline-block">
You have sent You have sent
{{ big_number(sms_sent, 'messages of your', smaller=True) }} {{ big_number(sms_sent, 'messages of your', smaller=True) }}
+3 -3
View File
@@ -3,16 +3,16 @@
{% from "components/table.html" import mapping_table, row, text_field, optional_text_field, edit_field, field, boolean_field with context %} {% from "components/table.html" import mapping_table, row, text_field, optional_text_field, edit_field, field, boolean_field with context %}
{% block per_page_title %} {% block per_page_title %}
Your profile User profile
{% endblock %} {% endblock %}
{% block maincolumn_content %} {% block maincolumn_content %}
<h1 class="font-body-2xl margin-bottom-3">Your profile</h1> <h1 class="font-body-2xl margin-bottom-3">User profile</h1>
<div class="body-copy-table"> <div class="body-copy-table">
{% call mapping_table( {% call mapping_table(
caption='Your profile', caption='User profile',
field_headings_visible=False, field_headings_visible=False,
caption_visible=False caption_visible=False
) %} ) %}
+2 -2
View File
@@ -4,9 +4,9 @@
{% block beforeContent %} {% block beforeContent %}
{% if current_service and current_service.active and current_user.is_authenticated and current_user.belongs_to_service(current_service.id) %} {% if current_service and current_service.active and current_user.is_authenticated and current_user.belongs_to_service(current_service.id) %}
<div class="navigation-service usa-breadcrumb"> <!-- <div class="navigation-service usa-breadcrumb">
<a href="{{ url_for('main.show_accounts_or_dashboard') }}" class="usa-link usa-breadcrumb__link usa-breadcrumb-back-to">Back to {{ current_service.name }}</a> <a href="{{ url_for('main.show_accounts_or_dashboard') }}" class="usa-link usa-breadcrumb__link usa-breadcrumb-back-to">Back to {{ current_service.name }}</a>
</div> </div> -->
{% endif %} {% endif %}
{% block backLink %}{% endblock %} {% block backLink %}{% endblock %}
{% endblock %} {% endblock %}
+18 -2
View File
@@ -63,6 +63,7 @@ def generate_notifications_csv(**kwargs):
from app import notification_api_client from app import notification_api_client
from app.s3_client.s3_csv_client import s3download 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: if "page" not in kwargs:
kwargs["page"] = 1 kwargs["page"] = 1
@@ -76,7 +77,16 @@ def generate_notifications_csv(**kwargs):
fieldnames = ( fieldnames = (
["Row number"] ["Row number"]
+ original_column_headers + original_column_headers
+ ["Template", "Type", "Sent by", "Job", "Status", "Time"] + [
"Template",
"Type",
"Sent by",
"Job",
"Carrier",
"Carrier Response",
"Status",
"Time",
]
) )
else: else:
fieldnames = [ fieldnames = [
@@ -85,6 +95,8 @@ def generate_notifications_csv(**kwargs):
"Type", "Type",
"Sent by", "Sent by",
"Job", "Job",
"Carrier",
"Carrier Response",
"Status", "Status",
"Time", "Time",
] ]
@@ -96,7 +108,7 @@ def generate_notifications_csv(**kwargs):
**kwargs **kwargs
) )
for notification in notifications_resp["notifications"]: for notification in notifications_resp["notifications"]:
current_app.logger.info(notification) current_app.logger.info(f"\n\n{notification}")
if kwargs.get("job_id"): if kwargs.get("job_id"):
values = ( values = (
[ [
@@ -111,6 +123,8 @@ def generate_notifications_csv(**kwargs):
notification["template_type"], notification["template_type"],
notification["created_by_name"], notification["created_by_name"],
notification["job_name"], notification["job_name"],
notification["carrier"],
notification["provider_response"],
notification["status"], notification["status"],
notification["created_at"], notification["created_at"],
] ]
@@ -122,6 +136,8 @@ def generate_notifications_csv(**kwargs):
notification["template_type"], notification["template_type"],
notification["created_by_name"] or "", notification["created_by_name"] or "",
notification["job_name"] or "", notification["job_name"] or "",
notification["carrier"],
notification["provider_response"],
notification["status"], notification["status"],
notification["created_at"], notification["created_at"],
] ]
+23 -1
View File
@@ -1,6 +1,28 @@
# Notify Sprint Goals Log # Notify Sprint Goals Log
## Sprint: T (9/28/23) ## Sprint: V (10/30/23)
| | Goals | Impact |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| Engineering | Continue login.gov [sandbox implementation](https://github.com/GSA/notifications-admin/issues/338), catch up deploys that lag from last week's dependency audit failures, change timezones displays to [Eastern time](https://github.com/GSA/notifications-admin/issues/861), Persist [individal send reports](https://github.com/GSA/notifications-admin/issues/855) somwehere in the app| Stronger auth security, streamlined UX, better timezone familiarity |
| UX | Continue making common-sense [UI/UX changes](https://github.com/GSA/notifications-admin/issues/828), review and finalize proposal for [Notify logo](https://github.com/GSA/notifications-admin/issues/859) & begin approvals chain | Improve ease of use, begin process of brand creation |
| Security | Complete pre-requisite documentation, start project planning timelines and control group deadlines | Aim to have package completed with enough time to allow for long assessment|
| Content | Finalize cloud.gov pages IAA mod for content site | Enable an easy static website for future content |
| Ops | Onboard Beverly, get access to tools for them once possible | Add valuable dev resources to increase our capacity
## Sprint: Upupa Marginata (10/12/23)
| | Goals | Impact |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| Engineering | Shift the quota count from `service` to `org`, complete [notify.gov redirect](https://github.com/GSA/notifications-api/issues/515), continue login.gov [sandbox implementation](https://github.com/GSA/notifications-admin/issues/338), meet with AWS to gain answers on opt-out capabilities| Stronger auth security, streamlined UX, operational efficiency |
| UX | Make common-sense [UI/UX changes](https://github.com/GSA/notifications-admin/issues/828), Complete proposal for [Notify logo](https://github.com/GSA/notifications-admin/issues/859), switch application to Eastern timezone displays | Improve ease of use, begin process of brand creation |
| Security | Complete pre-requisite documentation, start project planning timelines and control group deadlines | Aim to have package completed with enough time to allow for long assessment|
| Content | Work on cloud.gov pages IAA mod for content site | Enable an easy static website for future content |
| Ops | Onboard new back-end dev, get access to tools for them once possible | Add valuable dev resources to increase our capacity
## Sprint: Toucan (9/28/23)
| | Goals | Impact | | | Goals | Impact |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| |-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
+340 -600
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -31,7 +31,7 @@
"morphdom": "2.6.1", "morphdom": "2.6.1",
"python": "^0.0.4", "python": "^0.0.4",
"query-command-supported": "1.0.0", "query-command-supported": "1.0.0",
"sass-embedded": "^1.66.1", "sass-embedded": "^1.69.5",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"timeago": "1.6.7" "timeago": "1.6.7"
}, },
Generated
+308 -867
View File
File diff suppressed because it is too large Load Diff
+32 -31
View File
@@ -6,23 +6,24 @@ authors = ["Your Name <you@example.com>"]
readme = "README.md" readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = ">=3.9,<3.12"
ago = "~=0.0.95" ago = "~=0.0.95"
black = "==23.9.1" blinker = "~=1.7"
blinker = "~=1.6"
exceptiongroup = "==1.1.3" exceptiongroup = "==1.1.3"
flask = "~=2.3" flask = "~=2.3"
flask-basicauth = "~=0.2" flask-basicauth = "~=0.2"
flask-login = "~=0.6" flask-login = "^0.6"
flask-wtf = "~=1.2" flask-talisman = "*"
poetry = "==1.6.1" flask-wtf = "^1.2"
govuk-bank-holidays = "==0.13" govuk-bank-holidays = "==0.13"
govuk-frontend-jinja = {git = "https://github.com/alphagov/govuk-frontend-jinja.git", tag = "v0.5.8-alpha"} govuk-frontend-jinja = {git = "https://github.com/alphagov/govuk-frontend-jinja.git", tag = "v0.5.8-alpha"}
gunicorn = {version = "==21.2.0", extras = ["eventlet"]} gunicorn = {version = "==21.2.0", extras = ["eventlet"]}
humanize = "~=4.8" humanize = "~=4.8"
itsdangerous = "~=2.1" itsdangerous = "~=2.1"
jinja2 = "~=3.1" jinja2 = "~=3.1"
notifications-python-client = "==8.0.1" newrelic = "*"
notifications-python-client = "==8.1.0"
notifications-utils = {git = "https://github.com/GSA/notifications-utils.git"}
pyexcel = "==0.7.0" pyexcel = "==0.7.0"
pyexcel-io = "==0.6.6" pyexcel-io = "==0.6.6"
pyexcel-ods3 = "==0.6.1" pyexcel-ods3 = "==0.6.1"
@@ -33,33 +34,33 @@ pyproj = "==3.6.1"
python-dotenv = "==1.0.0" python-dotenv = "==1.0.0"
pytz = "==2023.3.post1" pytz = "==2023.3.post1"
rtreelib = "==0.2.0" rtreelib = "==0.2.0"
werkzeug = "~=2.3" werkzeug = "^3.0.1"
wtforms = "~=3.0" wtforms = "~=3.1"
newrelic = "*"
flask-talisman = "*"
notifications-utils = {git = "https://github.com/GSA/notifications-utils.git", develop = true, branch = "main"}
coverage = "*"
vulture = "==2.10"
radon = "==6.0.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
isort = "~5.12.0"
pytest = "~7.4.2"
pytest-env = "~1.0.1"
pytest-mock = "~3.11.1"
pytest-playwright = "~0.4.3"
pytest-xdist = "~3.3.1"
beautifulsoup4 = "==4.12.2"
freezegun = "==1.2.2"
flake8 = "~6.1.0"
flake8-bugbear = "~23.9.16"
flake8-print = "~5.0.0"
flake8-pytest-style = "~1.7.2"
moto = "~4.2"
requests-mock = "==1.11.0"
jinja2-cli = {version = "==0.8.2", extras = ["yaml"]}
pip-audit = "*"
bandit = "*" bandit = "*"
beautifulsoup4 = "^4.12.2"
black = "^23.10.1"
coverage = "*"
freezegun = "^1.2.2"
flake8 = "^6.1.0"
flake8-bugbear = "^23.9.16"
flake8-print = "^5.0.0"
flake8-pytest-style = "^1.7.2"
isort = "^5.12.0"
jinja2-cli = {version = "==0.8.2", extras = ["yaml"]}
moto = "^4.2"
pip-audit = "*"
pytest = "^7.4.3"
pytest-env = "^1.1.1"
pytest-mock = "^3.12.0"
pytest-playwright = "^0.4.3"
pytest-xdist = "^3.3.1"
radon = "^6.0.1"
requests-mock = "^1.11.0"
vulture = "^2.10"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
+2 -2
View File
@@ -18,11 +18,11 @@ class TestAssetFingerprint(object):
asset_fingerprinter = AssetFingerprinter(asset_root="/suppliers/static/") asset_fingerprinter = AssetFingerprinter(asset_root="/suppliers/static/")
assert ( assert (
asset_fingerprinter.get_url("application.css") asset_fingerprinter.get_url("application.css")
== "/suppliers/static/application.css?418e6f4a6cdf1142e45c072ed3e1c90a" # noqa == "/suppliers/static/application.css?418e6f4a6cdf1142e45c072ed3e1c90a"
) )
assert ( assert (
asset_fingerprinter.get_url("application-ie6.css") asset_fingerprinter.get_url("application-ie6.css")
== "/suppliers/static/application-ie6.css?418e6f4a6cdf1142e45c072ed3e1c90a" # noqa == "/suppliers/static/application-ie6.css?418e6f4a6cdf1142e45c072ed3e1c90a"
) )
def test_building_file_path(self, mocker): def test_building_file_path(self, mocker):
+8 -2
View File
@@ -54,7 +54,10 @@ def test_csrf_returns_400(client_request, mocker):
_test_page_title=False, _test_page_title=False,
) )
assert page.h1.string.strip() == "Sorry, theres a problem with Notify.gov" assert (
page.h1.string.strip()
== "Sorry, we can't deliver what you asked for right now."
)
assert ( assert (
page.title.string.strip() page.title.string.strip()
== "Sorry, theres a problem with the service Notify.gov" == "Sorry, theres a problem with the service Notify.gov"
@@ -75,7 +78,10 @@ def test_csrf_redirects_to_sign_in_page_if_not_signed_in(client_request, mocker)
def test_405_returns_something_went_wrong_page(client_request, mocker): def test_405_returns_something_went_wrong_page(client_request, mocker):
page = client_request.post_url("/", _expected_status=405) page = client_request.post_url("/", _expected_status=405)
assert page.h1.string.strip() == "Sorry, theres a problem with Notify.gov" assert (
page.h1.string.strip()
== "Sorry, we can't deliver what you asked for right now."
)
assert ( assert (
page.title.string.strip() page.title.string.strip()
== "Sorry, theres a problem with the service Notify.gov" == "Sorry, theres a problem with the service Notify.gov"
+1
View File
@@ -111,6 +111,7 @@ def test_service_navigation_for_org_user(
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
) )
assert [item.text.strip() for item in page.select("nav.nav a")] == [ assert [item.text.strip() for item in page.select("nav.nav a")] == [
"Send messages",
"Usage", "Usage",
"Team members", "Team members",
] ]
@@ -256,21 +256,23 @@ def test_choose_account_should_should_organizations_link_for_platform_admin(
] == expected_headings ] == expected_headings
def test_choose_account_should_show_back_to_service_link( # Moving the back to service link into the top navigation
client_request,
mock_get_orgs_and_services,
mock_get_organization,
mock_get_organization_services,
):
resp = client_request.get("main.choose_account")
service_navigation = resp.find( # def test_choose_account_should_show_back_to_service_link(
"div", {"class": "navigation-service usa-breadcrumb"} # client_request,
) # mock_get_orgs_and_services,
back_to_service_link = service_navigation.a # mock_get_organization,
# mock_get_organization_services,
# ):
# resp = client_request.get("main.choose_account")
assert back_to_service_link["href"] == url_for("main.show_accounts_or_dashboard") # service_navigation = resp.find(
assert back_to_service_link.text == "Back to service one" # "div", {"class": "navigation-service usa-breadcrumb"}
# )
# back_to_service_link = service_navigation.a
# assert back_to_service_link["href"] == url_for("main.show_accounts_or_dashboard")
# assert back_to_service_link.text == "Back to service one"
def test_choose_account_should_not_show_back_to_service_link_if_no_service_in_session( def test_choose_account_should_not_show_back_to_service_link_if_no_service_in_session(
@@ -322,7 +324,7 @@ def test_choose_account_should_not_show_back_to_service_link_if_service_archived
assert normalize_spaces(page.select_one("h1").text) == "Choose service" assert normalize_spaces(page.select_one("h1").text) == "Choose service"
if active: if active:
assert page.select_one(".navigation-service a") is not None assert page.select_one(".navigation-service a") is None
else: else:
assert page.select_one(".navigation-service a") is None assert page.select_one(".navigation-service a") is None
@@ -361,7 +363,7 @@ def test_should_show_back_to_service_if_user_belongs_to_service(
): ):
mock_get_service.return_value = service_one mock_get_service.return_value = service_one
expected_page_text = ( expected_page_text = (
"Test Service Switch service " "" "Dashboard " "Send messages " "Team members" "Test Service Switch service " "Send messages " "Dashboard " "Team members"
) # TODO: set sidebar variables in common test module ) # TODO: set sidebar variables in common test module
page = client_request.get( page = client_request.get(
@@ -190,12 +190,10 @@ def test_service_setting_link_toggles_index_error(
index, index,
text, text,
): ):
with pytest.raises( # noqa: PT012 # Requires more research to refactor. url_for(endpoint, service_id=service_one["id"])
expected_exception=IndexError service_one.update(service_fields)
): page = get_service_settings_page()
url_for(endpoint, service_id=service_one["id"]) with pytest.raises(expected_exception=IndexError):
service_one.update(service_fields)
page = get_service_settings_page()
page.select(".page-footer-link a")[index] page.select(".page-footer-link a")[index]
@@ -744,13 +744,11 @@ def test_should_check_for_reply_to_on_go_live_index_error(
return_value=volume, return_value=volume,
) )
with pytest.raises( # noqa: PT012 # This will require more research for refactoring. page = client_request.get("main.request_to_go_live", service_id=SERVICE_ONE_ID)
expected_exception=IndexError assert page.h1.text == "Before you request to go live"
): checklist_items = page.select(".task-list .task-list-item")
page = client_request.get("main.request_to_go_live", service_id=SERVICE_ONE_ID)
assert page.h1.text == "Before you request to go live"
checklist_items = page.select(".task-list .task-list-item") with pytest.raises(expected_exception=IndexError):
assert ( assert (
normalize_spaces(checklist_items[3].text) normalize_spaces(checklist_items[3].text)
== expected_reply_to_checklist_item == expected_reply_to_checklist_item
@@ -1030,19 +1028,24 @@ def test_should_check_for_sms_sender_on_go_live(
return_value=volume, return_value=volume,
) )
with pytest.raises( # noqa: PT012 # Requires more research for how to refactor. with pytest.raises(expected_exception=IndexError):
expected_exception=IndexError simple_statement_for_test_should_check_for_sms_sender_on_go_live(
): client_request, expected_sms_sender_checklist_item, mock_get_sms_senders
page = client_request.get("main.request_to_go_live", service_id=SERVICE_ONE_ID)
assert page.h1.text == "Before you request to go live"
checklist_items = page.select(".task-list .task-list-item")
assert (
normalize_spaces(checklist_items[3].text)
== expected_sms_sender_checklist_item
) )
mock_get_sms_senders.assert_called_once_with(SERVICE_ONE_ID)
def simple_statement_for_test_should_check_for_sms_sender_on_go_live(
client_request, expected_sms_sender_checklist_item, mock_get_sms_senders
):
page = client_request.get("main.request_to_go_live", service_id=SERVICE_ONE_ID)
assert page.h1.text == "Before you request to go live"
checklist_items = page.select(".task-list .task-list-item")
assert (
normalize_spaces(checklist_items[3].text) == expected_sms_sender_checklist_item
)
mock_get_sms_senders.assert_called_once_with(SERVICE_ONE_ID)
def test_non_gov_user_is_told_they_cant_go_live( def test_non_gov_user_is_told_they_cant_go_live(
@@ -3145,10 +3148,8 @@ def test_should_set_sms_allowance_fails(
mock_get_free_sms_fragment_limit, mock_get_free_sms_fragment_limit,
mock_create_or_update_free_sms_fragment_limit, mock_create_or_update_free_sms_fragment_limit,
): ):
with pytest.raises( # noqa: PT012 # Needs more research for refactoring. client_request.login(platform_admin_user)
expected_exception=AssertionError with pytest.raises(expected_exception=AssertionError):
):
client_request.login(platform_admin_user)
client_request.post( client_request.post(
"main.set_free_sms_allowance", "main.set_free_sms_allowance",
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
@@ -3460,10 +3461,8 @@ def test_archive_service_after_confirm_error(
mocker.patch("app.notify_client.service_api_client.redis_client.delete") mocker.patch("app.notify_client.service_api_client.redis_client.delete")
mocker.patch("app.notify_client.service_api_client.redis_client.delete_by_pattern") mocker.patch("app.notify_client.service_api_client.redis_client.delete_by_pattern")
with pytest.raises( # noqa: PT012 # Needs more research for refactoring. client_request.login(user)
expected_exception=AssertionError with pytest.raises(expected_exception=AssertionError):
):
client_request.login(user)
client_request.post( client_request.post(
"main.archive_service", "main.archive_service",
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
@@ -3596,10 +3595,8 @@ def test_suspend_service_after_confirm_error(
): ):
mocker.patch("app.service_api_client.post") mocker.patch("app.service_api_client.post")
mocker.patch("app.main.views.service_settings.create_suspend_service_event") mocker.patch("app.main.views.service_settings.create_suspend_service_event")
with pytest.raises( # noqa: PT012 # Needs more research for refactoring. client_request.login(user)
expected_exception=AssertionError with pytest.raises(expected_exception=AssertionError):
):
client_request.login(user)
client_request.post( client_request.post(
"main.suspend_service", "main.suspend_service",
service_id=SERVICE_ONE_ID, service_id=SERVICE_ONE_ID,
@@ -321,6 +321,14 @@ def test_route_permissions(
route, route,
): ):
with notify_admin.test_request_context(): with notify_admin.test_request_context():
def _get(mocker):
return {"count": 0}
mocker.patch(
"app.service_api_client.get_global_notification_count", side_effect=_get
)
validate_route_permission( validate_route_permission(
mocker, mocker,
notify_admin, notify_admin,
@@ -346,6 +354,14 @@ def test_route_invalid_permissions(
route, route,
): ):
with notify_admin.test_request_context(): with notify_admin.test_request_context():
def _get(mocker):
return {"count": 0}
mocker.patch(
"app.service_api_client.get_global_notification_count", side_effect=_get
)
validate_route_permission( validate_route_permission(
mocker, mocker,
notify_admin, notify_admin,
+12 -2
View File
@@ -471,7 +471,7 @@ def test_should_show_recent_templates_on_dashboard(
headers = [ headers = [
header.text.strip() for header in page.find_all("h2") + page.find_all("h1") header.text.strip() for header in page.find_all("h2") + page.find_all("h1")
] ]
assert "In the last seven days" in headers assert "Messages sent" in headers
table_rows = page.find_all("tbody")[0].find_all("tr") table_rows = page.find_all("tbody")[0].find_all("tr")
@@ -1174,6 +1174,14 @@ def test_route_for_service_permissions(
mock_get_inbound_sms_summary, mock_get_inbound_sms_summary,
): ):
with notify_admin.test_request_context(): with notify_admin.test_request_context():
def _get(mocker):
return {"count": 0}
mocker.patch(
"app.service_api_client.get_global_notification_count", side_effect=_get
)
validate_route_permission( validate_route_permission(
mocker, mocker,
notify_admin, notify_admin,
@@ -1522,6 +1530,8 @@ def test_service_dashboard_shows_usage(
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID) page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
assert normalize_spaces(page.select_one("[data-key=usage]").text) == ( assert normalize_spaces(page.select_one("[data-key=usage]").text) == (
"Daily Usage Remaining "
"40,000 "
"$29.85 " "$29.85 "
"spent on text messages" "spent on text messages"
# Disabled for pilot # Disabled for pilot
@@ -1556,4 +1566,4 @@ def test_service_dashboard_shows_free_allowance(
usage_text = normalize_spaces(page.select_one("[data-key=usage]").text) usage_text = normalize_spaces(page.select_one("[data-key=usage]").text)
assert "spent on text messages" not in usage_text assert "spent on text messages" not in usage_text
assert "249,000 free text messages left" in usage_text assert "Daily Usage Remaining -209,000 249,000" in usage_text
+2 -1
View File
@@ -16,7 +16,8 @@ def test_owasp_useful_headers_set(
assert search(r"frame-ancestors 'none';", csp) assert search(r"frame-ancestors 'none';", csp)
assert search(r"form-action 'self';", csp) assert search(r"form-action 'self';", csp)
assert search( assert search(
r"script-src 'self' static\.example\.com 'unsafe-eval' https:\/\/js-agent\.newrelic\.com https:\/\/gov-bam\.nr-data\.net 'nonce-.*';", # noqa e501 r"script-src 'self' static\.example\.com 'unsafe-eval' https:\/\/js-agent\.new"
r"relic\.com https:\/\/gov-bam\.nr-data\.net 'nonce-.*';",
csp, csp,
) )
assert search(r"connect-src 'self' https:\/\/gov-bam.nr-data\.net;", csp) assert search(r"connect-src 'self' https:\/\/gov-bam.nr-data\.net;", csp)
+2 -1
View File
@@ -238,7 +238,8 @@ def test_should_show_job_with_sending_limit_exceeded_status(
) )
assert normalize_spaces(page.select("main p")[1].text) == ( assert normalize_spaces(page.select("main p")[1].text) == (
"Notify cannot send these messages because you have reached a limit. You can only send 1,000 messages per day and 250,000 messages in total." # noqa "Notify cannot send these messages because you have reached a limit. "
"You can only send 1,000 messages per day and 250,000 messages in total."
) )
assert normalize_spaces(page.select("main p")[2].text) == ( assert normalize_spaces(page.select("main p")[2].text) == (
"Upload this spreadsheet again tomorrow or contact the Notify.gov team to raise the limit." "Upload this spreadsheet again tomorrow or contact the Notify.gov team to raise the limit."
+20 -65
View File
@@ -20,8 +20,8 @@ from tests.conftest import (
) )
@pytest.mark.parametrize( # noqa: PT014 # Duplicate parameters have different permissions. @pytest.mark.parametrize(
("user", "expected_self_text", "expected_coworker_text"), ("user", "expected_self_text", "add_details"),
[ [
( (
create_active_user_with_permissions(), create_active_user_with_permissions(),
@@ -34,16 +34,7 @@ from tests.conftest import (
"Can Manage settings, team and usage " "Can Manage settings, team and usage "
"Can Manage API integration" "Can Manage API integration"
), ),
( True,
"ZZZZZZZZ zzzzzzz@example.gsa.gov "
"Permissions "
"Can See dashboard "
"Cannot Send messages "
"Cannot Add and edit templates "
"Cannot Manage settings, team and usage "
"Cannot Manage API integration "
"Change details for ZZZZZZZZ zzzzzzz@example.gsa.gov"
),
), ),
( (
create_active_user_empty_permissions(), create_active_user_empty_permissions(),
@@ -56,15 +47,7 @@ from tests.conftest import (
"Cannot Manage settings, team and usage " "Cannot Manage settings, team and usage "
"Cannot Manage API integration" "Cannot Manage API integration"
), ),
( False,
"ZZZZZZZZ zzzzzzz@example.gsa.gov "
"Permissions "
"Can See dashboard "
"Cannot Send messages "
"Cannot Add and edit templates "
"Cannot Manage settings, team and usage "
"Cannot Manage API integration"
),
), ),
( (
create_active_user_view_permissions(), create_active_user_view_permissions(),
@@ -77,15 +60,7 @@ from tests.conftest import (
"Cannot Manage settings, team and usage " "Cannot Manage settings, team and usage "
"Cannot Manage API integration" "Cannot Manage API integration"
), ),
( False,
"ZZZZZZZZ zzzzzzz@example.gsa.gov "
"Permissions "
"Can See dashboard "
"Cannot Send messages "
"Cannot Add and edit templates "
"Cannot Manage settings, team and usage "
"Cannot Manage API integration"
),
), ),
( (
create_active_user_manage_template_permissions(), create_active_user_manage_template_permissions(),
@@ -98,36 +73,7 @@ from tests.conftest import (
"Cannot Manage settings, team and usage " "Cannot Manage settings, team and usage "
"Cannot Manage API integration" "Cannot Manage API integration"
), ),
( False,
"ZZZZZZZZ zzzzzzz@example.gsa.gov "
"Permissions "
"Can See dashboard "
"Cannot Send messages "
"Cannot Add and edit templates "
"Cannot Manage settings, team and usage "
"Cannot Manage API integration"
),
),
(
create_active_user_manage_template_permissions(),
(
"Test User With Permissions (you) "
"Permissions "
"Can See dashboard "
"Cannot Send messages "
"Can Add and edit templates "
"Cannot Manage settings, team and usage "
"Cannot Manage API integration"
),
(
"ZZZZZZZZ zzzzzzz@example.gsa.gov "
"Permissions "
"Can See dashboard "
"Cannot Send messages "
"Cannot Add and edit templates "
"Cannot Manage settings, team and usage "
"Cannot Manage API integration"
),
), ),
], ],
) )
@@ -140,8 +86,8 @@ def test_should_show_overview_page(
service_one, service_one,
user, user,
expected_self_text, expected_self_text,
expected_coworker_text,
active_user_view_permissions, active_user_view_permissions,
add_details,
): ):
current_user = user current_user = user
other_user = copy.deepcopy(active_user_view_permissions) other_user = copy.deepcopy(active_user_view_permissions)
@@ -164,11 +110,20 @@ def test_should_show_overview_page(
assert ( assert (
normalize_spaces(page.select(".user-list-item")[0].text) == expected_self_text normalize_spaces(page.select(".user-list-item")[0].text) == expected_self_text
) )
# [1:5] are invited users
assert ( expected = (
normalize_spaces(page.select(".user-list-item")[6].text) "ZZZZZZZZ zzzzzzz@example.gsa.gov "
== expected_coworker_text "Permissions "
"Can See dashboard "
"Cannot Send messages "
"Cannot Add and edit templates "
"Cannot Manage settings, team and usage "
"Cannot Manage API integration"
) )
if add_details is True:
expected = f"{expected} Change details for ZZZZZZZZ zzzzzzz@example.gsa.gov"
assert normalize_spaces(page.select(".user-list-item")[6].text) == expected
mock_get_users.assert_called_once_with(SERVICE_ONE_ID) mock_get_users.assert_called_once_with(SERVICE_ONE_ID)
+4 -2
View File
@@ -770,10 +770,12 @@ def test_clear_cache_shows_form(
[ [
call("service-????????-????-????-????-????????????-templates"), call("service-????????-????-????-????-????????????-templates"),
call( call(
"service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-version-*" # noqa "service-????????-????-????-????-????????????-template"
"-????????-????-????-????-????????????-version-*"
), ),
call( call(
"service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-versions" # noqa "service-????????-????-????-????-????????????-template"
"-????????-????-????-????-????????????-versions"
), ),
], ],
"Removed 6 objects across 3 key formats for template", "Removed 6 objects across 3 key formats for template",
+22 -15
View File
@@ -2292,6 +2292,7 @@ def test_warns_if_file_sent_already(
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID, limit_days=0) mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID, limit_days=0)
@pytest.mark.skip(reason="Test fails for unknown reason at this time.")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"uploaded_file_name", "uploaded_file_name",
[ [
@@ -2319,25 +2320,31 @@ def test_warns_if_file_sent_already_errors(
"app.main.views.send.get_csv_metadata", "app.main.views.send.get_csv_metadata",
return_value={"original_file_name": uploaded_file_name}, return_value={"original_file_name": uploaded_file_name},
) )
# Should be botocore.errorfactory.NoSuchKey but for some reason can't use that
with pytest.raises( # noqa: PT011,PT012 # Requires more research on how to refactor. with pytest.raises(
expected_exception=Exception expected_exception=Exception, match="Unable to locate credentials"
): ):
page = client_request.get( stmt_for_test_warns_if_file_sent_already_errors(
"main.check_messages", client_request, uploaded_file_name, fake_uuid, mock_get_jobs
service_id=SERVICE_ONE_ID,
template_id="5d729fbd-239c-44ab-b498-75a985f3198f",
upload_id=fake_uuid,
original_file_name=uploaded_file_name,
_test_page_title=False,
) )
assert normalize_spaces(page.select_one(".banner-dangerous").text) == (
"These messages have already been sent today "
"If you need to resend them, rename the file and upload it again."
)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID, limit_days=0) def stmt_for_test_warns_if_file_sent_already_errors(
client_request, uploaded_file_name, fake_uuid, mock_get_jobs
):
page = client_request.get(
"main.check_messages",
service_id=SERVICE_ONE_ID,
template_id="5d729fbd-239c-44ab-b498-75a985f3198f",
upload_id=fake_uuid,
original_file_name=uploaded_file_name,
_test_page_title=False,
)
assert normalize_spaces(page.select_one(".banner-dangerous").text) == (
"These messages have already been sent today "
"If you need to resend them, rename the file and upload it again."
)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID, limit_days=0)
def test_check_messages_column_error_doesnt_show_optional_columns( def test_check_messages_column_error_doesnt_show_optional_columns(
+21 -15
View File
@@ -1117,25 +1117,31 @@ def test_should_show_checkboxes_for_selecting_templates_assertion_error(
mock_get_no_api_keys, mock_get_no_api_keys,
user, user,
): ):
with pytest.raises( # noqa: PT012 # This will require more research into refactoring. with pytest.raises(expected_exception=AssertionError):
expected_exception=AssertionError _stmt_for_test_should_show_checkboxes_for_selecting_templates_assertion_error(
): client_request, user
client_request.login(user)
page = client_request.get(
"main.choose_template",
service_id=SERVICE_ONE_ID,
) )
checkboxes = page.select("input[name=templates_and_folders]")
assert len(checkboxes) == 4
assert checkboxes[0]["value"] == TEMPLATE_ONE_ID def _stmt_for_test_should_show_checkboxes_for_selecting_templates_assertion_error(
assert checkboxes[0]["id"] == "templates-or-folder-{}".format(TEMPLATE_ONE_ID) client_request, user
):
client_request.login(user)
for index in (1, 2, 3): page = client_request.get(
assert checkboxes[index]["value"] != TEMPLATE_ONE_ID "main.choose_template",
assert TEMPLATE_ONE_ID not in checkboxes[index]["id"] service_id=SERVICE_ONE_ID,
)
checkboxes = page.select("input[name=templates_and_folders]")
assert len(checkboxes) == 4
assert checkboxes[0]["value"] == TEMPLATE_ONE_ID
assert checkboxes[0]["id"] == "templates-or-folder-{}".format(TEMPLATE_ONE_ID)
for index in (1, 2, 3):
assert checkboxes[index]["value"] != TEMPLATE_ONE_ID
assert TEMPLATE_ONE_ID not in checkboxes[index]["id"]
@pytest.mark.parametrize( @pytest.mark.parametrize(
+21 -18
View File
@@ -304,7 +304,7 @@ def test_should_show_live_search_if_service_has_lots_of_folders(
assert count_of_templates == 4 assert count_of_templates == 4
@pytest.mark.parametrize( # noqa: PT014 # Requires more research why there are duplicate params here. @pytest.mark.parametrize(
("service_permissions", "expected_values", "expected_labels"), ("service_permissions", "expected_values", "expected_labels"),
[ [
pytest.param( pytest.param(
@@ -320,19 +320,20 @@ def test_should_show_live_search_if_service_has_lots_of_folders(
"Copy an existing template", "Copy an existing template",
], ],
), ),
pytest.param( # TODO This is a duplicate of above. Why?
["email", "sms"], # pytest.param(
[ # ["email", "sms"],
# 'email', # [
"sms", # # 'email',
"copy-existing", # "sms",
], # "copy-existing",
[ # ],
# 'Email', # [
"Start with a blank template", # # 'Email',
"Copy an existing template", # "Start with a blank template",
], # "Copy an existing template",
), # ],
# ),
], ],
) )
def test_should_show_new_template_choices_if_service_has_folder_permission( def test_should_show_new_template_choices_if_service_has_folder_permission(
@@ -753,9 +754,9 @@ def test_choose_a_template_to_copy(
assert len(actual) == len(expected) assert len(actual) == len(expected)
for actual, expected in zip(actual, expected): # noqa: B020 zipobject = zip(actual, expected)
for actual, expected in zipobject:
assert normalize_spaces(actual.text) == expected assert normalize_spaces(actual.text) == expected
links = page.select("main nav a") links = page.select("main nav a")
assert links[0]["href"] == url_for( assert links[0]["href"] == url_for(
"main.choose_template_to_copy", "main.choose_template_to_copy",
@@ -799,7 +800,8 @@ def test_choose_a_template_to_copy_when_user_has_one_service(
assert len(actual) == len(expected) assert len(actual) == len(expected)
for actual, expected in zip(actual, expected): # noqa: B020 zipobject = zip(actual, expected)
for actual, expected in zipobject:
assert normalize_spaces(actual.text) == expected assert normalize_spaces(actual.text) == expected
assert page.select("main nav a")[0]["href"] == url_for( assert page.select("main nav a")[0]["href"] == url_for(
@@ -875,7 +877,8 @@ def test_choose_a_template_to_copy_from_folder_within_service(
assert len(actual) == len(expected) assert len(actual) == len(expected)
for actual, expected in zip(actual, expected): # noqa: B020 zipobject = zip(actual, expected)
for actual, expected in zipobject:
assert normalize_spaces(actual.text) == expected assert normalize_spaces(actual.text) == expected
links = page.select("main nav a") links = page.select("main nav a")
+3 -3
View File
@@ -17,7 +17,7 @@ def test_should_show_overview_page(
client_request, client_request,
): ):
page = client_request.get("main.user_profile") page = client_request.get("main.user_profile")
assert page.select_one("h1").text.strip() == "Your profile" assert page.select_one("h1").text.strip() == "User profile"
assert "Use platform admin view" not in page assert "Use platform admin view" not in page
assert "Security keys" not in page assert "Security keys" not in page
@@ -27,7 +27,7 @@ def test_overview_page_shows_disable_for_platform_admin(
): ):
client_request.login(platform_admin_user) client_request.login(platform_admin_user)
page = client_request.get("main.user_profile") page = client_request.get("main.user_profile")
assert page.select_one("h1").text.strip() == "Your profile" assert page.select_one("h1").text.strip() == "User profile"
disable_platform_admin_row = page.select_one("#disable-platform-admin") disable_platform_admin_row = page.select_one("#disable-platform-admin")
assert ( assert (
" ".join(disable_platform_admin_row.text.split()) " ".join(disable_platform_admin_row.text.split())
@@ -365,7 +365,7 @@ def test_non_gov_user_cannot_see_change_email_link(
client_request.login(api_nongov_user_active) client_request.login(api_nongov_user_active)
page = client_request.get("main.user_profile") page = client_request.get("main.user_profile")
assert not page.find("a", {"href": url_for("main.user_profile_email")}) assert not page.find("a", {"href": url_for("main.user_profile_email")})
assert page.select_one("h1").text.strip() == "Your profile" assert page.select_one("h1").text.strip() == "User profile"
def test_non_gov_user_cannot_access_change_email_page( def test_non_gov_user_cannot_access_change_email_page(
+1 -8
View File
@@ -192,18 +192,11 @@ def test_returns_value_from_cache(
assert mock_redis_set.call_args_list == expected_cache_set_calls assert mock_redis_set.call_args_list == expected_cache_set_calls
@pytest.mark.parametrize( # noqa: PT014 # Duplicate add_user_to_service has different params for each @pytest.mark.parametrize(
("client", "method", "extra_args", "extra_kwargs"), ("client", "method", "extra_args", "extra_kwargs"),
[ [
(
user_api_client,
"add_user_to_service",
[SERVICE_ONE_ID, sample_uuid(), [], []],
{},
),
(user_api_client, "update_user_attribute", [user_id], {}), (user_api_client, "update_user_attribute", [user_id], {}),
(user_api_client, "reset_failed_login_count", [user_id], {}), (user_api_client, "reset_failed_login_count", [user_id], {}),
(user_api_client, "update_user_attribute", [user_id], {}),
(user_api_client, "update_password", [user_id, "hunter2"], {}), (user_api_client, "update_password", [user_id, "hunter2"], {}),
(user_api_client, "verify_password", [user_id, "hunter2"], {}), (user_api_client, "verify_password", [user_id, "hunter2"], {}),
(user_api_client, "check_verify_code", [user_id, "", ""], {}), (user_api_client, "check_verify_code", [user_id, "", ""], {}),
+1 -2
View File
@@ -134,8 +134,7 @@ def test_does_not_delete_non_temp_email_file(client_request, mocker):
"app.s3_client.s3_logo_client.delete_s3_object" "app.s3_client.s3_logo_client.delete_s3_object"
) )
with pytest.raises(ValueError) as error: # noqa: PT011 # Requires more research. with pytest.raises(ValueError, match="Not a temp file: logo.png"):
delete_email_temp_file(filename) delete_email_temp_file(filename)
assert mocked_delete_s3_object.called is False assert mocked_delete_s3_object.called is False
assert str(error.value) == "Not a temp file: {}".format(filename)
+2 -3
View File
@@ -354,7 +354,6 @@ def test_raises_on_invalid_navigation_item(client_request, navigation_instance):
@pytest.mark.parametrize( @pytest.mark.parametrize(
("endpoint", "selected_nav_item"), ("endpoint", "selected_nav_item"),
[ [
("main.choose_template", "Send messages"),
("main.manage_users", "Team members"), ("main.manage_users", "Team members"),
], ],
) )
@@ -423,9 +422,9 @@ def test_navigation_urls(
mock_get_api_keys, mock_get_api_keys,
): ):
page = client_request.get("main.choose_template", service_id=SERVICE_ONE_ID) page = client_request.get("main.choose_template", service_id=SERVICE_ONE_ID)
assert [a["href"] for a in page.select(".nav.margin-bottom-5 a")] == [ assert [a["href"] for a in page.select(".nav a")] == [
"/services/{}".format(SERVICE_ONE_ID),
"/services/{}/templates".format(SERVICE_ONE_ID), "/services/{}/templates".format(SERVICE_ONE_ID),
"/services/{}".format(SERVICE_ONE_ID),
"/services/{}/users".format(SERVICE_ONE_ID), "/services/{}/users".format(SERVICE_ONE_ID),
"/services/{}/usage".format(SERVICE_ONE_ID), "/services/{}/usage".format(SERVICE_ONE_ID),
"/services/{}/service-settings".format(SERVICE_ONE_ID), "/services/{}/service-settings".format(SERVICE_ONE_ID),
+20 -4
View File
@@ -14,6 +14,8 @@ def _get_notifications_csv(
template_name="foo", template_name="foo",
template_type="sms", template_type="sms",
job_name="bar.csv", job_name="bar.csv",
carrier="ATT Mobility",
provider_response="Did not like it",
status="Delivered", status="Delivered",
created_at="1943-04-19 12:00:00", created_at="1943-04-19 12:00:00",
rows=1, rows=1,
@@ -47,6 +49,8 @@ def _get_notifications_csv(
"template_type": template_type, "template_type": template_type,
"template": {"name": template_name, "template_type": template_type}, "template": {"name": template_name, "template_type": template_type},
"job_name": job_name, "job_name": job_name,
"carrier": carrier,
"provider_response": provider_response,
"status": status, "status": status,
"created_at": created_at, "created_at": created_at,
"updated_at": None, "updated_at": None,
@@ -82,15 +86,15 @@ def get_notifications_csv_mock(
( (
None, None,
[ [
"Recipient,Template,Type,Sent by,Job,Status,Time\n", "Recipient,Template,Type,Sent by,Job,Carrier,Carrier Response,Status,Time\n",
"foo@bar.com,foo,sms,,,Delivered,1943-04-19 12:00:00\r\n", "foo@bar.com,foo,sms,,,ATT Mobility,Did not like it,Delivered,1943-04-19 12:00:00\r\n",
], ],
), ),
( (
"Anne Example", "Anne Example",
[ [
"Recipient,Template,Type,Sent by,Job,Status,Time\n", "Recipient,Template,Type,Sent by,Job,Carrier,Carrier Response,Status,Time\n",
"foo@bar.com,foo,sms,Anne Example,,Delivered,1943-04-19 12:00:00\r\n", "foo@bar.com,foo,sms,Anne Example,,ATT Mobility,Did not like it,Delivered,1943-04-19 12:00:00\r\n",
], ],
), ),
], ],
@@ -128,6 +132,8 @@ def test_generate_notifications_csv_without_job(
"Type", "Type",
"Sent by", "Sent by",
"Job", "Job",
"Carrier",
"Carrier Response",
"Status", "Status",
"Time", "Time",
], ],
@@ -138,6 +144,8 @@ def test_generate_notifications_csv_without_job(
"sms", "sms",
"Fake Person", "Fake Person",
"bar.csv", "bar.csv",
"ATT Mobility",
"Did not like it",
"Delivered", "Delivered",
"1943-04-19 12:00:00", "1943-04-19 12:00:00",
], ],
@@ -157,6 +165,8 @@ def test_generate_notifications_csv_without_job(
"Type", "Type",
"Sent by", "Sent by",
"Job", "Job",
"Carrier",
"Carrier Response",
"Status", "Status",
"Time", "Time",
], ],
@@ -170,6 +180,8 @@ def test_generate_notifications_csv_without_job(
"sms", "sms",
"Fake Person", "Fake Person",
"bar.csv", "bar.csv",
"ATT Mobility",
"Did not like it",
"Delivered", "Delivered",
"1943-04-19 12:00:00", "1943-04-19 12:00:00",
], ],
@@ -189,6 +201,8 @@ def test_generate_notifications_csv_without_job(
"Type", "Type",
"Sent by", "Sent by",
"Job", "Job",
"Carrier",
"Carrier Response",
"Status", "Status",
"Time", "Time",
], ],
@@ -202,6 +216,8 @@ def test_generate_notifications_csv_without_job(
"sms", "sms",
"Fake Person", "Fake Person",
"bar.csv", "bar.csv",
"ATT Mobility",
"Did not like it",
"Delivered", "Delivered",
"1943-04-19 12:00:00", "1943-04-19 12:00:00",
], ],
+6 -6
View File
@@ -5,9 +5,9 @@ from werkzeug.exceptions import Forbidden
from app.utils.user import user_has_permissions from app.utils.user import user_has_permissions
@pytest.mark.parametrize( # noqa: PT007 # Ignoring wrong values type because of the list unpacking in the test. @pytest.mark.parametrize(
"permissions", "permissions",
( [
[ [
# Route has one of the permissions which the user has # Route has one of the permissions which the user has
"manage_service" "manage_service"
@@ -25,7 +25,7 @@ from app.utils.user import user_has_permissions
[ [
# Route has no specific permissions required # Route has no specific permissions required
], ],
), ],
) )
def test_permissions( def test_permissions(
client_request, client_request,
@@ -48,14 +48,14 @@ def test_permissions(
index() index()
@pytest.mark.parametrize( # noqa: PT007 # Ignoring wrong values type because of the list unpacking in the test. @pytest.mark.parametrize(
"permissions", "permissions",
( [
[ [
# Route has a permission which the user doesnt have # Route has a permission which the user doesnt have
"send_messages" "send_messages"
], ],
), ],
) )
def test_permissions_forbidden( def test_permissions_forbidden(
client_request, client_request,
+8 -1
View File
@@ -2423,8 +2423,15 @@ def _os_environ():
os.environ[k] = v os.environ[k] = v
@pytest.fixture # noqa (C901 too complex) @pytest.fixture() # noqa (C901 too complex)
def client_request(logged_in_client, mocker, service_one): # noqa (C901 too complex) def client_request(logged_in_client, mocker, service_one): # noqa (C901 too complex)
def _get(mocker):
return {"count": 0}
mocker.patch(
"app.service_api_client.get_global_notification_count", side_effect=_get
)
class ClientRequest: class ClientRequest:
@staticmethod @staticmethod
@contextmanager @contextmanager