From 6e73e81201c4b01f0d3147f5c454d052a0acfb6b Mon Sep 17 00:00:00 2001
From: Kenneth Kehl <@kkehl@flexion.us>
Date: Wed, 2 Oct 2024 13:40:19 -0700
Subject: [PATCH] ugh fix tests
---
tests/app/delivery/test_send_to_providers.py | 1208 ++++++++++++++----
1 file changed, 946 insertions(+), 262 deletions(-)
diff --git a/tests/app/delivery/test_send_to_providers.py b/tests/app/delivery/test_send_to_providers.py
index 4c0c39890..20b0f7186 100644
--- a/tests/app/delivery/test_send_to_providers.py
+++ b/tests/app/delivery/test_send_to_providers.py
@@ -1,313 +1,997 @@
import json
-import os
-from contextlib import suppress
-from urllib import parse
+from collections import namedtuple
+from unittest.mock import ANY
import pytest
-from cachetools import TTLCache, cached
from flask import current_app
+from requests import HTTPError
-from app import (
- aws_pinpoint_client,
- create_uuid,
- db,
- notification_provider_clients,
- redis_store,
+import app
+from app import aws_sns_client, notification_provider_clients
+from app.cloudfoundry_config import cloud_config
+from app.dao import notifications_dao
+from app.dao.provider_details_dao import get_provider_details_by_identifier
+from app.delivery import send_to_providers
+from app.delivery.send_to_providers import (
+ _experimentally_validate_phone_numbers,
+ get_html_email_options,
+ get_logo_url,
)
-from app.aws.s3 import get_personalisation_from_s3, get_phone_number_from_s3
-from app.celery.test_key_tasks import send_email_response, send_sms_response
-from app.dao.email_branding_dao import dao_get_email_branding_by_id
-from app.dao.notifications_dao import dao_update_notification
-from app.dao.provider_details_dao import get_provider_details_by_notification_type
-from app.dao.service_sms_sender_dao import dao_get_sms_senders_by_service_id
-from app.delivery.send_to_providers import _experimentally_validate_phone_numbers
from app.enums import BrandType, KeyType, NotificationStatus, NotificationType
from app.exceptions import NotificationTechnicalFailureException
-from app.serialised_models import SerialisedService, SerialisedTemplate
-from app.utils import hilite, utc_now
-from notifications_utils.template import (
- HTMLEmailTemplate,
- PlainTextEmailTemplate,
- SMSMessageTemplate,
+from app.models import EmailBranding, Notification
+from app.serialised_models import SerialisedService
+from app.utils import utc_now
+from tests.app.db import (
+ create_email_branding,
+ create_notification,
+ create_reply_to_email,
+ create_service,
+ create_service_sms_sender,
+ create_service_with_defined_sms_sender,
+ create_template,
)
-def send_sms_to_provider(notification):
- """Final step in the message send flow.
-
- Get data for recipient, template,
- notification and send it to sns.
- """
- # we no longer store the personalisation in the db,
- # need to retrieve from s3 before generating content
- # However, we are still sending the initial verify code through personalisation
- # so if there is some value there, don't overwrite it
- if not notification.personalisation:
- personalisation = get_personalisation_from_s3(
- notification.service_id,
- notification.job_id,
- notification.job_row_number,
- )
- notification.personalisation = personalisation
-
- service = SerialisedService.from_id(notification.service_id)
- message_id = None
- if not service.active:
- technical_failure(notification=notification)
- return
-
- if notification.status == NotificationStatus.CREATED:
- # We get the provider here (which is only aws sns)
- provider = provider_to_use(NotificationType.SMS, notification.international)
- if not provider:
- technical_failure(notification=notification)
- return
-
- template_model = SerialisedTemplate.from_id_and_service_id(
- template_id=notification.template_id,
- service_id=service.id,
- version=notification.template_version,
- )
-
- template = SMSMessageTemplate(
- template_model.__dict__,
- values=notification.personalisation,
- prefix=service.name,
- show_prefix=service.prefix_sms,
- )
- if notification.key_type == KeyType.TEST:
- update_notification_to_sending(notification, provider)
- send_sms_response(provider.name, str(notification.id))
-
- else:
- try:
- # End DB session here so that we don't have a connection stuck open waiting on the call
- # to one of the SMS providers
- # We don't want to tie our DB connections being open to the performance of our SMS
- # providers as a slow down of our providers can cause us to run out of DB connections
- # Therefore we pull all the data from our DB models into `send_sms_kwargs`now before
- # closing the session (as otherwise it would be reopened immediately)
-
- # We start by trying to get the phone number from a job in s3. If we fail, we assume
- # the phone number is for the verification code on login, which is not a job.
- recipient = None
- # It is our 2facode, maybe
- recipient = _get_verify_code(notification)
-
- if recipient is None:
- recipient = get_phone_number_from_s3(
- notification.service_id,
- notification.job_id,
- notification.job_row_number,
- )
-
- # TODO This is temporary to test the capability of validating phone numbers
- # The future home of the validation is TBD
- if "+" not in recipient:
- recipient_lookup = f"+{recipient}"
- else:
- recipient_lookup = recipient
- if recipient_lookup in current_app.config[
- "SIMULATED_SMS_NUMBERS"
- ] and os.getenv("NOTIFY_ENVIRONMENT") in ["development", "test"]:
- current_app.logger.info(hilite("#validate-phone-number fired"))
- aws_pinpoint_client.validate_phone_number("01", recipient)
- else:
- current_app.logger.info(hilite("#validate-phone-number not fired"))
-
- sender_numbers = get_sender_numbers(notification)
- if notification.reply_to_text not in sender_numbers:
- raise ValueError(
- f"{notification.reply_to_text} not in {sender_numbers} #notify-admin-1701"
- )
-
- send_sms_kwargs = {
- "to": recipient,
- "content": str(template),
- "reference": str(notification.id),
- "sender": notification.reply_to_text,
- "international": notification.international,
- }
- db.session.close() # no commit needed as no changes to objects have been made above
-
- message_id = provider.send_sms(**send_sms_kwargs)
- current_app.logger.info(f"got message_id {message_id}")
- except Exception as e:
- n = notification
- msg = f"FAILED send to sms, job_id: {n.job_id} row_number {n.job_row_number} message_id {message_id}"
- current_app.logger.exception(hilite(msg))
-
- notification.billable_units = template.fragment_count
- dao_update_notification(notification)
- raise e
- else:
- # Here we map the job_id and row number to the aws message_id
- n = notification
- msg = f"Send to aws for job_id {n.job_id} row_number {n.job_row_number} message_id {message_id}"
- current_app.logger.info(hilite(msg))
- notification.billable_units = template.fragment_count
- update_notification_to_sending(notification, provider)
- return message_id
+def setup_function(_function):
+ # pytest will run this function before each test. It makes sure the
+ # state of the cache is not shared between tests.
+ send_to_providers.provider_cache.clear()
-def _get_verify_code(notification):
- key = f"2facode-{notification.id}".replace(" ", "")
- recipient = redis_store.get(key)
- with suppress(AttributeError):
- recipient = recipient.decode("utf-8")
- return recipient
+@pytest.mark.parametrize(
+ "international_provider_priority",
+ (
+ # Since there’s only one international provider it should always
+ # be used, no matter what its priority is set to
+ 0,
+ 50,
+ 100,
+ ),
+)
+def test_provider_to_use_should_only_return_sns_for_international(
+ mocker,
+ notify_db_session,
+ international_provider_priority,
+):
+ sns = get_provider_details_by_identifier("sns")
+ sns.priority = international_provider_priority
+
+ ret = send_to_providers.provider_to_use(NotificationType.SMS, international=True)
+
+ assert ret.name == "sns"
-def get_sender_numbers(notification):
- possible_senders = dao_get_sms_senders_by_service_id(notification.service_id)
- sender_numbers = []
- for possible_sender in possible_senders:
- sender_numbers.append(possible_sender.sms_sender)
- return sender_numbers
+def test_provider_to_use_raises_if_no_active_providers(
+ mocker, restore_provider_details
+):
+ sns = get_provider_details_by_identifier("sns")
+ sns.active = False
+
+ # flake8 doesn't like raises with a generic exception
+ try:
+ send_to_providers.provider_to_use(NotificationType.SMS)
+ assert 1 == 0
+ except Exception:
+ assert 1 == 1
-def send_email_to_provider(notification):
- # Someone needs an email, possibly new registration
- recipient = redis_store.get(f"email-address-{notification.id}")
- recipient = recipient.decode("utf-8")
- personalisation = redis_store.get(f"email-personalisation-{notification.id}")
- if personalisation:
- personalisation = personalisation.decode("utf-8")
- notification.personalisation = json.loads(personalisation)
+def test_should_send_personalised_template_to_correct_sms_provider_and_persist(
+ sample_sms_template_with_html, mocker
+):
- service = SerialisedService.from_id(notification.service_id)
- if not service.active:
- technical_failure(notification=notification)
- return
- if notification.status == NotificationStatus.CREATED:
- provider = provider_to_use(NotificationType.EMAIL, False)
- template_dict = SerialisedTemplate.from_id_and_service_id(
- template_id=notification.template_id,
- service_id=service.id,
- version=notification.template_version,
- ).__dict__
+ mocker.patch("app.delivery.send_to_providers._get_verify_code", return_value=None)
+ db_notification = create_notification(
+ template=sample_sms_template_with_html,
+ personalisation={},
+ status=NotificationStatus.CREATED,
+ reply_to_text=sample_sms_template_with_html.service.get_default_sms_sender(),
+ )
- html_email = HTMLEmailTemplate(
- template_dict,
- values=notification.personalisation,
- **get_html_email_options(service),
- )
+ mocker.patch("app.aws_sns_client.send_sms")
- plain_text_email = PlainTextEmailTemplate(
- template_dict, values=notification.personalisation
- )
+ mock_s3 = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_s3.return_value = "2028675309"
- if notification.key_type == KeyType.TEST:
- notification.reference = str(create_uuid())
- update_notification_to_sending(notification, provider)
- send_email_response(notification.reference, recipient)
- else:
- from_address = (
- f'"{service.name}" <{service.email_from}@'
- f'{current_app.config["NOTIFY_EMAIL_DOMAIN"]}>'
- )
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"name": "Jo"}
- reference = provider.send_email(
- from_address,
- recipient,
- plain_text_email.subject,
- body=str(plain_text_email),
- html_body=str(html_email),
- reply_to_address=notification.reply_to_text,
- )
- notification.reference = reference
- update_notification_to_sending(notification, provider)
+ send_to_providers.send_sms_to_provider(db_notification)
+
+ aws_sns_client.send_sms.assert_called_once_with(
+ to="2028675309",
+ content="Sample service: Hello Jo\nHere is some HTML & entities",
+ reference=str(db_notification.id),
+ sender=current_app.config["FROM_NUMBER"],
+ international=False,
+ )
+
+ notification = Notification.query.filter_by(id=db_notification.id).one()
+
+ assert notification.status == NotificationStatus.SENDING
+ assert notification.sent_at <= utc_now()
+ assert notification.sent_by == "sns"
+ assert notification.billable_units == 1
+ assert notification.personalisation == {"name": "Jo"}
-def update_notification_to_sending(notification, provider):
- notification.sent_at = utc_now()
- notification.sent_by = provider.name
- if notification.status not in NotificationStatus.completed_types():
- notification.status = NotificationStatus.SENDING
+def test_should_send_personalised_template_to_correct_email_provider_and_persist(
+ sample_email_template_with_html, mocker
+):
- dao_update_notification(notification)
+ mock_redis = mocker.patch("app.delivery.send_to_providers.redis_store")
+ utf8_encoded_email = "jo.smith@example.com".encode("utf-8")
+ mock_redis.get.return_value = utf8_encoded_email
+ email = utf8_encoded_email
+ personalisation = {
+ "name": "Jo",
+ }
+ personalisation = json.dumps(personalisation)
+ personalisation = personalisation.encode("utf-8")
+ mock_redis.get.side_effect = [email, personalisation]
+ db_notification = create_notification(
+ template=sample_email_template_with_html,
+ )
+ db_notification.personalisation = {"name": "Jo"}
+ mocker.patch("app.aws_ses_client.send_email", return_value="reference")
+ send_to_providers.send_email_to_provider(db_notification)
+ app.aws_ses_client.send_email.assert_called_once_with(
+ f'"Sample service" ',
+ "jo.smith@example.com",
+ "Jo some HTML",
+ body="Hello Jo\nThis is an email from GOV.\u200bUK with some HTML\n",
+ html_body=ANY,
+ reply_to_address=None,
+ )
+
+ assert " version_on_notification
+
+ send_to_providers.send_sms_to_provider(db_notification)
+
+ aws_sns_client.send_sms.assert_called_once_with(
+ to="2028675309",
+ content="Sample service: This is a template:\nwith a newline",
+ reference=str(db_notification.id),
+ sender=current_app.config["FROM_NUMBER"],
+ international=False,
+ )
+
+ t = dao_get_template_by_id(expected_template_id)
+
+ persisted_notification = notifications_dao.get_notification_by_id(
+ db_notification.id
+ )
+ assert persisted_notification.to == db_notification.to
+ assert persisted_notification.template_id == expected_template_id
+ assert persisted_notification.template_version == version_on_notification
+ assert persisted_notification.template_version != t.version
+ assert persisted_notification.status == NotificationStatus.SENDING
+
+
+def test_should_have_sending_status_if_fake_callback_function_fails(
+ sample_notification, mocker
+):
+ mocker.patch(
+ "app.delivery.send_to_providers.send_sms_response",
+ side_effect=HTTPError,
+ )
+
+ mock_s3 = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_s3.return_value = "2028675309"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+
+ sample_notification.key_type = KeyType.TEST
+ with pytest.raises(HTTPError):
+ send_to_providers.send_sms_to_provider(sample_notification)
+ assert sample_notification.status == NotificationStatus.SENDING
+ assert sample_notification.sent_by == "sns"
+
+
+def test_should_not_send_to_provider_when_status_is_not_created(
+ sample_template, mocker
+):
+ notification = create_notification(
+ template=sample_template,
+ status=NotificationStatus.SENDING,
+ )
+ mocker.patch("app.aws_sns_client.send_sms")
+ response_mock = mocker.patch("app.delivery.send_to_providers.send_sms_response")
+
+ mock_s3 = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_s3.return_value = "2028675309"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+
+ send_to_providers.send_sms_to_provider(notification)
+
+ app.aws_sns_client.send_sms.assert_not_called()
+ response_mock.assert_not_called()
+
+
+def test_should_send_sms_with_downgraded_content(notify_db_session, mocker):
+ # é, o, and u are in GSM.
+ # ī, grapes, tabs, zero width space and ellipsis are not
+ # ó isn't in GSM, but it is in the welsh alphabet so will still be sent
+
+ mocker.patch("app.delivery.send_to_providers.redis_store", return_value=None)
+ mocker.patch(
+ "app.delivery.send_to_providers.get_sender_numbers", return_value=["testing"]
+ )
+ msg = "a é ī o u 🍇 foo\tbar\u200bbaz((misc))…"
+ placeholder = "∆∆∆abc"
+ gsm_message = "?ódz Housing Service: a é i o u ? foo barbaz???abc..."
+ service = create_service(service_name="Łódź Housing Service")
+ template = create_template(service, content=msg)
+ db_notification = create_notification(
+ template=template,
+ )
+ db_notification.personalisation = {"misc": placeholder}
+ db_notification.reply_to_text = "testing"
+
+ mocker.patch("app.aws_sns_client.send_sms")
+
+ mock_phone = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_phone.return_value = "15555555555"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"misc": placeholder}
+
+ send_to_providers.send_sms_to_provider(db_notification)
+
+ aws_sns_client.send_sms.assert_called_once_with(
+ to=ANY, content=gsm_message, reference=ANY, sender=ANY, international=False
+ )
+
+
+def test_send_sms_should_use_service_sms_sender(
+ sample_service, sample_template, mocker
+):
+
+ mocker.patch("app.delivery.send_to_providers.redis_store", return_value=None)
+ mocker.patch("app.aws_sns_client.send_sms")
+
+ sms_sender = create_service_sms_sender(
+ service=sample_service, sms_sender="123456", is_default=False
+ )
+ db_notification = create_notification(
+ template=sample_template, reply_to_text=sms_sender.sms_sender
+ )
+ expected_sender_name = sms_sender.sms_sender
+ mock_phone = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_phone.return_value = "15555555555"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+
+ send_to_providers.send_sms_to_provider(
+ db_notification,
+ )
+
+ app.aws_sns_client.send_sms.assert_called_once_with(
+ to=ANY,
+ content=ANY,
+ reference=ANY,
+ sender=expected_sender_name,
+ international=False,
+ )
+
+
+def test_send_email_to_provider_should_not_send_to_provider_when_status_is_not_created(
+ sample_email_template, mocker
+):
+ mock_redis = mocker.patch("app.delivery.send_to_providers.redis_store")
+ mock_redis.get.return_value = "test@example.com".encode("utf-8")
+
+ notification = create_notification(
+ template=sample_email_template, status=NotificationStatus.SENDING
+ )
+ mocker.patch("app.aws_ses_client.send_email")
+ mocker.patch("app.delivery.send_to_providers.send_email_response")
+ mock_phone = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_phone.return_value = "15555555555"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+ send_to_providers.send_sms_to_provider(notification)
+ app.aws_ses_client.send_email.assert_not_called()
+ app.delivery.send_to_providers.send_email_response.assert_not_called()
+
+
+def test_send_email_should_use_service_reply_to_email(
+ sample_service, sample_email_template, mocker
+):
+ mocker.patch("app.aws_ses_client.send_email", return_value="reference")
+
+ mock_redis = mocker.patch("app.delivery.send_to_providers.redis_store")
+ mock_redis.get.return_value = "test@example.com".encode("utf-8")
+
+ mock_redis = mocker.patch("app.delivery.send_to_providers.redis_store")
+ email = "foo@bar.com".encode("utf-8")
+ personalisation = {}
+
+ personalisation = json.dumps(personalisation)
+ personalisation = personalisation.encode("utf-8")
+ mock_redis.get.side_effect = [email, personalisation]
+
+ db_notification = create_notification(
+ template=sample_email_template, reply_to_text="foo@bar.com"
+ )
+ create_reply_to_email(service=sample_service, email_address="foo@bar.com")
+
+ send_to_providers.send_email_to_provider(db_notification)
+
+ app.aws_ses_client.send_email.assert_called_once_with(
+ ANY,
+ ANY,
+ ANY,
+ body=ANY,
+ html_body=ANY,
+ reply_to_address="foo@bar.com",
+ )
+
+
+def test_get_html_email_renderer_should_return_for_normal_service(sample_service):
+ options = send_to_providers.get_html_email_options(sample_service)
+ assert options["govuk_banner"] is True
+ assert "brand_colour" not in options.keys()
+ assert "brand_logo" not in options.keys()
+ assert "brand_text" not in options.keys()
+ assert "brand_name" not in options.keys()
+
+
+@pytest.mark.parametrize(
+ "branding_type, govuk_banner",
+ [(BrandType.ORG, False), (BrandType.BOTH, True), (BrandType.ORG_BANNER, False)],
+)
+def test_get_html_email_renderer_with_branding_details(
+ branding_type, govuk_banner, notify_db_session, sample_service
+):
+ email_branding = EmailBranding(
+ brand_type=branding_type,
+ colour="#000000",
+ logo="justice-league.png",
+ name="Justice League",
+ text="League of Justice",
+ )
+ sample_service.email_branding = email_branding
+ notify_db_session.add_all([sample_service, email_branding])
+ notify_db_session.commit()
+
+ options = send_to_providers.get_html_email_options(sample_service)
+
+ assert options["govuk_banner"] == govuk_banner
+ assert options["brand_colour"] == "#000000"
+ assert options["brand_text"] == "League of Justice"
+ assert options["brand_name"] == "Justice League"
+
+ if branding_type == BrandType.ORG_BANNER:
+ assert options["brand_banner"] is True
+ else:
+ assert options["brand_banner"] is False
+
+
+def test_get_html_email_renderer_with_branding_details_and_render_govuk_banner_only(
+ notify_db_session, sample_service
+):
+ 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 == {"govuk_banner": True, "brand_banner": False}
+
+
+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"],
+ )
+
+ email_branding = EmailBranding(
+ brand_type=BrandType.ORG,
+ colour="#000000",
+ logo="justice-league.png",
+ name="Justice League",
+ text="League of Justice",
+ )
+ service = Service(
+ email_branding=email_branding,
+ )
+
+ renderer = send_to_providers.get_html_email_options(service)
+
+ assert (
+ renderer["brand_logo"] == "http://static-logos.notify.tools/justice-league.png"
+ )
+
+
+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"],
+ )
+
+ email_branding = EmailBranding(
+ brand_type=BrandType.ORG_BANNER,
+ colour="#000000",
+ logo=None,
+ name="Justice League",
+ text="League of Justice",
+ )
+ service = Service(
+ email_branding=email_branding,
+ )
+
+ renderer = send_to_providers.get_html_email_options(service)
+
+ assert renderer["govuk_banner"] is False
+ assert renderer["brand_banner"] is True
+ assert renderer["brand_logo"] is None
+ assert renderer["brand_text"] == "League of Justice"
+ assert renderer["brand_colour"] == "#000000"
+ assert renderer["brand_name"] == "Justice League"
+
+
+@pytest.mark.parametrize(
+ "base_url, expected_url",
+ [
+ # don't change localhost to prevent errors when testing locally
+ ("http://localhost:6012", "http://static-logos.notify.tools/filename.png"),
+ (
+ "https://www.notifications.service.gov.uk",
+ "https://static-logos.notifications.service.gov.uk/filename.png",
+ ),
+ ("https://notify.works", "https://static-logos.notify.works/filename.png"),
+ (
+ "https://staging-notify.works",
+ "https://static-logos.staging-notify.works/filename.png",
+ ),
+ ("https://www.notify.works", "https://static-logos.notify.works/filename.png"),
+ (
+ "https://www.staging-notify.works",
+ "https://static-logos.staging-notify.works/filename.png",
+ ),
+ ],
+)
+def test_get_logo_url_works_for_different_environments(base_url, expected_url):
+ logo_file = "filename.png"
+
+ logo_url = send_to_providers.get_logo_url(base_url, logo_file)
+
+ assert logo_url == expected_url
+
+
+@pytest.mark.parametrize(
+ "starting_status, expected_status",
+ [
+ (NotificationStatus.DELIVERED, NotificationStatus.DELIVERED),
+ (NotificationStatus.CREATED, NotificationStatus.SENDING),
+ (NotificationStatus.TECHNICAL_FAILURE, NotificationStatus.TECHNICAL_FAILURE),
+ ],
+)
+def test_update_notification_to_sending_does_not_update_status_from_a_final_status(
+ sample_service, notify_db_session, starting_status, expected_status
+):
+ template = create_template(sample_service)
+ notification = create_notification(template=template, status=starting_status)
+ send_to_providers.update_notification_to_sending(
+ notification,
+ notification_provider_clients.get_client_by_name_and_type(
+ "sns", NotificationType.SMS
+ ),
+ )
+ assert notification.status == expected_status
+
+
+def __update_notification(notification_to_update, research_mode, expected_status):
+ if research_mode or notification_to_update.key_type == KeyType.TEST:
+ notification_to_update.status = expected_status
+
+
+@pytest.mark.parametrize(
+ "research_mode,key_type, billable_units, expected_status",
+ [
+ (True, KeyType.NORMAL, 0, NotificationStatus.DELIVERED),
+ (False, KeyType.NORMAL, 1, NotificationStatus.SENDING),
+ (False, KeyType.TEST, 0, NotificationStatus.SENDING),
+ (True, KeyType.TEST, 0, NotificationStatus.SENDING),
+ (True, KeyType.TEAM, 0, NotificationStatus.DELIVERED),
+ (False, KeyType.TEAM, 1, NotificationStatus.SENDING),
+ ],
+)
+def test_should_update_billable_units_and_status_according_to_research_mode_and_key_type(
+ sample_template, mocker, research_mode, key_type, billable_units, expected_status
+):
+
+ mocker.patch("app.delivery.send_to_providers.redis_store", return_value=None)
+ mocker.patch(
+ "app.delivery.send_to_providers.get_sender_numbers", return_value=["testing"]
+ )
+ notification = create_notification(
+ template=sample_template,
+ billable_units=0,
+ status=NotificationStatus.CREATED,
+ key_type=key_type,
+ reply_to_text="testing",
+ )
+ mocker.patch("app.aws_sns_client.send_sms")
+ mocker.patch(
+ "app.delivery.send_to_providers.send_sms_response",
+ side_effect=__update_notification(notification, research_mode, expected_status),
+ )
+
+ if research_mode:
+ sample_template.service.research_mode = True
+
+ mock_phone = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_phone.return_value = "15555555555"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ # So we don't treat it as a one off and have to mock other things
+ mock_personalisation.return_value = {"ignore": "ignore"}
+
+ send_to_providers.send_sms_to_provider(notification)
+ assert notification.billable_units == billable_units
+ assert notification.status == expected_status
+
+
+def test_should_set_notification_billable_units_and_reduces_provider_priority_if_sending_to_provider_fails(
+ sample_notification,
+ mocker,
+):
+ mocker.patch("app.aws_sns_client.send_sms", side_effect=Exception())
+
+ sample_notification.billable_units = 0
+ assert sample_notification.sent_by is None
+
+ mock_phone = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_phone.return_value = "15555555555"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+
+ # flake8 no longer likes raises with a generic exception
+ try:
+ send_to_providers.send_sms_to_provider(sample_notification)
+ assert 1 == 0
+ except Exception:
+ assert 1 == 1
+
+ assert sample_notification.billable_units == 1
+
+
+def test_should_send_sms_to_international_providers(
+ sample_template, sample_user, mocker
+):
+
+ mocker.patch("app.delivery.send_to_providers._get_verify_code", return_value=None)
+ mocker.patch("app.aws_sns_client.send_sms")
+
+ notification_international = create_notification(
+ template=sample_template,
+ to_field="+6011-17224412",
+ personalisation={"name": "Jo"},
+ status=NotificationStatus.CREATED,
+ international=True,
+ reply_to_text=sample_template.service.get_default_sms_sender(),
+ normalised_to="601117224412",
+ )
+
+ mock_s3 = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_s3.return_value = "601117224412"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+
+ send_to_providers.send_sms_to_provider(notification_international)
+
+ aws_sns_client.send_sms.assert_called_once_with(
+ to="601117224412",
+ content=ANY,
+ reference=str(notification_international.id),
+ sender=current_app.config["FROM_NUMBER"],
+ international=True,
+ )
+
+ assert notification_international.status == NotificationStatus.SENDING
+ assert notification_international.sent_by == "sns"
+
+
+@pytest.mark.parametrize(
+ "sms_sender, expected_sender, prefix_sms, expected_content",
+ [
+ ("foo", "foo", False, "bar"),
+ ("foo", "foo", True, "Sample service: bar"),
+ # if 40604 is actually in DB then treat that as if entered manually
+ ("40604", "40604", False, "bar"),
+ # 'testing' is the FROM_NUMBER during unit tests
+ ("testing", "testing", True, "Sample service: bar"),
+ ("testing", "testing", False, "bar"),
+ ],
+)
+def test_should_handle_sms_sender_and_prefix_message(
+ mocker, sms_sender, prefix_sms, expected_sender, expected_content, notify_db_session
+):
+
+ mocker.patch("app.delivery.send_to_providers.redis_store", return_value=None)
+ mocker.patch("app.aws_sns_client.send_sms")
+ service = create_service_with_defined_sms_sender(
+ sms_sender_value=sms_sender, prefix_sms=prefix_sms
+ )
+ template = create_template(service, content="bar")
+ notification = create_notification(template, reply_to_text=sms_sender)
+
+ mock_phone = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_phone.return_value = "15555555555"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+
+ send_to_providers.send_sms_to_provider(notification)
+
+ aws_sns_client.send_sms.assert_called_once_with(
+ content=expected_content,
+ sender=expected_sender,
+ to=ANY,
+ reference=ANY,
+ international=False,
+ )
+
+
+def test_send_email_to_provider_uses_reply_to_from_notification(
+ sample_email_template, mocker
+):
+ mock_redis = mocker.patch("app.delivery.send_to_providers.redis_store")
+ mock_redis.get.side_effect = [
+ "test@example.com".encode("utf-8"),
+ json.dumps({}).encode("utf-8"),
]
- if not active_providers:
- current_app.logger.error(f"{notification_type} failed as no active providers")
- raise Exception(f"No active {notification_type} providers")
+ mocker.patch("app.aws_ses_client.send_email", return_value="reference")
- # we only have sns
- chosen_provider = active_providers[0]
+ db_notification = create_notification(
+ template=sample_email_template,
+ reply_to_text="test@test.com",
+ )
- return notification_provider_clients.get_client_by_name_and_type(
- chosen_provider.identifier, notification_type
+ send_to_providers.send_email_to_provider(db_notification)
+
+ app.aws_ses_client.send_email.assert_called_once_with(
+ ANY,
+ ANY,
+ ANY,
+ body=ANY,
+ html_body=ANY,
+ reply_to_address="test@test.com",
)
-def get_logo_url(base_url, logo_file):
- base_url = parse.urlparse(base_url)
- netloc = base_url.netloc
+def test_send_sms_to_provider_should_use_normalised_to(mocker, client, sample_template):
- if base_url.netloc.startswith("localhost"):
- netloc = "notify.tools"
- elif base_url.netloc.startswith("www"):
- # strip "www."
- netloc = base_url.netloc[4:]
-
- logo_url = parse.ParseResult(
- scheme=base_url.scheme,
- netloc="static-logos." + netloc,
- path=logo_file,
- params=base_url.params,
- query=base_url.query,
- fragment=base_url.fragment,
+ mocker.patch("app.delivery.send_to_providers._get_verify_code", return_value=None)
+ mocker.patch(
+ "app.delivery.send_to_providers.get_sender_numbers", return_value=["testing"]
)
- return parse.urlunparse(logo_url)
-
-
-def get_html_email_options(service):
- if service.email_branding is None:
- return {
- "govuk_banner": True,
- "brand_banner": False,
- }
- if isinstance(service, SerialisedService):
- branding = dao_get_email_branding_by_id(service.email_branding)
- else:
- branding = service.email_branding
-
- logo_url = (
- get_logo_url(current_app.config["ADMIN_BASE_URL"], branding.logo)
- if branding.logo
- else None
+ send_mock = mocker.patch("app.aws_sns_client.send_sms")
+ notification = create_notification(
+ template=sample_template,
+ to_field="+12028675309",
+ normalised_to="2028675309",
+ reply_to_text="testing",
)
- return {
+ mock_s3 = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_s3.return_value = "12028675309"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+ send_to_providers.send_sms_to_provider(notification)
+ send_mock.assert_called_once_with(
+ to="12028675309",
+ content=ANY,
+ reference=str(notification.id),
+ sender=notification.reply_to_text,
+ international=False,
+ )
+
+
+def test_send_email_to_provider_should_user_normalised_to(
+ mocker, client, sample_email_template
+):
+ send_mock = mocker.patch("app.aws_ses_client.send_email", return_value="reference")
+ notification = create_notification(
+ template=sample_email_template,
+ )
+ mock_redis = mocker.patch("app.delivery.send_to_providers.redis_store")
+ mock_redis.get.return_value = "test@example.com".encode("utf-8")
+
+ mock_redis = mocker.patch("app.delivery.send_to_providers.redis_store")
+ mock_redis.get.return_value = "jo.smith@example.com".encode("utf-8")
+ email = "test@example.com".encode("utf-8")
+ personalisation = {}
+
+ personalisation = json.dumps(personalisation)
+ personalisation = personalisation.encode("utf-8")
+ mock_redis.get.side_effect = [email, personalisation]
+
+ send_to_providers.send_email_to_provider(notification)
+ send_mock.assert_called_once_with(
+ ANY,
+ "test@example.com",
+ ANY,
+ body=ANY,
+ html_body=ANY,
+ reply_to_address=notification.reply_to_text,
+ )
+
+
+def test_send_sms_to_provider_should_return_template_if_found_in_redis(
+ mocker, client, sample_template
+):
+
+ mocker.patch("app.delivery.send_to_providers._get_verify_code", return_value=None)
+ mocker.patch(
+ "app.delivery.send_to_providers.get_sender_numbers", return_value=["testing"]
+ )
+ from app.schemas import service_schema, template_schema
+
+ service_dict = service_schema.dump(sample_template.service)
+ template_dict = template_schema.dump(sample_template)
+
+ mocker.patch(
+ "app.redis_store.get",
+ side_effect=[
+ json.dumps({"data": service_dict}).encode("utf-8"),
+ json.dumps({"data": template_dict}).encode("utf-8"),
+ ],
+ )
+ mock_get_template = mocker.patch(
+ "app.dao.templates_dao.dao_get_template_by_id_and_service_id"
+ )
+ mock_get_service = mocker.patch("app.dao.services_dao.dao_fetch_service_by_id")
+
+ send_mock = mocker.patch("app.aws_sns_client.send_sms")
+ notification = create_notification(
+ template=sample_template,
+ to_field="+447700900855",
+ normalised_to="447700900855",
+ reply_to_text="testing",
+ )
+
+ mock_s3 = mocker.patch("app.delivery.send_to_providers.get_phone_number_from_s3")
+ mock_s3.return_value = "447700900855"
+
+ mock_personalisation = mocker.patch(
+ "app.delivery.send_to_providers.get_personalisation_from_s3"
+ )
+ mock_personalisation.return_value = {"ignore": "ignore"}
+
+ send_to_providers.send_sms_to_provider(notification)
+ assert mock_get_template.called is False
+ assert mock_get_service.called is False
+ send_mock.assert_called_once_with(
+ to="447700900855",
+ content=ANY,
+ reference=str(notification.id),
+ sender=notification.reply_to_text,
+ international=False,
+ )
+
+
+def test_send_email_to_provider_should_return_template_if_found_in_redis(
+ mocker, client, sample_email_template
+):
+ from app.schemas import service_schema, template_schema
+
+ # mock_redis = mocker.patch("app.delivery.send_to_providers.redis_store")
+ # mock_redis.get.return_value = "jo.smith@example.com".encode("utf-8")
+ email = "test@example.com".encode("utf-8")
+ personalisation = {
+ "name": "Jo",
+ }
+
+ personalisation = json.dumps(personalisation)
+ personalisation = personalisation.encode("utf-8")
+ # mock_redis.get.side_effect = [email, personalisation]
+
+ service_dict = service_schema.dump(sample_email_template.service)
+ template_dict = template_schema.dump(sample_email_template)
+
+ mocker.patch(
+ "app.redis_store.get",
+ side_effect=[
+ email,
+ personalisation,
+ json.dumps({"data": service_dict}).encode("utf-8"),
+ json.dumps({"data": template_dict}).encode("utf-8"),
+ ],
+ )
+ mock_get_template = mocker.patch(
+ "app.dao.templates_dao.dao_get_template_by_id_and_service_id"
+ )
+ mock_get_service = mocker.patch("app.dao.services_dao.dao_fetch_service_by_id")
+ send_mock = mocker.patch("app.aws_ses_client.send_email", return_value="reference")
+ notification = create_notification(
+ template=sample_email_template,
+ )
+
+ send_to_providers.send_email_to_provider(notification)
+ assert mock_get_template.called is False
+ assert mock_get_service.called is False
+ send_mock.assert_called_once_with(
+ ANY,
+ "test@example.com",
+ ANY,
+ body=ANY,
+ html_body=ANY,
+ reply_to_address=notification.reply_to_text,
+ )
+
+
+def test_get_html_email_options_return_email_branding_from_serialised_service(
+ sample_service,
+):
+ branding = create_email_branding()
+ sample_service.email_branding = branding
+ service = SerialisedService.from_id(sample_service.id)
+ email_options = get_html_email_options(service)
+ assert email_options is not None
+ assert email_options == {
"govuk_banner": branding.brand_type == BrandType.BOTH,
"brand_banner": branding.brand_type == BrandType.ORG_BANNER,
"brand_colour": branding.colour,
- "brand_logo": logo_url,
+ "brand_logo": get_logo_url(current_app.config["ADMIN_BASE_URL"], branding.logo),
"brand_text": branding.text,
"brand_name": branding.name,
}
-def technical_failure(notification):
- notification.status = NotificationStatus.TECHNICAL_FAILURE
- dao_update_notification(notification)
- raise NotificationTechnicalFailureException(
- f"Send {notification.notification_type} for notification id {notification.id} "
- f"to provider is not allowed: service {notification.service_id} is inactive"
- )
+def test_get_html_email_options_add_email_branding_from_service(sample_service):
+ branding = create_email_branding()
+ sample_service.email_branding = branding
+ email_options = get_html_email_options(sample_service)
+ assert email_options is not None
+ assert email_options == {
+ "govuk_banner": branding.brand_type == BrandType.BOTH,
+ "brand_banner": branding.brand_type == BrandType.ORG_BANNER,
+ "brand_colour": branding.colour,
+ "brand_logo": get_logo_url(current_app.config["ADMIN_BASE_URL"], branding.logo),
+ "brand_text": branding.text,
+ "brand_name": branding.name,
+ }
@pytest.mark.parametrize(