From 23210bcc5ca159534c702a9e6b8a81580a3e87d5 Mon Sep 17 00:00:00 2001 From: Steve Astels Date: Wed, 1 May 2024 15:27:02 -0400 Subject: [PATCH 1/7] add script to send arbitrary many notifications (#2166) --- tests_smoke/send_many.py | 72 +++++++++++++++++++++++++++++++++++++ tests_smoke/smoke/common.py | 8 +++-- 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests_smoke/send_many.py diff --git a/tests_smoke/send_many.py b/tests_smoke/send_many.py new file mode 100644 index 0000000000..62adca2ecd --- /dev/null +++ b/tests_smoke/send_many.py @@ -0,0 +1,72 @@ +import argparse +import time +from datetime import datetime + +import requests +from dotenv import load_dotenv +from smoke.common import ( # type: ignore + Config, + Notification_type, + create_jwt_token, + job_line, + rows_to_csv, + s3upload, + set_metadata_on_csv_upload, +) + +DEFAULT_JOB_SIZE = 50000 + + +def send_admin_csv(notification_type: Notification_type, job_size: int): + """Send a bulk job of notifications by uploading a CSV + + Args: + notification_type (Notification_type): email or sms + job_size (int): number of notifications to send + """ + + template_id = Config.EMAIL_TEMPLATE_ID if notification_type == Notification_type.EMAIL else Config.SMS_TEMPLATE_ID + to = Config.EMAIL_TO if notification_type == Notification_type.EMAIL else Config.SMS_TO + header = "email address" if notification_type == Notification_type.EMAIL else "phone number" + + csv = rows_to_csv([[header, "var"], *job_line(to, job_size)]) + upload_id = s3upload(Config.SERVICE_ID, csv) + metadata_kwargs = { + "notification_count": 1, + "template_id": template_id, + "valid": True, + "original_file_name": f"Large send {datetime.utcnow().isoformat()}.csv", + } + set_metadata_on_csv_upload(Config.SERVICE_ID, upload_id, **metadata_kwargs) + + token = create_jwt_token(Config.ADMIN_CLIENT_SECRET, client_id=Config.ADMIN_CLIENT_USER_NAME) + response = requests.post( + f"{Config.API_HOST_NAME}/service/{Config.SERVICE_ID}/job", + json={"id": upload_id, "created_by": Config.USER_ID}, + headers={"Authorization": f"Bearer {token}"}, + ) + if response.status_code != 201: + print(response.json()) + print("FAILED: post to start send failed") + exit(1) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-n", "--notifications", default=1, type=int, help="total number of notifications") + parser.add_argument("-j", "--job_size", default=DEFAULT_JOB_SIZE, type=int, help=f"size of bulk send jobs (default {DEFAULT_JOB_SIZE})") + parser.add_argument("--sms", default=False, action='store_true', help="send sms instead of emails") + + args = parser.parse_args() + load_dotenv() + + notification_type = Notification_type.SMS if args.sms else Notification_type.EMAIL + for start_n in range(0, args.notifications, args.job_size): + num_sending = min(args.notifications - start_n, args.job_size) + print(f"Sending {start_n} - {start_n + num_sending - 1} of {args.notifications}") + send_admin_csv(notification_type, num_sending) + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/tests_smoke/smoke/common.py b/tests_smoke/smoke/common.py index 8c189baf19..3b52669b7a 100644 --- a/tests_smoke/smoke/common.py +++ b/tests_smoke/smoke/common.py @@ -16,6 +16,10 @@ from dotenv import load_dotenv from notifications_python_client.authentication import create_jwt_token +# from app/config.py +INTERNAL_TEST_NUMBER = "+16135550123" +INTERNAL_TEST_EMAIL_ADDRESS = "internal.test@cds-snc.ca" + load_dotenv() @@ -32,8 +36,8 @@ class Config: AWS_SECRET_ACCESS_KEY = os.environ.get("SMOKE_AWS_SECRET_ACCESS_KEY") SERVICE_ID = os.environ.get("SMOKE_SERVICE_ID", "") USER_ID = os.environ.get("SMOKE_USER_ID") - EMAIL_TO = os.environ.get("SMOKE_EMAIL_TO", "") - SMS_TO = os.environ.get("SMOKE_SMS_TO", "") + EMAIL_TO = os.environ.get("SMOKE_EMAIL_TO", INTERNAL_TEST_EMAIL_ADDRESS) + SMS_TO = os.environ.get("SMOKE_SMS_TO", INTERNAL_TEST_NUMBER) EMAIL_TEMPLATE_ID = os.environ.get("SMOKE_EMAIL_TEMPLATE_ID") SMS_TEMPLATE_ID = os.environ.get("SMOKE_SMS_TEMPLATE_ID") API_KEY = os.environ.get("SMOKE_API_KEY", "") From 36fb113bf9e3414c8279b370699a732ad9661484 Mon Sep 17 00:00:00 2001 From: Jumana B Date: Thu, 2 May 2024 10:55:45 -0400 Subject: [PATCH 2/7] remove bold (#2168) --- .../versions/0448_update_verify_code2.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 migrations/versions/0448_update_verify_code2.py diff --git a/migrations/versions/0448_update_verify_code2.py b/migrations/versions/0448_update_verify_code2.py new file mode 100644 index 0000000000..39f3acee1b --- /dev/null +++ b/migrations/versions/0448_update_verify_code2.py @@ -0,0 +1,97 @@ +""" + +Revision ID: 0448_update_verify_code2 +Revises: 0447_update_verify_code_template +Create Date: 2023-10-05 00:00:00 + +""" +from datetime import datetime + +from alembic import op +from flask import current_app + +revision = "0448_update_verify_code2" +down_revision = "0447_update_verify_code_template" + +near_content = "\n".join( + [ + "[[en]]", + "Hi ((name)),", + "", + "Here is your security code to log in to GC Notify:", + "", + "^ ((verify_code))", + "[[/en]]", + "", + "---", + "", + "[[fr]]", + "Bonjour ((name)),", + "", + "Voici votre code de sécurité pour vous connecter à Notification GC:", + "", + "^ ((verify_code))", + "[[/fr]]", + ] +) + + +templates = [ + { + "id": current_app.config["EMAIL_2FA_TEMPLATE_ID"], + "template_type": "email", + "subject": "Sign in | Connectez-vous", + "content": near_content, + "process_type": "priority", + }, +] + + +def upgrade(): + conn = op.get_bind() + + for template in templates: + current_version = conn.execute("select version from templates where id='{}'".format(template["id"])).fetchone() + name = conn.execute("select name from templates where id='{}'".format(template["id"])).fetchone() + template["version"] = current_version[0] + 1 + template["name"] = name[0] + + template_update = """ + UPDATE templates SET content = '{}', subject = '{}', version = '{}', updated_at = '{}' + WHERE id = '{}' + """ + template_history_insert = """ + INSERT INTO templates_history (id, name, template_type, created_at, content, archived, service_id, subject, + created_by_id, version, process_type, hidden) + VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', {}, '{}', false) + """ + + for template in templates: + op.execute( + template_update.format( + template["content"], + template["subject"], + template["version"], + datetime.utcnow(), + template["id"], + ) + ) + + op.execute( + template_history_insert.format( + template["id"], + template["name"], + template["template_type"], + datetime.utcnow(), + template["content"], + current_app.config["NOTIFY_SERVICE_ID"], + template["subject"], + current_app.config["NOTIFY_USER_ID"], + template["version"], + template["process_type"], + ) + ) + + +def downgrade(): + pass From d95e7b3f81215e5689c73def562eac3523d984be Mon Sep 17 00:00:00 2001 From: Steve Astels Date: Thu, 2 May 2024 13:29:15 -0400 Subject: [PATCH 3/7] fix/use a service's fixed phone number if set (#2163) --- app/celery/tasks.py | 22 +++++++---- app/job/rest.py | 5 +++ app/v2/notifications/post_notifications.py | 6 +++ tests/app/celery/test_tasks.py | 4 +- tests/app/job/test_rest.py | 37 ++++++++++++++++++- .../notifications/test_post_notifications.py | 28 ++++++++++++++ 6 files changed, 93 insertions(+), 9 deletions(-) diff --git a/app/celery/tasks.py b/app/celery/tasks.py index df10c1db29..64f8dd669d 100644 --- a/app/celery/tasks.py +++ b/app/celery/tasks.py @@ -8,7 +8,10 @@ from flask import current_app from itsdangerous import BadSignature from more_itertools import chunked -from notifications_utils.recipients import RecipientCSV +from notifications_utils.recipients import ( + RecipientCSV, + try_validate_and_format_phone_number, +) from notifications_utils.statsd_decorators import statsd from notifications_utils.template import SMSMessageTemplate, WithSubjectTemplate from notifications_utils.timezones import convert_utc_to_local_timezone @@ -243,13 +246,18 @@ def save_smss(self, service_id: Optional[str], signed_notifications: List[Signed sender_id = _notification.get("sender_id") # type: ignore notification_id = _notification.get("id", create_uuid()) - reply_to_text = "" # type: ignore - if sender_id: - reply_to_text = dao_get_service_sms_senders_by_id(service_id, sender_id).sms_sender - elif template.service: - reply_to_text = template.get_reply_to_text() + if "reply_to_text" in _notification and _notification["reply_to_text"]: + reply_to_text = _notification["reply_to_text"] else: - reply_to_text = service.get_default_sms_sender() # type: ignore + reply_to_text = "" # type: ignore + if sender_id: + reply_to_text = try_validate_and_format_phone_number( + dao_get_service_sms_senders_by_id(service_id, sender_id).sms_sender + ) + elif template.service: + reply_to_text = template.get_reply_to_text() + else: + reply_to_text = service.get_default_sms_sender() # type: ignore notification: VerifiedNotification = { **_notification, # type: ignore diff --git a/app/job/rest.py b/app/job/rest.py index 8b29b73ccf..28bfeafd33 100644 --- a/app/job/rest.py +++ b/app/job/rest.py @@ -167,6 +167,11 @@ def create_job(service_id): ) if template.template_type == SMS_TYPE: + # set sender_id if missing + default_senders = [x for x in service.service_sms_senders if x.is_default] + default_sender_id = default_senders[0].id if default_senders else None + data["sender_id"] = data.get("sender_id", default_sender_id) + # calculate the number of simulated recipients numberOfSimulated = sum(simulated_recipient(i["phone_number"].data, template.template_type) for i in recipient_csv.rows) mixedRecipients = numberOfSimulated > 0 and numberOfSimulated != len(recipient_csv) diff --git a/app/v2/notifications/post_notifications.py b/app/v2/notifications/post_notifications.py index 9a1640b9a2..b1fc62e447 100644 --- a/app/v2/notifications/post_notifications.py +++ b/app/v2/notifications/post_notifications.py @@ -229,6 +229,12 @@ def post_bulk(): increment_email_daily_count_send_warnings_if_needed(authenticated_service, len(list(recipient_csv.get_rows()))) if template.template_type == SMS_TYPE: + # set sender_id if missing + if form["validated_sender_id"] is None: + default_senders = [x for x in authenticated_service.service_sms_senders if x.is_default] + default_sender_id = default_senders[0].id if default_senders else None + form["validated_sender_id"] = default_sender_id + # calculate the number of simulated recipients numberOfSimulated = sum( simulated_recipient(i["phone_number"].data, template.template_type) for i in list(recipient_csv.get_rows()) diff --git a/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index 437d59e7ec..a2f0c367a7 100644 --- a/tests/app/celery/test_tasks.py +++ b/tests/app/celery/test_tasks.py @@ -1154,7 +1154,7 @@ def test_save_sms_should_use_redis_cache_to_retrieve_service_and_template_when_p notification["sender_id"] = sender_id sms_sender = ServiceSmsSender() - sms_sender.sms_sender = "+16502532222" + sms_sender.sms_sender = "6135550123" mocked_get_sender_id = mocker.patch("app.celery.tasks.dao_get_service_sms_senders_by_id", return_value=sms_sender) celery_task = "deliver_throttled_sms" if sender_id else "deliver_sms" mocked_deliver_sms = mocker.patch(f"app.celery.provider_tasks.{celery_task}.apply_async") @@ -1191,6 +1191,8 @@ def test_save_sms_should_use_redis_cache_to_retrieve_service_and_template_when_p assert persisted_notification.personalisation == {"name": "Jo"} assert persisted_notification._personalisation == signer_personalisation.sign({"name": "Jo"}) assert persisted_notification.notification_type == "sms" + assert persisted_notification.reply_to_text == (f"+1{sms_sender.sms_sender}" if sender_id else None) + mocked_deliver_sms.assert_called_once_with( [str(persisted_notification.id)], queue="send-throttled-sms-tasks" if sender_id else QueueNames.SEND_SMS_MEDIUM ) diff --git a/tests/app/job/test_rest.py b/tests/app/job/test_rest.py index 76f89ebdf9..338d3a5d84 100644 --- a/tests/app/job/test_rest.py +++ b/tests/app/job/test_rest.py @@ -8,12 +8,14 @@ import app.celery.tasks from app.dao.templates_dao import dao_update_template -from app.models import JOB_STATUS_PENDING, JOB_STATUS_TYPES +from app.models import JOB_STATUS_PENDING, JOB_STATUS_TYPES, ServiceSmsSender from tests import create_authorization_header from tests.app.db import ( create_ft_notification_status, create_job, create_notification, + create_service_with_inbound_number, + create_template, save_notification, ) from tests.conftest import set_config @@ -263,6 +265,39 @@ def test_create_unscheduled_job_with_sender_id_in_metadata(client, sample_templa app.celery.tasks.process_job.apply_async.assert_called_once_with(([str(fake_uuid)]), queue="job-tasks") +def test_create_job_sets_sender_id_from_database(client, mocker, fake_uuid, sample_user): + service = create_service_with_inbound_number(inbound_number="12345") + template = create_template(service=service) + sms_sender = ServiceSmsSender.query.filter_by(service_id=service.id).first() + + mocker.patch("app.celery.tasks.process_job.apply_async") + mocker.patch( + "app.job.rest.get_job_metadata_from_s3", + return_value={ + "template_id": str(template.id), + "original_file_name": "thisisatest.csv", + "notification_count": "1", + "valid": "True", + }, + ) + mocker.patch( + "app.job.rest.get_job_from_s3", + return_value="phone number\r\n6502532222", + ) + data = { + "id": fake_uuid, + "created_by": str(template.created_by.id), + } + path = "/service/{}/job".format(service.id) + auth_header = create_authorization_header() + headers = [("Content-Type", "application/json"), auth_header] + + response = client.post(path, data=json.dumps(data), headers=headers) + resp_json = json.loads(response.get_data(as_text=True)) + + assert resp_json["data"]["sender_id"] == str(sms_sender.id) + + @freeze_time("2016-01-01 12:00:00.000000") def test_create_scheduled_job(client, sample_template, mocker, fake_uuid): scheduled_date = (datetime.utcnow() + timedelta(hours=95, minutes=59)).isoformat() diff --git a/tests/app/v2/notifications/test_post_notifications.py b/tests/app/v2/notifications/test_post_notifications.py index 2e3d1d36b2..93b81c47cd 100644 --- a/tests/app/v2/notifications/test_post_notifications.py +++ b/tests/app/v2/notifications/test_post_notifications.py @@ -26,6 +26,7 @@ ApiKey, Notification, ScheduledNotification, + ServiceSmsSender, ) from app.schema_validation import validate from app.utils import get_document_url @@ -2490,6 +2491,33 @@ def test_post_bulk_creates_job_and_dispatches_celery_task( } } + def test_post_bulk_sms_sets_sender_id_from_database( + self, + client, + mocker, + notify_user, + notify_api, + ): + service = create_service_with_inbound_number(inbound_number="12345") + template = create_template(service=service) + sms_sender = ServiceSmsSender.query.filter_by(service_id=service.id).first() + data = {"name": "job_name", "template_id": template.id, "rows": [["phone number"], ["6135550111"]]} + job_id = str(uuid.uuid4()) + mocker.patch("app.v2.notifications.post_notifications.upload_job_to_s3", return_value=job_id) + mocker.patch("app.v2.notifications.post_notifications.process_job.apply_async") + + client.post( + "/v2/notifications/bulk", + data=json.dumps(data), + headers=[ + ("Content-Type", "application/json"), + create_authorization_header(service_id=service.id), + ], + ) + + job = dao_get_job_by_id(job_id) + assert job.sender_id == sms_sender.id + def test_post_bulk_with_too_large_sms_fails(self, client, notify_db, notify_db_session, mocker): mocker.patch("app.sms_normal_publish.publish") mocker.patch("app.v2.notifications.post_notifications.create_bulk_job", return_value=str(uuid.uuid4())) From 22ada2210d2e706d89aa91d4798687ac51c6eb14 Mon Sep 17 00:00:00 2001 From: Steve Astels Date: Thu, 2 May 2024 14:43:40 -0400 Subject: [PATCH 4/7] Better nightly performance test (#2075) --- bin/execute_and_publish_performance_test.sh | 15 +++++- tests-perf/locust/README.md | 9 +++- tests-perf/locust/send_rate_email.py | 60 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 tests-perf/locust/send_rate_email.py diff --git a/bin/execute_and_publish_performance_test.sh b/bin/execute_and_publish_performance_test.sh index ef7fe0a5bf..fa2d50b05a 100755 --- a/bin/execute_and_publish_performance_test.sh +++ b/bin/execute_and_publish_performance_test.sh @@ -1,13 +1,24 @@ #!/bin/bash +# Setup current_time=$(date "+%Y.%m.%d-%H.%M.%S") perf_test_aws_s3_bucket=${PERF_TEST_AWS_S3_BUCKET:-notify-performance-test-results-staging} perf_test_csv_directory_path=${PERF_TEST_CSV_DIRECTORY_PATH:-/tmp/notify_performance_test} - mkdir -p $perf_test_csv_directory_path/$current_time +# Run old performance test and copy results to S3 locust --headless --config tests-perf/locust/locust.conf --html $perf_test_csv_directory_path/$current_time/index.html --csv $perf_test_csv_directory_path/$current_time/perf_test - aws s3 cp $perf_test_csv_directory_path/ "s3://$perf_test_aws_s3_bucket" --recursive || exit 1 +# Sleep 15 minutes to allow the system to stabilize +sleep 900 + +# Run email send rate performance test +# This configuration should send 10K emails / minute for 10 minutes for 100K emails total. +# We run this test on Tuesday through Friday (just after midnight UTC) only. +if [ "$(date +%u)" -ge 2 ] && [ "$(date +%u)" -le 5 ]; then + locust --headless --host https://api.staging.notification.cdssandbox.xyz --locustfile tests-perf/locust/send_rate_email.py --users 5 --run-time 10m --spawn-rate 1 +fi + +# Cleanup rm -rf $perf_test_csv_directory_path/$current_time diff --git a/tests-perf/locust/README.md b/tests-perf/locust/README.md index 561adafdd2..afe4c639f0 100644 --- a/tests-perf/locust/README.md +++ b/tests-perf/locust/README.md @@ -36,7 +36,7 @@ You should not have to modify the configuration to run the stress-tests locally. There are two ways to run Locust, with the UI or headless. -### Add the following to your .env file (ask a coworker): +### Add the following to your .env file (see 1Password): ``` PERF_TEST_AUTH_HEADER = @@ -67,6 +67,13 @@ locust -f .\locust-notifications.py --headless --users=5500 --spawn-rate=200 --r You can also modify the *locust.config* file to enable the headless mode and define the necessary users, spawn rate and run time. +## Email send rate test + +We also max out the email send rate by sending 2000 x 5 emails per minute for 10 minutes. This can be run manually with the command +``` +locust --headless --host https://api.staging.notification.cdssandbox.xyz --locustfile tests-perf/locust/send_rate_email.py --users 5 --run-time 10m --spawn-rate 1 +``` + ### Performance Testing on AWS We run Notify performance tests on a daily manner through AWS ECS tasks diff --git a/tests-perf/locust/send_rate_email.py b/tests-perf/locust/send_rate_email.py new file mode 100644 index 0000000000..a2a26fef73 --- /dev/null +++ b/tests-perf/locust/send_rate_email.py @@ -0,0 +1,60 @@ +""" send_rate_email.py + isort:skip_file +""" +# flake8: noqa + +BULK_EMAIL_SIZE = 2000 + +import os +import sys +from datetime import datetime +from dataclasses import make_dataclass + +sys.path.append(os.path.abspath(os.path.join("..", "tests_smoke"))) + +from dotenv import load_dotenv +from locust import HttpUser, constant_pacing, task +from tests_smoke.smoke.common import job_line, rows_to_csv # type: ignore + +load_dotenv() +NotifyApiUserTemplateGroup = make_dataclass('NotifyApiUserTemplateGroup', [ + 'bulk_email_id', + 'email_id', + 'email_with_attachment_id', + 'email_with_link_id', + 'sms_id', +]) + + +class NotifyApiUser(HttpUser): + + wait_time = constant_pacing(60) # 60 seconds between each task + host = os.getenv("PERF_TEST_DOMAIN", "https://api.staging.notification.cdssandbox.xyz") + + def __init__(self, *args, **kwargs): + super(NotifyApiUser, self).__init__(*args, **kwargs) + + self.headers = {"Authorization": os.getenv("PERF_TEST_AUTH_HEADER")} + self.email = os.getenv("PERF_TEST_EMAIL", "success@simulator.amazonses.com") + self.phone_number = os.getenv("PERF_TEST_PHONE_NUMBER", "16135550123") + self.template_group = NotifyApiUserTemplateGroup( + bulk_email_id=os.getenv("PERF_TEST_BULK_EMAIL_TEMPLATE_ID"), + email_id=os.getenv("PERF_TEST_EMAIL_TEMPLATE_ID"), + email_with_attachment_id=os.getenv("PERF_TEST_EMAIL_WITH_ATTACHMENT_TEMPLATE_ID"), + email_with_link_id=os.getenv("PERF_TEST_EMAIL_WITH_LINK_TEMPLATE_ID"), + sms_id=os.getenv("PERF_TEST_SMS_TEMPLATE_ID"), + ) + + @task(1) + def send_bulk_email_notifications(self): + """ + Send BULK_EMAIL_SIZE emails through the /bulk endpoint + """ + + json = { + "name": f"Send rate test {datetime.utcnow().isoformat()}", + "template_id": self.template_group.bulk_email_id, + "csv": rows_to_csv([["email address", "application_file"], *job_line(self.email, BULK_EMAIL_SIZE)]) + } + + self.client.post("/v2/notifications/bulk", json=json, headers=self.headers) From 43f1949e280708f263438abe19d9a2577b1d80c3 Mon Sep 17 00:00:00 2001 From: Steve Astels Date: Thu, 2 May 2024 15:52:07 -0400 Subject: [PATCH 5/7] Update utils to 52.2.2 (#2167) --- .github/workflows/test.yaml | 2 +- poetry.lock | 176 ++++++++++++++++++------------------ pyproject.toml | 4 +- 3 files changed, 91 insertions(+), 91 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8d5c023e27..b38d18159d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,7 +67,7 @@ jobs: run: | cp -f .env.example .env - name: Checks for new endpoints against AWS WAF rules - uses: cds-snc/notification-utils/.github/actions/waffles@06a40db6286f525fe3551e029418458d33342592 # 52.1.0 + uses: cds-snc/notification-utils/.github/actions/waffles@578b4147fe6c7a8f89241649b763f94ef24b2ad4 # 52.2.2 with: app-loc: '/github/workspace' app-libs: '/github/workspace/env/site-packages' diff --git a/poetry.lock b/poetry.lock index 5002212cb4..5a3fc9e36d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -210,17 +210,17 @@ aiohttp = "*" [[package]] name = "awscli" -version = "1.32.25" +version = "1.32.89" description = "Universal Command Line Environment for AWS." optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "awscli-1.32.25-py3-none-any.whl", hash = "sha256:eea617961175e8bd1bd3aeda706948c89dc353fc934e81c156fe1ea484ca7a31"}, - {file = "awscli-1.32.25.tar.gz", hash = "sha256:091bbdb852b984d81fb5d8bf00100edd9e40750c77e1542f7ce3ac952a01df6d"}, + {file = "awscli-1.32.89-py3-none-any.whl", hash = "sha256:6928106f755312eb8126eebe317e947d64d0337d0a0f468c8ed06c724eed0286"}, + {file = "awscli-1.32.89.tar.gz", hash = "sha256:0cb9f2145b3c84e7df253ad01589e8d31412644ad83c9ffe4bd3e45ffc2dd9d2"}, ] [package.dependencies] -botocore = "1.34.25" +botocore = "1.34.89" colorama = ">=0.2.5,<0.4.5" docutils = ">=0.10,<0.17" PyYAML = ">=3.10,<6.1" @@ -371,17 +371,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.25" +version = "1.34.89" description = "The AWS SDK for Python" optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "boto3-1.34.25-py3-none-any.whl", hash = "sha256:87532469188f1eeef4dca67dffbd3f0cc1d51cef7d5e5b5dc95d3b8125f8446e"}, - {file = "boto3-1.34.25.tar.gz", hash = "sha256:1b415e0553679ea05b9e2aed3eb271431011a67a165e3e0aefa323e13b8b7e92"}, + {file = "boto3-1.34.89-py3-none-any.whl", hash = "sha256:f9166f485d64b012d46acd212fb29a45b195a85ff66a645b05b06d9f7572af36"}, + {file = "boto3-1.34.89.tar.gz", hash = "sha256:e0940e43810fe82f5b77442c751491fcc2768af7e7c3e8c15ea158e1ca9b586c"}, ] [package.dependencies] -botocore = ">=1.34.25,<1.35.0" +botocore = ">=1.34.89,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -390,22 +390,22 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.25" +version = "1.34.89" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "botocore-1.34.25-py3-none-any.whl", hash = "sha256:35dfab5bdb4620f73ac7c557c4e0d012429706d8760b100f099feea34b5505f8"}, - {file = "botocore-1.34.25.tar.gz", hash = "sha256:a39070bb760bd9545b0eef52a8bcb2d03918206e67a5a786ea4bd6f4bd949edd"}, + {file = "botocore-1.34.89-py3-none-any.whl", hash = "sha256:35205ed7db13058a3f7114c28e93058a8ff1490dfc6a5b5dff9c581c738fbf59"}, + {file = "botocore-1.34.89.tar.gz", hash = "sha256:6624b69bcdf2c5d0568b7bc9cbac13e605f370e7ea06710c61e2e2dc76831141"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.19.19)"] +crt = ["awscrt (==0.20.9)"] [[package]] name = "brotli" @@ -1985,71 +1985,71 @@ testing = ["pytest"] [[package]] name = "markupsafe" -version = "2.1.4" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, - {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -2444,7 +2444,7 @@ requests = ">=2.0.0" [[package]] name = "notifications-utils" -version = "52.2.0" +version = "52.2.2" description = "Shared python code for Notification - Provides logging utils etc." optional = false python-versions = "~3.10.9" @@ -2452,9 +2452,9 @@ files = [] develop = false [package.dependencies] -awscli = "1.32.25" +awscli = "1.32.89" bleach = "6.1.0" -boto3 = "1.34.25" +boto3 = "1.34.89" cachetools = "4.2.4" certifi = "^2023.7.22" cryptography = "^42.0.3" @@ -2462,10 +2462,10 @@ Flask = "2.3.3" Flask-Redis = "0.4.0" itsdangerous = "2.1.2" Jinja2 = "^3.0.0" -markupsafe = "2.1.4" +markupsafe = "2.1.5" mistune = "0.8.4" ordered-set = "4.1.0" -phonenumbers = "8.13.28" +phonenumbers = "8.13.35" py_w3c = "0.3.1" pypdf2 = "1.28.6" python-json-logger = "2.0.7" @@ -2479,8 +2479,8 @@ werkzeug = "2.3.7" [package.source] type = "git" url = "https://github.com/cds-snc/notifier-utils.git" -reference = "52.2.0" -resolved_reference = "0146718a423b0144566392ab3082d15779f4a73f" +reference = "52.2.2" +resolved_reference = "578b4147fe6c7a8f89241649b763f94ef24b2ad4" [[package]] name = "ordered-set" @@ -2554,13 +2554,13 @@ pytzdata = ">=2020.1" [[package]] name = "phonenumbers" -version = "8.13.28" +version = "8.13.35" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false python-versions = "*" files = [ - {file = "phonenumbers-8.13.28-py2.py3-none-any.whl", hash = "sha256:ad7bc7d7fd6599a124423ffb840409630777c72d0ee58ba8070cc8e7efcb4c38"}, - {file = "phonenumbers-8.13.28.tar.gz", hash = "sha256:e22f276b0c4a70bd5b3f6d668d19cab2578f660b8df44d6418f81d64320151b9"}, + {file = "phonenumbers-8.13.35-py2.py3-none-any.whl", hash = "sha256:58286a8e617bd75f541e04313b28c36398be6d4443a778c85e9617a93c391310"}, + {file = "phonenumbers-8.13.35.tar.gz", hash = "sha256:64f061a967dcdae11e1c59f3688649e697b897110a33bb74d5a69c3e35321245"}, ] [[package]] @@ -4255,4 +4255,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "~3.10.9" -content-hash = "18ed30bed79c84db2cc559fecdba223a88c8d8546f4fb951f85ef1d76142a714" +content-hash = "94cd1ef6449c0b8e8dc6d90152b64b6295aa6123b5d78ceb414eb1e139110462" diff --git a/pyproject.toml b/pyproject.toml index 973ced94f6..933b1066bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,10 +61,10 @@ awscli-cwlogs = "1.4.6" aws-embedded-metrics = "1.0.8" # Putting upgrade on hold due to new version introducing breaking changes Werkzeug = "2.3.7" -MarkupSafe = "2.1.4" +MarkupSafe = "2.1.5" # REVIEW: v2 is using sha512 instead of sha1 by default (in v1) itsdangerous = "2.1.2" -notifications-utils = { git = "https://github.com/cds-snc/notifier-utils.git", tag = "52.2.0" } +notifications-utils = { git = "https://github.com/cds-snc/notifier-utils.git", tag = "52.2.2" } # rsa = "4.9 # awscli 1.22.38 depends on rsa<4.8 typing-extensions = "4.7.1" greenlet = "2.0.2" From 043337a44b23f3e12880d7489303d578dda1ac41 Mon Sep 17 00:00:00 2001 From: Jumana B Date: Mon, 6 May 2024 09:37:06 -0400 Subject: [PATCH 6/7] Add alt text en/fr intake (#2162) * Add alt text en/fr intake * test fixes * fix * Util changes for alt text (#2165) * Util changes for alt text * update utils --- .github/workflows/test.yaml | 2 +- app/delivery/send_to_providers.py | 6 +++ app/email_branding/email_branding_schema.py | 6 ++- app/user/contact_request.py | 2 + app/user/rest.py | 2 + tests/app/delivery/test_send_to_providers.py | 13 ++++-- tests/app/email_branding/test_rest.py | 44 ++++++++++++++++---- tests/app/user/test_rest.py | 2 + 8 files changed, 63 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b38d18159d..fb65539f1f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,7 +67,7 @@ jobs: run: | cp -f .env.example .env - name: Checks for new endpoints against AWS WAF rules - uses: cds-snc/notification-utils/.github/actions/waffles@578b4147fe6c7a8f89241649b763f94ef24b2ad4 # 52.2.2 + uses: cds-snc/notification-utils/.github/actions/waffles@52.2.2 with: app-loc: '/github/workspace' app-libs: '/github/workspace/env/site-packages' diff --git a/app/delivery/send_to_providers.py b/app/delivery/send_to_providers.py index 33590c7667..ce24bc1131 100644 --- a/app/delivery/send_to_providers.py +++ b/app/delivery/send_to_providers.py @@ -353,12 +353,16 @@ def get_html_email_options(service: Service): "fip_banner_english": False, "fip_banner_french": True, "logo_with_background_colour": False, + "alt_text_en": None, + "alt_text_fr": None, } else: return { "fip_banner_english": True, "fip_banner_french": False, "logo_with_background_colour": False, + "alt_text_en": None, + "alt_text_fr": None, } logo_url = get_logo_url(service.email_branding.logo) if service.email_branding.logo else None @@ -371,6 +375,8 @@ def get_html_email_options(service: Service): "brand_logo": logo_url, "brand_text": service.email_branding.text, "brand_name": service.email_branding.name, + "alt_text_en": service.email_branding.alt_text_en, + "alt_text_fr": service.email_branding.alt_text_fr, } diff --git a/app/email_branding/email_branding_schema.py b/app/email_branding/email_branding_schema.py index b7070eafba..06366bb8c1 100644 --- a/app/email_branding/email_branding_schema.py +++ b/app/email_branding/email_branding_schema.py @@ -10,8 +10,10 @@ "text": {"type": ["string", "null"]}, "logo": {"type": ["string", "null"]}, "brand_type": {"enum": BRANDING_TYPES}, + "alt_text_en": {"type": "string"}, + "alt_text_fr": {"type": "string"}, }, - "required": ["name"], + "required": ["name", "alt_text_en", "alt_text_fr"], } post_update_email_branding_schema = { @@ -24,6 +26,8 @@ "text": {"type": ["string", "null"]}, "logo": {"type": ["string", "null"]}, "brand_type": {"enum": BRANDING_TYPES}, + "alt_text_en": {"type": "string"}, + "alt_text_fr": {"type": "string"}, }, "required": [], } diff --git a/app/user/contact_request.py b/app/user/contact_request.py index edb02ad956..74c5a96a35 100644 --- a/app/user/contact_request.py +++ b/app/user/contact_request.py @@ -31,6 +31,8 @@ class ContactRequest: notification_types: str = field(default="") expected_volume: str = field(default="") branding_url: str = field(default="") + alt_text_en: str = field(default="") + alt_text_fr: str = field(default="") def __post_init__(self): # email address is mandatory for us diff --git a/app/user/rest.py b/app/user/rest.py index f52893f252..a80e280548 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -500,6 +500,8 @@ def send_branding_request(user_id): organisation_id=data["organisation_id"], department_org_name=data["organisation_name"], branding_url=get_logo_url(data["filename"]), + alt_text_en=data["alt_text_en"], + alt_text_fr=data["alt_text_fr"], ) contact.tags = ["z_skip_opsgenie", "z_skip_urgent_escalation"] diff --git a/tests/app/delivery/test_send_to_providers.py b/tests/app/delivery/test_send_to_providers.py index 018df49123..ea91e1a503 100644 --- a/tests/app/delivery/test_send_to_providers.py +++ b/tests/app/delivery/test_send_to_providers.py @@ -596,19 +596,20 @@ def test_get_html_email_renderer_with_branding_details_and_render_fip_banner_eng sample_service.email_branding = None notify_db.session.add_all([sample_service]) notify_db.session.commit() - options = send_to_providers.get_html_email_options(sample_service) assert options == { "fip_banner_english": True, "fip_banner_french": False, "logo_with_background_colour": False, + "alt_text_en": None, + "alt_text_fr": None, } def test_get_html_email_renderer_prepends_logo_path(notify_api): Service = namedtuple("Service", ["email_branding"]) - EmailBranding = namedtuple("EmailBranding", ["brand_type", "colour", "name", "logo", "text"]) + EmailBranding = namedtuple("EmailBranding", ["brand_type", "colour", "name", "logo", "text", "alt_text_en", "alt_text_fr"]) email_branding = EmailBranding( brand_type=BRANDING_ORG_NEW, @@ -616,6 +617,8 @@ def test_get_html_email_renderer_prepends_logo_path(notify_api): logo="justice-league.png", name="Justice League", text="League of Justice", + alt_text_en="alt_text_en", + alt_text_fr="alt_text_fr", ) service = Service( email_branding=email_branding, @@ -628,7 +631,7 @@ def test_get_html_email_renderer_prepends_logo_path(notify_api): def test_get_html_email_renderer_handles_email_branding_without_logo(notify_api): Service = namedtuple("Service", ["email_branding"]) - EmailBranding = namedtuple("EmailBranding", ["brand_type", "colour", "name", "logo", "text"]) + EmailBranding = namedtuple("EmailBranding", ["brand_type", "colour", "name", "logo", "text", "alt_text_en", "alt_text_fr"]) email_branding = EmailBranding( brand_type=BRANDING_ORG_BANNER_NEW, @@ -636,6 +639,8 @@ def test_get_html_email_renderer_handles_email_branding_without_logo(notify_api) logo=None, name="Justice League", text="League of Justice", + alt_text_en="alt_text_en", + alt_text_fr="alt_text_fr", ) service = Service( email_branding=email_branding, @@ -649,6 +654,8 @@ def test_get_html_email_renderer_handles_email_branding_without_logo(notify_api) assert renderer["brand_text"] == "League of Justice" assert renderer["brand_colour"] == "#000000" assert renderer["brand_name"] == "Justice League" + assert renderer["alt_text_en"] == "alt_text_en" + assert renderer["alt_text_fr"] == "alt_text_fr" def test_should_not_update_notification_if_research_mode_on_exception(sample_service, sample_notification, mocker): diff --git a/tests/app/email_branding/test_rest.py b/tests/app/email_branding/test_rest.py index 5cee670554..f2a360a17e 100644 --- a/tests/app/email_branding/test_rest.py +++ b/tests/app/email_branding/test_rest.py @@ -78,6 +78,8 @@ def test_post_create_email_branding(admin_request, notify_db_session): "colour": "#0000ff", "logo": "/images/test_x2.png", "brand_type": BRANDING_ORG_NEW, + "alt_text_en": "hello world", + "alt_text_fr": "bonjour le monde", } response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=201) assert data["name"] == response["data"]["name"] @@ -85,6 +87,8 @@ def test_post_create_email_branding(admin_request, notify_db_session): assert data["logo"] == response["data"]["logo"] assert data["name"] == response["data"]["text"] assert data["brand_type"] == response["data"]["brand_type"] + assert data["alt_text_en"] == response["data"]["alt_text_en"] + assert data["alt_text_fr"] == response["data"]["alt_text_fr"] def test_post_create_email_branding_without_brand_type_defaults(admin_request, notify_db_session): @@ -92,16 +96,15 @@ def test_post_create_email_branding_without_brand_type_defaults(admin_request, n "name": "test email_branding", "colour": "#0000ff", "logo": "/images/test_x2.png", + "alt_text_en": "hello world", + "alt_text_fr": "bonjour le monde", } response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=201) assert BRANDING_ORG_NEW == response["data"]["brand_type"] def test_post_create_email_branding_without_logo_is_ok(admin_request, notify_db_session): - data = { - "name": "test email_branding", - "colour": "#0000ff", - } + data = {"name": "test email_branding", "colour": "#0000ff", "alt_text_en": "hello", "alt_text_fr": "bonjour"} response = admin_request.post( "email_branding.create_email_branding", _data=data, @@ -111,13 +114,15 @@ def test_post_create_email_branding_without_logo_is_ok(admin_request, notify_db_ def test_post_create_email_branding_colour_is_valid(admin_request, notify_db_session): - data = {"logo": "images/text_x2.png", "name": "test branding"} + data = {"logo": "images/text_x2.png", "name": "test branding", "alt_text_en": "hello", "alt_text_fr": "bonjour"} response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=201) assert response["data"]["logo"] == data["logo"] assert response["data"]["name"] == "test branding" assert response["data"]["colour"] is None assert response["data"]["text"] == "test branding" + assert response["data"]["alt_text_en"] == "hello" + assert response["data"]["alt_text_fr"] == "bonjour" def test_post_create_email_branding_with_text(admin_request, notify_db_session): @@ -125,6 +130,8 @@ def test_post_create_email_branding_with_text(admin_request, notify_db_session): "text": "text for brand", "logo": "images/text_x2.png", "name": "test branding", + "alt_text_en": "hello", + "alt_text_fr": "bonjour", } response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=201) @@ -132,6 +139,8 @@ def test_post_create_email_branding_with_text(admin_request, notify_db_session): assert response["data"]["name"] == "test branding" assert response["data"]["colour"] is None assert response["data"]["text"] == "text for brand" + assert response["data"]["alt_text_en"] == "hello" + assert response["data"]["alt_text_fr"] == "bonjour" def test_post_create_email_branding_with_text_and_name(admin_request, notify_db_session): @@ -139,6 +148,8 @@ def test_post_create_email_branding_with_text_and_name(admin_request, notify_db_ "name": "name for brand", "text": "text for brand", "logo": "images/text_x2.png", + "alt_text_en": "hello", + "alt_text_fr": "bonjour", } response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=201) @@ -146,20 +157,35 @@ def test_post_create_email_branding_with_text_and_name(admin_request, notify_db_ assert response["data"]["name"] == "name for brand" assert response["data"]["colour"] is None assert response["data"]["text"] == "text for brand" + assert response["data"]["alt_text_en"] == "hello" + assert response["data"]["alt_text_fr"] == "bonjour" def test_post_create_email_branding_with_text_as_none_and_name(admin_request, notify_db_session): - data = {"name": "name for brand", "text": None, "logo": "images/text_x2.png"} + data = { + "name": "name for brand", + "text": None, + "logo": "images/text_x2.png", + "alt_text_en": "hello", + "alt_text_fr": "bonjour", + } response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=201) assert response["data"]["logo"] == data["logo"] assert response["data"]["name"] == "name for brand" assert response["data"]["colour"] is None assert response["data"]["text"] is None + assert response["data"]["alt_text_en"] == "hello" + assert response["data"]["alt_text_fr"] == "bonjour" def test_post_create_email_branding_returns_400_when_name_is_missing(admin_request, notify_db_session): - data = {"text": "some text", "logo": "images/text_x2.png"} + data = { + "text": "some text", + "logo": "images/text_x2.png", + "alt_text_en": "hello", + "alt_text_fr": "bonjour", + } response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=400) assert response["errors"][0]["message"] == "name is a required property" @@ -176,7 +202,7 @@ def test_post_create_email_branding_returns_400_when_name_is_missing(admin_reque ], ) def test_post_update_email_branding_updates_field(admin_request, notify_db_session, data_update): - data = {"name": "test email_branding", "logo": "images/text_x2.png"} + data = {"name": "test email_branding", "logo": "images/text_x2.png", "alt_text_en": "hello", "alt_text_fr": "bonjour"} response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=201) email_branding_id = response["data"]["id"] @@ -205,7 +231,7 @@ def test_post_update_email_branding_updates_field(admin_request, notify_db_sessi ], ) def test_post_update_email_branding_updates_field_with_text(admin_request, notify_db_session, data_update): - data = {"name": "test email_branding", "logo": "images/text_x2.png"} + data = {"name": "test email_branding", "logo": "images/text_x2.png", "alt_text_en": "hello", "alt_text_fr": "bonjour"} response = admin_request.post("email_branding.create_email_branding", _data=data, _expected_status=201) email_branding_id = response["data"]["id"] diff --git a/tests/app/user/test_rest.py b/tests/app/user/test_rest.py index 8a480848bd..bdffcf4cdb 100644 --- a/tests/app/user/test_rest.py +++ b/tests/app/user/test_rest.py @@ -964,6 +964,8 @@ def test_send_branding_request(client, sample_service, sample_organisation, mock "organisation_id": str(sample_service.organisation.id), "organisation_name": sample_service.organisation.name, "filename": "branding_url", + "alt_text_en": "hello world", + "alt_text_fr": "bonjour", } mocked_freshdesk = mocker.patch("app.user.rest.Freshdesk.send_ticket", return_value=201) mocked_salesforce_client = mocker.patch("app.user.rest.salesforce_client") From 435c2d81a06c6bd2bc0305415a58de376ccc498b Mon Sep 17 00:00:00 2001 From: Steve Astels Date: Mon, 6 May 2024 11:51:01 -0400 Subject: [PATCH 7/7] local smoke tests (#1923) --- .../scripts/notify-dev-entrypoint.sh | 1 + Makefile | 4 ++++ tests_smoke/README.md | 14 +++++++++++++ tests_smoke/smoke/common.py | 8 +++---- tests_smoke/smoke/test_admin_csv.py | 19 ++++++++++------- tests_smoke/smoke/test_admin_one_off.py | 19 ++++++++++------- tests_smoke/smoke/test_api_bulk.py | 17 ++++++++------- tests_smoke/smoke/test_api_one_off.py | 20 ++++++++++-------- tests_smoke/smoke_test.py | 21 +++++++++++++------ 9 files changed, 80 insertions(+), 43 deletions(-) create mode 100644 tests_smoke/README.md diff --git a/.devcontainer/scripts/notify-dev-entrypoint.sh b/.devcontainer/scripts/notify-dev-entrypoint.sh index e2f99ea29a..a2d1fa10de 100755 --- a/.devcontainer/scripts/notify-dev-entrypoint.sh +++ b/.devcontainer/scripts/notify-dev-entrypoint.sh @@ -25,6 +25,7 @@ echo -e "complete -F __start_kubectl k" >> ~/.zshrc # Smoke test # requires adding files .env_staging and .env_prod to the root of the project +echo -e "alias smoke-local='cd /workspace && cp .env_smoke_local tests_smoke/.env && poetry run make smoke-test-local'" >> ~/.zshrc echo -e "alias smoke-staging='cd /workspace && cp .env_smoke_staging tests_smoke/.env && poetry run make smoke-test'" >> ~/.zshrc echo -e "alias smoke-prod='cd /workspace && cp .env_smoke_prod tests_smoke/.env && poetry run make smoke-test'" >> ~/.zshrc diff --git a/Makefile b/Makefile index debab9d61e..919458bf49 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,10 @@ format: smoke-test: cd tests_smoke && poetry run python smoke_test.py +.PHONY: smoke-test-local +smoke-test-local: + cd tests_smoke && poetry run python smoke_test.py --local --nofiles + .PHONY: run run: ## Run the web app poetry run flask run -p 6011 --host=0.0.0.0 diff --git a/tests_smoke/README.md b/tests_smoke/README.md new file mode 100644 index 0000000000..531de7879a --- /dev/null +++ b/tests_smoke/README.md @@ -0,0 +1,14 @@ +# Smoke Tests + +This repository contains a set of smoke tests for our application. Smoke testing, also known as "Build Verification Testing", is a type of software testing that comprises of a non-exhaustive set of tests that aim at ensuring that the most important functions work. The phrase 'smoke testing' comes from the hardware testing, where you plug in a new piece of hardware and turn it on for the first time. If it starts smoking, you know you have a problem. + +## Getting Started + +These smoke tests are designed to run in the api devcontainer. + +in the root of the repo create `.env` files for the environments you with to smoke test, for example `.env_smoke_local`, `.env_smoke_staging`, and `.env_smoke_prod`. For required values see the [.env.example](.env.example) file). + +## Running the tests + +in the devcontainer run the aliases `smoke-local`, `smoke-staging`, or `smoke-prod` to run the tests. + diff --git a/tests_smoke/smoke/common.py b/tests_smoke/smoke/common.py index 3b52669b7a..f248bc8926 100644 --- a/tests_smoke/smoke/common.py +++ b/tests_smoke/smoke/common.py @@ -2,8 +2,6 @@ import json import os import time - -# from notifications_utils.s3 import s3upload as utils_s3upload import urllib import uuid from enum import Enum @@ -41,6 +39,7 @@ class Config: EMAIL_TEMPLATE_ID = os.environ.get("SMOKE_EMAIL_TEMPLATE_ID") SMS_TEMPLATE_ID = os.environ.get("SMOKE_SMS_TEMPLATE_ID") API_KEY = os.environ.get("SMOKE_API_KEY", "") + JOB_SIZE = int(os.environ.get("SMOKE_JOB_SIZE", 2)) boto_session = Session( @@ -67,8 +66,8 @@ def rows_to_csv(rows: List[List[str]]): return output.getvalue() -def job_line(data: str, number_of_lines: int) -> Iterator[List[str]]: - return map(lambda n: [data, f"var{n}"], range(0, number_of_lines)) +def job_line(data: str, number_of_lines: int, prefix: str = "") -> Iterator[List[str]]: + return map(lambda n: [data, f"{prefix} {n}"], range(0, number_of_lines)) def pretty_print(data: Any): @@ -120,7 +119,6 @@ def job_succeeded(service_id: str, job_id: str) -> bool: return success -# from notifications_utils.s3 import s3upload as utils_s3upload def utils_s3upload(filedata, region, bucket_name, file_location, content_type="binary/octet-stream", tags=None): _s3 = boto_session.resource("s3") diff --git a/tests_smoke/smoke/test_admin_csv.py b/tests_smoke/smoke/test_admin_csv.py index fc2d035a0a..e6f962266f 100644 --- a/tests_smoke/smoke/test_admin_csv.py +++ b/tests_smoke/smoke/test_admin_csv.py @@ -13,13 +13,13 @@ ) -def test_admin_csv(notification_type: Notification_type): +def test_admin_csv(notification_type: Notification_type, local: bool = False): print(f"test_admin_csv ({notification_type.value})... ", end="", flush=True) if notification_type == Notification_type.EMAIL: - data = rows_to_csv([["email address", "var"], *job_line(Config.EMAIL_TO, 2)]) + data = rows_to_csv([["email address", "var"], *job_line(Config.EMAIL_TO, Config.JOB_SIZE, prefix="smoke test admin csv")]) else: - data = rows_to_csv([["phone number", "var"], *job_line(Config.SMS_TO, 2)]) + data = rows_to_csv([["phone number", "var"], *job_line(Config.SMS_TO, Config.JOB_SIZE, prefix="smoke test admin csv")]) upload_id = s3upload(Config.SERVICE_ID, data) metadata_kwargs = { @@ -42,8 +42,11 @@ def test_admin_csv(notification_type: Notification_type): print("FAILED: post to send_notification failed") exit(1) - success = job_succeeded(Config.SERVICE_ID, upload_id) - if not success: - print("FAILED: job didn't finish successfully") - exit(1) - print("Success") + if local: + print(f"Check manually for {Config.JOB_SIZE} {notification_type.value}s") + else: + success = job_succeeded(Config.SERVICE_ID, upload_id) + if not success: + print("FAILED: job didn't finish successfully") + exit(1) + print("Success") diff --git a/tests_smoke/smoke/test_admin_one_off.py b/tests_smoke/smoke/test_admin_one_off.py index 8d52ea55a6..faaee84c92 100644 --- a/tests_smoke/smoke/test_admin_one_off.py +++ b/tests_smoke/smoke/test_admin_one_off.py @@ -4,7 +4,7 @@ from .common import Config, Notification_type, pretty_print, single_succeeded -def test_admin_one_off(notification_type: Notification_type): +def test_admin_one_off(notification_type: Notification_type, local: bool = False): print(f"test_admin_one_off ({notification_type.value})... ", end="", flush=True) token = create_jwt_token(Config.ADMIN_CLIENT_SECRET, client_id=Config.ADMIN_CLIENT_USER_NAME) @@ -17,7 +17,7 @@ def test_admin_one_off(notification_type: Notification_type): "to": to, "template_id": template_id, "created_by": Config.USER_ID, - "personalisation": {"var": "var"}, + "personalisation": {"var": "smoke test admin one off"}, }, headers={"Authorization": f"Bearer {token}"}, ) @@ -28,9 +28,12 @@ def test_admin_one_off(notification_type: Notification_type): print("FAILED: post to send_notification failed") exit(1) - uri = f"{Config.API_HOST_NAME}/service/{Config.SERVICE_ID}/notifications/{body['id']}" - success = single_succeeded(uri, use_jwt=True) - if not success: - print("FAILED: job didn't finish successfully") - exit(1) - print("Success") + if local: + print(f"Check manually for 1 {notification_type.value}") + else: + uri = f"{Config.API_HOST_NAME}/service/{Config.SERVICE_ID}/notifications/{body['id']}" + success = single_succeeded(uri, use_jwt=True) + if not success: + print("FAILED: job didn't finish successfully") + exit(1) + print("Success") diff --git a/tests_smoke/smoke/test_api_bulk.py b/tests_smoke/smoke/test_api_bulk.py index e31455589d..91897770cf 100644 --- a/tests_smoke/smoke/test_api_bulk.py +++ b/tests_smoke/smoke/test_api_bulk.py @@ -12,7 +12,7 @@ ) -def test_api_bulk(notification_type: Notification_type): +def test_api_bulk(notification_type: Notification_type, local: bool = False): print(f"test_api_bulk ({notification_type.value})... ", end="", flush=True) template_id = Config.EMAIL_TEMPLATE_ID if notification_type == Notification_type.EMAIL else Config.SMS_TEMPLATE_ID to = Config.EMAIL_TO if notification_type == Notification_type.EMAIL else Config.SMS_TO @@ -23,7 +23,7 @@ def test_api_bulk(notification_type: Notification_type): json={ "name": f"My bulk name {datetime.utcnow().isoformat()}", "template_id": template_id, - "csv": rows_to_csv([[header, "var"], *job_line(to, 2)]), + "csv": rows_to_csv([[header, "var"], *job_line(to, Config.JOB_SIZE, prefix="smoke test api bulk")]), }, headers={"Authorization": f"ApiKey-v1 {Config.API_KEY}"}, ) @@ -32,8 +32,11 @@ def test_api_bulk(notification_type: Notification_type): print("FAILED: post failed") exit(1) - success = job_succeeded(Config.SERVICE_ID, response.json()["data"]["id"]) - if not success: - print("FAILED: job didn't finish successfully") - exit(1) - print("Success") + if local: + print(f"Check manually for {Config.JOB_SIZE} {notification_type.value}s") + else: + success = job_succeeded(Config.SERVICE_ID, response.json()["data"]["id"]) + if not success: + print("FAILED: job didn't finish successfully") + exit(1) + print("Success") diff --git a/tests_smoke/smoke/test_api_one_off.py b/tests_smoke/smoke/test_api_one_off.py index 643192bb64..d4ab8e470a 100644 --- a/tests_smoke/smoke/test_api_one_off.py +++ b/tests_smoke/smoke/test_api_one_off.py @@ -11,7 +11,7 @@ ) -def test_api_one_off(notification_type: Notification_type, attachment_type: Attachment_type = Attachment_type.NONE): +def test_api_one_off(notification_type: Notification_type, attachment_type: Attachment_type = Attachment_type.NONE, local: bool = False): if attachment_type is Attachment_type.NONE: print(f"test_api_oneoff ({notification_type.value})... ", end="", flush=True) else: @@ -51,7 +51,7 @@ def test_api_one_off(notification_type: Notification_type, attachment_type: Atta } else: data["personalisation"] = { - "var": "var", + "var": "smoke test api one off", } response = requests.post( @@ -64,10 +64,12 @@ def test_api_one_off(notification_type: Notification_type, attachment_type: Atta print(f"FAILED: post to v2/notifications/{notification_type.value} failed") exit(1) - uri = response.json()["uri"] - - success = single_succeeded(uri, use_jwt=False) - if not success: - print("FAILED: job didn't finish successfully") - exit(1) - print("Success") + if local: + print(f"Check manually for 1 {notification_type.value}") + else: + uri = response.json()["uri"] + success = single_succeeded(uri, use_jwt=False) + if not success: + print("FAILED: job didn't finish successfully") + exit(1) + print("Success") diff --git a/tests_smoke/smoke_test.py b/tests_smoke/smoke_test.py index 2b9fc2399b..ccde49ddef 100644 --- a/tests_smoke/smoke_test.py +++ b/tests_smoke/smoke_test.py @@ -1,3 +1,5 @@ +import argparse + from smoke.common import Attachment_type, Config, Notification_type # type: ignore from smoke.test_admin_csv import test_admin_csv # type: ignore from smoke.test_admin_one_off import test_admin_one_off # type: ignore @@ -5,15 +7,22 @@ from smoke.test_api_one_off import test_api_one_off # type: ignore if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-l", "--local", default=False, action='store_true', help="run locally, do not check for delivery success (default false)") + parser.add_argument("--nofiles", default=False, action='store_true', help="do not send files (default false)") + args = parser.parse_args() + print("API Smoke test\n") for key in ["API_HOST_NAME", "SERVICE_ID", "EMAIL_TEMPLATE_ID", "SMS_TEMPLATE_ID", "EMAIL_TO", "SMS_TO"]: print(f"{key:>17}: {Config.__dict__[key]}") print("") for notification_type in [Notification_type.EMAIL, Notification_type.SMS]: - test_admin_one_off(notification_type) - test_admin_csv(notification_type) - test_api_one_off(notification_type) - test_api_bulk(notification_type) - test_api_one_off(Notification_type.EMAIL, Attachment_type.ATTACHED) - test_api_one_off(Notification_type.EMAIL, Attachment_type.LINK) + test_admin_one_off(notification_type, local=args.local) + test_admin_csv(notification_type, local=args.local) + test_api_one_off(notification_type, local=args.local) + test_api_bulk(notification_type, local=args.local) + + if not args.nofiles: + test_api_one_off(Notification_type.EMAIL, attachment_type=Attachment_type.ATTACHED, local=args.local) + test_api_one_off(Notification_type.EMAIL, attachment_type=Attachment_type.LINK, local=args.local)