diff --git a/.ds.baseline b/.ds.baseline index 41cd12948..9e44afaf0 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -169,7 +169,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 117, + "line_number": 118, "is_secret": false } ], @@ -692,5 +692,5 @@ } ] }, - "generated_at": "2024-08-20T14:14:36Z" + "generated_at": "2024-09-03T17:36:57Z" } diff --git a/app/config.py b/app/config.py index 960d6331b..dece2728d 100644 --- a/app/config.py +++ b/app/config.py @@ -8,6 +8,7 @@ from notifications_utils import DAILY_MESSAGE_LIMIT class Config(object): + SIMULATED_SMS_NUMBERS = ("+14254147755", "+14254147167") NOTIFY_APP_NAME = "admin" NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development") API_HOST_NAME = getenv("API_HOST_NAME", "localhost") diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index f0b5983b5..6a121ecc2 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -5,7 +5,16 @@ from collections import OrderedDict from datetime import datetime 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 app import ( @@ -25,6 +34,7 @@ from app.main.forms import ( DateFilterForm, RequiredDateFilterForm, ) +from app.main.views.send import _send_notification from app.statistics_utils import ( get_formatted_percentage, get_formatted_percentage_two_dp, @@ -771,3 +781,80 @@ def _get_user_row(r): row.append(r["password_changed_at"]) row.append(r["state"]) 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 diff --git a/app/main/views/send.py b/app/main/views/send.py index e6843da18..7899af5a8 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -1,3 +1,4 @@ +import os import time import uuid 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) def send_notification(service_id, template_id): - scheduled_for = session.pop("scheduled_for", "") recipient = get_recipient() + if not recipient: return redirect( url_for( @@ -952,53 +953,10 @@ def send_notification(service_id, template_id): template_id=template_id, ) ) + upload_id = _send_notification(service_id, template_id) - 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" - ) - ) - - 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") + session.pop("recipient", "") + session.pop("placeholders", "") # We have to wait for the job to run and create the notification in the database 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(): if session.get("sender_id"): return current_service.get_email_reply_to_address(session["sender_id"])[ diff --git a/app/models/user.py b/app/models/user.py index 6991dc035..ba478feda 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,3 +1,4 @@ +import os from datetime import datetime from flask import abort, current_app, request, session @@ -219,6 +220,14 @@ class User(JSONModel, UserMixin): def has_permissions( 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 if unknown_permissions: raise TypeError( diff --git a/app/templates/views/platform-admin/services.html b/app/templates/views/platform-admin/services.html index 65f10e281..280ce796f 100644 --- a/app/templates/views/platform-admin/services.html +++ b/app/templates/views/platform-admin/services.html @@ -28,6 +28,10 @@ {% if not service['active'] %}  Archived {% endif %} + {% if service['name'] == 'Test service' %} + Load Test + {% endif %} + {% endcall %} {% endcall %} diff --git a/poetry.lock b/poetry.lock index e43760f37..5cae02fce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index 24403e895..1d8e696f8 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -2815,9 +2815,7 @@ def test_send_notification_clears_session( ], ) def test_send_notification_redirects_if_missing_data( - client_request, - fake_uuid, - session_data, + client_request, fake_uuid, session_data, mocker ): with client_request.session_transaction() as session: session.update(session_data) diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index a9ce4ea87..58ff9f342 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -121,6 +121,7 @@ EXCLUDED_ENDPOINTS = tuple( "link_service_to_organization", "live_services", "live_services_csv", + "load_test", "manage_org_users", "manage_template_folder", "manage_users",