diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 412f4ee73..1b789e590 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -8,8 +8,7 @@

Domain managers can update all information related to a domain within the - .gov registrar, including contact details, senior official, security - email, and DNS name servers. + .gov registrar, including, security email and DNS name servers.

{% if domain.permissions %} diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index 87e2d551f..21ba06316 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -71,7 +71,7 @@ class TestEmails(TestCase): "doesnotexist@igorville.com", context={"domain_request": self}, bcc_address=None, - cc=["test_email1@example.com", "test_email2@example.com"] + cc_addresses=["test_email1@example.com", "test_email2@example.com"] ) # check that an email was sent diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 43ea9bdd4..d258dc472 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model from waffle.testutils import override_flag from api.tests.common import less_console_noise_decorator from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from registrar.utility.email import send_templated_email from .common import MockEppLib, MockSESClient, create_user # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore @@ -67,6 +68,10 @@ class TestWithDomainPermissions(TestWithUser): datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) ), ) + self.domain_dns_needed, _ = Domain.objects.get_or_create( + name="dns-needed.gov", + state=Domain.State.DNS_NEEDED, + ) self.domain_deleted, _ = Domain.objects.get_or_create( name="deleted.gov", state=Domain.State.DELETED, @@ -85,6 +90,12 @@ class TestWithDomainPermissions(TestWithUser): self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + self.security_contact, _ = PublicContact.objects.get_or_create( + domain=self.domain, + contact_type=PublicContact.ContactTypeChoices.SECURITY, + email="security@igorville.gov", + ) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata) DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata) DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none) @@ -93,6 +104,8 @@ class TestWithDomainPermissions(TestWithUser): DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver) DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold) DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dns_needed) + self.role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER @@ -101,6 +114,9 @@ class TestWithDomainPermissions(TestWithUser): UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_dns_needed, role=UserDomainRole.Roles.MANAGER + ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_multdsdata, @@ -1976,6 +1992,7 @@ class TestDomainChangeNotifications(TestDomainOverview): super().setUpClass() allowed_emails = [ AllowedEmail(email="info@example.com"), + AllowedEmail(email="doesnotexist@igorville.com"), ] AllowedEmail.objects.bulk_create(allowed_emails) @@ -1990,10 +2007,15 @@ class TestDomainChangeNotifications(TestDomainOverview): AllowedEmail.objects.all().delete() @boto3_mocking.patching - def test_notification_email_sent_on_org_name_change(self): + @less_console_noise_decorator + def test_notification_on_org_name_change(self): """Test that an email is sent when the organization name is changed.""" self.domain_information.organization_name = "Town of Igorville" + self.domain_information.address_line1 = "123 Main St" + self.domain_information.city = "Igorville" + self.domain_information.state_territory = "IL" + self.domain_information.zipcode = "62052" self.domain_information.save() org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) @@ -2003,22 +2025,215 @@ class TestDomainChangeNotifications(TestDomainOverview): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - success_result_page = org_name_page.form.submit() - # Check that the page loads successfully - self.assertEqual(success_result_page.status_code, 200) - self.assertContains(success_result_page, "Not igorville") + org_name_page.form.submit() # Check that an email was sent self.assertTrue(self.mock_client.send_email.called) + # Check email content # check the call sequence for the email - args, kwargs = self.mock_client.send_email.call_args + _, kwargs = self.mock_client.send_email.call_args self.assertIn("Content", kwargs) self.assertIn("Simple", kwargs["Content"]) self.assertIn("Subject", kwargs["Content"]["Simple"]) self.assertIn("Body", kwargs["Content"]["Simple"]) - # check for things in the email content (not an exhaustive list) body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn("DOMAIN:", body) + self.assertIn("DOMAIN: igorville.gov", body) + self.assertIn("UPDATED BY: First Last info@example.com", body) + self.assertIn("INFORMATION UPDATED: Org Name/Address", body) + + @boto3_mocking.patching + @less_console_noise_decorator + def test_no_notification_on_org_name_change_with_portfolio(self): + """Test that an email is not sent on org name change when the domain is in a portfolio""" + + portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user) + + self.domain_information.organization_name = "Town of Igorville" + self.domain_information.address_line1 = "123 Main St" + self.domain_information.city = "Igorville" + self.domain_information.state_territory = "IL" + self.domain_information.zipcode = "62052" + self.domain_information.portfolio = portfolio + self.domain_information.save() + + org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + org_name_page.form["organization_name"] = "Not igorville" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + org_name_page.form.submit() + + # Check that an email was not sent + self.assertFalse(self.mock_client.send_email.called) + + @boto3_mocking.patching + @less_console_noise_decorator + def test_notification_on_security_email_change(self): + """Test that an email is sent when the security email is changed.""" + + security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + security_email_page.form["security_email"] = "new_security@example.com" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + security_email_page.form.submit() + + self.assertTrue(self.mock_client.send_email.called) + + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + self.assertIn("DOMAIN: igorville.gov", body) + self.assertIn("UPDATED BY: First Last info@example.com", body) + self.assertIn("INFORMATION UPDATED: Security Email", body) + + @boto3_mocking.patching + @less_console_noise_decorator + def test_notification_on_dnssec_enable(self): + """Test that an email is sent when DNSSEC is enabled.""" + + page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id})) + self.assertContains(page, "Disable DNSSEC") + + # Prepare the data for the POST request + post_data = { + "disable_dnssec": "Disable DNSSEC", + } + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + updated_page = self.client.post( + reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), + post_data, + follow=True, + ) + + self.assertEqual(updated_page.status_code, 200) + + self.assertContains(updated_page, "Enable DNSSEC") + + self.assertTrue(self.mock_client.send_email.called) + + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + self.assertIn("DOMAIN: igorville.gov", body) + self.assertIn("UPDATED BY: First Last info@example.com", body) + self.assertIn("INFORMATION UPDATED: DNSSec", body) + + @boto3_mocking.patching + @less_console_noise_decorator + def test_notification_on_ds_data_change(self): + """Test that an email is sent when DS data is changed.""" + + ds_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # Add DS data + ds_data_page.forms[0]["form-0-key_tag"] = "12345" + ds_data_page.forms[0]["form-0-algorithm"] = "13" + ds_data_page.forms[0]["form-0-digest_type"] = "2" + ds_data_page.forms[0]["form-0-digest"] = "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + ds_data_page.forms[0].submit() + + # check that the email was sent + self.assertTrue(self.mock_client.send_email.called) + + # check some stuff about the email + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + self.assertIn("DOMAIN: igorville.gov", body) + self.assertIn("UPDATED BY: First Last info@example.com", body) + self.assertIn("INFORMATION UPDATED: DS Data", body) + + @boto3_mocking.patching + @less_console_noise_decorator + def test_notification_on_senior_official_change(self): + """Test that an email is sent when the senior official information is changed.""" + + self.domain_information.senior_official = Contact.objects.create( + first_name="Old", last_name="Official", title="Manager", email="old_official@example.com" + ) + self.domain_information.save() + + senior_official_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + senior_official_page.form["first_name"] = "New" + senior_official_page.form["last_name"] = "Official" + senior_official_page.form["title"] = "Director" + senior_official_page.form["email"] = "new_official@example.com" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + senior_official_page.form.submit() + + self.assertTrue(self.mock_client.send_email.called) + + _, kwargs = self.mock_client.send_email.call_args + body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + self.assertIn("DOMAIN: igorville.gov", body) + self.assertIn("UPDATED BY: First Last info@example.com", body) + self.assertIn("INFORMATION UPDATED: Senior Official", body) + + @boto3_mocking.patching + @less_console_noise_decorator + def test_no_notification_on_senior_official_when_portfolio(self): + """Test that an email is not sent when the senior official information is changed + and the domain is in a portfolio.""" + + self.domain_information.senior_official = Contact.objects.create( + first_name="Old", last_name="Official", title="Manager", email="old_official@example.com" + ) + portfolio, _ =Portfolio.objects.get_or_create( + organization_name="portfolio", + creator=self.user, + ) + self.domain_information.portfolio = portfolio + self.domain_information.save() + + senior_official_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + senior_official_page.form["first_name"] = "New" + senior_official_page.form["last_name"] = "Official" + senior_official_page.form["title"] = "Director" + senior_official_page.form["email"] = "new_official@example.com" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + senior_official_page.form.submit() + + self.assertFalse(self.mock_client.send_email.called) + + @boto3_mocking.patching + @less_console_noise_decorator + def test_no_notification_when_dns_needed(self): + """Test that an email is not sent when nameservers are changed while the state is DNS_NEEDED.""" + + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain_dns_needed.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # add nameservers + nameservers_page.form["form-0-server"] = "ns1-new.igorville.gov" + nameservers_page.form["form-0-ip"] = "192.168.1.1" + nameservers_page.form["form-1-server"] = "ns2-new.igorville.gov" + nameservers_page.form["form-1-ip"] = "192.168.1.2" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + nameservers_page.form.submit() + + # Check that an email was not sent + self.assertFalse(self.mock_client.send_email.called) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 04b740f6e..22ece989b 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -119,6 +119,7 @@ class DomainFormBaseView(DomainBaseView, FormMixin): if form.is_valid(): return self.form_valid(form) else: + logger.debug(f"Form errors: {form.errors}") return self.form_invalid(form) def form_valid(self, form): @@ -164,6 +165,7 @@ class DomainFormBaseView(DomainBaseView, FormMixin): DomainDsdataFormset: "DS Data", DomainOrgNameAddressForm: "Org Name/Address", SeniorOfficialContactForm: "Senior Official", + NameserverFormset: "Nameservers", } # forms of these types should not send notifications if they're part of a portfolio/Organization @@ -179,14 +181,13 @@ class DomainFormBaseView(DomainBaseView, FormMixin): # some forms shouldn't cause notifications if they are in a portfolio info = self.get_domain_info_from_domain() if not info or info.portfolio: + logger.info(f"Not notifying because of portfolio") should_notify = False else: # don't notify for any other types of forms should_notify=False - + logger.info(f"Not notifying for {form.__class__}") if (should_notify and form.has_changed()) or force_send: - logger.info("Sending email to domain managers") - context={ "domain": self.object.name, "user": self.request.user, @@ -194,6 +195,8 @@ class DomainFormBaseView(DomainBaseView, FormMixin): "changes": form_label_dict[form.__class__] } self.email_domain_managers(self.object, "emails/update_to_approved_domain.txt", "emails/update_to_approved_domain_subject.txt", context) + else: + logger.info(f"Not notifying for {form.__class__}, form changes: {form.has_changed()}, force_send: {force_send}") def email_domain_managers(self, domain_name, template: str, subject_template: str, context: any = {}): """Send a single email built from a template to all managers for a given domain. @@ -489,8 +492,7 @@ class DomainNameserversView(DomainFormBaseView): This post method harmonizes using DomainBaseView and FormMixin """ - - logger.info("Posted to Namservers View") + logger.info(f"POST request to DomainNameserversView") self._get_domain(request) formset = self.get_form() @@ -500,6 +502,7 @@ class DomainNameserversView(DomainFormBaseView): return HttpResponseRedirect(url) if formset.is_valid(): + logger.info(f"Formset is valid") return self.form_valid(formset) else: return self.form_invalid(formset) @@ -507,8 +510,6 @@ class DomainNameserversView(DomainFormBaseView): def form_valid(self, formset): """The formset is valid, perform something with it.""" - logger.info("------ Nameserver Form is valid -------") - self.request.session["nameservers_form_domain"] = self.object # Set the nameservers from the formset @@ -550,7 +551,8 @@ class DomainNameserversView(DomainFormBaseView): messages.error(self.request, NameserverError(code=nsErrorCodes.BAD_DATA)) logger.error(f"Registry error: {Err}") else: - self.send_update_notification(formset) + if self.object.state == Domain.State.READY: + self.send_update_notification(formset) messages.success( self.request, "The name servers for this domain have been updated. "