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/migrations/0061_domain_security_contact_registry_id.py b/src/registrar/migrations/0061_domain_security_contact_registry_id.py new file mode 100644 index 000000000..1648587db --- /dev/null +++ b/src/registrar/migrations/0061_domain_security_contact_registry_id.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.7 on 2023-12-23 01:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0060_domain_deleted_domain_first_ready"), + ] + + operations = [ + migrations.AddField( + model_name="domain", + name="security_contact_registry_id", + field=models.TextField( + editable=False, + help_text="Duplication of registry's security contact id for when registry unavailable", + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1ae9b8029..001937b89 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -31,7 +31,7 @@ from epplibwrapper import ( from registrar.models.utility.contact_error import ContactError, ContactErrorCodes -from django.db.models import DateField +from django.db.models import DateField, TextField from .utility.domain_field import DomainField from .utility.domain_helper import DomainHelper from .utility.time_stamped_model import TimeStampedModel @@ -974,6 +974,12 @@ class Domain(TimeStampedModel, DomainHelper): help_text=("Duplication of registry's expiration date saved for ease of reporting"), ) + security_contact_registry_id = TextField( + null=True, + help_text=("Duplication of registry's security contact id for when registry unavailable"), + editable=False, + ) + deleted = DateField( null=True, editable=False, @@ -1127,15 +1133,21 @@ class Domain(TimeStampedModel, DomainHelper): # Grab from cache contacts = self._get_property(desired_property) except KeyError as error: - logger.error(f"Could not find {contact_type_choice}: {error}") - return None - else: - cached_contact = self.get_contact_in_keys(contacts, contact_type_choice) - if cached_contact is None: - # TODO - #1103 - raise ContactError("No contact was found in cache or the registry") + # if contact type is security, attempt to retrieve registry id + # for the security contact from domain.security_contact_registry_id + if contact_type_choice == PublicContact.ContactTypeChoices.SECURITY and self.security_contact_registry_id: + logger.info(f"Could not access registry, using fallback value of {self.security_contact_registry_id}") + contacts = {PublicContact.ContactTypeChoices.SECURITY: self.security_contact_registry_id} + else: + logger.error(f"Could not find {contact_type_choice}: {error}") + return None - return cached_contact + cached_contact = self.get_contact_in_keys(contacts, contact_type_choice) + if cached_contact is None: + # TODO - #1103 + raise ContactError("No contact was found in cache or the registry") + + return cached_contact def get_default_security_contact(self): """Gets the default security contact.""" @@ -1630,6 +1642,8 @@ class Domain(TimeStampedModel, DomainHelper): self._update_hosts_and_contacts(cleaned, fetch_hosts, fetch_contacts) if fetch_hosts: self._update_hosts_and_ips_in_db(cleaned) + if fetch_contacts: + self._update_security_contact_in_db(cleaned) self._update_dates(cleaned) self._cache = cleaned @@ -1739,6 +1753,23 @@ class Domain(TimeStampedModel, DomainHelper): for ip_address in cleaned_ips: HostIP.objects.get_or_create(address=ip_address, host=host_in_db) + def _update_security_contact_in_db(self, cleaned): + """Update security contact registry id in database if retrieved from registry. + If no value is retrieved from registry, set to empty string in db. + + Parameters: + self: the domain to be updated with security from cleaned + cleaned: dict containing contact registry ids. Security contact is of type + PublicContact.ContactTypeChoices.SECURITY + """ + cleaned_contacts = cleaned["contacts"] + security_contact_registry_id = "" + security_contact = cleaned_contacts[PublicContact.ContactTypeChoices.SECURITY] + if security_contact: + security_contact_registry_id = security_contact + self.security_contact_registry_id = security_contact_registry_id + self.save() + def _update_dates(self, cleaned): """Update dates (expiration and creation) from cleaned""" requires_save = False diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 2910b6b4f..178a140a4 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/templates/application_intro.html b/src/registrar/templates/application_intro.html new file mode 100644 index 000000000..5663ce273 --- /dev/null +++ b/src/registrar/templates/application_intro.html @@ -0,0 +1,34 @@ +{% extends 'base.html' %} +{% load static form_helpers url_helpers %} + +{% block content %} +
+
+ +
+ {% csrf_token %} + +

You’re about to start your .gov domain request.

+

You don’t have to complete the process in one session. You can save what you enter and come back to it when you’re ready.

+

We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll also verify that the domain you request meets our guidelines.

+

Time to complete the form

+

If you have all the information you need, + completing your domain request might take around 15 minutes.

+

Contact us if you need help with your request.

+ +{% block form_buttons %} +
+ +
+{% endblock %} + +
+ +
+
+{% endblock %} 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 22202ce5f..672b8f465 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,11 +492,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_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() def test_has_rationale_returns_true(self): """has_rationale() returns true when an application has no_other_contacts_rationale""" @@ -466,16 +524,27 @@ class TestDomainApplication(TestCase): 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") @@ -486,13 +555,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") @@ -594,11 +675,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..9026832cd 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) @@ -739,6 +745,50 @@ class TestRegistrantContacts(MockEppLib): self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) + def test_security_email_returns_on_registry_error(self): + """ + Scenario: Security email previously set through EPP and stored in registrar's database. + Registry is unavailable and throws exception when attempting to build cache from + registry. Security email retrieved from database. + """ + # Use self.domain_contact which has been initialized with existing contacts, including securityContact + + # call get_security_email to initially set the security_contact_registry_id in the domain model + self.domain_contact.get_security_email() + # invalidate the cache so the next time get_security_email is called, it has to attempt to populate cache + self.domain_contact._invalidate_cache() + + # mock that registry throws an error on the EPP send + def side_effect(_request, cleaned): + raise RegistryError(code=ErrorCode.COMMAND_FAILED) + + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + + # when get_security_email is called, the registry error will force the security contact + # to be retrieved using the security_contact_registry_id in the domain model + security_email = self.domain_contact.get_security_email() + + # assert that the proper security contact was retrieved by testing the email matches expected value + self.assertEqual(security_email, "security@mail.gov") + patcher.stop() + + def test_security_email_stored_on_fetch_cache(self): + """ + Scenario: Security email is stored in db when security contact is retrieved from fetch_cache. + Verify the success of this by asserting get_or_create calls to db. + The mocked data for the EPP calls for the freeman.gov domain returns a security + contact with registry id of securityContact when InfoContact is called + """ + # Use self.domain_contact which has been initialized with existing contacts, including securityContact + + # force fetch_cache to be called, which will return above documented mocked hosts + self.domain_contact.get_security_email() + + # assert that the security_contact_registry_id in the db matches "securityContact" + self.assertEqual(self.domain_contact.security_contact_registry_id, "securityContact") + def test_not_disclosed_on_other_contacts(self): """ Scenario: Registrant creates a new domain with multiple contacts 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 6de622936..38ab9b96b 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 @@ -110,7 +110,7 @@ class LoggedInTests(TestWithUser): response = self.client.get("/register/", follow=True) self.assertContains( response, - "What kind of U.S.-based government organization do you represent?", + "You’re about to start your .gov domain request.", ) def test_domain_application_form_with_ineligible_user(self): @@ -139,24 +139,70 @@ class DomainApplicationTests(TestWithUser, WebTest): self.app.set_user(self.user.username) self.TITLES = ApplicationWizard.TITLES + def test_application_form_intro_acknowledgement(self): + """Tests that user is presented with intro acknowledgement page""" + intro_page = self.app.get(reverse("application:")) + self.assertContains(intro_page, "You’re about to start your .gov domain request") + + def test_application_form_intro_is_skipped_when_edit_access(self): + """Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'""" + completed_application(status=DomainApplication.ApplicationStatus.STARTED, user=self.user) + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Edit" link + detail_page = home_page.click("Edit", index=0) + # Check that the response is a redirect + self.assertEqual(detail_page.status_code, 302) + # You can access the 'Location' header to get the redirect URL + redirect_url = detail_page.url + self.assertEqual(redirect_url, "/register/organization_type/") + def test_application_form_empty_submit(self): - # 302 redirect to the first form - page = self.app.get(reverse("application:")).follow() + """Tests empty submit on the first page after the acknowledgement page""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # submitting should get back the same page if the required field is empty - result = page.forms[0].submit() + result = type_page.forms[0].submit() self.assertIn("What kind of U.S.-based government organization do you represent?", result) def test_application_multiple_applications_exist(self): """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(): - page = self.app.get("/register/").follow() - self.assertContains(page, "You cannot submit this request yet") + intro_page = self.app.get(reverse("application:")) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + self.assertContains(type_page, "You cannot submit this request yet") @boto3_mocking.patching def test_application_form_submission(self): @@ -175,13 +221,22 @@ class DomainApplicationTests(TestWithUser, WebTest): SKIPPED_PAGES = 3 num_pages = len(self.TITLES) - SKIPPED_PAGES - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # ---- TYPE PAGE ---- type_form = type_page.forms[0] type_form["organization_type-organization_type"] = "federal" @@ -543,13 +598,22 @@ class DomainApplicationTests(TestWithUser, WebTest): def test_application_form_conditional_federal(self): """Federal branch question is shown for federal organizations.""" - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # ---- TYPE PAGE ---- # the conditional step titles shouldn't appear initially @@ -589,13 +653,22 @@ class DomainApplicationTests(TestWithUser, WebTest): def test_application_form_conditional_elections(self): """Election question is shown for other organizations.""" - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # ---- TYPE PAGE ---- # the conditional step titles shouldn't appear initially @@ -634,13 +707,22 @@ class DomainApplicationTests(TestWithUser, WebTest): def test_application_form_section_skipping(self): """Can skip forward and back in sections""" - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + type_form = type_page.forms[0] type_form["organization_type-organization_type"] = "federal" self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -662,13 +744,22 @@ class DomainApplicationTests(TestWithUser, WebTest): def test_application_form_nonfederal(self): """Non-federal organizations don't have to provide their federal agency.""" - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + type_form = type_page.forms[0] type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -698,13 +789,22 @@ class DomainApplicationTests(TestWithUser, WebTest): def test_application_about_your_organization_special(self): """Special districts have to answer an additional question.""" - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + type_form = type_page.forms[0] type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.SPECIAL_DISTRICT self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -1068,13 +1168,22 @@ class DomainApplicationTests(TestWithUser, WebTest): def test_application_about_your_organiztion_interstate(self): """Special districts have to answer an additional question.""" - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + type_form = type_page.forms[0] type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -1087,12 +1196,22 @@ class DomainApplicationTests(TestWithUser, WebTest): def test_application_tribal_government(self): """Tribal organizations have to answer an additional question.""" - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + type_form = type_page.forms[0] type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.TRIBAL self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -1107,13 +1226,22 @@ class DomainApplicationTests(TestWithUser, WebTest): self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) def test_application_ao_dynamic_text(self): - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # ---- TYPE PAGE ---- type_form = type_page.forms[0] type_form["organization_type-organization_type"] = "federal" @@ -1169,12 +1297,22 @@ class DomainApplicationTests(TestWithUser, WebTest): self.assertContains(ao_page, "Domain requests from cities") def test_application_dotgov_domain_dynamic_text(self): - type_page = self.app.get(reverse("application:")).follow() + intro_page = self.app.get(reverse("application:")) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # ---- TYPE PAGE ---- type_form = type_page.forms[0] type_form["organization_type-organization_type"] = "federal" @@ -1418,8 +1556,22 @@ class DomainApplicationTests(TestWithUser, WebTest): Make sure the long name is displaying in the application form, org step """ - request = self.app.get(reverse("application:")).follow() - self.assertContains(request, "Federal: an agency of the U.S. government") + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + + self.assertContains(type_page, "Federal: an agency of the U.S. government") def test_long_org_name_in_application_manage(self): """ @@ -1693,6 +1845,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") @@ -1702,7 +1855,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( @@ -1731,7 +1888,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() @@ -1757,7 +1919,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() @@ -1777,11 +1944,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( @@ -1803,11 +1971,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( @@ -1841,11 +2010,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( @@ -1883,11 +2053,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( @@ -1922,15 +2093,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." @@ -1959,11 +2130,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." @@ -1977,7 +2149,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) @@ -1989,8 +2165,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 @@ -2006,7 +2185,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) @@ -2061,6 +2244,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 @@ -2396,8 +2580,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"], @@ -2743,9 +2929,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/registrar/views/application.py b/src/registrar/views/application.py index bca982916..84c917604 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -206,16 +206,16 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): # if accessing this class directly, redirect to the first step # in other words, if `ApplicationWizard` is called as view # directly by some redirect or url handler, we'll send users - # to the first step in the processes; subclasses will NOT - # be redirected. The purpose of this is to allow code to + # either to an acknowledgement page or to the first step in + # the processes (if an edit rather than a new request); subclasses + # will NOT be redirected. The purpose of this is to allow code to # send users "to the application wizard" without needing to # know which view is first in the list of steps. if self.__class__ == ApplicationWizard: - # if starting a new application, clear the storage if request.path_info == self.NEW_URL_NAME: - del self.storage - - return self.goto(self.steps.first) + return render(request, "application_intro.html") + else: + return self.goto(self.steps.first) self.steps.current = current_url context = self.get_context_data() @@ -370,13 +370,20 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): def post(self, request, *args, **kwargs) -> HttpResponse: """This method handles POST requests.""" - # if accessing this class directly, redirect to the first step - if self.__class__ == ApplicationWizard: - return self.goto(self.steps.first) # which button did the user press? button: str = request.POST.get("submit_button", "") + # if user has acknowledged the intro message + if button == "intro_acknowledge": + if request.path_info == self.NEW_URL_NAME: + del self.storage + return self.goto(self.steps.first) + + # if accessing this class directly, redirect to the first step + if self.__class__ == ApplicationWizard: + return self.goto(self.steps.first) + forms = self.get_forms(use_post=True) if self.is_valid(forms): # always save progress 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