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.
@@ -17,7 +16,8 @@
- After adding a domain manager, an email invitation will be sent to that user with
instructions on how to set up an account.
- All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.
- - Domains must have at least one domain manager. You can’t remove yourself as a domain manager if you’re the only one assigned to this domain. Add another domain manager before you remove yourself from this domain.
+ - All domain managers will be notified when updates are made to this domain.
+ - Domains must have at least one domain manager. You can’t remove yourself as a domain manager if you’re the only one assigned to this domain.
{% 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. "