diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index bc46c60ba..2de7e6eb2 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -519,7 +519,7 @@ LOGIN_REQUIRED_IGNORE_PATHS = [ ] # where to go after logging out -LOGOUT_REDIRECT_URL = "home" +LOGOUT_REDIRECT_URL = "https://get.gov/" # disable dynamic client registration, # only the OP inside OIDC_PROVIDERS will be available diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py index ad3ae0820..92094b876 100644 --- a/src/registrar/fixtures_applications.py +++ b/src/registrar/fixtures_applications.py @@ -218,5 +218,8 @@ class DomainFixture(DomainApplicationFixture): creator=user, status=DomainApplication.ApplicationStatus.IN_REVIEW ).last() logger.debug(f"Approving {application} for {user}") - application.approve() + + # We don't want fixtures sending out real emails to + # fake email addresses, so we just skip that and log it instead + application.approve(send_email=False) application.save() diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 243f029ae..e181849ac 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -570,17 +570,25 @@ class DomainApplication(TimeStampedModel): return not self.approved_domain.is_active() return True - def _send_status_update_email(self, new_status, email_template, email_template_subject): - """Send a atatus update email to the submitter. + def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True): + """Send a status update email to the submitter. The email goes to the email address that the submitter gave as their contact information. If there is not submitter information, then do nothing. + + send_email: bool -> Used to bypass the send_templated_email function, in the event + we just want to log that an email would have been sent, rather than actually sending one. """ if self.submitter is None or self.submitter.email is None: logger.warning(f"Cannot send {new_status} email, no submitter email address.") - return + return None + + if not send_email: + logger.info(f"Email was not sent. Would send {new_status} email: {self.submitter.email}") + return None + try: send_templated_email( email_template, @@ -684,7 +692,7 @@ class DomainApplication(TimeStampedModel): ], target=ApplicationStatus.APPROVED, ) - def approve(self): + def approve(self, send_email=True): """Approve an application that has been submitted. This has substantial side-effects because it creates another database @@ -713,6 +721,7 @@ class DomainApplication(TimeStampedModel): "application approved", "emails/status_change_approved.txt", "emails/status_change_approved_subject.txt", + send_email, ) @transition( diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 810e36170..8dded9de9 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -18,9 +18,11 @@ from registrar.admin import ( from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website from registrar.models.user_domain_role import UserDomainRole from .common import ( + MockSESClient, AuditedAdminMockData, completed_application, generic_domain_object, + less_console_noise, mock_user, create_superuser, create_user, @@ -35,7 +37,6 @@ from unittest.mock import patch from unittest import skip from django.conf import settings -from unittest.mock import MagicMock import boto3_mocking # type: ignore import logging @@ -58,7 +59,10 @@ class TestDomainAdmin(MockEppLib): """ self.client.force_login(self.superuser) application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - application.approve() + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + application.approve() response = self.client.get("/admin/registrar/domain/") @@ -326,6 +330,7 @@ class TestDomainApplicationAdmin(MockEppLib): url="/admin/registrar/DomainApplication/", model=DomainApplication, ) + self.mock_client = MockSESClient() def test_domain_sortable(self): """Tests if the DomainApplication sorts by domain correctly""" @@ -420,25 +425,23 @@ class TestDomainApplicationAdmin(MockEppLib): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Create a sample application + application = completed_application() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - # Create a sample application - application = completed_application() + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.SUBMITTED - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.SUBMITTED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email - call_args = mock_client_instance.send_email.call_args - args, kwargs = call_args + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[0]["kwargs"] # Retrieve the email details from the arguments from_email = kwargs.get("FromEmailAddress") @@ -452,8 +455,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(to_email, EMAIL) self.assertIn(expected_string, email_body) - # Perform assertions on the mock call itself - mock_client_instance.send_email.assert_called_once() + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) @boto3_mocking.patching def test_save_model_sends_in_review_email(self): @@ -461,25 +463,23 @@ class TestDomainApplicationAdmin(MockEppLib): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED) - with boto3_mocking.clients.handler_for("sesv2", mock_client): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.IN_REVIEW - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.IN_REVIEW - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email - call_args = mock_client_instance.send_email.call_args - args, kwargs = call_args + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[0]["kwargs"] # Retrieve the email details from the arguments from_email = kwargs.get("FromEmailAddress") @@ -493,8 +493,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(to_email, EMAIL) self.assertIn(expected_string, email_body) - # Perform assertions on the mock call itself - mock_client_instance.send_email.assert_called_once() + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) @boto3_mocking.patching def test_save_model_sends_approved_email(self): @@ -502,25 +501,23 @@ class TestDomainApplicationAdmin(MockEppLib): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", mock_client): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.APPROVED - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.APPROVED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email - call_args = mock_client_instance.send_email.call_args - args, kwargs = call_args + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[0]["kwargs"] # Retrieve the email details from the arguments from_email = kwargs.get("FromEmailAddress") @@ -534,9 +531,9 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(to_email, EMAIL) self.assertIn(expected_string, email_body) - # Perform assertions on the mock call itself - mock_client_instance.send_email.assert_called_once() + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + @boto3_mocking.patching def test_save_model_sets_approved_domain(self): # make sure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -548,11 +545,13 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a mock request request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.APPROVED + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.APPROVED - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) # Test that approved domain exists and equals requested domain self.assertEqual(application.requested_domain.name, application.approved_domain.name) @@ -563,25 +562,23 @@ class TestDomainApplicationAdmin(MockEppLib): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", mock_client): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email - call_args = mock_client_instance.send_email.call_args - args, kwargs = call_args + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[0]["kwargs"] # Retrieve the email details from the arguments from_email = kwargs.get("FromEmailAddress") @@ -595,8 +592,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(to_email, EMAIL) self.assertIn(expected_string, email_body) - # Perform assertions on the mock call itself - mock_client_instance.send_email.assert_called_once() + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) @boto3_mocking.patching def test_save_model_sends_rejected_email(self): @@ -604,25 +600,23 @@ class TestDomainApplicationAdmin(MockEppLib): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", mock_client): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.REJECTED - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.REJECTED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email - call_args = mock_client_instance.send_email.call_args - args, kwargs = call_args + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[0]["kwargs"] # Retrieve the email details from the arguments from_email = kwargs.get("FromEmailAddress") @@ -636,9 +630,9 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(to_email, EMAIL) self.assertIn(expected_string, email_body) - # Perform assertions on the mock call itself - mock_client_instance.send_email.assert_called_once() + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + @boto3_mocking.patching def test_save_model_sets_restricted_status_on_user(self): # make sure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -650,19 +644,23 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a mock request request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.INELIGIBLE + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.INELIGIBLE - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) # Test that approved domain exists and equals requested domain self.assertEqual(application.creator.status, "restricted") def test_readonly_when_restricted_creator(self): application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - application.creator.status = User.RESTRICTED - application.creator.save() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.creator.status = User.RESTRICTED + application.creator.save() request = self.factory.get("/") request.user = self.superuser @@ -740,8 +738,10 @@ class TestDomainApplicationAdmin(MockEppLib): def test_saving_when_restricted_creator(self): # Create an instance of the model application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - application.creator.status = User.RESTRICTED - application.creator.save() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.creator.status = User.RESTRICTED + application.creator.save() # Create a request object with a superuser request = self.factory.get("/") @@ -763,8 +763,10 @@ class TestDomainApplicationAdmin(MockEppLib): def test_change_view_with_restricted_creator(self): # Create an instance of the model application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - application.creator.status = User.RESTRICTED - application.creator.save() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.creator.status = User.RESTRICTED + application.creator.save() with patch("django.contrib.messages.warning") as mock_warning: # Create a request object with a superuser @@ -779,6 +781,7 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) + @boto3_mocking.patching def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): # Create an instance of the model application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) @@ -800,9 +803,11 @@ class TestDomainApplicationAdmin(MockEppLib): stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) stack.enter_context(patch.object(messages, "error")) - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.REJECTED - self.admin.save_model(request, application, None, True) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Simulate saving the model + application.status = DomainApplication.ApplicationStatus.REJECTED + self.admin.save_model(request, application, None, True) # Assert that the error message was called with the correct argument messages.error.assert_called_once_with( @@ -831,10 +836,11 @@ class TestDomainApplicationAdmin(MockEppLib): # Patch Domain.is_active and django.contrib.messages.error simultaneously stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) stack.enter_context(patch.object(messages, "error")) - - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.REJECTED - self.admin.save_model(request, application, None, True) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Simulate saving the model + application.status = DomainApplication.ApplicationStatus.REJECTED + self.admin.save_model(request, application, None, True) # Assert that the error message was never called messages.error.assert_not_called() @@ -1091,6 +1097,7 @@ class TestDomainApplicationAdmin(MockEppLib): User.objects.all().delete() Contact.objects.all().delete() Website.objects.all().delete() + self.mock_client.EMAILS_SENT.clear() class DomainInvitationAdminTest(TestCase): diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index eb6da072d..61c950255 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock from django.test import TestCase -from .common import completed_application +from .common import completed_application, less_console_noise import boto3_mocking # type: ignore @@ -20,7 +20,8 @@ class TestEmails(TestCase): application = completed_application() with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() # check that an email was sent self.assertTrue(self.mock_client.send_email.called) @@ -56,7 +57,8 @@ class TestEmails(TestCase): """Test line spacing without current_website.""" application = completed_application(has_current_website=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("Current website for your organization:", body) @@ -68,7 +70,8 @@ class TestEmails(TestCase): """Test line spacing with current_website.""" application = completed_application(has_current_website=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("Current website for your organization:", body) @@ -81,7 +84,8 @@ class TestEmails(TestCase): """Test line spacing with other contacts.""" application = completed_application(has_other_contacts=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("Other employees from your organization:", body) @@ -94,7 +98,8 @@ class TestEmails(TestCase): """Test line spacing without other contacts.""" application = completed_application(has_other_contacts=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("Other employees from your organization:", body) @@ -106,7 +111,8 @@ class TestEmails(TestCase): """Test line spacing with alternative .gov domain.""" application = completed_application(has_alternative_gov_domain=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("city1.gov", body) @@ -118,7 +124,8 @@ class TestEmails(TestCase): """Test line spacing without alternative .gov domain.""" application = completed_application(has_alternative_gov_domain=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("city1.gov", body) @@ -130,7 +137,8 @@ class TestEmails(TestCase): """Test line spacing with about your organization.""" application = completed_application(has_about_your_organization=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("About your organization:", body) @@ -142,7 +150,8 @@ class TestEmails(TestCase): """Test line spacing without about your organization.""" application = completed_application(has_about_your_organization=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("About your organization:", body) @@ -154,7 +163,8 @@ class TestEmails(TestCase): """Test line spacing with anything else.""" application = completed_application(has_anything_else=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] # spacing should be right between adjacent elements @@ -165,7 +175,8 @@ class TestEmails(TestCase): """Test line spacing without anything else.""" application = completed_application(has_anything_else=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - application.submit() + with less_console_noise(): + application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("Anything else", body) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 6124b76f3..d06248b2e 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -19,8 +19,6 @@ from registrar.models.transition_domain import TransitionDomain # type: ignore from .common import MockSESClient, less_console_noise, completed_application from django_fsm import TransitionNotAllowed -boto3_mocking.clients.register_handler("sesv2", MockSESClient) - # Test comment for push -- will remove # The DomainApplication submit method has a side effect of sending an email @@ -53,6 +51,12 @@ class TestDomainApplication(TestCase): status=DomainApplication.ApplicationStatus.INELIGIBLE, name="ineligible.gov" ) + self.mock_client = MockSESClient() + + def tearDown(self): + super().tearDown() + self.mock_client.EMAILS_SENT.clear() + def assertNotRaises(self, exception_type): """Helper method for testing allowed transitions.""" return self.assertRaises(Exception, None, exception_type) @@ -130,17 +134,23 @@ class TestDomainApplication(TestCase): def test_status_fsm_submit_fail(self): user, _ = User.objects.get_or_create(username="testy") application = DomainApplication.objects.create(creator=user) - with self.assertRaises(ValueError): - # can't submit an application with a null domain name - application.submit() + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + with self.assertRaises(ValueError): + # can't submit an application with a null domain name + application.submit() def test_status_fsm_submit_succeed(self): user, _ = User.objects.get_or_create(username="testy") site = DraftDomain.objects.create(name="igorville.gov") application = DomainApplication.objects.create(creator=user, requested_domain=site) + # no submitter email so this emits a log warning - with less_console_noise(): - application.submit() + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) def test_submit_sends_email(self): @@ -154,7 +164,10 @@ class TestDomainApplication(TestCase): submitter=contact, ) application.save() - application.submit() + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() # check to see if an email was sent self.assertGreater( @@ -179,12 +192,14 @@ class TestDomainApplication(TestCase): (self.withdrawn_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.submit() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.submit() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_submit_transition_not_allowed(self): """ @@ -197,10 +212,12 @@ class TestDomainApplication(TestCase): (self.ineligible_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.submit() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.submit() def test_in_review_transition_allowed(self): """ @@ -214,12 +231,14 @@ class TestDomainApplication(TestCase): (self.ineligible_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.in_review() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.in_review() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_in_review_transition_not_allowed(self): """ @@ -231,10 +250,12 @@ class TestDomainApplication(TestCase): (self.withdrawn_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.in_review() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.in_review() def test_action_needed_transition_allowed(self): """ @@ -247,12 +268,14 @@ class TestDomainApplication(TestCase): (self.ineligible_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.action_needed() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.action_needed() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_action_needed_transition_not_allowed(self): """ @@ -265,10 +288,12 @@ class TestDomainApplication(TestCase): (self.withdrawn_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.action_needed() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.action_needed() def test_approved_transition_allowed(self): """ @@ -281,12 +306,27 @@ class TestDomainApplication(TestCase): (self.rejected_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.approve() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.approve() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") + + def test_approved_skips_sending_email(self): + """ + Test that calling .approve with send_email=False doesn't actually send + an email + """ + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + self.submitted_application.approve(send_email=False) + + # Assert that no emails were sent + self.assertEqual(len(self.mock_client.EMAILS_SENT), 0) def test_approved_transition_not_allowed(self): """ @@ -299,10 +339,12 @@ class TestDomainApplication(TestCase): (self.ineligible_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.approve() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.approve() def test_withdraw_transition_allowed(self): """ @@ -314,12 +356,14 @@ class TestDomainApplication(TestCase): (self.action_needed_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.withdraw() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.withdraw() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_withdraw_transition_not_allowed(self): """ @@ -333,10 +377,12 @@ class TestDomainApplication(TestCase): (self.ineligible_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.withdraw() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.withdraw() def test_reject_transition_allowed(self): """ @@ -348,12 +394,14 @@ class TestDomainApplication(TestCase): (self.approved_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.reject() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.reject() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_reject_transition_not_allowed(self): """ @@ -367,10 +415,12 @@ class TestDomainApplication(TestCase): (self.ineligible_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.reject() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.reject() def test_reject_with_prejudice_transition_allowed(self): """ @@ -383,12 +433,14 @@ class TestDomainApplication(TestCase): (self.rejected_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.reject_with_prejudice() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.reject_with_prejudice() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_reject_with_prejudice_transition_not_allowed(self): """ @@ -401,10 +453,12 @@ class TestDomainApplication(TestCase): (self.ineligible_application, TransitionNotAllowed), ] - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.reject_with_prejudice() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.reject_with_prejudice() def test_transition_not_allowed_approved_rejected_when_domain_is_active(self): """Create an application with status approved, create a matching domain that @@ -418,11 +472,13 @@ class TestDomainApplication(TestCase): def custom_is_active(self): return True # Override to return True - # Use patch to temporarily replace is_active with the custom implementation - with patch.object(Domain, "is_active", custom_is_active): - # Now, when you call is_active on Domain, it will return True - with self.assertRaises(TransitionNotAllowed): - self.approved_application.reject() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_application.reject() def test_transition_not_allowed_approved_ineligible_when_domain_is_active(self): """Create an application with status approved, create a matching domain that @@ -436,24 +492,37 @@ class TestDomainApplication(TestCase): def custom_is_active(self): return True # Override to return True - # Use patch to temporarily replace is_active with the custom implementation - with patch.object(Domain, "is_active", custom_is_active): - # Now, when you call is_active on Domain, it will return True - with self.assertRaises(TransitionNotAllowed): - self.approved_application.reject_with_prejudice() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_application.reject_with_prejudice() class TestPermissions(TestCase): - """Test the User-Domain-Role connection.""" + def setUp(self): + super().setUp() + self.mock_client = MockSESClient() + + def tearDown(self): + super().tearDown() + self.mock_client.EMAILS_SENT.clear() + + @boto3_mocking.patching def test_approval_creates_role(self): draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") user, _ = User.objects.get_or_create() application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain) - # skip using the submit method - application.status = DomainApplication.ApplicationStatus.SUBMITTED - application.approve() + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # skip using the submit method + application.status = DomainApplication.ApplicationStatus.SUBMITTED + application.approve() # should be a role for this user domain = Domain.objects.get(name="igorville.gov") @@ -464,13 +533,25 @@ class TestDomainInfo(TestCase): """Test creation of Domain Information when approved.""" + def setUp(self): + super().setUp() + self.mock_client = MockSESClient() + + def tearDown(self): + super().tearDown() + self.mock_client.EMAILS_SENT.clear() + + @boto3_mocking.patching def test_approval_creates_info(self): draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") user, _ = User.objects.get_or_create() application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain) - # skip using the submit method - application.status = DomainApplication.ApplicationStatus.SUBMITTED - application.approve() + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # skip using the submit method + application.status = DomainApplication.ApplicationStatus.SUBMITTED + application.approve() # should be an information present for this domain domain = Domain.objects.get(name="igorville.gov") @@ -572,11 +653,12 @@ class TestUser(TestCase): caps_email = "MAYOR@igorville.gov" # mock the domain invitation save routine with patch("registrar.models.DomainInvitation.save") as save_mock: - DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain) - self.user.check_domain_invitations_on_login() - # if check_domain_invitations_on_login properly matches exactly one - # Domain Invitation, then save routine should be called exactly once - save_mock.assert_called_once() + with less_console_noise(): + DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain) + self.user.check_domain_invitations_on_login() + # if check_domain_invitations_on_login properly matches exactly one + # Domain Invitation, then save routine should be called exactly once + save_mock.assert_called_once() class TestContact(TestCase): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index c0d4bd27b..b86941183 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -29,8 +29,9 @@ from epplibwrapper import ( RegistryError, ErrorCode, ) -from .common import MockEppLib +from .common import MockEppLib, MockSESClient, less_console_noise import logging +import boto3_mocking # type: ignore logger = logging.getLogger(__name__) @@ -252,6 +253,7 @@ class TestDomainCache(MockEppLib): class TestDomainCreation(MockEppLib): """Rule: An approved domain application must result in a domain""" + @boto3_mocking.patching def test_approved_application_creates_domain_locally(self): """ Scenario: Analyst approves a domain application @@ -262,10 +264,14 @@ class TestDomainCreation(MockEppLib): draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") user, _ = User.objects.get_or_create() application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain) - # skip using the submit method - application.status = DomainApplication.ApplicationStatus.SUBMITTED - # transition to approve state - application.approve() + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + # skip using the submit method + application.status = DomainApplication.ApplicationStatus.SUBMITTED + # transition to approve state + application.approve() # should have information present for this domain domain = Domain.objects.get(name="igorville.gov") self.assertTrue(domain) diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index f3fd76e88..4774e085f 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -18,7 +18,8 @@ from unittest.mock import patch from registrar.models.contact import Contact -from .common import MockEppLib, less_console_noise +from .common import MockEppLib, MockSESClient, less_console_noise +import boto3_mocking # type: ignore class TestExtendExpirationDates(MockEppLib): @@ -706,17 +707,21 @@ class TestMigrations(TestCase): def run_master_script(self): # noqa here (E501) because splitting this up makes it # confusing to read. - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "master_domain_migrations", - runMigrations=True, - migrationDirectory=self.test_data_file_location, - migrationJSON=self.migration_json_filename, - disablePrompts=True, - ) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + with patch("registrar.utility.email.send_templated_email", return_value=None): + call_command( + "master_domain_migrations", + runMigrations=True, + migrationDirectory=self.test_data_file_location, + migrationJSON=self.migration_json_filename, + disablePrompts=True, + ) + print(f"here: {mock_client.EMAILS_SENT}") def compare_tables( self, @@ -1019,6 +1024,7 @@ class TestMigrations(TestCase): expected_missing_domain_invitations, ) + @boto3_mocking.patching def test_send_domain_invitations_email(self): """Can send only a single domain invitation email.""" with less_console_noise(): @@ -1027,9 +1033,12 @@ class TestMigrations(TestCase): # this is one of the email addresses in data/test_contacts.txt output_stream = StringIO() - # also have to re-point the logging handlers to output_stream - with less_console_noise(output_stream): - call_command("send_domain_invitations", "testuser@gmail.com", stdout=output_stream) + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + # also have to re-point the logging handlers to output_stream + with less_console_noise(output_stream): + call_command("send_domain_invitations", "testuser@gmail.com", stdout=output_stream) # Check that we had the right numbers in our output output = output_stream.getvalue() @@ -1037,6 +1046,7 @@ class TestMigrations(TestCase): self.assertIn("Found 1 transition domains", output) self.assertTrue("would send email to testuser@gmail.com", output) + @boto3_mocking.patching def test_send_domain_invitations_two_emails(self): """Can send only a single domain invitation email.""" with less_console_noise(): @@ -1045,11 +1055,14 @@ class TestMigrations(TestCase): # these are two email addresses in data/test_contacts.txt output_stream = StringIO() - # also have to re-point the logging handlers to output_stream - with less_console_noise(output_stream): - call_command( - "send_domain_invitations", "testuser@gmail.com", "agustina.wyman7@test.com", stdout=output_stream - ) + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + # also have to re-point the logging handlers to output_stream + with less_console_noise(output_stream): + call_command( + "send_domain_invitations", "testuser@gmail.com", "agustina.wyman7@test.com", stdout=output_stream + ) # Check that we had the right numbers in our output output = output_stream.getvalue() diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index a195f5f1a..8f812b815 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -5,7 +5,7 @@ from django.conf import settings from django.test import Client, TestCase from django.urls import reverse from django.contrib.auth import get_user_model -from .common import MockEppLib, completed_application, create_user # type: ignore +from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore @@ -149,8 +149,11 @@ class DomainApplicationTests(TestWithUser, WebTest): """Test that an info message appears when user has multiple applications already""" # create and submit an application application = completed_application(user=self.user) - application.submit() - application.save() + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + application.submit() + application.save() # now, attempt to create another one with less_console_noise(): @@ -1440,6 +1443,7 @@ class TestDomainManagers(TestDomainOverview): response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) self.assertContains(response, "Add a domain manager") + @boto3_mocking.patching def test_domain_user_add_form(self): """Adding an existing user works.""" other_user, _ = get_user_model().objects.get_or_create(email="mayor@igorville.gov") @@ -1449,7 +1453,11 @@ class TestDomainManagers(TestDomainOverview): add_page.form["email"] = "mayor@igorville.gov" self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_result = add_page.form.submit() + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() self.assertEqual(success_result.status_code, 302) self.assertEqual( @@ -1478,7 +1486,12 @@ class TestDomainManagers(TestDomainOverview): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] add_page.form["email"] = email_address self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_result = add_page.form.submit() + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_page = success_result.follow() @@ -1504,7 +1517,12 @@ class TestDomainManagers(TestDomainOverview): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] add_page.form["email"] = caps_email_address self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_result = add_page.form.submit() + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_page = success_result.follow() @@ -1524,11 +1542,12 @@ class TestDomainManagers(TestDomainOverview): mock_client = MagicMock() mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() # check the mock instance to see if `send_email` was called right mock_client_instance.send_email.assert_called_once_with( @@ -1550,11 +1569,12 @@ class TestDomainManagers(TestDomainOverview): mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() # check the mock instance to see if `send_email` was called right mock_client_instance.send_email.assert_called_once_with( @@ -1588,11 +1608,12 @@ class TestDomainManagers(TestDomainOverview): mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() # check the mock instance to see if `send_email` was called right mock_client_instance.send_email.assert_called_once_with( @@ -1630,11 +1651,12 @@ class TestDomainManagers(TestDomainOverview): mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() # check the mock instance to see if `send_email` was called right mock_client_instance.send_email.assert_called_once_with( @@ -1669,15 +1691,15 @@ class TestDomainManagers(TestDomainOverview): self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) mock_client = MagicMock() - mock_error_message = MagicMock() with boto3_mocking.clients.handler_for("sesv2", mock_client): with patch("django.contrib.messages.error") as mock_error_message: - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit().follow() + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit().follow() expected_message_content = "Can't send invitation email. No email is associated with your account." @@ -1706,11 +1728,12 @@ class TestDomainManagers(TestDomainOverview): mock_error_message = MagicMock() with boto3_mocking.clients.handler_for("sesv2", mock_client): with patch("django.contrib.messages.error") as mock_error_message: - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit().follow() + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit().follow() expected_message_content = "Can't send invitation email. No email is associated with your account." @@ -1724,7 +1747,11 @@ class TestDomainManagers(TestDomainOverview): """Posting to the delete view deletes an invitation.""" email_address = "mayor@igorville.gov" invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) - self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + mock_client.EMAILS_SENT.clear() with self.assertRaises(DomainInvitation.DoesNotExist): DomainInvitation.objects.get(id=invitation.id) @@ -1736,8 +1763,11 @@ class TestDomainManagers(TestDomainOverview): other_user = User() other_user.save() self.client.force_login(other_user) - with less_console_noise(): # permission denied makes console errors - result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): # permission denied makes console errors + result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + self.assertEqual(result.status_code, 403) @boto3_mocking.patching @@ -1753,7 +1783,11 @@ class TestDomainManagers(TestDomainOverview): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] add_page.form["email"] = email_address self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page.form.submit() # user was invited, create them new_user = User.objects.create(username=email_address, email=email_address) @@ -1808,6 +1842,7 @@ class TestDomainNameservers(TestDomainOverview): # attempt to submit the form without two hosts, both subdomains, # only one has ips nameservers_page.form["form-1-server"] = "ns2.igorville.gov" + with less_console_noise(): # swallow log warning message result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 @@ -2143,8 +2178,10 @@ class TestDomainSecurityEmail(TestDomainOverview): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] security_email_page.form["security_email"] = "mayor@igorville.gov" self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - with less_console_noise(): # swallow log warning message - result = security_email_page.form.submit() + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): # swallow log warning message + result = security_email_page.form.submit() self.assertEqual(result.status_code, 302) self.assertEqual( result["Location"], @@ -2490,9 +2527,12 @@ class TestApplicationStatus(TestWithUser, WebTest): self.assertContains(detail_page, "Admin Tester") self.assertContains(detail_page, "Status:") # click the "Withdraw request" button - withdraw_page = detail_page.click("Withdraw request") - self.assertContains(withdraw_page, "Withdraw request for") - home_page = withdraw_page.click("Withdraw request") + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + withdraw_page = detail_page.click("Withdraw request") + self.assertContains(withdraw_page, "Withdraw request for") + home_page = withdraw_page.click("Withdraw request") # confirm that it has redirected, and the status has been updated to withdrawn self.assertRedirects( home_page, diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 79d2d43ce..d56c02cbf 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -1,11 +1,14 @@ """Utilities for sending emails.""" import boto3 - +import logging from django.conf import settings from django.template.loader import get_template +logger = logging.getLogger(__name__) + + class EmailSendingError(RuntimeError): """Local error for handling all failures when sending email.""" @@ -20,7 +23,7 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr context as Django's HTML templates. context gives additional information that the template may use. """ - + logger.info(f"An email was sent! Template name: {template_name} to {to_address}") template = get_template(template_name) email_body = template.render(context=context) diff --git a/src/zap.conf b/src/zap.conf index e7dc980b0..7a1e5c96d 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -67,6 +67,7 @@ 10038 OUTOFSCOPE http://app:8080/dns/nameservers 10038 OUTOFSCOPE http://app:8080/dns/dnssec 10038 OUTOFSCOPE http://app:8080/dns/dnssec/dsdata +10038 OUTOFSCOPE http://app:8080/org-name-address # This URL always returns 404, so include it as well. 10038 OUTOFSCOPE http://app:8080/todo # OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers