mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-26 04:28:39 +02:00
portfolio removed email and associated tests
This commit is contained in:
parent
f5ef200216
commit
041ca70a8b
6 changed files with 204 additions and 12 deletions
24
src/registrar/templates/emails/portfolio_removal.txt
Normal file
24
src/registrar/templates/emails/portfolio_removal.txt
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %}
|
||||
|
||||
{{ requestor_email }} has removed you from {{ portfolio.organization_name }}.
|
||||
|
||||
You can no longer view this organization or its related domains within the .gov registrar.
|
||||
|
||||
|
||||
SOMETHING WRONG?
|
||||
If you have questions or concerns, reach out to the person who removed you from the
|
||||
organization, or reply to this email.
|
||||
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
You've been removed from a .gov organization
|
|
@ -16,6 +16,7 @@ from registrar.utility.email_invitations import (
|
|||
send_portfolio_admin_addition_emails,
|
||||
send_portfolio_admin_removal_emails,
|
||||
send_portfolio_invitation_email,
|
||||
send_portfolio_member_permission_remove_email,
|
||||
send_portfolio_member_permission_update_email,
|
||||
)
|
||||
|
||||
|
@ -962,3 +963,76 @@ class TestSendPortfolioMemberPermissionUpdateEmail(unittest.TestCase):
|
|||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_not_called() # Function should fail before logging email failure
|
||||
|
||||
|
||||
class TestSendPortfolioMemberPermissionRemoveEmail(unittest.TestCase):
|
||||
"""Unit tests for send_portfolio_member_permission_remove_email function."""
|
||||
|
||||
@patch("registrar.utility.email_invitations.send_templated_email")
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
def test_send_email_success(self, mock_get_requestor_email, mock_send_email):
|
||||
"""Test that the email is sent successfully when there are no errors."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
permissions = MagicMock(spec=UserPortfolioPermission)
|
||||
permissions.user.email = "user@example.com"
|
||||
permissions.portfolio.organization_name = "Test Portfolio"
|
||||
|
||||
mock_get_requestor_email.return_value = "requestor@example.com"
|
||||
|
||||
# Call function
|
||||
result = send_portfolio_member_permission_remove_email(requestor, permissions)
|
||||
|
||||
# Assertions
|
||||
mock_get_requestor_email.assert_called_once_with(requestor, portfolio=permissions.portfolio)
|
||||
mock_send_email.assert_called_once_with(
|
||||
"emails/portfolio_removal.txt",
|
||||
"emails/portfolio_removal_subject.txt",
|
||||
to_address="user@example.com",
|
||||
context={
|
||||
"requested_user": permissions.user,
|
||||
"portfolio": permissions.portfolio,
|
||||
"requestor_email": "requestor@example.com",
|
||||
},
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
@patch("registrar.utility.email_invitations.send_templated_email", side_effect=EmailSendingError("Email failed"))
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
@patch("registrar.utility.email_invitations.logger")
|
||||
def test_send_email_failure(self, mock_logger, mock_get_requestor_email, mock_send_email):
|
||||
"""Test that the function returns False and logs an error when email sending fails."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
permissions = MagicMock(spec=UserPortfolioPermission)
|
||||
permissions.user.email = "user@example.com"
|
||||
permissions.portfolio.organization_name = "Test Portfolio"
|
||||
|
||||
mock_get_requestor_email.return_value = "requestor@example.com"
|
||||
|
||||
# Call function
|
||||
result = send_portfolio_member_permission_remove_email(requestor, permissions)
|
||||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_called_once_with(
|
||||
"Could not send email organization member removal notification to %s for portfolio: %s",
|
||||
permissions.user.email,
|
||||
permissions.portfolio.organization_name,
|
||||
exc_info=True,
|
||||
)
|
||||
self.assertFalse(result)
|
||||
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email", side_effect=Exception("Unexpected error"))
|
||||
@patch("registrar.utility.email_invitations.logger")
|
||||
def test_requestor_email_retrieval_failure(self, mock_logger, mock_get_requestor_email):
|
||||
"""Test that an exception in retrieving requestor email is logged."""
|
||||
# Mock data
|
||||
requestor = MagicMock()
|
||||
permissions = MagicMock(spec=UserPortfolioPermission)
|
||||
|
||||
# Call function
|
||||
with self.assertRaises(Exception):
|
||||
send_portfolio_member_permission_remove_email(requestor, permissions)
|
||||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_not_called() # Function should fail before logging email failure
|
||||
|
|
|
@ -1669,7 +1669,8 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_delete_view_members_table_active_requests(self, send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_delete_view_members_table_active_requests(self, send_member_removal, send_removal_emails):
|
||||
"""Error state w/ deleting a member with active request on Members Table"""
|
||||
# I'm a user
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
|
@ -1709,12 +1710,15 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# assert that send_portfolio_admin_removal_emails is not called
|
||||
send_removal_emails.assert_not_called()
|
||||
# assert that send_portfolio_member_permission_remove_email is not called
|
||||
send_member_removal.assert_not_called()
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_delete_view_members_table_only_admin(self, send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_delete_view_members_table_only_admin(self, send_member_removal, send_removal_emails):
|
||||
"""Error state w/ deleting a member that's the only admin on Members Table"""
|
||||
|
||||
# I'm a user with admin permission
|
||||
|
@ -1744,12 +1748,15 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# assert that send_portfolio_admin_removal_emails is not called
|
||||
send_removal_emails.assert_not_called()
|
||||
# assert that send_portfolio_member_permission_remove_email is not called
|
||||
send_member_removal.assert_not_called()
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_table_delete_member_success(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_table_delete_member_success(self, send_member_removal, mock_send_removal_emails):
|
||||
"""Success state with deleting on Members Table page bc no active request AND not only admin"""
|
||||
|
||||
# I'm a user
|
||||
|
@ -1774,6 +1781,9 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||
)
|
||||
|
||||
# Member removal email sent successfully
|
||||
send_member_removal.return_value = True
|
||||
|
||||
# And set that the member has no active requests AND it's not the only admin
|
||||
with patch.object(User, "get_active_requests_count_in_portfolio", return_value=0), patch.object(
|
||||
User, "is_only_admin_of_portfolio", return_value=False
|
||||
|
@ -1796,12 +1806,23 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
# assert that send_portfolio_admin_removal_emails is not called
|
||||
# because member being removed is not an admin
|
||||
mock_send_removal_emails.assert_not_called()
|
||||
# assert that send_portfolio_member_permission_remove_email is called
|
||||
send_member_removal.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_member_permission_remove_email
|
||||
_, called_kwargs = send_member_removal.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["permissions"].user, upp.user)
|
||||
self.assertEqual(called_kwargs["permissions"].portfolio, upp.portfolio)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_table_delete_admin_success(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_table_delete_admin_success(self, send_member_removal, mock_send_removal_emails):
|
||||
"""Success state with deleting on Members Table page bc no active request AND
|
||||
not only admin. Because admin, removal emails are sent."""
|
||||
|
||||
|
@ -1828,6 +1849,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
)
|
||||
|
||||
mock_send_removal_emails.return_value = True
|
||||
send_member_removal.return_value = True
|
||||
|
||||
# And set that the member has no active requests AND it's not the only admin
|
||||
with patch.object(User, "get_active_requests_count_in_portfolio", return_value=0), patch.object(
|
||||
|
@ -1850,6 +1872,8 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# assert that send_portfolio_admin_removal_emails is called
|
||||
mock_send_removal_emails.assert_called_once()
|
||||
# assert that send_portfolio_member_permission_remove_email is called
|
||||
send_member_removal.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_admin_addition_emails
|
||||
_, called_kwargs = mock_send_removal_emails.call_args
|
||||
|
@ -1859,13 +1883,23 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
|
||||
|
||||
# Get the arguments passed to send_portfolio_member_permission_remove_email
|
||||
_, called_kwargs = send_member_removal.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["permissions"].user, upp.user)
|
||||
self.assertEqual(called_kwargs["permissions"].portfolio, upp.portfolio)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||
def test_portfolio_member_table_delete_admin_success_removal_email_fail(self, mock_send_removal_emails):
|
||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||
def test_portfolio_member_table_delete_admin_success_removal_email_fail(self, send_member_removal, mock_send_removal_emails):
|
||||
"""Success state with deleting on Members Table page bc no active request AND
|
||||
not only admin. Because admin, removal emails are sent, but fail to send."""
|
||||
not only admin. Because admin, removal emails are sent, but fail to send.
|
||||
Email to removed member also fails to send."""
|
||||
|
||||
# I'm a user
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
|
@ -1890,6 +1924,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
)
|
||||
|
||||
mock_send_removal_emails.return_value = False
|
||||
send_member_removal.return_value = False
|
||||
|
||||
# And set that the member has no active requests AND it's not the only admin
|
||||
with patch.object(User, "get_active_requests_count_in_portfolio", return_value=0), patch.object(
|
||||
|
@ -1912,6 +1947,8 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# assert that send_portfolio_admin_removal_emails is called
|
||||
mock_send_removal_emails.assert_called_once()
|
||||
# assert that send_portfolio_member_permission_remove_email is called
|
||||
send_member_removal.assert_called_once()
|
||||
|
||||
# Get the arguments passed to send_portfolio_admin_addition_emails
|
||||
_, called_kwargs = mock_send_removal_emails.call_args
|
||||
|
@ -1920,6 +1957,15 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.assertEqual(called_kwargs["email"], member_email)
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
|
||||
|
||||
# Get the arguments passed to send_portfolio_member_permission_remove_email
|
||||
_, called_kwargs = send_member_removal.call_args
|
||||
|
||||
# Assert the email content
|
||||
self.assertEqual(called_kwargs["requestor"], self.user)
|
||||
self.assertEqual(called_kwargs["permissions"].user, upp.user)
|
||||
self.assertEqual(called_kwargs["permissions"].portfolio, upp.portfolio)
|
||||
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
|
|
@ -268,6 +268,47 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo
|
|||
return True
|
||||
|
||||
|
||||
def send_portfolio_member_permission_remove_email(requestor, permissions: UserPortfolioPermission):
|
||||
"""
|
||||
Sends an email notification to a portfolio member when their permissions are deleted.
|
||||
|
||||
This function retrieves the requestor's email and sends a templated email to the affected user,
|
||||
notifying them of the removal of their portfolio permissions.
|
||||
|
||||
Args:
|
||||
requestor (User): The user initiating the permission update.
|
||||
permissions (UserPortfolioPermission): The updated permissions object containing the affected user
|
||||
and the portfolio details.
|
||||
|
||||
Returns:
|
||||
bool: True if the email was sent successfully, False if an EmailSendingError occurred.
|
||||
|
||||
Raises:
|
||||
MissingEmailError: If the requestor has no email associated with their account.
|
||||
"""
|
||||
requestor_email = _get_requestor_email(requestor, portfolio=permissions.portfolio)
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/portfolio_removal.txt",
|
||||
"emails/portfolio_removal_subject.txt",
|
||||
to_address=permissions.user.email,
|
||||
context={
|
||||
"requested_user": permissions.user,
|
||||
"portfolio": permissions.portfolio,
|
||||
"requestor_email": requestor_email,
|
||||
},
|
||||
)
|
||||
except EmailSendingError:
|
||||
logger.warning(
|
||||
"Could not send email organization member removal notification to %s " "for portfolio: %s",
|
||||
permissions.user.email,
|
||||
permissions.portfolio.organization_name,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def send_portfolio_admin_addition_emails(email: str, requestor, portfolio: Portfolio):
|
||||
"""
|
||||
Notifies all portfolio admins of the provided portfolio of a newly invited portfolio admin
|
||||
|
|
|
@ -20,6 +20,7 @@ from registrar.utility.email_invitations import (
|
|||
send_portfolio_admin_addition_emails,
|
||||
send_portfolio_admin_removal_emails,
|
||||
send_portfolio_invitation_email,
|
||||
send_portfolio_member_permission_remove_email,
|
||||
send_portfolio_member_permission_update_email,
|
||||
)
|
||||
from registrar.utility.errors import MissingEmailError
|
||||
|
@ -149,18 +150,23 @@ class PortfolioMemberDeleteView(PortfolioMemberPermission, View):
|
|||
messages.error(request, error_message)
|
||||
return redirect(reverse("member", kwargs={"pk": pk}))
|
||||
|
||||
# if member being removed is an admin
|
||||
if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_member_permission.roles:
|
||||
try:
|
||||
try:
|
||||
# if member being removed is an admin
|
||||
if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_member_permission.roles:
|
||||
# attempt to send notification emails of the removal to other portfolio admins
|
||||
if not send_portfolio_admin_removal_emails(
|
||||
email=portfolio_member_permission.user.email,
|
||||
requestor=request.user,
|
||||
portfolio=portfolio_member_permission.portfolio,
|
||||
):
|
||||
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
||||
except Exception as e:
|
||||
self._handle_exceptions(e)
|
||||
messages.warning(request, "Could not send email notification to existing organization admins.")
|
||||
# send notification email to member being removed
|
||||
if not send_portfolio_member_permission_remove_email(
|
||||
requestor=request.user, permissions=portfolio_member_permission
|
||||
):
|
||||
messages.warning(request, f"Could not send email notification to {member.email}")
|
||||
except Exception as e:
|
||||
self._handle_exceptions(e)
|
||||
|
||||
# passed all error conditions
|
||||
portfolio_member_permission.delete()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue