mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-16 20:48:37 -04:00
merge from main
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user