diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 8aeb36961..ae46ba388 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -69,7 +69,7 @@ from django.contrib.sessions.backends.db import SessionStore from django.contrib.auth import get_user_model from django.contrib import messages -from unittest.mock import ANY, patch, Mock +from unittest.mock import ANY, call, patch, Mock from django.forms import ValidationError @@ -143,6 +143,7 @@ class TestDomainInvitationAdmin(TestCase): def tearDown(self): """Delete all DomainInvitation objects""" + PortfolioInvitation.objects.all().delete() DomainInvitation.objects.all().delete() DomainInformation.objects.all().delete() Portfolio.objects.all().delete() @@ -194,14 +195,255 @@ class TestDomainInvitationAdmin(TestCase): self.assertContains(response, retrieved_html, count=1) @less_console_noise_decorator + @override_flag("organization_feature", active=True) @patch("registrar.admin.send_domain_invitation_email") @patch("registrar.admin.send_portfolio_invitation_email") @patch("django.contrib.messages.success") - def test_save_model_user_exists(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): - """Test saving a domain invitation when the user exists. + def test_add_domain_invitation_success_when_user_not_portfolio_member(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and is not a portfolio member. + + Should send out domain and portfolio invites. + Should trigger success messages for both email sends. + Should attempt to retrieve the domain invitation. + Should attempt to retrieve the portfolio invitation.""" + + user = User.objects.create_user(email="test@example.com", username="username") + + # Create a domain invitation instance + invitation = DomainInvitation(email="test@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=user, + ) + mock_send_portfolio_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + portfolio=self.portfolio, + ) + + # Assert success message + mock_messages_success.assert_has_calls([ + call(request, "test@example.com has been invited to the organization: new portfolio"), + call(request, "test@example.com has been invited to the domain: example.com"), + ]) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(DomainInvitation.objects.first().email, "test@example.com") + self.assertEqual(PortfolioInvitation.objects.count(), 1) + self.assertEqual(PortfolioInvitation.objects.first().email, "test@example.com") + + # Assert invitations were retrieved + domain_invitation = DomainInvitation.objects.get(email=user.email, domain=self.domain) + portfolio_invitation = PortfolioInvitation.objects.get(email=user.email, portfolio=self.portfolio) + + self.assertEqual(domain_invitation.status, DomainInvitation.DomainInvitationStatus.RETRIEVED) + self.assertEqual(portfolio_invitation.status, PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) + self.assertEqual(UserDomainRole.objects.count(), 1) + self.assertEqual(UserDomainRole.objects.first().user, user) + self.assertEqual(UserPortfolioPermission.objects.count(), 1) + self.assertEqual(UserPortfolioPermission.objects.first().user, user) + + @less_console_noise_decorator + @override_flag("organization_feature", active=False) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + def test_add_domain_invitation_success_when_user_not_portfolio_member_and_organization_feature_off(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and organization_feature flag is off. + + Should send out a domain invitation. + Should not send a out portfolio invitation. + Should trigger success message for the domain invitation. + Should retrieve the domain invitation. + Should not create a portfolio invitation.""" + + user = User.objects.create_user(email="test@example.com", username="username") + + # Create a domain invitation instance + invitation = DomainInvitation(email="test@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain but not portfolio + mock_send_domain_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=user, + ) + mock_send_portfolio_email.assert_not_called() + + # Assert correct invite was created + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + # Assert success message + mock_messages_success.assert_called_once_with( + request, "test@example.com has been invited to the domain: example.com" + ) + + # Assert the domain invitation was saved + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(DomainInvitation.objects.first().email, "test@example.com") + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + # Assert the domain invitation was retrieved + domain_invitation = DomainInvitation.objects.get(email=user.email, domain=self.domain) + + self.assertEqual(domain_invitation.status, DomainInvitation.DomainInvitationStatus.RETRIEVED) + self.assertEqual(UserDomainRole.objects.count(), 1) + self.assertEqual(UserDomainRole.objects.first().user, user) + self.assertEqual(UserPortfolioPermission.objects.count(), 0) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("multiple_portfolios", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + def test_add_domain_invitation_success_when_user_not_portfolio_member_and_multiple_portfolio_feature_on(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and multiple_portfolio flag is on. + + Should send out a domain invitation. + Should not send a out portfolio invitation. + Should trigger success message for the domain invitation. + Should retrieve the domain invitation. + Should not create a portfolio invitation.""" + + user = User.objects.create_user(email="test@example.com", username="username") + + # Create a domain invitation instance + invitation = DomainInvitation(email="test@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain but not portfolio + mock_send_domain_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=user, + ) + mock_send_portfolio_email.assert_not_called() + + # Assert correct invite was created + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + # Assert success message + mock_messages_success.assert_called_once_with( + request, "test@example.com has been invited to the domain: example.com" + ) + + # Assert the domain invitation was saved + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(DomainInvitation.objects.first().email, "test@example.com") + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + # Assert the domain invitation was retrieved + domain_invitation = DomainInvitation.objects.get(email=user.email, domain=self.domain) + + self.assertEqual(domain_invitation.status, DomainInvitation.DomainInvitationStatus.RETRIEVED) + self.assertEqual(UserDomainRole.objects.count(), 1) + self.assertEqual(UserDomainRole.objects.first().user, user) + self.assertEqual(UserPortfolioPermission.objects.count(), 0) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + def test_add_domain_invitation_success_when_user_existing_portfolio_member(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and a portfolio invitation exists. + + Should send out domain invitation only. + Should trigger success message for the domain invitation. + Should retrieve the domain invitation.""" + + user = User.objects.create_user(email="test@example.com", username="username") + + # Create a domain invitation instance + invitation = DomainInvitation(email="test@example.com", domain=self.domain) + + UserPortfolioPermission.objects.create(user=user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=user, + ) + mock_send_portfolio_email.assert_not_called + + # Assert retrieve was not called + domain_invitation_mock_retrieve.assert_called_once() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert success message + mock_messages_success.assert_called_once_with( + request, "test@example.com has been invited to the domain: example.com" + ) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(DomainInvitation.objects.first().email, "test@example.com") + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.error") + def test_add_domain_invitation_when_user_not_portfolio_member_raises_exception_sending_portfolio_email(self, mock_messages_error, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and is not a portfolio member raises sending portfolio email exception. + + Should only attempt to send the portfolio invitation. + Should trigger error message on portfolio invitation. + Should not attempt to retrieve the domain invitation.""" + + mock_send_portfolio_email.side_effect = MissingEmailError("craving a burger") - Should attempt to retrieve the domain invitation.""" - # Create a user with the same email User.objects.create_user(email="test@example.com", username="username") # Create a domain invitation instance @@ -213,25 +455,172 @@ class TestDomainInvitationAdmin(TestCase): request = self.factory.post("/admin/registrar/DomainInvitation/add/") request.user = self.superuser - # Patch the retrieve method - with patch.object(DomainInvitation, "retrieve") as mock_retrieve: - admin_instance.save_model(request, invitation, form=None, change=False) + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) - # Assert retrieve was called - mock_retrieve.assert_called_once() + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_not_called() + mock_send_portfolio_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + portfolio=self.portfolio, + ) - # Assert the invitation was saved - self.assertEqual(DomainInvitation.objects.count(), 1) - self.assertEqual(DomainInvitation.objects.first().email, "test@example.com") + # Assert retrieve on domain invite only was called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert error message + mock_messages_error.assert_called_once_with( + request, "Can't send invitation email. No email is associated with your user account." + ) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 0) + self.assertEqual(PortfolioInvitation.objects.count(), 0) @less_console_noise_decorator + @override_flag("organization_feature", active=True) @patch("registrar.admin.send_domain_invitation_email") @patch("registrar.admin.send_portfolio_invitation_email") @patch("django.contrib.messages.success") - def test_save_model_user_does_not_exist(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + @patch("django.contrib.messages.error") + def test_add_domain_invitation_when_user_not_portfolio_member_raises_exception_sending_domain_email(self, mock_messages_error, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and is not a portfolio member raises sending domain email exception. + + Should send out the portfolio invitation and attempt to send the domain invitation. + Should trigger portfolio invitation success message. + Should trigger domain invitation error message. + Should not attempt to retrieve the domain invitation. + Should attempt to retrieve the portfolio invitation.""" + + mock_send_domain_email.side_effect = MissingEmailError("craving a burger") + + user = User.objects.create_user(email="test@example.com", username="username") + + # Create a domain invitation instance + invitation = DomainInvitation(email="test@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=user, + ) + mock_send_portfolio_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + portfolio=self.portfolio, + ) + + # Assert retrieve on domain invite only was called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_called_once() + + # Assert success message + mock_messages_success.assert_called_once_with( + request, "test@example.com has been invited to the organization: new portfolio" + ) + + # Assert error message + mock_messages_error.assert_called_once_with( + request, "Can't send invitation email. No email is associated with your user account." + ) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 0) + self.assertEqual(PortfolioInvitation.objects.count(), 1) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + @patch("django.contrib.messages.error") + def test_add_domain_invitation_when_user_existing_portfolio_member_raises_exception_sending_domain_email(self, mock_messages_error, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and is not a portfolio member raises sending domain email exception. + + Should send out the portfolio invitation and attempt to send the domain invitation. + Should trigger portfolio invitation success message. + Should trigger domain invitation error message. + Should not attempt to retrieve the domain invitation. + Should attempt to retrieve the portfolio invitation.""" + + mock_send_domain_email.side_effect = MissingEmailError("craving a burger") + + user = User.objects.create_user(email="test@example.com", username="username") + + UserPortfolioPermission.objects.create( + user=user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER] + ) + + # Create a domain invitation instance + invitation = DomainInvitation(email="test@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_called_once_with( + email="test@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=user, + ) + mock_send_portfolio_email.assert_not_called() + + # Assert retrieve on domain invite only was called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert success message + mock_messages_success.assert_not_called() + + # Assert error message + mock_messages_error.assert_called_once_with( + request, "Can't send invitation email. No email is associated with your user account." + ) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 0) + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + def test_add_domain_invitation_success_when_email_not_portfolio_member(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): """Test saving a domain invitation when the user does not exist. - Should not attempt to retrieve the domain invitation.""" + Should send out domain and portfolio invitations. + Should trigger success messages. + Should not attempt to retrieve the domain invitation. + Should not attempt to retrieve the portfolio invitation.""" # Create a domain invitation instance invitation = DomainInvitation(email="nonexistent@example.com", domain=self.domain) @@ -242,15 +631,370 @@ class TestDomainInvitationAdmin(TestCase): request.user = self.superuser # Patch the retrieve method to ensure it is not called - with patch.object(DomainInvitation, "retrieve") as mock_retrieve: - admin_instance.save_model(request, invitation, form=None, change=False) + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=None, + ) + mock_send_portfolio_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + portfolio=self.portfolio, + ) # Assert retrieve was not called - mock_retrieve.assert_not_called() + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() - # Assert the invitation was saved + # Assert success message + mock_messages_success.assert_has_calls([ + call(request, "nonexistent@example.com has been invited to the organization: new portfolio"), + call(request, "nonexistent@example.com has been invited to the domain: example.com"), + ]) + + # Assert the invitations were saved self.assertEqual(DomainInvitation.objects.count(), 1) self.assertEqual(DomainInvitation.objects.first().email, "nonexistent@example.com") + self.assertEqual(PortfolioInvitation.objects.count(), 1) + self.assertEqual(PortfolioInvitation.objects.first().email, "nonexistent@example.com") + + @less_console_noise_decorator + @override_flag("organization_feature", active=False) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + def test_add_domain_invitation_success_when_email_not_portfolio_member_and_organization_feature_off(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user does not exist and organization_feature flag is off. + + Should send out a domain invitation. + Should not send a out portfolio invitation. + Should trigger success message for domain invitation. + Should not retrieve the domain invitation. + Should not create a portfolio invitation.""" + # Create a domain invitation instance + invitation = DomainInvitation(email="nonexistent@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain but not portfolio + mock_send_domain_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=None, + ) + mock_send_portfolio_email.assert_not_called() + + # Assert retrieve on domain invite only was called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert success message + mock_messages_success.assert_called_once_with( + request, "nonexistent@example.com has been invited to the domain: example.com" + ) + + # Assert the domain invitation was saved + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(DomainInvitation.objects.first().email, "nonexistent@example.com") + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("multiple_portfolios", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + def test_add_domain_invitation_success_when_email_not_portfolio_member_and_multiple_portfolio_feature_on(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user does not exist and multiple_portfolio flag is on. + + Should send out a domain invitation. + Should not send a out portfolio invitation. + Should trigger success message for domain invitation. + Should not retrieve the domain invitation. + Should not create a portfolio invitation.""" + # Create a domain invitation instance + invitation = DomainInvitation(email="nonexistent@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain but not portfolio + mock_send_domain_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=None, + ) + mock_send_portfolio_email.assert_not_called() + + # Assert retrieve on domain invite only was called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert success message + mock_messages_success.assert_called_once_with( + request, "nonexistent@example.com has been invited to the domain: example.com" + ) + + # Assert the domain invitation was saved + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(DomainInvitation.objects.first().email, "nonexistent@example.com") + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + def test_add_domain_invitation_success_when_email_existing_portfolio_invitation(self, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user does not exist and a portfolio invitation exists. + + Should send out domain invitation only. + Should trigger success message for the domain invitation. + Should not attempt to retrieve the domain invitation. + Should not attempt to retrieve the portfolio invitation.""" + + PortfolioInvitation.objects.create(email="nonexistent@example.com", portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]) + + # Create a domain invitation instance + invitation = DomainInvitation(email="nonexistent@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=False, + requested_user=None, + ) + mock_send_portfolio_email.assert_not_called + + # Assert retrieve was not called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert success message + mock_messages_success.assert_called_once_with( + request, "nonexistent@example.com has been invited to the domain: example.com" + ) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 1) + self.assertEqual(DomainInvitation.objects.first().email, "nonexistent@example.com") + self.assertEqual(PortfolioInvitation.objects.count(), 1) + self.assertEqual(PortfolioInvitation.objects.first().email, "nonexistent@example.com") + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.error") + def test_add_domain_invitation_when_user_not_portfolio_email_raises_exception_sending_portfolio_email(self, mock_messages_error, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and is not a portfolio member raises sending portfolio email exception. + + Should only attempt to send the portfolio invitation. + Should trigger error message on portfolio invitation. + Should not attempt to retrieve the domain invitation. + Should not attempt to retrieve the portfolio invitation.""" + + mock_send_portfolio_email.side_effect = MissingEmailError("craving a burger") + + # Create a domain invitation instance + invitation = DomainInvitation(email="nonexistent@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_not_called() + mock_send_portfolio_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + portfolio=self.portfolio, + ) + + # Assert retrieve on domain invite only was called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert error message + mock_messages_error.assert_called_once_with( + request, "Can't send invitation email. No email is associated with your user account." + ) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 0) + self.assertEqual(PortfolioInvitation.objects.count(), 0) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + @patch("django.contrib.messages.error") + def test_add_domain_invitation_when_user_not_portfolio_email_raises_exception_sending_domain_email(self, mock_messages_error, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and is not a portfolio member raises sending domain email exception. + + Should send out the portfolio invitation and attempt to send the domain invitation. + Should trigger portfolio invitation success message. + Should trigger domain invitation error message. + Should not attempt to retrieve the domain invitation. + Should attempt to retrieve the portfolio invitation.""" + + mock_send_domain_email.side_effect = MissingEmailError("craving a burger") + + # Create a domain invitation instance + invitation = DomainInvitation(email="nonexistent@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=None, + requested_user=None, + ) + mock_send_portfolio_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + portfolio=self.portfolio, + ) + + # Assert retrieve on domain invite only was called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert success message + mock_messages_success.assert_called_once_with( + request, "nonexistent@example.com has been invited to the organization: new portfolio" + ) + + # Assert error message + mock_messages_error.assert_called_once_with( + request, "Can't send invitation email. No email is associated with your user account." + ) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 0) + self.assertEqual(PortfolioInvitation.objects.count(), 1) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @patch("registrar.admin.send_domain_invitation_email") + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") + @patch("django.contrib.messages.error") + def test_add_domain_invitation_when_user_existing_portfolio_email_raises_exception_sending_domain_email(self, mock_messages_error, mock_messages_success, mock_send_portfolio_email, mock_send_domain_email): + """Test saving a domain invitation when the user exists and is not a portfolio member raises sending domain email exception. + + Should send out the portfolio invitation and attempt to send the domain invitation. + Should trigger portfolio invitation success message. + Should trigger domain invitation error message. + Should not attempt to retrieve the domain invitation. + Should attempt to retrieve the portfolio invitation.""" + + mock_send_domain_email.side_effect = MissingEmailError("craving a burger") + + PortfolioInvitation.objects.create( + email="nonexistent@example.com", portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER] + ) + + # Create a domain invitation instance + invitation = DomainInvitation(email="nonexistent@example.com", domain=self.domain) + + admin_instance = DomainInvitationAdmin(DomainInvitation, admin_site=None) + + # Create a request object + request = self.factory.post("/admin/registrar/DomainInvitation/add/") + request.user = self.superuser + + # Patch the retrieve method to ensure it is not called + with patch.object(DomainInvitation, "retrieve") as domain_invitation_mock_retrieve: + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, invitation, form=None, change=False) + + # Assert sends appropriate emails - domain and portfolio invites + mock_send_domain_email.assert_called_once_with( + email="nonexistent@example.com", + requestor=self.superuser, + domains=self.domain, + is_member_of_different_org=False, + requested_user=None, + ) + mock_send_portfolio_email.assert_not_called() + + # Assert retrieve on domain invite only was called + domain_invitation_mock_retrieve.assert_not_called() + portfolio_invitation_mock_retrieve.assert_not_called() + + # Assert success message + mock_messages_success.assert_not_called() + + # Assert error message + mock_messages_error.assert_called_once_with( + request, "Can't send invitation email. No email is associated with your user account." + ) + + # Assert the invitations were saved + self.assertEqual(DomainInvitation.objects.count(), 0) + self.assertEqual(PortfolioInvitation.objects.count(), 1) class TestUserPortfolioPermissionAdmin(TestCase): @@ -387,6 +1131,7 @@ class TestPortfolioInvitationAdmin(TestCase): Portfolio.objects.all().delete() PortfolioInvitation.objects.all().delete() Contact.objects.all().delete() + User.objects.all().delete() @classmethod def tearDownClass(self): @@ -543,34 +1288,32 @@ class TestPortfolioInvitationAdmin(TestCase): @less_console_noise_decorator def test_get_filters(self): """Ensures that our filters are displaying correctly""" - with less_console_noise(): - self.client.force_login(self.superuser) + self.client.force_login(self.superuser) - response = self.client.get( - "/admin/registrar/portfolioinvitation/", - {}, - follow=True, - ) + response = self.client.get( + "/admin/registrar/portfolioinvitation/", + {}, + follow=True, + ) - # Assert that the filters are added - self.assertContains(response, "invited", count=4) - self.assertContains(response, "Invited", count=2) - self.assertContains(response, "retrieved", count=2) - self.assertContains(response, "Retrieved", count=2) + # Assert that the filters are added + self.assertContains(response, "invited", count=4) + self.assertContains(response, "Invited", count=2) + self.assertContains(response, "retrieved", count=2) + self.assertContains(response, "Retrieved", count=2) - # Check for the HTML context specificially - invited_html = 'Invited' - retrieved_html = 'Retrieved' + # Check for the HTML context specificially + invited_html = 'Invited' + retrieved_html = 'Retrieved' - self.assertContains(response, invited_html, count=1) - self.assertContains(response, retrieved_html, count=1) + self.assertContains(response, invited_html, count=1) + self.assertContains(response, retrieved_html, count=1) @less_console_noise_decorator @patch("registrar.admin.send_portfolio_invitation_email") @patch("django.contrib.messages.success") # Mock the `messages.warning` call - def test_save_sends_email(self, mock_messages_warning, mock_send_email): - """On save_model, an email is NOT sent if an invitation already exists.""" - self.client.force_login(self.superuser) + def test_save_sends_email(self, mock_messages_success, mock_send_email): + """On save_model, an email is sent if an invitation already exists.""" # Create an instance of the admin class admin_instance = PortfolioInvitationAdmin(PortfolioInvitation, admin_site=None) @@ -589,7 +1332,7 @@ class TestPortfolioInvitationAdmin(TestCase): # Call the save_model method admin_instance.save_model(request, portfolio_invitation, None, None) - # Assert that send_portfolio_invitation_email is not called + # Assert that send_portfolio_invitation_email is called mock_send_email.assert_called() # Get the arguments passed to send_portfolio_invitation_email @@ -601,7 +1344,7 @@ class TestPortfolioInvitationAdmin(TestCase): self.assertEqual(called_kwargs["portfolio"], self.portfolio) # Assert that a warning message was triggered - mock_messages_warning.assert_called_once_with(request, "james.gordon@gotham.gov has been invited.") + mock_messages_success.assert_called_once_with(request, "james.gordon@gotham.gov has been invited.") @less_console_noise_decorator @patch("registrar.admin.send_portfolio_invitation_email") @@ -638,6 +1381,90 @@ class TestPortfolioInvitationAdmin(TestCase): # Assert that a warning message was triggered mock_messages_warning.assert_called_once_with(request, "User is already a member of this portfolio.") + @less_console_noise_decorator + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") # Mock the `messages.warning` call + def test_add_portfolio_invitation_auto_retrieves_invitation_when_user_exists(self, mock_messages_success, mock_send_email): + """On save_model, we create and retrieve a portfolio invitation if the user exists.""" + + # Create an instance of the admin class + admin_instance = PortfolioInvitationAdmin(PortfolioInvitation, admin_site=None) + + User.objects.create_user(email="james.gordon@gotham.gov", username="username") + + # Create a PortfolioInvitation instance + portfolio_invitation = PortfolioInvitation( + email="james.gordon@gotham.gov", + portfolio=self.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], + ) + + # Create a request object + request = self.factory.post("/admin/registrar/PortfolioInvitation/add/") + request.user = self.superuser + + # Call the save_model method + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, portfolio_invitation, None, None) + + # Assert that send_portfolio_invitation_email is called + mock_send_email.assert_called() + + # Get the arguments passed to send_portfolio_invitation_email + _, called_kwargs = mock_send_email.call_args + + # Assert the email content + self.assertEqual(called_kwargs["email"], "james.gordon@gotham.gov") + self.assertEqual(called_kwargs["requestor"], self.superuser) + self.assertEqual(called_kwargs["portfolio"], self.portfolio) + + # Assert that a warning message was triggered + mock_messages_success.assert_called_once_with(request, "james.gordon@gotham.gov has been invited.") + + # The invitation is not retrieved + portfolio_invitation_mock_retrieve.assert_called_once() + + @less_console_noise_decorator + @patch("registrar.admin.send_portfolio_invitation_email") + @patch("django.contrib.messages.success") # Mock the `messages.warning` call + def test_add_portfolio_invitation_does_not_retrieve_invitation_when_no_user(self, mock_messages_success, mock_send_email): + """On save_model, we create but do not retrieve a portfolio invitation if the user does not exist.""" + + # Create an instance of the admin class + admin_instance = PortfolioInvitationAdmin(PortfolioInvitation, admin_site=None) + + # Create a PortfolioInvitation instance + portfolio_invitation = PortfolioInvitation( + email="james.gordon@gotham.gov", + portfolio=self.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], + ) + + # Create a request object + request = self.factory.post("/admin/registrar/PortfolioInvitation/add/") + request.user = self.superuser + + # Call the save_model method + with patch.object(PortfolioInvitation, "retrieve") as portfolio_invitation_mock_retrieve: + admin_instance.save_model(request, portfolio_invitation, None, None) + + # Assert that send_portfolio_invitation_email is called + mock_send_email.assert_called() + + # Get the arguments passed to send_portfolio_invitation_email + _, called_kwargs = mock_send_email.call_args + + # Assert the email content + self.assertEqual(called_kwargs["email"], "james.gordon@gotham.gov") + self.assertEqual(called_kwargs["requestor"], self.superuser) + self.assertEqual(called_kwargs["portfolio"], self.portfolio) + + # Assert that a warning message was triggered + mock_messages_success.assert_called_once_with(request, "james.gordon@gotham.gov has been invited.") + + # The invitation is not retrieved + portfolio_invitation_mock_retrieve.assert_not_called() + @less_console_noise_decorator @patch("registrar.admin.send_portfolio_invitation_email") @patch("django.contrib.messages.error") # Mock the `messages.error` call @@ -667,7 +1494,7 @@ class TestPortfolioInvitationAdmin(TestCase): # Assert that messages.error was called with the correct message mock_messages_error.assert_called_once_with( - request, "Could not send email invitation. Portfolio invitation not saved." + request, "Email service unavailable" ) @less_console_noise_decorator @@ -705,7 +1532,7 @@ class TestPortfolioInvitationAdmin(TestCase): @less_console_noise_decorator @patch("registrar.admin.send_portfolio_invitation_email") - @patch("django.contrib.messages.error") # Mock the `messages.error` call + @patch("django.contrib.messages.warning") # Mock the `messages.error` call def test_save_exception_generic_error(self, mock_messages_error, mock_send_email): """Handle generic exceptions correctly during portfolio invitation.""" self.client.force_login(self.superuser) @@ -732,7 +1559,7 @@ class TestPortfolioInvitationAdmin(TestCase): # Assert that messages.error was called with the correct message mock_messages_error.assert_called_once_with( - request, "Could not send email invitation. Portfolio invitation not saved." + request, "Could not send email invitation." )