merge from main

This commit is contained in:
Kenneth Kehl
2024-12-30 09:11:11 -08:00
12 changed files with 10432 additions and 412 deletions

View File

@@ -7,11 +7,7 @@ from celery.exceptions import MaxRetriesExceededError
import app
from app.celery import provider_tasks
from app.celery.provider_tasks import (
check_sms_delivery_receipt,
deliver_email,
deliver_sms,
)
from app.celery.provider_tasks import deliver_email, deliver_sms
from app.clients.email import EmailClientNonRetryableException
from app.clients.email.aws_ses import (
AwsSesClientException,
@@ -27,110 +23,10 @@ def test_should_have_decorated_tasks_functions():
assert deliver_email.__wrapped__.__name__ == "deliver_email"
def test_should_check_delivery_receipts_success(sample_notification, mocker):
mocker.patch("app.delivery.send_to_providers.send_sms_to_provider")
mocker.patch(
"app.celery.provider_tasks.aws_cloudwatch_client.is_localstack",
return_value=False,
)
mocker.patch(
"app.celery.provider_tasks.aws_cloudwatch_client.check_sms",
return_value=("success", "okay", "AT&T"),
)
mock_sanitize = mocker.patch(
"app.celery.provider_tasks.sanitize_successful_notification_by_id"
)
check_sms_delivery_receipt(
"message_id", sample_notification.id, "2024-10-20 00:00:00+0:00"
)
# This call should be made if the message was successfully delivered
mock_sanitize.assert_called_once()
def test_should_check_delivery_receipts_failure(sample_notification, mocker):
mocker.patch("app.delivery.send_to_providers.send_sms_to_provider")
mocker.patch(
"app.celery.provider_tasks.aws_cloudwatch_client.is_localstack",
return_value=False,
)
mock_update = mocker.patch(
"app.celery.provider_tasks.update_notification_status_by_id"
)
mocker.patch(
"app.celery.provider_tasks.aws_cloudwatch_client.check_sms",
return_value=("failure", "not okay", "AT&T"),
)
mock_sanitize = mocker.patch(
"app.celery.provider_tasks.sanitize_successful_notification_by_id"
)
check_sms_delivery_receipt(
"message_id", sample_notification.id, "2024-10-20 00:00:00+0:00"
)
mock_sanitize.assert_not_called()
mock_update.assert_called_once()
def test_should_check_delivery_receipts_client_error(sample_notification, mocker):
mocker.patch("app.delivery.send_to_providers.send_sms_to_provider")
mocker.patch(
"app.celery.provider_tasks.aws_cloudwatch_client.is_localstack",
return_value=False,
)
mock_update = mocker.patch(
"app.celery.provider_tasks.update_notification_status_by_id"
)
error_response = {"Error": {"Code": "SomeCode", "Message": "Some Message"}}
operation_name = "SomeOperation"
mocker.patch(
"app.celery.provider_tasks.aws_cloudwatch_client.check_sms",
side_effect=ClientError(error_response, operation_name),
)
mock_sanitize = mocker.patch(
"app.celery.provider_tasks.sanitize_successful_notification_by_id"
)
try:
check_sms_delivery_receipt(
"message_id", sample_notification.id, "2024-10-20 00:00:00+0:00"
)
assert 1 == 0
except ClientError:
mock_sanitize.assert_not_called()
mock_update.assert_called_once()
def test_should_check_delivery_receipts_ntfe(sample_notification, mocker):
mocker.patch("app.delivery.send_to_providers.send_sms_to_provider")
mocker.patch(
"app.celery.provider_tasks.aws_cloudwatch_client.is_localstack",
return_value=False,
)
mock_update = mocker.patch(
"app.celery.provider_tasks.update_notification_status_by_id"
)
mocker.patch(
"app.celery.provider_tasks.aws_cloudwatch_client.check_sms",
side_effect=NotificationTechnicalFailureException(),
)
mock_sanitize = mocker.patch(
"app.celery.provider_tasks.sanitize_successful_notification_by_id"
)
try:
check_sms_delivery_receipt(
"message_id", sample_notification.id, "2024-10-20 00:00:00+0:00"
)
assert 1 == 0
except NotificationTechnicalFailureException:
mock_sanitize.assert_not_called()
mock_update.assert_called_once()
def test_should_call_send_sms_to_provider_from_deliver_sms_task(
sample_notification, mocker
):
mocker.patch("app.delivery.send_to_providers.send_sms_to_provider")
mocker.patch("app.celery.provider_tasks.check_sms_delivery_receipt")
deliver_sms(sample_notification.id)
app.delivery.send_to_providers.send_sms_to_provider.assert_called_with(

View File

@@ -23,6 +23,8 @@ from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTick
from tests.app import load_example_csv
from tests.app.db import create_job, create_notification, create_template
CHECK_JOB_STATUS_TOO_OLD_MINUTES = 241
def test_should_call_delete_codes_on_delete_verify_codes_task(
notify_db_session, mocker
@@ -108,8 +110,9 @@ def test_check_job_status_task_calls_process_incomplete_jobs(mocker, sample_temp
job = create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
processing_started=utc_now()
- timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.IN_PROGRESS,
)
create_notification(template=sample_template, job=job)
@@ -125,9 +128,10 @@ def test_check_job_status_task_calls_process_incomplete_jobs_when_scheduled_job_
job = create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(hours=2),
scheduled_for=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(hours=5),
scheduled_for=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
processing_started=utc_now()
- timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.IN_PROGRESS,
)
check_job_status()
@@ -142,8 +146,8 @@ def test_check_job_status_task_calls_process_incomplete_jobs_for_pending_schedul
job = create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(hours=2),
scheduled_for=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(hours=5),
scheduled_for=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.PENDING,
)
@@ -175,17 +179,19 @@ def test_check_job_status_task_calls_process_incomplete_jobs_for_multiple_jobs(
job = create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(hours=2),
scheduled_for=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(hours=5),
scheduled_for=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
processing_started=utc_now()
- timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.IN_PROGRESS,
)
job_2 = create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(hours=2),
scheduled_for=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(hours=5),
scheduled_for=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
processing_started=utc_now()
- timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.IN_PROGRESS,
)
check_job_status()
@@ -200,23 +206,24 @@ def test_check_job_status_task_only_sends_old_tasks(mocker, sample_template):
job = create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(hours=2),
scheduled_for=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(hours=5),
scheduled_for=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
processing_started=utc_now()
- timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.IN_PROGRESS,
)
create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=29),
created_at=utc_now() - timedelta(minutes=300),
processing_started=utc_now() - timedelta(minutes=239),
job_status=JobStatus.IN_PROGRESS,
)
create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(minutes=50),
scheduled_for=utc_now() - timedelta(minutes=29),
created_at=utc_now() - timedelta(minutes=300),
scheduled_for=utc_now() - timedelta(minutes=239),
job_status=JobStatus.PENDING,
)
check_job_status()
@@ -230,16 +237,17 @@ def test_check_job_status_task_sets_jobs_to_error(mocker, sample_template):
job = create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(hours=2),
scheduled_for=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(hours=5),
scheduled_for=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
processing_started=utc_now()
- timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.IN_PROGRESS,
)
job_2 = create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=29),
created_at=utc_now() - timedelta(minutes=300),
processing_started=utc_now() - timedelta(minutes=239),
job_status=JobStatus.IN_PROGRESS,
)
check_job_status()
@@ -311,16 +319,18 @@ def test_check_job_status_task_does_not_raise_error(sample_template):
create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(hours=2),
scheduled_for=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(hours=5),
scheduled_for=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
processing_started=utc_now()
- timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.FINISHED,
)
create_job(
template=sample_template,
notification_count=3,
created_at=utc_now() - timedelta(minutes=31),
processing_started=utc_now() - timedelta(minutes=31),
created_at=utc_now() - timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
processing_started=utc_now()
- timedelta(minutes=CHECK_JOB_STATUS_TOO_OLD_MINUTES),
job_status=JobStatus.FINISHED,
)

View File

@@ -1,8 +1,9 @@
import json
import pytest
from flask import current_app
from app import aws_cloudwatch_client
from app.utils import utc_now
def test_check_sms_no_event_error_condition(notify_api, mocker):
@@ -74,51 +75,6 @@ def test_warn_if_dev_is_opted_out(response, notify_id, expected_message):
assert result == expected_message
def test_check_sms_success(notify_api, mocker):
aws_cloudwatch_client.init_app(current_app)
boto_mock = mocker.patch.object(aws_cloudwatch_client, "_client", create=True)
boto_mock.filter_log_events.side_effect = side_effect
mocker.patch.dict(
"os.environ",
{"SES_DOMAIN_ARN": "arn:aws:ses:us-west-2:12345:identity/ses-xxx.xxx.xxx.xxx"},
)
message_id = "succeed"
notification_id = "ccc"
created_at = utc_now()
with notify_api.app_context():
aws_cloudwatch_client.check_sms(message_id, notification_id, created_at)
# We check the 'success' log group first and if we find the message_id, we are done, so there is only 1 call
assert boto_mock.filter_log_events.call_count == 1
mock_call = str(boto_mock.filter_log_events.mock_calls[0])
assert "Failure" not in mock_call
assert "succeed" in mock_call
assert "notification.messageId" in mock_call
def test_check_sms_failure(notify_api, mocker):
aws_cloudwatch_client.init_app(current_app)
boto_mock = mocker.patch.object(aws_cloudwatch_client, "_client", create=True)
boto_mock.filter_log_events.side_effect = side_effect
mocker.patch.dict(
"os.environ",
{"SES_DOMAIN_ARN": "arn:aws:ses:us-west-2:12345:identity/ses-xxx.xxx.xxx.xxx"},
)
message_id = "fail"
notification_id = "bbb"
created_at = utc_now()
with notify_api.app_context():
aws_cloudwatch_client.check_sms(message_id, notification_id, created_at)
# We check the 'success' log group and find nothing, so we then check the 'fail' log group -- two calls.
assert boto_mock.filter_log_events.call_count == 2
mock_call = str(boto_mock.filter_log_events.mock_calls[1])
assert "Failure" in mock_call
assert "fail" in mock_call
assert "notification.messageId" in mock_call
def test_extract_account_number_gov_cloud():
domain_arn = "arn:aws-us-gov:ses:us-gov-west-1:12345:identity/ses-abc.xxx.xxx.xxx"
actual_account_number = aws_cloudwatch_client._extract_account_number(domain_arn)
@@ -133,3 +89,65 @@ def test_extract_account_number_gov_staging():
assert len(actual_account_number) == 6
expected_account_number = "12345"
assert actual_account_number[4] == expected_account_number
def test_check_delivery_receipts():
pass
def test_aws_value_or_default():
event = {
"delivery": {"phoneCarrier": "AT&T"},
"notification": {"timestamp": "2024-01-01T:12:00:00Z"},
}
assert (
aws_cloudwatch_client._aws_value_or_default(event, "delivery", "phoneCarrier")
== "AT&T"
)
assert (
aws_cloudwatch_client._aws_value_or_default(
event, "delivery", "providerResponse"
)
== ""
)
assert (
aws_cloudwatch_client._aws_value_or_default(event, "notification", "timestamp")
== "2024-01-01T:12:00:00Z"
)
assert (
aws_cloudwatch_client._aws_value_or_default(event, "nonexistent", "field") == ""
)
def test_event_to_db_format_with_missing_fields():
event = {
"notification": {"messageId": "12345"},
"status": "UNKNOWN",
"delivery": {},
}
result = aws_cloudwatch_client.event_to_db_format(event)
assert result == {
"notification.messageId": "12345",
"status": "UNKNOWN",
"delivery.phoneCarrier": "",
"delivery.providerResponse": "",
"@timestamp": "",
}
def test_event_to_db_format_with_string_input():
event = json.dumps(
{
"notification": {"messageId": "67890", "timestamp": "2024-01-01T14:00:00Z"},
"status": "FAILED",
"delivery": {"phoneCarrier": "Verizon", "providerResponse": "Error"},
}
)
result = aws_cloudwatch_client.event_to_db_format(event)
assert result == {
"notification.messageId": "67890",
"status": "FAILED",
"delivery.phoneCarrier": "Verizon",
"delivery.providerResponse": "Error",
"@timestamp": "2024-01-01T14:00:00Z",
}

View File

@@ -1,6 +1,7 @@
import uuid
from datetime import date, datetime, timedelta
from functools import partial
from unittest.mock import MagicMock, patch
import pytest
from freezegun import freeze_time
@@ -19,6 +20,7 @@ from app.dao.notifications_dao import (
dao_get_notification_history_by_reference,
dao_get_notifications_by_recipient_or_reference,
dao_timeout_notifications,
dao_update_delivery_receipts,
dao_update_notification,
dao_update_notifications_by_reference,
get_notification_by_id,
@@ -2000,6 +2002,34 @@ def test_notifications_not_yet_sent_return_no_rows(sample_service, notification_
assert len(results) == 0
def test_update_delivery_receipts(mocker):
mock_session = mocker.patch("app.dao.notifications_dao.db.session")
receipts = [
'{"notification.messageId": "msg1", "delivery.phoneCarrier": "carrier1", "delivery.providerResponse": "resp1", "@timestamp": "2024-01-01T12:00:00"}', # noqa
'{"notification.messageId": "msg2", "delivery.phoneCarrier": "carrier2", "delivery.providerResponse": "resp2", "@timestamp": "2024-01-01T13:00:00"}', # noqa
]
delivered = True
mock_update = MagicMock()
mock_where = MagicMock()
mock_values = MagicMock()
mock_update.where.return_value = mock_where
mock_where.values.return_value = mock_values
mock_session.execute.return_value = None
with patch("app.dao.notifications_dao.update", return_value=mock_update):
dao_update_delivery_receipts(receipts, delivered)
mock_update.where.assert_called_once()
mock_where.values.assert_called_once()
mock_session.execute.assert_called_once_with(mock_values)
mock_session.commit.assert_called_once()
args, kwargs = mock_where.values.call_args
assert "carrier" in kwargs
assert "status" in kwargs
assert "sent_at" in kwargs
assert "provider_response" in kwargs
@pytest.mark.parametrize(
"created_at_utc,date_to_check,expected_count",
[