Files
notifications-api/tests/app/service/send_notification/test_send_notification.py
Cliff Hill 12658fe667 Even more format strings changed.
Signed-off-by: Cliff Hill <Clifford.hill@gsa.gov>
2024-02-28 12:54:17 -05:00

1415 lines
47 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import random
import string
import pytest
from flask import current_app, json
from freezegun import freeze_time
from notifications_python_client.authentication import create_jwt_token
from notifications_utils import SMS_CHAR_COUNT_LIMIT
import app
from app.dao import notifications_dao
from app.dao.api_key_dao import save_model_api_key
from app.dao.services_dao import dao_update_service
from app.dao.templates_dao import dao_get_all_templates_for_service, dao_update_template
from app.enums import KeyType, NotificationType, TemplateType
from app.errors import InvalidRequest
from app.models import ApiKey, Notification, NotificationHistory, Template
from app.service.send_notification import send_one_off_notification
from app.v2.errors import RateLimitError
from tests import create_service_authorization_header
from tests.app.db import (
create_api_key,
create_notification,
create_reply_to_email,
create_service,
create_service_guest_list,
create_template,
)
@pytest.mark.parametrize("template_type", [TemplateType.SMS, TemplateType.EMAIL])
def test_create_notification_should_reject_if_missing_required_fields(
notify_api, sample_api_key, mocker, template_type
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch(
f"app.celery.provider_tasks.deliver_{template_type}.apply_async"
)
data = {}
auth_header = create_service_authorization_header(
service_id=sample_api_key.service_id
)
response = client.post(
path=f"/notifications/{template_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
json_resp = json.loads(response.get_data(as_text=True))
mocked.assert_not_called()
assert json_resp["result"] == "error"
assert "Missing data for required field." in json_resp["message"]["to"][0]
assert (
"Missing data for required field."
in json_resp["message"]["template"][0]
)
assert response.status_code == 400
def test_should_reject_bad_phone_numbers(notify_api, sample_template, mocker):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
data = {"to": "invalid", "template": sample_template.id}
auth_header = create_service_authorization_header(
service_id=sample_template.service_id
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
json_resp = json.loads(response.get_data(as_text=True))
mocked.assert_not_called()
assert json_resp["result"] == "error"
assert len(json_resp["message"].keys()) == 1
assert (
"Invalid phone number: The string supplied did not seem to be a phone number."
in json_resp["message"]["to"]
)
assert response.status_code == 400
@pytest.mark.parametrize(
"template_type, to",
[
(TemplateType.SMS, "+447700900855"),
(TemplateType.EMAIL, "ok@ok.com"),
],
)
def test_send_notification_invalid_template_id(
notify_api, sample_template, mocker, fake_uuid, template_type, to
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch(
f"app.celery.provider_tasks.deliver_{template_type}.apply_async"
)
data = {"to": to, "template": fake_uuid}
auth_header = create_service_authorization_header(
service_id=sample_template.service_id
)
response = client.post(
path=f"/notifications/{template_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
json_resp = json.loads(response.get_data(as_text=True))
mocked.assert_not_called()
assert response.status_code == 400
test_string = "Template not found"
assert test_string in json_resp["message"]
@freeze_time("2016-01-01 11:09:00.061258")
def test_send_notification_with_placeholders_replaced(
notify_api, sample_email_template_with_placeholders, mocker
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
data = {
"to": "ok@ok.com",
"template": str(sample_email_template_with_placeholders.id),
"personalisation": {"name": "Jo"},
}
auth_header = create_service_authorization_header(
service_id=sample_email_template_with_placeholders.service.id
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
response_data = json.loads(response.data)["data"]
notification_id = response_data["notification"]["id"]
data.update(
{"template_version": sample_email_template_with_placeholders.version}
)
mocked.assert_called_once_with([notification_id], queue="send-email-tasks")
assert response.status_code == 201
assert response_data["body"] == "Hello Jo\nThis is an email from GOV.UK"
assert response_data["subject"] == "Jo"
@pytest.mark.parametrize(
"personalisation, expected_body, expected_subject",
[
(
["Jo", "John", "Josephine"],
(
"Hello \n\n"
"* Jo\n"
"* John\n"
"* Josephine\n"
"This is an email from GOV.UK"
),
"Jo, John and Josephine",
),
(
6,
("Hello 6\n" "This is an email from GOV.UK"),
"6",
),
],
)
def test_send_notification_with_placeholders_replaced_with_unusual_types(
client,
sample_email_template_with_placeholders,
mocker,
personalisation,
expected_body,
expected_subject,
):
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
response = client.post(
path="/notifications/email",
data=json.dumps(
{
"to": "ok@ok.com",
"template": str(sample_email_template_with_placeholders.id),
"personalisation": {"name": personalisation},
}
),
headers=[
("Content-Type", "application/json"),
create_service_authorization_header(
service_id=sample_email_template_with_placeholders.service.id
),
],
)
assert response.status_code == 201
response_data = json.loads(response.data)["data"]
assert response_data["body"] == expected_body
assert response_data["subject"] == expected_subject
@pytest.mark.parametrize(
"personalisation, expected_body, expected_subject",
[
(
None,
("we consider None equivalent to missing personalisation"),
"",
),
],
)
def test_send_notification_with_placeholders_replaced_with_unusual_types_no_personalization(
client,
sample_email_template_with_placeholders,
mocker,
personalisation,
expected_body,
expected_subject,
):
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
response = client.post(
path="/notifications/email",
data=json.dumps(
{
"to": "ok@ok.com",
"template": str(sample_email_template_with_placeholders.id),
"personalisation": {"name": personalisation},
}
),
headers=[
("Content-Type", "application/json"),
create_service_authorization_header(
service_id=sample_email_template_with_placeholders.service.id
),
],
)
assert response.status_code == 400
def test_should_not_send_notification_for_archived_template(
notify_api, sample_template
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
sample_template.archived = True
dao_update_template(sample_template)
json_data = json.dumps(
{"to": "+447700900855", "template": sample_template.id}
)
auth_header = create_service_authorization_header(
service_id=sample_template.service_id
)
resp = client.post(
path="/notifications/sms",
data=json_data,
headers=[("Content-Type", "application/json"), auth_header],
)
assert resp.status_code == 400
json_resp = json.loads(resp.get_data(as_text=True))
assert "Template has been deleted" in json_resp["message"]
@pytest.mark.parametrize(
"template_type, to",
[
(TemplateType.SMS, "+447700900855"),
(TemplateType.EMAIL, "not-someone-we-trust@email-address.com"),
],
)
def test_should_not_send_notification_if_restricted_and_not_a_service_user(
notify_api, sample_template, sample_email_template, mocker, template_type, to
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch(
f"app.celery.provider_tasks.deliver_{template_type}.apply_async"
)
template = (
sample_template
if template_type == TemplateType.SMS
else sample_email_template
)
template.service.restricted = True
dao_update_service(template.service)
data = {"to": to, "template": template.id}
auth_header = create_service_authorization_header(
service_id=template.service_id
)
response = client.post(
path=f"/notifications/{template_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
json_resp = json.loads(response.get_data(as_text=True))
mocked.assert_not_called()
assert response.status_code == 400
assert [
(
"Cant send to this recipient when service is in trial mode "
" see https://www.notifications.service.gov.uk/trial-mode"
)
] == json_resp["message"]["to"]
@pytest.mark.parametrize("template_type", [TemplateType.SMS, TemplateType.EMAIL])
def test_should_send_notification_if_restricted_and_a_service_user(
notify_api, sample_template, sample_email_template, template_type, mocker
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch(
f"app.celery.provider_tasks.deliver_{template_type}.apply_async"
)
template = (
sample_template
if template_type == TemplateType.SMS
else sample_email_template
)
to = (
template.service.created_by.mobile_number
if template_type == TemplateType.SMS
else template.service.created_by.email_address
)
template.service.restricted = True
dao_update_service(template.service)
data = {"to": to, "template": template.id}
auth_header = create_service_authorization_header(
service_id=template.service_id
)
response = client.post(
path=f"/notifications/{template_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert mocked.called == 1
assert response.status_code == 201
@pytest.mark.parametrize("template_type", [TemplateType.SMS, TemplateType.EMAIL])
def test_should_not_allow_template_from_another_service(
notify_api, service_factory, sample_user, mocker, template_type
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch(
f"app.celery.provider_tasks.deliver_{template_type}.apply_async"
)
service_1 = service_factory.get(
"service 1", user=sample_user, email_from="service.1"
)
service_2 = service_factory.get(
"service 2", user=sample_user, email_from="service.2"
)
service_2_templates = dao_get_all_templates_for_service(
service_id=service_2.id
)
to = (
sample_user.mobile_number
if template_type == TemplateType.SMS
else sample_user.email_address
)
data = {"to": to, "template": service_2_templates[0].id}
auth_header = create_service_authorization_header(service_id=service_1.id)
response = client.post(
path=f"/notifications/{template_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
json_resp = json.loads(response.get_data(as_text=True))
mocked.assert_not_called()
assert response.status_code == 400
test_string = "Template not found"
assert test_string in json_resp["message"]
@freeze_time("2016-01-01 11:09:00.061258")
def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
data = {"to": "202 867 5309", "template": str(sample_template.id)}
auth_header = create_service_authorization_header(
service_id=sample_template.service_id
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
response_data = json.loads(response.data)["data"]
notification_id = response_data["notification"]["id"]
mocked.assert_called_once_with([notification_id], queue="send-sms-tasks")
assert response.status_code == 201
assert notification_id
assert "subject" not in response_data
assert response_data["body"] == sample_template.content
assert response_data["template_version"] == sample_template.version
def test_should_reject_email_notification_with_bad_email(
notify_api, sample_email_template, mocker
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocked = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
to_address = "bad-email"
data = {"to": to_address, "template": str(sample_email_template.service_id)}
auth_header = create_service_authorization_header(
service_id=sample_email_template.service_id
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
data = json.loads(response.get_data(as_text=True))
mocked.apply_async.assert_not_called()
assert response.status_code == 400
assert data["result"] == "error"
assert data["message"]["to"][0] == "Not a valid email address"
@freeze_time("2016-01-01 11:09:00.061258")
def test_should_allow_valid_email_notification(
notify_api, sample_email_template, mocker
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
data = {"to": "ok@ok.com", "template": str(sample_email_template.id)}
auth_header = create_service_authorization_header(
service_id=sample_email_template.service_id
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 201
response_data = json.loads(response.get_data(as_text=True))["data"]
notification_id = response_data["notification"]["id"]
app.celery.provider_tasks.deliver_email.apply_async.assert_called_once_with(
[notification_id], queue="send-email-tasks"
)
assert response.status_code == 201
assert notification_id
assert response_data["subject"] == "Email Subject"
assert response_data["body"] == sample_email_template.content
assert response_data["template_version"] == sample_email_template.version
@pytest.mark.parametrize("restricted", [True, False])
@freeze_time("2016-01-01 12:00:00.061258")
def test_should_allow_api_call_if_under_day_limit_regardless_of_type(
notify_api, sample_user, mocker, restricted
):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
service = create_service(restricted=restricted, message_limit=2)
email_template = create_template(service, template_type=TemplateType.EMAIL)
sms_template = create_template(service, template_type=TemplateType.SMS)
create_notification(template=email_template)
data = {"to": sample_user.mobile_number, "template": str(sms_template.id)}
auth_header = create_service_authorization_header(service_id=service.id)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 201
def test_should_not_return_html_in_body(notify_api, sample_service, mocker):
with notify_api.test_request_context():
with notify_api.test_client() as client:
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
email_template = create_template(
sample_service, template_type=TemplateType.EMAIL, content="hello\nthere"
)
data = {"to": "ok@ok.com", "template": str(email_template.id)}
auth_header = create_service_authorization_header(
service_id=email_template.service_id
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 201
assert (
json.loads(response.get_data(as_text=True))["data"]["body"]
== "hello\nthere"
)
def test_should_not_send_email_if_team_api_key_and_not_a_service_user(
client, sample_email_template, mocker
):
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
data = {
"to": "not-someone-we-trust@email-address.com",
"template": str(sample_email_template.id),
}
auth_header = create_service_authorization_header(
service_id=sample_email_template.service_id, key_type=KeyType.TEAM
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
json_resp = json.loads(response.get_data(as_text=True))
app.celery.provider_tasks.deliver_email.apply_async.assert_not_called()
assert response.status_code == 400
assert ["Cant send to this recipient using a team-only API key"] == json_resp[
"message"
]["to"]
def test_should_not_send_sms_if_team_api_key_and_not_a_service_user(
client, sample_template, mocker
):
mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
data = {
"to": "2028675300",
"template": str(sample_template.id),
}
auth_header = create_service_authorization_header(
service_id=sample_template.service_id, key_type=KeyType.TEAM
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
json_resp = json.loads(response.get_data(as_text=True))
app.celery.provider_tasks.deliver_sms.apply_async.assert_not_called()
assert response.status_code == 400
assert ["Cant send to this recipient using a team-only API key"] == json_resp[
"message"
]["to"]
def test_should_send_email_if_team_api_key_and_a_service_user(
client, sample_email_template, fake_uuid, mocker
):
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
mocker.patch(
"app.notifications.process_notifications.uuid.uuid4", return_value=fake_uuid
)
data = {
"to": sample_email_template.service.created_by.email_address,
"template": sample_email_template.id,
}
auth_header = create_service_authorization_header(
service_id=sample_email_template.service_id, key_type=KeyType.TEAM
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
app.celery.provider_tasks.deliver_email.apply_async.assert_called_once_with(
[fake_uuid], queue="send-email-tasks"
)
assert response.status_code == 201
@pytest.mark.parametrize("restricted", [True, False])
@pytest.mark.parametrize("limit", [0, 1])
def test_should_send_sms_to_anyone_with_test_key(
client, sample_template, mocker, restricted, limit, fake_uuid
):
mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
mocker.patch(
"app.notifications.process_notifications.uuid.uuid4", return_value=fake_uuid
)
data = {"to": "2028675300", "template": sample_template.id}
sample_template.service.restricted = restricted
sample_template.service.message_limit = limit
api_key = ApiKey(
service=sample_template.service,
name="test_key",
created_by=sample_template.created_by,
key_type=KeyType.TEST,
)
save_model_api_key(api_key)
auth_header = create_jwt_token(
secret=api_key.secret, client_id=str(api_key.service_id)
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[
("Content-Type", "application/json"),
("Authorization", f"Bearer {auth_header}"),
],
)
app.celery.provider_tasks.deliver_sms.apply_async.assert_called_once_with(
[fake_uuid], queue="send-sms-tasks"
)
assert response.status_code == 201
@pytest.mark.parametrize("restricted", [True, False])
@pytest.mark.parametrize("limit", [0, 1])
def test_should_send_email_to_anyone_with_test_key(
client, sample_email_template, mocker, restricted, limit, fake_uuid
):
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
mocker.patch(
"app.notifications.process_notifications.uuid.uuid4", return_value=fake_uuid
)
data = {"to": "anyone123@example.com", "template": sample_email_template.id}
sample_email_template.service.restricted = restricted
sample_email_template.service.message_limit = limit
api_key = ApiKey(
service=sample_email_template.service,
name="test_key",
created_by=sample_email_template.created_by,
key_type=KeyType.TEST,
)
save_model_api_key(api_key)
auth_header = create_jwt_token(
secret=api_key.secret, client_id=str(api_key.service_id)
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[
("Content-Type", "application/json"),
("Authorization", f"Bearer {auth_header}"),
],
)
app.celery.provider_tasks.deliver_email.apply_async.assert_called_once_with(
[fake_uuid], queue="send-email-tasks"
)
assert response.status_code == 201
def test_should_send_sms_if_team_api_key_and_a_service_user(
client, sample_template, fake_uuid, mocker
):
mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
mocker.patch(
"app.notifications.process_notifications.uuid.uuid4", return_value=fake_uuid
)
data = {
"to": sample_template.service.created_by.mobile_number,
"template": sample_template.id,
}
api_key = ApiKey(
service=sample_template.service,
name="team_key",
created_by=sample_template.created_by,
key_type=KeyType.TEAM,
)
save_model_api_key(api_key)
auth_header = create_jwt_token(
secret=api_key.secret, client_id=str(api_key.service_id)
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[
("Content-Type", "application/json"),
("Authorization", f"Bearer {auth_header}"),
],
)
app.celery.provider_tasks.deliver_sms.apply_async.assert_called_once_with(
[fake_uuid], queue="send-sms-tasks"
)
assert response.status_code == 201
@pytest.mark.parametrize(
"template_type,queue_name",
[
(TemplateType.SMS, "send-sms-tasks"),
(TemplateType.EMAIL, "send-email-tasks"),
],
)
def test_should_persist_notification(
client,
sample_template,
sample_email_template,
fake_uuid,
mocker,
template_type,
queue_name,
):
mocked = mocker.patch(
f"app.celery.provider_tasks.deliver_{template_type}.apply_async"
)
mocker.patch(
"app.notifications.process_notifications.uuid.uuid4", return_value=fake_uuid
)
template = (
sample_template if template_type == TemplateType.SMS else sample_email_template
)
to = (
sample_template.service.created_by.mobile_number
if template_type == TemplateType.SMS
else sample_email_template.service.created_by.email_address
)
data = {"to": to, "template": template.id}
api_key = ApiKey(
service=template.service,
name="team_key",
created_by=template.created_by,
key_type=KeyType.TEAM,
)
save_model_api_key(api_key)
auth_header = create_jwt_token(
secret=api_key.secret, client_id=str(api_key.service_id)
)
response = client.post(
path=f"/notifications/{template_type}",
data=json.dumps(data),
headers=[
("Content-Type", "application/json"),
("Authorization", f"Bearer {auth_header}"),
],
)
mocked.assert_called_once_with([fake_uuid], queue=queue_name)
assert response.status_code == 201
notification = notifications_dao.get_notification_by_id(fake_uuid)
assert notification.to == "1"
assert notification.template_id == template.id
assert notification.notification_type == template_type
@pytest.mark.parametrize(
"template_type,queue_name",
[(TemplateType.SMS, "send-sms-tasks"), (TemplateType.EMAIL, "send-email-tasks")],
)
def test_should_delete_notification_and_return_error_if_redis_fails(
client,
sample_email_template,
sample_template,
fake_uuid,
mocker,
template_type,
queue_name,
):
mocked = mocker.patch(
f"app.celery.provider_tasks.deliver_{template_type}.apply_async",
side_effect=Exception("failed to talk to redis"),
)
mocker.patch(
"app.notifications.process_notifications.uuid.uuid4", return_value=fake_uuid
)
template = (
sample_template if template_type == TemplateType.SMS else sample_email_template
)
to = (
sample_template.service.created_by.mobile_number
if template_type == TemplateType.SMS
else sample_email_template.service.created_by.email_address
)
data = {"to": to, "template": template.id}
api_key = ApiKey(
service=template.service,
name="team_key",
created_by=template.created_by,
key_type=KeyType.TEAM,
)
save_model_api_key(api_key)
auth_header = create_jwt_token(
secret=api_key.secret, client_id=str(api_key.service_id)
)
with pytest.raises(expected_exception=Exception) as e:
client.post(
path=f"/notifications/{template_type}",
data=json.dumps(data),
headers=[
("Content-Type", "application/json"),
("Authorization", f"Bearer {auth_header}"),
],
)
assert str(e.value) == "failed to talk to redis"
mocked.assert_called_once_with([fake_uuid], queue=queue_name)
assert not notifications_dao.get_notification_by_id(fake_uuid)
assert not NotificationHistory.query.get(fake_uuid)
@pytest.mark.parametrize(
"to_email",
[
"simulate-delivered@notifications.service.gov.uk",
"simulate-delivered-2@notifications.service.gov.uk",
"simulate-delivered-3@notifications.service.gov.uk",
],
)
def test_should_not_persist_notification_or_send_email_if_simulated_email(
client, to_email, sample_email_template, mocker
):
apply_async = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
data = {"to": to_email, "template": sample_email_template.id}
auth_header = create_service_authorization_header(
service_id=sample_email_template.service_id
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 201
apply_async.assert_not_called()
assert Notification.query.count() == 0
@pytest.mark.parametrize("to_sms", ["+14254147755", "+14254147167"])
def test_should_not_persist_notification_or_send_sms_if_simulated_number(
client, to_sms, sample_template, mocker
):
apply_async = mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
data = {"to": to_sms, "template": sample_template.id}
auth_header = create_service_authorization_header(
service_id=sample_template.service_id
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 201
apply_async.assert_not_called()
assert Notification.query.count() == 0
@pytest.mark.parametrize("key_type", [KeyType.NORMAL, KeyType.TEAM])
@pytest.mark.parametrize(
"notification_type, to",
[
(TemplateType.SMS, "2028675300"),
(TemplateType.EMAIL, "non_guest_list_recipient@mail.com"),
],
)
def test_should_not_send_notification_to_non_guest_list_recipient_in_trial_mode(
client, sample_service_guest_list, notification_type, to, key_type, mocker
):
service = sample_service_guest_list.service
service.restricted = True
service.message_limit = 2
apply_async = mocker.patch(
f"app.celery.provider_tasks.deliver_{notification_type}.apply_async"
)
template = create_template(service, template_type=notification_type)
assert sample_service_guest_list.service_id == service.id
assert to not in [member.recipient for member in service.guest_list]
create_notification(template=template)
data = {"to": to, "template": str(template.id)}
api_key = create_api_key(service, key_type=key_type)
auth_header = create_jwt_token(
secret=api_key.secret, client_id=str(api_key.service_id)
)
response = client.post(
path=f"/notifications/{notification_type}",
data=json.dumps(data),
headers=[
("Content-Type", "application/json"),
("Authorization", f"Bearer {auth_header}"),
],
)
expected_response_message = (
(
"Cant send to this recipient when service is in trial mode "
" see https://www.notifications.service.gov.uk/trial-mode"
)
if key_type == KeyType.NORMAL
else ("Cant send to this recipient using a team-only API key")
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 400
assert json_resp["result"] == "error"
assert expected_response_message in json_resp["message"]["to"]
apply_async.assert_not_called()
@pytest.mark.parametrize("service_restricted", [True, False])
@pytest.mark.parametrize("key_type", [KeyType.NORMAL, KeyType.TEAM])
@pytest.mark.parametrize(
"notification_type, to, normalized_to",
[
(NotificationType.SMS, "2028675300", "+12028675300"),
(NotificationType.EMAIL, "guest_list_recipient@mail.com", None),
],
)
def test_should_send_notification_to_guest_list_recipient(
client,
sample_service,
notification_type,
to,
normalized_to,
key_type,
service_restricted,
mocker,
):
sample_service.message_limit = 2
sample_service.restricted = service_restricted
apply_async = mocker.patch(
f"app.celery.provider_tasks.deliver_{notification_type}.apply_async"
)
template = create_template(sample_service, template_type=notification_type)
if notification_type == NotificationType.SMS:
service_guest_list = create_service_guest_list(sample_service, mobile_number=to)
elif notification_type == NotificationType.EMAIL:
service_guest_list = create_service_guest_list(sample_service, email_address=to)
assert service_guest_list.service_id == sample_service.id
assert (normalized_to or to) in [
member.recipient for member in sample_service.guest_list
]
create_notification(template=template)
data = {"to": to, "template": str(template.id)}
sample_key = create_api_key(sample_service, key_type=key_type)
auth_header = create_jwt_token(
secret=sample_key.secret, client_id=str(sample_key.service_id)
)
response = client.post(
path=f"/notifications/{notification_type}",
data=json.dumps(data),
headers=[
("Content-Type", "application/json"),
("Authorization", f"Bearer {auth_header}"),
],
)
json_resp = json.loads(response.get_data(as_text=True))
assert response.status_code == 201
assert json_resp["data"]["notification"]["id"]
assert json_resp["data"]["body"] == template.content
assert json_resp["data"]["template_version"] == template.version
assert apply_async.called
@pytest.mark.parametrize(
"notification_type, template_type, to",
[
(NotificationType.EMAIL, TemplateType.SMS, "notify@digital.fake.gov"),
(NotificationType.SMS, TemplateType.EMAIL, "+12028675309"),
],
)
def test_should_error_if_notification_type_does_not_match_template_type(
client, sample_service, template_type, notification_type, to
):
template = create_template(sample_service, template_type=template_type)
data = {"to": to, "template": template.id}
auth_header = create_service_authorization_header(service_id=template.service_id)
response = client.post(
f"/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 400
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp["result"] == "error"
assert (
f"{template_type} template is not suitable for {notification_type} notification"
in json_resp["message"]
)
def test_create_template_raises_invalid_request_exception_with_missing_personalisation(
sample_template_with_placeholders,
):
template = Template.query.get(sample_template_with_placeholders.id)
from app.notifications.rest import create_template_object_for_notification
with pytest.raises(InvalidRequest) as e:
create_template_object_for_notification(template, {})
assert {"template": ["Missing personalisation: Name"]} == e.value.message
def test_create_template_doesnt_raise_with_too_much_personalisation(
sample_template_with_placeholders,
):
from app.notifications.rest import create_template_object_for_notification
template = Template.query.get(sample_template_with_placeholders.id)
create_template_object_for_notification(template, {"name": "Jo", "extra": "stuff"})
@pytest.mark.parametrize(
"template_type, should_error",
[
(TemplateType.SMS, True),
(TemplateType.EMAIL, False),
],
)
def test_create_template_raises_invalid_request_when_content_too_large(
sample_service, template_type, should_error
):
sample = create_template(
sample_service, template_type=template_type, content="((long_text))"
)
template = Template.query.get(sample.id)
from app.notifications.rest import create_template_object_for_notification
try:
create_template_object_for_notification(
template,
{
"long_text": "".join(
random.choice(string.ascii_uppercase + string.digits)
for _ in range(SMS_CHAR_COUNT_LIMIT + 1)
)
},
)
if should_error:
pytest.fail("expected an InvalidRequest")
except InvalidRequest as e:
if not should_error:
pytest.fail("do not expect an InvalidRequest")
assert e.message == {
"content": [
f"Content has a character count greater than the limit of {SMS_CHAR_COUNT_LIMIT}"
]
}
@pytest.mark.parametrize(
"notification_type,send_to",
[
(NotificationType.SMS, "2028675309"),
(
NotificationType.EMAIL,
"sample@email.com",
),
],
)
def test_send_notification_uses_priority_queue_when_template_is_marked_as_priority(
client,
sample_service,
mocker,
notification_type,
send_to,
):
sample = create_template(
sample_service, template_type=notification_type, process_type="priority"
)
mocked = mocker.patch(
f"app.celery.provider_tasks.deliver_{notification_type}.apply_async"
)
data = {"to": send_to, "template": str(sample.id)}
auth_header = create_service_authorization_header(service_id=sample.service_id)
response = client.post(
path=f"/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
response_data = json.loads(response.data)["data"]
notification_id = response_data["notification"]["id"]
assert response.status_code == 201
mocked.assert_called_once_with([notification_id], queue="priority-tasks")
@pytest.mark.parametrize(
"notification_type, send_to",
[
(NotificationType.SMS, "2028675309"),
(
NotificationType.EMAIL,
"sample@email.com",
),
],
)
def test_returns_a_429_limit_exceeded_if_rate_limit_exceeded(
client, sample_service, mocker, notification_type, send_to
):
sample = create_template(sample_service, template_type=notification_type)
persist_mock = mocker.patch("app.notifications.rest.persist_notification")
deliver_mock = mocker.patch("app.notifications.rest.send_notification_to_queue")
mocker.patch(
"app.notifications.rest.check_rate_limiting",
side_effect=RateLimitError("LIMIT", "INTERVAL", "TYPE"),
)
data = {"to": send_to, "template": str(sample.id)}
auth_header = create_service_authorization_header(service_id=sample.service_id)
response = client.post(
path=f"/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
message = json.loads(response.data)["message"]
result = json.loads(response.data)["result"]
assert response.status_code == 429
assert result == "error"
assert message == (
"Exceeded rate limit for key type TYPE of LIMIT "
"requests per INTERVAL seconds"
)
assert not persist_mock.called
assert not deliver_mock.called
def test_should_allow_store_original_number_on_sms_notification(
client, sample_template, mocker
):
mocked = mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
data = {"to": "(202) 867-5309", "template": str(sample_template.id)}
auth_header = create_service_authorization_header(
service_id=sample_template.service_id
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
response_data = json.loads(response.data)["data"]
notification_id = response_data["notification"]["id"]
mocked.assert_called_once_with([notification_id], queue="send-sms-tasks")
assert response.status_code == 201
assert notification_id
notifications = Notification.query.all()
assert len(notifications) == 1
assert "1" == notifications[0].to
def test_should_not_allow_sending_to_international_number_without_international_permission(
client, sample_template, mocker
):
mocked = mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
data = {"to": "+(44) 7700-900 855", "template": str(sample_template.id)}
auth_header = create_service_authorization_header(
service_id=sample_template.service_id
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert not mocked.called
assert response.status_code == 400
error_json = json.loads(response.get_data(as_text=True))
assert error_json["result"] == "error"
assert error_json["message"] == "Cannot send to international mobile numbers"
def test_should_allow_sending_to_international_number_with_international_permission(
client, sample_service_full_permissions, mocker
):
mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
template = create_template(sample_service_full_permissions)
data = {"to": "+(44) 7700-900 855", "template": str(template.id)}
auth_header = create_service_authorization_header(
service_id=sample_service_full_permissions.id
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 201
def test_should_not_allow_sms_notifications_if_service_permission_not_set(
client,
mocker,
sample_template_without_sms_permission,
):
mocked = mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
data = {
"to": "+12028675309",
"template": str(sample_template_without_sms_permission.id),
}
auth_header = create_service_authorization_header(
service_id=sample_template_without_sms_permission.service_id
)
response = client.post(
path="/notifications/sms",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert not mocked.called
assert response.status_code == 400
error_json = json.loads(response.get_data(as_text=True))
assert error_json["result"] == "error"
assert error_json["message"]["service"][0] == "Cannot send text messages"
def test_should_not_allow_email_notifications_if_service_permission_not_set(
client,
mocker,
sample_template_without_email_permission,
):
mocked = mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
data = {
"to": "notify@digital.fake.gov",
"template": str(sample_template_without_email_permission.id),
}
auth_header = create_service_authorization_header(
service_id=sample_template_without_email_permission.service_id
)
response = client.post(
path="/notifications/email",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert not mocked.called
assert response.status_code == 400
error_json = json.loads(response.get_data(as_text=True))
assert error_json["result"] == "error"
assert error_json["message"]["service"][0] == "Cannot send emails"
@pytest.mark.parametrize(
"notification_type, err_msg",
[("apple", "apple notification type is not supported")],
)
def test_should_throw_exception_if_notification_type_is_invalid(
client, sample_service, notification_type, err_msg
):
auth_header = create_service_authorization_header(service_id=sample_service.id)
response = client.post(
path=f"/notifications/{notification_type}",
data={},
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 400
assert json.loads(response.get_data(as_text=True))["message"] == err_msg
@pytest.mark.parametrize(
"notification_type, recipient",
[
(NotificationType.SMS, "2028675309"),
(
NotificationType.EMAIL,
"test@gov.uk",
),
],
)
def test_post_notification_should_set_reply_to_text(
client, sample_service, mocker, notification_type, recipient
):
mocker.patch(f"app.celery.provider_tasks.deliver_{notification_type}.apply_async")
template = create_template(sample_service, template_type=notification_type)
expected_reply_to = current_app.config["FROM_NUMBER"]
if notification_type == NotificationType.EMAIL:
expected_reply_to = "reply_to@gov.uk"
create_reply_to_email(
service=sample_service, email_address=expected_reply_to, is_default=True
)
data = {"to": recipient, "template": str(template.id)}
response = client.post(
f"/notifications/{notification_type}",
data=json.dumps(data),
headers=[
("Content-Type", "application/json"),
create_service_authorization_header(service_id=sample_service.id),
],
)
assert response.status_code == 201
notifications = Notification.query.all()
assert len(notifications) == 1
assert notifications[0].reply_to_text == expected_reply_to
@pytest.mark.skip(reason="Rewrite without letters?")
@pytest.mark.parametrize("reference_paceholder,", [None, "ref2"])
def test_send_notification_should_set_client_reference_from_placeholder(
sample_letter_template, mocker, reference_paceholder
):
deliver_mock = mocker.patch(
"app.celery.tasks.letters_pdf_tasks.get_pdf_for_templated_letter.apply_async"
)
data = {
"template_id": sample_letter_template.id,
"personalisation": {
"address_line_1": "Jane",
"address_line_2": "Moss Lane",
"address_line_3": "SW1A 1AA",
},
"to": "Jane",
"created_by": sample_letter_template.service.created_by_id,
}
if reference_paceholder:
data["personalisation"]["reference"] = reference_paceholder
notification_id = send_one_off_notification(sample_letter_template.service_id, data)
assert deliver_mock.called
notification = Notification.query.get(notification_id["id"])
assert notification.client_reference == reference_paceholder