diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index ff8e8b59e..08343e863 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -469,7 +469,9 @@ class DomainApplication(TimeStampedModel): nothing. """ if self.submitter is None or self.submitter.email is None: - logger.warning("Cannot send confirmation email, no submitter email address.") + logger.warning( + "Cannot send confirmation email, no submitter email address." + ) return try: send_templated_email( diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt new file mode 100644 index 000000000..c72730f2d --- /dev/null +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -0,0 +1,6 @@ +You have been invited to manage a domain on get.gov, the registrar for +.gov domain names. + +To accept your invitation, go to <{{ domain_url }}>. + +You will need to log in with a Login.gov account using this email address. diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 10014db8e..8f7a7e54e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,4 +1,5 @@ from unittest import skip +from unittest.mock import MagicMock, ANY from django.conf import settings from django.test import Client, TestCase @@ -557,9 +558,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # the post request should return a redirect to the contact # question self.assertEqual(election_result.status_code, 302) - self.assertEqual( - election_result["Location"], "/register/organization_contact/" - ) + self.assertEqual(election_result["Location"], "/register/organization_contact/") self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) contact_page = election_result.follow() self.assertNotContains(contact_page, "Federal agency") @@ -1139,8 +1138,13 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): success_page = success_result.follow() self.assertContains(success_page, "mayor@igorville.gov") + @boto3_mocking.patching def test_domain_invitation_created(self): - """Add user on a nonexistent email creates an invitation.""" + """Add user on a nonexistent email creates an invitation. + + Adding a non-existent user sends an email as a side-effect, so mock + out the boto3 SES email sending here. + """ # make sure there is no user with this email EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() @@ -1159,10 +1163,36 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): self.assertContains(success_page, "Cancel") # link to cancel invitation self.assertTrue(DomainInvitation.objects.filter(email=EMAIL).exists()) + @boto3_mocking.patching + def test_domain_invitation_email_sent(self): + """Inviting a non-existent user sends them an email.""" + # make sure there is no user with this email + 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", 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 + 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( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [EMAIL]}, + Content=ANY, + ) + def test_domain_invitation_cancel(self): """Posting to the delete view deletes an invitation.""" EMAIL = "mayor@igorville.gov" - invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=EMAIL) + invitation, _ = DomainInvitation.objects.get_or_create( + domain=self.domain, email=EMAIL + ) self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) with self.assertRaises(DomainInvitation.DoesNotExist): DomainInvitation.objects.get(id=invitation.id) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index c8acded23..b235dd166 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -11,6 +11,7 @@ from django.views.generic.edit import DeleteView, FormMixin from registrar.models import Domain, DomainInvitation, User, UserDomainRole from ..forms import DomainAddUserForm +from ..utility.email import send_templated_email, EmailSendingError from .utility import DomainPermission @@ -56,6 +57,12 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView): else: return self.form_invalid(form) + def _domain_abs_url(self): + """Get an absolute URL for this domain.""" + return self.request.build_absolute_uri( + reverse("domain", kwargs={"pk": self.object.id}) + ) + def _make_invitation(self, email_address): """Make a Domain invitation for this email and redirect with a message.""" invitation, created = DomainInvitation.objects.get_or_create( @@ -68,7 +75,19 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView): f"{email_address} has already been invited to this domain.", ) else: - messages.success(self.request, f"Invited {email_address} to this domain.") + # created a new invitation in the database, so send an email + try: + send_templated_email( + "emails/domain_invitation.txt", + to_address=email_address, + context={"domain_url": self._domain_abs_url()}, + ) + except EmailSendingError: + messages.warning(self.request, "Could not send email invitation.") + else: + messages.success( + self.request, f"Invited {email_address} to this domain." + ) return redirect(self.get_success_url()) def form_valid(self, form):