Merge pull request #1911 from GSA/notify-api-1301

start load test work
This commit is contained in:
Kenneth Kehl
2024-09-09 09:21:48 -07:00
committed by GitHub
9 changed files with 162 additions and 53 deletions

View File

@@ -169,7 +169,7 @@
"filename": "app/config.py", "filename": "app/config.py",
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
"is_verified": false, "is_verified": false,
"line_number": 117, "line_number": 118,
"is_secret": false "is_secret": false
} }
], ],
@@ -692,5 +692,5 @@
} }
] ]
}, },
"generated_at": "2024-08-20T14:14:36Z" "generated_at": "2024-09-03T17:36:57Z"
} }

View File

@@ -8,6 +8,7 @@ from notifications_utils import DAILY_MESSAGE_LIMIT
class Config(object): class Config(object):
SIMULATED_SMS_NUMBERS = ("+14254147755", "+14254147167")
NOTIFY_APP_NAME = "admin" NOTIFY_APP_NAME = "admin"
NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development") NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development")
API_HOST_NAME = getenv("API_HOST_NAME", "localhost") API_HOST_NAME = getenv("API_HOST_NAME", "localhost")

View File

@@ -5,7 +5,16 @@ from collections import OrderedDict
from datetime import datetime from datetime import datetime
from io import StringIO from io import StringIO
from flask import Response, abort, flash, render_template, request, url_for from flask import (
Response,
abort,
current_app,
flash,
render_template,
request,
session,
url_for,
)
from notifications_python_client.errors import HTTPError from notifications_python_client.errors import HTTPError
from app import ( from app import (
@@ -25,6 +34,7 @@ from app.main.forms import (
DateFilterForm, DateFilterForm,
RequiredDateFilterForm, RequiredDateFilterForm,
) )
from app.main.views.send import _send_notification
from app.statistics_utils import ( from app.statistics_utils import (
get_formatted_percentage, get_formatted_percentage,
get_formatted_percentage_two_dp, get_formatted_percentage_two_dp,
@@ -771,3 +781,80 @@ def _get_user_row(r):
row.append(r["password_changed_at"]) row.append(r["password_changed_at"])
row.append(r["state"]) row.append(r["state"])
return row return row
@main.route(
"/platform-admin/load-test",
methods=["POST", "GET"],
)
@user_is_platform_admin
def load_test():
"""
The load test assumes that a service called 'Test service' exists. It will make
the platform admin a member of this service if the platform is not already. All
messagese will be sent in this service.
"""
service = _find_load_test_service()
_prepare_load_test_service(service)
example_template = _find_example_template(service)
# Simulated success
for _ in range(0, 250):
session["recipient"] = current_app.config["SIMULATED_SMS_NUMBERS"][0]
session["placeholders"] = {
"day of week": "Monday",
"color": "blue",
"phone number": current_app.config["SIMULATED_SMS_NUMBERS"][0],
}
_send_notification(service["id"], example_template["id"])
# Simulated failure
for _ in range(0, 250):
session["recipient"] = current_app.config["SIMULATED_SMS_NUMBERS"][1]
session["placeholders"] = {
"day of week": "Wednesday",
"color": "orange",
"phone number": current_app.config["SIMULATED_SMS_NUMBERS"][1],
}
_send_notification(service["id"], example_template["id"])
# For now, just redirect to the splash page so we know it's done
return render_template(
"views/platform-admin/splash-page.html",
)
def _find_example_template(service):
templates = service_api_client.get_service_templates(service["id"])
templates = templates["data"]
for template in templates:
# template = json.loads(template)
if template["name"] == "Example text message template":
return template
raise Exception("Could not find example template for load test")
def _find_load_test_service():
services = service_api_client.find_services_by_name("Test service")
services = services["data"]
for service in services:
if service["name"] == "Test service":
return service
raise Exception("Could not find 'Test service' for load test")
def _prepare_load_test_service(service):
users = user_api_client.get_all_users()
for user in users:
if user["platform_admin"] == "t":
try:
user_api_client.add_user_to_service(
service["id"], user["id"], ["send messages"]
)
except Exception:
current_app.logger.exception(
"Couldnt add user, may already be part of service"
)
pass

View File

@@ -1,3 +1,4 @@
import os
import time import time
import uuid import uuid
from string import ascii_uppercase from string import ascii_uppercase
@@ -942,8 +943,8 @@ def preview_notification(service_id, template_id):
) )
@user_has_permissions("send_messages", restrict_admin_usage=True) @user_has_permissions("send_messages", restrict_admin_usage=True)
def send_notification(service_id, template_id): def send_notification(service_id, template_id):
scheduled_for = session.pop("scheduled_for", "")
recipient = get_recipient() recipient = get_recipient()
if not recipient: if not recipient:
return redirect( return redirect(
url_for( url_for(
@@ -952,53 +953,10 @@ def send_notification(service_id, template_id):
template_id=template_id, template_id=template_id,
) )
) )
upload_id = _send_notification(service_id, template_id)
keys = [] session.pop("recipient", "")
values = [] session.pop("placeholders", "")
for k, v in session["placeholders"].items():
keys.append(k)
values.append(v)
data = ",".join(keys)
vals = ",".join(values)
data = f"{data}\r\n{vals}"
filename = (
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
)
my_data = {"filename": filename, "template_id": template_id, "data": data}
upload_id = s3upload(service_id, my_data)
# To debug messages that the user reports have not been sent, we log
# the csv filename and the job id. The user will give us the file name,
# so we can search on that to obtain the job id, which we can use elsewhere
# on the API side to find out what happens to the message.
current_app.logger.info(
hilite(
f"One-off file: {filename} job_id: {upload_id} s3 location: service-{service_id}-notify/{upload_id}.csv"
)
)
form = CsvUploadForm()
form.file.data = my_data
form.file.name = filename
check_message_output = check_messages(service_id, template_id, upload_id, 2)
if "You cannot send to" in check_message_output:
return check_messages(service_id, template_id, upload_id, 2)
job_api_client.create_job(
upload_id,
service_id,
scheduled_for=scheduled_for,
template_id=template_id,
original_file_name=filename,
notification_count=1,
valid="True",
)
session.pop("recipient")
session.pop("placeholders")
# We have to wait for the job to run and create the notification in the database # We have to wait for the job to run and create the notification in the database
time.sleep(0.1) time.sleep(0.1)
@@ -1046,6 +1004,56 @@ def send_notification(service_id, template_id):
) )
def _send_notification(service_id, template_id):
scheduled_for = session.pop("scheduled_for", "")
keys = []
values = []
for k, v in session["placeholders"].items():
keys.append(k)
values.append(v)
data = ",".join(keys)
vals = ",".join(values)
data = f"{data}\r\n{vals}"
filename = (
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
)
my_data = {"filename": filename, "template_id": template_id, "data": data}
upload_id = s3upload(service_id, my_data)
# To debug messages that the user reports have not been sent, we log
# the csv filename and the job id. The user will give us the file name,
# so we can search on that to obtain the job id, which we can use elsewhere
# on the API side to find out what happens to the message.
current_app.logger.info(
hilite(
f"One-off file: {filename} job_id: {upload_id} s3 location: service-{service_id}-notify/{upload_id}.csv"
)
)
# For load testing we want to skip these checks. They are doing some fine-grained
# comparison about what is in the preview, but the load test just blast messages
# and doesn't care about the preview.
if os.getenv("NOTIFY_ENVIRONMENT") not in ("development", "staging", "demo"):
form = CsvUploadForm()
form.file.data = my_data
form.file.name = filename
check_message_output = check_messages(service_id, template_id, upload_id, 2)
if "You cannot send to" in check_message_output:
return check_messages(service_id, template_id, upload_id, 2)
job_api_client.create_job(
upload_id,
service_id,
scheduled_for=scheduled_for,
template_id=template_id,
original_file_name=filename,
notification_count=1,
valid="True",
)
return upload_id
def get_email_reply_to_address_from_session(): def get_email_reply_to_address_from_session():
if session.get("sender_id"): if session.get("sender_id"):
return current_service.get_email_reply_to_address(session["sender_id"])[ return current_service.get_email_reply_to_address(session["sender_id"])[

View File

@@ -1,3 +1,4 @@
import os
from datetime import datetime from datetime import datetime
from flask import abort, current_app, request, session from flask import abort, current_app, request, session
@@ -219,6 +220,14 @@ class User(JSONModel, UserMixin):
def has_permissions( def has_permissions(
self, *permissions, restrict_admin_usage=False, allow_org_user=False self, *permissions, restrict_admin_usage=False, allow_org_user=False
): ):
# TODO need this for load test, but breaks unit tests
if self.platform_admin and os.getenv("NOTIFY_ENVIRONMENT") in (
"development",
"staging",
"demo",
):
return True
unknown_permissions = set(permissions) - all_ui_permissions unknown_permissions = set(permissions) - all_ui_permissions
if unknown_permissions: if unknown_permissions:
raise TypeError( raise TypeError(

View File

@@ -28,6 +28,10 @@
{% if not service['active'] %} {% if not service['active'] %}
<span class="heading-medium hint">&ensp;Archived</span> <span class="heading-medium hint">&ensp;Archived</span>
{% endif %} {% endif %}
{% if service['name'] == 'Test service' %}
<a class="usa-link" href="{{ url_for('main.load_test') }}">Load Test</a>
{% endif %}
{% endcall %} {% endcall %}
{% endcall %} {% endcall %}

1
poetry.lock generated
View File

@@ -1622,6 +1622,7 @@ files = [
{file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"},
{file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"},
{file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"},
{file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"},
{file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"},
] ]

View File

@@ -2815,9 +2815,7 @@ def test_send_notification_clears_session(
], ],
) )
def test_send_notification_redirects_if_missing_data( def test_send_notification_redirects_if_missing_data(
client_request, client_request, fake_uuid, session_data, mocker
fake_uuid,
session_data,
): ):
with client_request.session_transaction() as session: with client_request.session_transaction() as session:
session.update(session_data) session.update(session_data)

View File

@@ -121,6 +121,7 @@ EXCLUDED_ENDPOINTS = tuple(
"link_service_to_organization", "link_service_to_organization",
"live_services", "live_services",
"live_services_csv", "live_services_csv",
"load_test",
"manage_org_users", "manage_org_users",
"manage_template_folder", "manage_template_folder",
"manage_users", "manage_users",