Merge branch 'main' of https://github.com/GSA/notifications-api into invite-expiration-fix

This commit is contained in:
Andrew Shumway
2025-01-16 08:59:38 -07:00
17 changed files with 278 additions and 46 deletions

View File

@@ -1,17 +1,20 @@
import json
from collections import namedtuple
from datetime import timedelta
from unittest import mock
from unittest.mock import ANY, call
from unittest.mock import ANY, MagicMock, call
import pytest
from app.celery import scheduled_tasks
from app.celery.scheduled_tasks import (
batch_insert_notifications,
check_for_missing_rows_in_completed_jobs,
check_for_services_with_high_failure_rates_or_sending_to_tv_numbers,
check_job_status,
delete_verify_codes,
expire_or_delete_invitations,
process_delivery_receipts,
replay_created_notifications,
run_scheduled_jobs,
)
@@ -308,10 +311,10 @@ def test_replay_created_notifications(notify_db_session, sample_service, mocker)
replay_created_notifications()
email_delivery_queue.assert_called_once_with(
[str(old_email.id)], queue="send-email-tasks"
[str(old_email.id)], queue="send-email-tasks", countdown=60
)
sms_delivery_queue.assert_called_once_with(
[str(old_sms.id)], queue="send-sms-tasks"
[str(old_sms.id)], queue="send-sms-tasks", countdown=60
)
@@ -523,3 +526,101 @@ def test_check_for_services_with_high_failure_rates_or_sending_to_tv_numbers(
technical_ticket=True,
)
mock_send_ticket_to_zendesk.assert_called_once()
def test_batch_insert_with_valid_notifications(mocker):
mocker.patch("app.celery.scheduled_tasks.dao_batch_insert_notifications")
rs = MagicMock()
mocker.patch("app.celery.scheduled_tasks.redis_store", rs)
notifications = [
{"id": 1, "notification_status": "pending"},
{"id": 2, "notification_status": "pending"},
]
serialized_notifications = [json.dumps(n).encode("utf-8") for n in notifications]
pipeline_mock = MagicMock()
rs.pipeline.return_value.__enter__.return_value = pipeline_mock
rs.llen.return_value = len(notifications)
rs.lpop.side_effect = serialized_notifications
batch_insert_notifications()
rs.llen.assert_called_once_with("message_queue")
rs.lpop.assert_called_with("message_queue")
def test_batch_insert_with_expired_notifications(mocker):
expired_time = utc_now() - timedelta(minutes=2)
mocker.patch(
"app.celery.scheduled_tasks.dao_batch_insert_notifications",
side_effect=Exception("DB Error"),
)
rs = MagicMock()
mocker.patch("app.celery.scheduled_tasks.redis_store", rs)
notifications = [
{
"id": 1,
"notification_status": "pending",
"created_at": utc_now().isoformat(),
},
{
"id": 2,
"notification_status": "pending",
"created_at": expired_time.isoformat(),
},
]
serialized_notifications = [json.dumps(n).encode("utf-8") for n in notifications]
pipeline_mock = MagicMock()
rs.pipeline.return_value.__enter__.return_value = pipeline_mock
rs.llen.return_value = len(notifications)
rs.lpop.side_effect = serialized_notifications
batch_insert_notifications()
rs.llen.assert_called_once_with("message_queue")
rs.rpush.assert_called_once()
requeued_notification = json.loads(rs.rpush.call_args[0][1])
assert requeued_notification["id"] == 1
def test_batch_insert_with_malformed_notifications(mocker):
rs = MagicMock()
mocker.patch("app.celery.scheduled_tasks.redis_store", rs)
malformed_data = b"not_a_valid_json"
pipeline_mock = MagicMock()
rs.pipeline.return_value.__enter__.return_value = pipeline_mock
rs.llen.return_value = 1
rs.lpop.side_effect = [malformed_data]
with pytest.raises(json.JSONDecodeError):
batch_insert_notifications()
rs.llen.assert_called_once_with("message_queue")
rs.rpush.assert_not_called()
def test_process_delivery_receipts_success(mocker):
dao_update_mock = mocker.patch(
"app.celery.scheduled_tasks.dao_update_delivery_receipts"
)
cloudwatch_mock = mocker.patch("app.celery.scheduled_tasks.AwsCloudwatchClient")
cloudwatch_mock.return_value.check_delivery_receipts.return_value = (
range(2000),
range(500),
)
current_app_mock = mocker.patch("app.celery.scheduled_tasks.current_app")
current_app_mock.return_value = MagicMock()
processor = MagicMock()
processor.process_delivery_receipts = process_delivery_receipts
processor.retry = MagicMock()
processor.process_delivery_receipts()
assert dao_update_mock.call_count == 3
dao_update_mock.assert_any_call(list(range(1000)), True)
dao_update_mock.assert_any_call(list(range(1000, 2000)), True)
dao_update_mock.assert_any_call(list(range(500)), False)
processor.retry.assert_not_called()

View File

@@ -434,7 +434,7 @@ def test_should_send_template_to_correct_sms_task_and_persist(
assert persisted_notification.personalisation == {}
assert persisted_notification.notification_type == NotificationType.SMS
mocked_deliver_sms.assert_called_once_with(
[str(persisted_notification.id)], queue="send-sms-tasks"
[str(persisted_notification.id)], queue="send-sms-tasks", countdown=60
)
@@ -470,7 +470,7 @@ def test_should_save_sms_if_restricted_service_and_valid_number(
assert not persisted_notification.personalisation
assert persisted_notification.notification_type == NotificationType.SMS
provider_tasks.deliver_sms.apply_async.assert_called_once_with(
[str(persisted_notification.id)], queue="send-sms-tasks"
[str(persisted_notification.id)], queue="send-sms-tasks", countdown=60
)
@@ -598,7 +598,7 @@ def test_should_save_sms_template_to_and_persist_with_job_id(sample_job, mocker)
assert persisted_notification.notification_type == NotificationType.SMS
provider_tasks.deliver_sms.apply_async.assert_called_once_with(
[str(persisted_notification.id)], queue="send-sms-tasks"
[str(persisted_notification.id)], queue="send-sms-tasks", countdown=60
)
@@ -938,7 +938,7 @@ def test_save_sms_uses_sms_sender_reply_to_text(mocker, notify_db_session):
notification = _notification_json(template, to="2028675301")
mocker.patch("app.celery.provider_tasks.deliver_sms.apply_async")
notification_id = uuid.uuid4()
notification_id = str(uuid.uuid4())
save_sms(
service.id,
notification_id,

View File

@@ -263,7 +263,9 @@ def test_send_notification_to_queue(
send_notification_to_queue(notification=notification, queue=requested_queue)
mocked.assert_called_once_with([str(notification.id)], queue=expected_queue)
mocked.assert_called_once_with(
[str(notification.id)], queue=expected_queue, countdown=60
)
def test_send_notification_to_queue_throws_exception_deletes_notification(
@@ -276,8 +278,7 @@ def test_send_notification_to_queue_throws_exception_deletes_notification(
with pytest.raises(Boto3Error):
send_notification_to_queue(sample_notification, False)
mocked.assert_called_once_with(
[(str(sample_notification.id))],
queue="send-sms-tasks",
[(str(sample_notification.id))], queue="send-sms-tasks", countdown=60
)
assert _get_notification_query_count() == 0

View File

@@ -73,7 +73,7 @@ def test_create_invited_org_user(
# assert len(notification.personalisation["url"]) > len(expected_start_of_invite_url)
mocked.assert_called_once_with(
[(str(notification.id))], queue="notify-internal-tasks"
[(str(notification.id))], queue="notify-internal-tasks", countdown=60
)

View File

@@ -150,7 +150,9 @@ def test_send_notification_with_placeholders_replaced(
{"template_version": sample_email_template_with_placeholders.version}
)
mocked.assert_called_once_with([notification_id], queue="send-email-tasks")
mocked.assert_called_once_with(
[notification_id], queue="send-email-tasks", countdown=60
)
assert response.status_code == 201
assert response_data["body"] == "Hello Jo\nThis is an email from GOV.UK"
assert response_data["subject"] == "Jo"
@@ -420,7 +422,9 @@ def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker
response_data = json.loads(response.data)["data"]
notification_id = response_data["notification"]["id"]
mocked.assert_called_once_with([notification_id], queue="send-sms-tasks")
mocked.assert_called_once_with(
[notification_id], queue="send-sms-tasks", countdown=60
)
assert response.status_code == 201
assert notification_id
assert "subject" not in response_data
@@ -476,7 +480,7 @@ def test_should_allow_valid_email_notification(
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"
[notification_id], queue="send-email-tasks", countdown=60
)
assert response.status_code == 201
@@ -620,7 +624,7 @@ def test_should_send_email_if_team_api_key_and_a_service_user(
)
app.celery.provider_tasks.deliver_email.apply_async.assert_called_once_with(
[fake_uuid], queue="send-email-tasks"
[fake_uuid], queue="send-email-tasks", countdown=60
)
assert response.status_code == 201
@@ -658,7 +662,7 @@ def test_should_send_sms_to_anyone_with_test_key(
],
)
app.celery.provider_tasks.deliver_sms.apply_async.assert_called_once_with(
[fake_uuid], queue="send-sms-tasks"
[fake_uuid], queue="send-sms-tasks", countdown=60
)
assert response.status_code == 201
@@ -697,7 +701,7 @@ def test_should_send_email_to_anyone_with_test_key(
)
app.celery.provider_tasks.deliver_email.apply_async.assert_called_once_with(
[fake_uuid], queue="send-email-tasks"
[fake_uuid], queue="send-email-tasks", countdown=60
)
assert response.status_code == 201
@@ -735,7 +739,7 @@ def test_should_send_sms_if_team_api_key_and_a_service_user(
)
app.celery.provider_tasks.deliver_sms.apply_async.assert_called_once_with(
[fake_uuid], queue="send-sms-tasks"
[fake_uuid], queue="send-sms-tasks", countdown=60
)
assert response.status_code == 201
@@ -792,7 +796,7 @@ def test_should_persist_notification(
],
)
mocked.assert_called_once_with([fake_uuid], queue=queue_name)
mocked.assert_called_once_with([fake_uuid], queue=queue_name, countdown=60)
assert response.status_code == 201
notification = notifications_dao.get_notification_by_id(fake_uuid)
@@ -853,7 +857,7 @@ def test_should_delete_notification_and_return_error_if_redis_fails(
)
assert str(e.value) == "failed to talk to redis"
mocked.assert_called_once_with([fake_uuid], queue=queue_name)
mocked.assert_called_once_with([fake_uuid], queue=queue_name, countdown=60)
assert not notifications_dao.get_notification_by_id(fake_uuid)
assert not NotificationHistory.query.get(fake_uuid)
@@ -1185,7 +1189,9 @@ def test_should_allow_store_original_number_on_sms_notification(
response_data = json.loads(response.data)["data"]
notification_id = response_data["notification"]["id"]
mocked.assert_called_once_with([notification_id], queue="send-sms-tasks")
mocked.assert_called_once_with(
[notification_id], queue="send-sms-tasks", countdown=60
)
assert response.status_code == 201
assert notification_id
notifications = Notification.query.all()

View File

@@ -3025,7 +3025,7 @@ def test_verify_reply_to_email_address_should_send_verification_email(
assert notification.template_id == verify_reply_to_address_email_template.id
assert response["data"] == {"id": str(notification.id)}
mocked.assert_called_once_with(
[str(notification.id)], queue="notify-internal-tasks"
[str(notification.id)], queue="notify-internal-tasks", countdown=60
)
assert (
notification.reply_to_text

View File

@@ -90,7 +90,7 @@ def test_create_invited_user(
)
mocked.assert_called_once_with(
[(str(notification.id))], queue="notify-internal-tasks"
[(str(notification.id))], queue="notify-internal-tasks", countdown=60
)

View File

@@ -664,7 +664,7 @@ def test_send_already_registered_email(
stmt = select(Notification)
notification = db.session.execute(stmt).scalars().first()
mocked.assert_called_once_with(
([str(notification.id)]), queue="notify-internal-tasks"
([str(notification.id)]), queue="notify-internal-tasks", countdown=60
)
assert (
notification.reply_to_text
@@ -703,7 +703,7 @@ def test_send_user_confirm_new_email_returns_204(
stmt = select(Notification)
notification = db.session.execute(stmt).scalars().first()
mocked.assert_called_once_with(
([str(notification.id)]), queue="notify-internal-tasks"
([str(notification.id)]), queue="notify-internal-tasks", countdown=60
)
assert (
notification.reply_to_text

View File

@@ -231,7 +231,7 @@ def test_send_user_sms_code(client, sample_user, sms_code_template, mocker):
assert notification.reply_to_text == notify_service.get_default_sms_sender()
app.celery.provider_tasks.deliver_sms.apply_async.assert_called_once_with(
([str(notification.id)]), queue="notify-internal-tasks"
([str(notification.id)]), queue="notify-internal-tasks", countdown=60
)
@@ -267,7 +267,7 @@ def test_send_user_code_for_sms_with_optional_to_field(
notification = Notification.query.first()
assert notification.to == "1"
app.celery.provider_tasks.deliver_sms.apply_async.assert_called_once_with(
([str(notification.id)]), queue="notify-internal-tasks"
([str(notification.id)]), queue="notify-internal-tasks", countdown=60
)
@@ -349,7 +349,7 @@ def test_send_new_user_email_verification(
notification = Notification.query.first()
assert _get_verify_code_count() == 0
mocked.assert_called_once_with(
([str(notification.id)]), queue="notify-internal-tasks"
([str(notification.id)]), queue="notify-internal-tasks", countdown=60
)
assert (
notification.reply_to_text
@@ -494,7 +494,9 @@ def test_send_user_email_code(
)
assert noti.to == "1"
assert str(noti.template_id) == current_app.config["EMAIL_2FA_TEMPLATE_ID"]
deliver_email.assert_called_once_with([str(noti.id)], queue="notify-internal-tasks")
deliver_email.assert_called_once_with(
[str(noti.id)], queue="notify-internal-tasks", countdown=60
)
@pytest.mark.skip(reason="Broken email functionality")