merge from main

This commit is contained in:
Kenneth Kehl
2024-03-01 13:50:09 -08:00
140 changed files with 8031 additions and 5017 deletions

View File

@@ -8,13 +8,15 @@ from flask import current_app, json
from app.dao import templates_dao
from app.dao.service_sms_sender_dao import dao_update_service_sms_sender
from app.models import (
EMAIL_TYPE,
INTERNATIONAL_SMS_TYPE,
NOTIFICATION_CREATED,
SMS_TYPE,
Notification,
from app.enums import (
KeyType,
NotificationStatus,
NotificationType,
ServicePermissionType,
TemplateProcessType,
TemplateType,
)
from app.models import Notification
from app.schema_validation import validate
from app.v2.errors import RateLimitError
from app.v2.notifications.notification_schemas import (
@@ -60,7 +62,7 @@ def test_post_sms_notification_returns_201(
assert validate(resp_json, post_sms_response) == resp_json
notifications = Notification.query.all()
assert len(notifications) == 1
assert notifications[0].status == NOTIFICATION_CREATED
assert notifications[0].status == NotificationStatus.CREATED
notification_id = notifications[0].id
assert notifications[0].document_download_count is None
assert resp_json["id"] == str(notification_id)
@@ -69,16 +71,13 @@ def test_post_sms_notification_returns_201(
"body"
] == sample_template_with_placeholders.content.replace("(( Name))", "Jo")
assert resp_json["content"]["from_number"] == current_app.config["FROM_NUMBER"]
assert "v2/notifications/{}".format(notification_id) in resp_json["uri"]
assert f"v2/notifications/{notification_id}" in resp_json["uri"]
assert resp_json["template"]["id"] == str(sample_template_with_placeholders.id)
assert resp_json["template"]["version"] == sample_template_with_placeholders.version
assert (
"services/{}/templates/{}".format(
sample_template_with_placeholders.service_id,
sample_template_with_placeholders.id,
)
in resp_json["template"]["uri"]
)
f"services/{sample_template_with_placeholders.service_id}/templates/"
f"{sample_template_with_placeholders.id}"
) in resp_json["template"]["uri"]
assert not resp_json["scheduled_for"]
assert mocked.called
@@ -374,8 +373,8 @@ def test_should_return_template_if_found_in_redis(mocker, client, sample_templat
@pytest.mark.parametrize(
"notification_type, key_send_to, send_to",
[
("sms", "phone_number", "+447700900855"),
("email", "email_address", "sample@email.com"),
(NotificationType.SMS, "phone_number", "+447700900855"),
(NotificationType.EMAIL, "email_address", "sample@email.com"),
],
)
def test_post_notification_returns_400_and_missing_template(
@@ -385,7 +384,7 @@ def test_post_notification_returns_400_and_missing_template(
auth_header = create_service_authorization_header(service_id=sample_service.id)
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
@@ -403,8 +402,8 @@ def test_post_notification_returns_400_and_missing_template(
@pytest.mark.parametrize(
"notification_type, key_send_to, send_to",
[
("sms", "phone_number", "+447700900855"),
("email", "email_address", "sample@email.com"),
(NotificationType.SMS, "phone_number", "+447700900855"),
(NotificationType.EMAIL, "email_address", "sample@email.com"),
],
)
def test_post_notification_returns_401_and_well_formed_auth_error(
@@ -413,7 +412,7 @@ def test_post_notification_returns_401_and_well_formed_auth_error(
data = {key_send_to: send_to, "template_id": str(sample_template.id)}
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json")],
)
@@ -433,8 +432,8 @@ def test_post_notification_returns_401_and_well_formed_auth_error(
@pytest.mark.parametrize(
"notification_type, key_send_to, send_to",
[
("sms", "phone_number", "+447700900855"),
("email", "email_address", "sample@email.com"),
(NotificationType.SMS, "phone_number", "+447700900855"),
(NotificationType.EMAIL, "email_address", "sample@email.com"),
],
)
def test_notification_returns_400_and_for_schema_problems(
@@ -446,7 +445,7 @@ def test_notification_returns_400_and_for_schema_problems(
)
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
@@ -491,7 +490,7 @@ def test_post_email_notification_returns_201(
resp_json = json.loads(response.get_data(as_text=True))
assert validate(resp_json, post_email_response) == resp_json
notification = Notification.query.one()
assert notification.status == NOTIFICATION_CREATED
assert notification.status == NotificationStatus.CREATED
assert resp_json["id"] == str(notification.id)
assert resp_json["reference"] == reference
assert notification.reference is None
@@ -503,11 +502,11 @@ def test_post_email_notification_returns_201(
assert resp_json["content"][
"subject"
] == sample_email_template_with_placeholders.subject.replace("((name))", "Bob")
assert resp_json["content"]["from_email"] == "{}@{}".format(
sample_email_template_with_placeholders.service.email_from,
current_app.config["NOTIFY_EMAIL_DOMAIN"],
assert resp_json["content"]["from_email"] == (
f"{sample_email_template_with_placeholders.service.email_from}@"
f"{current_app.config['NOTIFY_EMAIL_DOMAIN']}"
)
assert "v2/notifications/{}".format(notification.id) in resp_json["uri"]
assert f"v2/notifications/{notification.id}" in resp_json["uri"]
assert resp_json["template"]["id"] == str(
sample_email_template_with_placeholders.id
)
@@ -516,12 +515,9 @@ def test_post_email_notification_returns_201(
== sample_email_template_with_placeholders.version
)
assert (
"services/{}/templates/{}".format(
str(sample_email_template_with_placeholders.service_id),
str(sample_email_template_with_placeholders.id),
)
in resp_json["template"]["uri"]
)
f"services/{sample_email_template_with_placeholders.service_id}/templates/"
f"{sample_email_template_with_placeholders.id}"
) in resp_json["template"]["uri"]
assert not resp_json["scheduled_for"]
assert mocked.called
@@ -529,21 +525,21 @@ def test_post_email_notification_returns_201(
@pytest.mark.parametrize(
"recipient, notification_type",
[
("simulate-delivered@notifications.service.gov.uk", EMAIL_TYPE),
("simulate-delivered-2@notifications.service.gov.uk", EMAIL_TYPE),
("simulate-delivered-3@notifications.service.gov.uk", EMAIL_TYPE),
("+14254147167", "sms"),
("+14254147755", "sms"),
("simulate-delivered@notifications.service.gov.uk", NotificationType.EMAIL),
("simulate-delivered-2@notifications.service.gov.uk", NotificationType.EMAIL),
("simulate-delivered-3@notifications.service.gov.uk", NotificationType.EMAIL),
("+14254147167", NotificationType.SMS),
("+14254147755", NotificationType.SMS),
],
)
def test_should_not_persist_or_send_notification_if_simulated_recipient(
client, recipient, notification_type, sample_email_template, sample_template, mocker
):
apply_async = mocker.patch(
"app.celery.provider_tasks.deliver_{}.apply_async".format(notification_type)
f"app.celery.provider_tasks.deliver_{notification_type}.apply_async"
)
if notification_type == "sms":
if notification_type == NotificationType.SMS:
data = {"phone_number": recipient, "template_id": str(sample_template.id)}
else:
data = {
@@ -556,7 +552,7 @@ def test_should_not_persist_or_send_notification_if_simulated_recipient(
)
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
@@ -570,22 +566,27 @@ def test_should_not_persist_or_send_notification_if_simulated_recipient(
@pytest.mark.parametrize(
"notification_type, key_send_to, send_to",
[
("sms", "phone_number", "2028675309"),
("email", "email_address", "sample@email.com"),
(NotificationType.SMS, "phone_number", "2028675309"),
(NotificationType.EMAIL, "email_address", "sample@email.com"),
],
)
def test_send_notification_uses_priority_queue_when_template_is_marked_as_priority(
client, sample_service, mocker, notification_type, key_send_to, send_to
client,
sample_service,
mocker,
notification_type,
key_send_to,
send_to,
):
mocker.patch(
"app.celery.provider_tasks.deliver_{}.apply_async".format(notification_type)
)
mocker.patch(f"app.celery.provider_tasks.deliver_{notification_type}.apply_async")
sample = create_template(
service=sample_service, template_type=notification_type, process_type="priority"
service=sample_service,
template_type=notification_type,
process_type=TemplateProcessType.PRIORITY,
)
mocked = mocker.patch(
"app.celery.provider_tasks.deliver_{}.apply_async".format(notification_type)
f"app.celery.provider_tasks.deliver_{notification_type}.apply_async"
)
data = {key_send_to: send_to, "template_id": str(sample.id)}
@@ -593,7 +594,7 @@ def test_send_notification_uses_priority_queue_when_template_is_marked_as_priori
auth_header = create_service_authorization_header(service_id=sample.service_id)
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
@@ -607,8 +608,8 @@ def test_send_notification_uses_priority_queue_when_template_is_marked_as_priori
@pytest.mark.parametrize(
"notification_type, key_send_to, send_to",
[
("sms", "phone_number", "2028675309"),
("email", "email_address", "sample@email.com"),
(NotificationType.SMS, "phone_number", "2028675309"),
(NotificationType.EMAIL, "email_address", "sample@email.com"),
],
)
def test_returns_a_429_limit_exceeded_if_rate_limit_exceeded(
@@ -631,7 +632,7 @@ def test_returns_a_429_limit_exceeded_if_rate_limit_exceeded(
auth_header = create_service_authorization_header(service_id=sample.service_id)
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
@@ -655,7 +656,7 @@ def test_post_sms_notification_returns_400_if_not_allowed_to_send_int_sms(
client,
notify_db_session,
):
service = create_service(service_permissions=[SMS_TYPE])
service = create_service(service_permissions=[ServicePermissionType.SMS])
template = create_template(service=service)
data = {"phone_number": "+20-12-1234-1234", "template_id": template.id}
@@ -703,19 +704,29 @@ def test_post_sms_notification_with_archived_reply_to_id_returns_400(
assert response.status_code == 400
resp_json = json.loads(response.get_data(as_text=True))
assert (
"sms_sender_id {} does not exist in database for service id {}".format(
archived_sender.id, sample_template.service_id
)
in resp_json["errors"][0]["message"]
)
f"sms_sender_id {archived_sender.id} does not exist in database for "
f"service id {sample_template.service_id}"
) in resp_json["errors"][0]["message"]
assert "BadRequestError" in resp_json["errors"][0]["error"]
@pytest.mark.parametrize(
"recipient,label,permission_type, notification_type,expected_error",
[
("2028675309", "phone_number", "email", "sms", "text messages"),
("someone@test.com", "email_address", "sms", "email", "emails"),
(
"2028675309",
"phone_number",
ServicePermissionType.EMAIL,
NotificationType.SMS,
"text messages",
),
(
"someone@test.com",
"email_address",
ServicePermissionType.SMS,
NotificationType.EMAIL,
"emails",
),
],
)
def test_post_sms_notification_returns_400_if_not_allowed_to_send_notification(
@@ -737,9 +748,7 @@ def test_post_sms_notification_returns_400_if_not_allowed_to_send_notification(
)
response = client.post(
path="/v2/notifications/{}".format(
sample_template_without_permission.template_type
),
path=f"/v2/notifications/{sample_template_without_permission.template_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
@@ -752,7 +761,7 @@ def test_post_sms_notification_returns_400_if_not_allowed_to_send_notification(
assert error_json["errors"] == [
{
"error": "BadRequestError",
"message": "Service is not allowed to send {}".format(expected_error),
"message": f"Service is not allowed to send {expected_error}",
}
]
@@ -762,17 +771,22 @@ def test_post_sms_notification_returns_400_if_number_not_in_guest_list(
notify_db_session, client, restricted
):
service = create_service(
restricted=restricted, service_permissions=[SMS_TYPE, INTERNATIONAL_SMS_TYPE]
restricted=restricted,
service_permissions=[
ServicePermissionType.SMS,
ServicePermissionType.INTERNATIONAL_SMS,
],
)
template = create_template(service=service)
create_api_key(service=service, key_type="team")
create_api_key(service=service, key_type=KeyType.TEAM)
data = {
"phone_number": "+327700900855",
"template_id": template.id,
}
auth_header = create_service_authorization_header(
service_id=service.id, key_type="team"
service_id=service.id,
key_type=KeyType.TEAM,
)
response = client.post(
@@ -856,11 +870,13 @@ def test_post_notification_raises_bad_request_if_not_valid_notification_type(
assert "The requested URL was not found on the server." in error_json["message"]
@pytest.mark.parametrize("notification_type", ["sms", "email"])
@pytest.mark.parametrize(
"notification_type", [NotificationType.SMS, NotificationType.EMAIL]
)
def test_post_notification_with_wrong_type_of_sender(
client, sample_template, sample_email_template, notification_type, fake_uuid
):
if notification_type == EMAIL_TYPE:
if notification_type == NotificationType.EMAIL:
template = sample_email_template
form_label = "sms_sender_id"
data = {
@@ -868,7 +884,7 @@ def test_post_notification_with_wrong_type_of_sender(
"template_id": str(sample_email_template.id),
form_label: fake_uuid,
}
elif notification_type == SMS_TYPE:
elif notification_type == ServicePermissionType.SMS:
template = sample_template
form_label = "email_reply_to_id"
data = {
@@ -879,14 +895,14 @@ def test_post_notification_with_wrong_type_of_sender(
auth_header = create_service_authorization_header(service_id=template.service_id)
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
assert response.status_code == 400
resp_json = json.loads(response.get_data(as_text=True))
assert (
"Additional properties are not allowed ({} was unexpected)".format(form_label)
f"Additional properties are not allowed ({form_label} was unexpected)"
in resp_json["errors"][0]["message"]
)
assert "ValidationError" in resp_json["errors"][0]["error"]
@@ -943,11 +959,9 @@ def test_post_email_notification_with_invalid_reply_to_id_returns_400(
assert response.status_code == 400
resp_json = json.loads(response.get_data(as_text=True))
assert (
"email_reply_to_id {} does not exist in database for service id {}".format(
fake_uuid, sample_email_template.service_id
)
in resp_json["errors"][0]["message"]
)
f"email_reply_to_id {fake_uuid} does not exist in database for service id "
f"{sample_email_template.service_id}"
) in resp_json["errors"][0]["message"]
assert "BadRequestError" in resp_json["errors"][0]["error"]
@@ -977,11 +991,9 @@ def test_post_email_notification_with_archived_reply_to_id_returns_400(
assert response.status_code == 400
resp_json = json.loads(response.get_data(as_text=True))
assert (
"email_reply_to_id {} does not exist in database for service id {}".format(
archived_reply_to.id, sample_email_template.service_id
)
in resp_json["errors"][0]["message"]
)
f"email_reply_to_id {archived_reply_to.id} does not exist in database for "
f"service id {sample_email_template.service_id}"
) in resp_json["errors"][0]["message"]
assert "BadRequestError" in resp_json["errors"][0]["error"]
@@ -1000,11 +1012,11 @@ def test_post_email_notification_with_archived_reply_to_id_returns_400(
def test_post_notification_with_document_upload(
client, notify_db_session, mocker, csv_param
):
service = create_service(service_permissions=[EMAIL_TYPE])
service = create_service(service_permissions=[ServicePermissionType.EMAIL])
service.contact_link = "contact.me@gov.uk"
template = create_template(
service=service,
template_type="email",
template_type=TemplateType.EMAIL,
content="Document 1: ((first_link)). Document 2: ((second_link))",
)
@@ -1042,8 +1054,8 @@ def test_post_notification_with_document_upload(
]
notification = Notification.query.one()
assert notification.status == NOTIFICATION_CREATED
assert notification.status == NotificationStatus.CREATED
assert notification.personalisation == {
"first_link": "abababab-link",
"second_link": "cdcdcdcd-link",
@@ -1059,10 +1071,12 @@ def test_post_notification_with_document_upload(
def test_post_notification_with_document_upload_simulated(
client, notify_db_session, mocker
):
service = create_service(service_permissions=[EMAIL_TYPE])
service = create_service(service_permissions=[ServicePermissionType.EMAIL])
service.contact_link = "contact.me@gov.uk"
template = create_template(
service=service, template_type="email", content="Document: ((document))"
service=service,
template_type=TemplateType.EMAIL,
content="Document: ((document))",
)
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
@@ -1096,9 +1110,11 @@ def test_post_notification_with_document_upload_simulated(
def test_post_notification_without_document_upload_permission(
client, notify_db_session, mocker
):
service = create_service(service_permissions=[EMAIL_TYPE])
service = create_service(service_permissions=[ServicePermissionType.EMAIL])
template = create_template(
service=service, template_type="email", content="Document: ((document))"
service=service,
template_type=TemplateType.EMAIL,
content="Document: ((document))",
)
mocker.patch("app.celery.provider_tasks.deliver_email.apply_async")
@@ -1140,21 +1156,24 @@ def test_post_notification_returns_400_when_get_json_throws_exception(
@pytest.mark.parametrize(
"notification_type, content_type",
[
("email", "application/json"),
("email", "application/text"),
("sms", "application/json"),
("sms", "application/text"),
(NotificationType.EMAIL, "application/json"),
(NotificationType.EMAIL, "application/text"),
(NotificationType.SMS, "application/json"),
(NotificationType.SMS, "application/text"),
],
)
def test_post_notification_when_payload_is_invalid_json_returns_400(
client, sample_service, notification_type, content_type
client,
sample_service,
notification_type,
content_type,
):
auth_header = create_service_authorization_header(service_id=sample_service.id)
payload_not_json = {
"template_id": "dont-convert-to-json",
}
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=payload_not_json,
headers=[("Content-Type", content_type), auth_header],
)
@@ -1165,51 +1184,67 @@ def test_post_notification_when_payload_is_invalid_json_returns_400(
assert error_msg == "Invalid JSON supplied in POST data"
@pytest.mark.parametrize("notification_type", ["email", "sms"])
@pytest.mark.parametrize(
"notification_type",
[
NotificationType.EMAIL,
NotificationType.SMS,
],
)
def test_post_notification_returns_201_when_content_type_is_missing_but_payload_is_valid_json(
client, sample_service, notification_type, mocker
):
template = create_template(service=sample_service, template_type=notification_type)
mocker.patch(
"app.celery.provider_tasks.deliver_{}.apply_async".format(notification_type)
)
mocker.patch(f"app.celery.provider_tasks.deliver_{notification_type}.apply_async")
auth_header = create_service_authorization_header(service_id=sample_service.id)
valid_json = {
"template_id": str(template.id),
}
if notification_type == "email":
if notification_type == NotificationType.EMAIL:
valid_json.update({"email_address": sample_service.users[0].email_address})
else:
valid_json.update({"phone_number": "+447700900855"})
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(valid_json),
headers=[auth_header],
)
assert response.status_code == 201
@pytest.mark.parametrize("notification_type", ["email", "sms"])
@pytest.mark.parametrize(
"notification_type",
[
NotificationType.EMAIL,
NotificationType.SMS,
],
)
def test_post_email_notification_when_data_is_empty_returns_400(
client, sample_service, notification_type
):
auth_header = create_service_authorization_header(service_id=sample_service.id)
data = None
response = client.post(
path="/v2/notifications/{}".format(notification_type),
path=f"/v2/notifications/{notification_type}",
data=json.dumps(data),
headers=[("Content-Type", "application/json"), auth_header],
)
error_msg = json.loads(response.get_data(as_text=True))["errors"][0]["message"]
assert response.status_code == 400
if notification_type == "sms":
if notification_type == NotificationType.SMS:
assert error_msg == "phone_number is a required property"
else:
assert error_msg == "email_address is a required property"
@pytest.mark.parametrize("notification_type", ("email", "sms"))
@pytest.mark.parametrize(
"notification_type",
(
NotificationType.EMAIL,
NotificationType.SMS,
),
)
def test_post_notifications_saves_email_or_sms_to_queue(
client, notify_db_session, mocker, notification_type
):
@@ -1238,7 +1273,7 @@ def test_post_notifications_saves_email_or_sms_to_queue(
}
data.update(
{"email_address": "joe.citizen@example.com"}
) if notification_type == EMAIL_TYPE else data.update(
) if notification_type == NotificationType.EMAIL else data.update(
{"phone_number": "+447700900855"}
)
@@ -1271,7 +1306,13 @@ def test_post_notifications_saves_email_or_sms_to_queue(
botocore.parsers.ResponseParserError("exceeded max HTTP body length"),
],
)
@pytest.mark.parametrize("notification_type", ("email", "sms"))
@pytest.mark.parametrize(
"notification_type",
(
NotificationType.EMAIL,
NotificationType.SMS,
),
)
def test_post_notifications_saves_email_or_sms_normally_if_saving_to_queue_fails(
client, notify_db_session, mocker, notification_type, exception
):
@@ -1301,7 +1342,7 @@ def test_post_notifications_saves_email_or_sms_normally_if_saving_to_queue_fails
}
data.update(
{"email_address": "joe.citizen@example.com"}
) if notification_type == EMAIL_TYPE else data.update(
) if notification_type == NotificationType.EMAIL else data.update(
{"phone_number": "+447700900855"}
)
@@ -1329,7 +1370,13 @@ def test_post_notifications_saves_email_or_sms_normally_if_saving_to_queue_fails
assert Notification.query.count() == 1
@pytest.mark.parametrize("notification_type", ("email", "sms"))
@pytest.mark.parametrize(
"notification_type",
(
NotificationType.EMAIL,
NotificationType.SMS,
),
)
def test_post_notifications_doesnt_use_save_queue_for_test_notifications(
client, notify_db_session, mocker, notification_type
):
@@ -1357,7 +1404,7 @@ def test_post_notifications_doesnt_use_save_queue_for_test_notifications(
}
data.update(
{"email_address": "joe.citizen@example.com"}
) if notification_type == EMAIL_TYPE else data.update(
) if notification_type == NotificationType.EMAIL else data.update(
{"phone_number": "+447700900855"}
)
response = client.post(
@@ -1366,7 +1413,8 @@ def test_post_notifications_doesnt_use_save_queue_for_test_notifications(
headers=[
("Content-Type", "application/json"),
create_service_authorization_header(
service_id=service.id, key_type="test"
service_id=service.id,
key_type=KeyType.TEST,
),
],
)