diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index c1350bdb4..5967046fd 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -3493,10 +3493,21 @@ def test_email_send_fails_approrpiately_when_over_limits( ), ) - # mock that `num_sent_this_year` have already been sent this year - mock_notification_counts_client.get_all_notification_counts_for_year.return_value = { - "sms": 1000, # not used in test but needs a value - "email": num_sent_this_year, + mock_notification_counts_client.get_limit_stats.return_value = { + "email": { + "annual": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": 10000 + - num_sent_this_year + - num_sent_today, # The number of email notifications remaining this year + }, + "daily": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": 1000 - num_sent_today, # The number of email notifications remaining today + }, + } } # mock that we've already sent `emails_sent_today` emails today @@ -3584,14 +3595,23 @@ def test_sms_send_fails_approrpiately_when_over_limits( ["phone number"] + ([mock_get_users_by_service(None)[0]["mobile_number"]] * num_being_sent) ), ) - - # mock that `num_sent_this_year` have already been sent this year - mock_notification_counts_client.get_all_notification_counts_for_year.return_value = { - "sms": num_sent_this_year, - "email": 1000, # not used in test but needs a value + mock_notification_counts_client.get_limit_stats.return_value = { + "sms": { + "annual": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": 10000 + - num_sent_this_year + - num_sent_today, # The number of email notifications remaining this year + }, + "daily": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": 1000 - num_sent_today, # The number of email notifications remaining today + }, + } } - - # mock that we've already sent `emails_sent_today` emails today + # mock that we've already sent `num_sent_today` emails today mock_daily_email_count.return_value = 900 # not used in test but needs a value mock_daily_sms_fragment_count.return_value = num_sent_today @@ -3622,3 +3642,90 @@ def test_sms_send_fails_approrpiately_when_over_limits( assert page.find(attrs={"data-testid": "exceeds-daily"}) is not None else: assert page.find(attrs={"data-testid": "exceeds-daily"}) is None + + @pytest.mark.parametrize( + "num_to_send, remaining_daily, remaining_annual, error_shown", + [ + (2, 2, 2, "none"), + (5, 5, 4, "annual"), + (5, 4, 5, "daily"), + (5, 4, 4, "annual"), + ], + ) + def test_correct_error_displayed( + self, + mocker, + client_request, + mock_get_live_service, # set email_annual_limit and sms_annual_limit to 1000 + mock_get_users_by_service, + mock_get_service_email_template_without_placeholders, + mock_get_template_statistics, + mock_get_job_doesnt_exist, + mock_get_jobs, + mock_s3_set_metadata, + mock_daily_email_count, + mock_notification_counts_client, + fake_uuid, + num_to_send, + remaining_daily, + remaining_annual, + error_shown, + app_, + ): + with set_config(app_, "FF_ANNUAL_LIMIT", True): # REMOVE LINE WHEN FF REMOVED + # mock that `num_sent_this_year` have already been sent this year + mock_notification_counts_client.get_limit_stats.return_value = { + "email": { + "annual": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": remaining_annual, # The number of email notifications remaining this year + }, + "daily": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": remaining_daily, # The number of email notifications remaining today + }, + } + } + + # only change this value when we're expecting an error + if error_shown != "none": + mock_daily_email_count.return_value = 1000 - ( + num_to_send - 1 + ) # svc limit is 1000 - exceeding the daily limit is calculated based off of this + else: + mock_daily_email_count.return_value = 0 # none sent + + mocker.patch( + "app.main.views.send.s3download", + return_value=",\n".join( + ["email address"] + ([mock_get_users_by_service(None)[0]["email_address"]] * num_to_send) + ), + ) + with client_request.session_transaction() as session: + session["file_uploads"] = { + fake_uuid: { + "template_id": fake_uuid, + "notification_count": 1, + "valid": True, + } + } + page = client_request.get( + "main.check_messages", + service_id=SERVICE_ONE_ID, + template_id=fake_uuid, + upload_id=fake_uuid, + original_file_name="valid.csv", + _test_page_title=False, + ) + + if error_shown == "annual": + assert page.find(attrs={"data-testid": "exceeds-annual"}) is not None + assert page.find(attrs={"data-testid": "exceeds-daily"}) is None + elif error_shown == "daily": + assert page.find(attrs={"data-testid": "exceeds-annual"}) is None + assert page.find(attrs={"data-testid": "exceeds-daily"}) is not None + elif error_shown == "none": + assert page.find(attrs={"data-testid": "exceeds-annual"}) is None + assert page.find(attrs={"data-testid": "exceeds-daily"}) is None diff --git a/tests/app/main/views/test_templates.py b/tests/app/main/views/test_templates.py index b48234af1..54df21e58 100644 --- a/tests/app/main/views/test_templates.py +++ b/tests/app/main/views/test_templates.py @@ -1,6 +1,6 @@ from datetime import datetime from functools import partial -from unittest.mock import ANY, MagicMock, Mock +from unittest.mock import ANY, MagicMock, Mock, patch import pytest from flask import url_for @@ -54,6 +54,12 @@ DEFAULT_PROCESS_TYPE = TemplateProcessTypes.BULK.value +@pytest.fixture +def mock_notification_counts_client(): + with patch("app.main.views.templates.notification_counts_client") as mock: + yield mock + + class TestRedisPreviewUtilities: def test_set_get(self, fake_uuid, mocker): mock_redis_obj = MockRedis() @@ -2742,3 +2748,64 @@ def test_should_hide_category_name_from_template_list_if_marked_hidden( # assert that "HIDDEN_CATEGORY" is not found anywhere in the page using beautifulsoup assert "HIDDEN_CATEGORY" not in page.text assert not page.find(text="HIDDEN_CATEGORY") + + +class TestAnnualLimits: + @pytest.mark.parametrize( + "remaining_daily, remaining_annual, buttons_shown", + [ + (10, 100, True), # Within both limits + (0, 100, False), # Exceeds daily limit + (10, 0, False), # Exceeds annual limit + (0, 0, False), # Exceeds both limits + (1, 1, True), # Exactly at both limits + ], + ) + def test_should_hide_send_buttons_when_appropriate( + self, + client_request, + mock_get_service_template, + mock_get_template_folders, + mock_notification_counts_client, + fake_uuid, + remaining_daily, + remaining_annual, + buttons_shown, + ): + mock_notification_counts_client.get_limit_stats.return_value = { + "email": { + "annual": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": remaining_annual, # The number of email notifications remaining this year + }, + "daily": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": remaining_daily, # The number of email notifications remaining today + }, + }, + "sms": { + "annual": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": remaining_annual, # The number of email notifications remaining this year + }, + "daily": { + "limit": 1, # doesn't matter for our test + "sent": 1, # doesn't matter for our test + "remaining": remaining_daily, # The number of email notifications remaining today + }, + }, + } + + page = client_request.get( + ".view_template", + service_id=SERVICE_ONE_ID, + template_id=fake_uuid, + _test_page_title=False, + ) + if buttons_shown: + assert page.find(attrs={"data-testid": "send-buttons"}) is not None + else: + assert page.find(attrs={"data-testid": "send-buttons"}) is None diff --git a/tests/app/notify_client/test_notification_counts_client.py b/tests/app/notify_client/test_notification_counts_client.py index fa7183aea..342aaa291 100644 --- a/tests/app/notify_client/test_notification_counts_client.py +++ b/tests/app/notify_client/test_notification_counts_client.py @@ -1,4 +1,5 @@ -from unittest.mock import patch +from datetime import datetime +from unittest.mock import Mock, patch import pytest @@ -23,6 +24,12 @@ def mock_service_api(): yield mock +@pytest.fixture +def mock_get_all_notification_counts_for_today(): + with patch("app.notify_client.notification_counts_client.get_all_notification_counts_for_today") as mock: + yield mock + + class TestNotificationCounts: def test_get_all_notification_counts_for_today_redis_has_data(self, mock_redis): # Setup @@ -87,3 +94,106 @@ def test_get_all_notification_counts_for_year(self, mock_service_api): # Assert assert result["sms"] == 29 # 1 + 22 + 1 + 5 assert result["email"] == 21 # 1 + 1 + 12 + 1 + 1 + 5 + + def test_get_limit_stats(self, mocker): + # Setup + mock_service = Mock(id="service-1", email_annual_limit=1000, sms_annual_limit=500, message_limit=100, sms_daily_limit=50) + + mock_notification_client = NotificationCounts() + + # Mock the dependency methods + + mocker.patch.object( + mock_notification_client, "get_all_notification_counts_for_today", return_value={"email": 20, "sms": 10} + ) + mocker.patch.object( + mock_notification_client, "get_all_notification_counts_for_year", return_value={"email": 200, "sms": 100} + ) + + # Execute + result = mock_notification_client.get_limit_stats(mock_service) + + # Assert + assert result == { + "email": { + "annual": { + "limit": 1000, + "sent": 200, + "remaining": 800, + }, + "daily": { + "limit": 100, + "sent": 20, + "remaining": 80, + }, + }, + "sms": { + "annual": { + "limit": 500, + "sent": 100, + "remaining": 400, + }, + "daily": { + "limit": 50, + "sent": 10, + "remaining": 40, + }, + }, + } + + @pytest.mark.parametrize( + "today_counts,year_counts,expected_remaining", + [ + ( + {"email": 0, "sms": 0}, + {"email": 0, "sms": 0}, + {"email": {"annual": 1000, "daily": 100}, "sms": {"annual": 500, "daily": 50}}, + ), + ( + {"email": 100, "sms": 50}, + {"email": 1000, "sms": 500}, + {"email": {"annual": 0, "daily": 0}, "sms": {"annual": 0, "daily": 0}}, + ), + ( + {"email": 50, "sms": 25}, + {"email": 500, "sms": 250}, + {"email": {"annual": 500, "daily": 50}, "sms": {"annual": 250, "daily": 25}}, + ), + ], + ) + def test_get_limit_stats_remaining_calculations(self, mocker, today_counts, year_counts, expected_remaining): + # Setup + mock_service = Mock(id="service-1", email_annual_limit=1000, sms_annual_limit=500, message_limit=100, sms_daily_limit=50) + + mock_notification_client = NotificationCounts() + + mocker.patch.object(mock_notification_client, "get_all_notification_counts_for_today", return_value=today_counts) + mocker.patch.object(mock_notification_client, "get_all_notification_counts_for_year", return_value=year_counts) + + # Execute + result = mock_notification_client.get_limit_stats(mock_service) + + # Assert remaining counts + assert result["email"]["annual"]["remaining"] == expected_remaining["email"]["annual"] + assert result["email"]["daily"]["remaining"] == expected_remaining["email"]["daily"] + assert result["sms"]["annual"]["remaining"] == expected_remaining["sms"]["annual"] + assert result["sms"]["daily"]["remaining"] == expected_remaining["sms"]["daily"] + + def test_get_limit_stats_dependencies_called(self, mocker): + # Setup + mock_service = Mock(id="service-1", email_annual_limit=1000, sms_annual_limit=500, message_limit=100, sms_daily_limit=50) + mock_notification_client = NotificationCounts() + + mock_today = mocker.patch.object( + mock_notification_client, "get_all_notification_counts_for_today", return_value={"email": 0, "sms": 0} + ) + mock_year = mocker.patch.object( + mock_notification_client, "get_all_notification_counts_for_year", return_value={"email": 0, "sms": 0} + ) + + # Execute + mock_notification_client.get_limit_stats(mock_service) + + # Assert dependencies called + mock_today.assert_called_once_with(mock_service.id) + mock_year.assert_called_once_with(mock_service.id, datetime.now().year)