additional tests for email_invitations

This commit is contained in:
David Kennedy 2025-02-05 17:13:02 -05:00
parent 350508f9c1
commit cbc9cdbe34
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
7 changed files with 111 additions and 31 deletions

View file

@ -337,21 +337,21 @@ class BasePortfolioMemberForm(forms.ModelForm):
UserPortfolioRoleChoices.ORGANIZATION_ADMIN in previous_roles UserPortfolioRoleChoices.ORGANIZATION_ADMIN in previous_roles
and UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in new_roles and UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in new_roles
) )
def is_change(self) -> bool: def is_change(self) -> bool:
""" """
Determines if the form has changed by comparing the initial data Determines if the form has changed by comparing the initial data
with the submitted cleaned data. with the submitted cleaned data.
Returns: Returns:
bool: True if the form has changed, False otherwise. bool: True if the form has changed, False otherwise.
""" """
# Compare role values # Compare role values
previous_roles = set(self.initial.get("roles", [])) previous_roles = set(self.initial.get("roles", []))
new_roles = set(self.cleaned_data.get("roles", [])) new_roles = set(self.cleaned_data.get("roles", []))
# Compare additional permissions values # Compare additional permissions values
previous_permissions = set(self.initial.get("additional_permissions", [])) previous_permissions = set(self.initial.get("additional_permissions", []))
new_permissions = set(self.cleaned_data.get("additional_permissions", [])) new_permissions = set(self.cleaned_data.get("additional_permissions", []))
return previous_roles != new_roles or previous_permissions != new_permissions return previous_roles != new_roles or previous_permissions != new_permissions

View file

@ -101,41 +101,41 @@ class PortfolioInvitation(TimeStampedModel):
str: The display name of the user's role. str: The display name of the user's role.
""" """
return get_role_display(self.roles) return get_role_display(self.roles)
@property @property
def domains_display(self): def domains_display(self):
""" """
Returns a string representation of the user's domain access level. Returns a string representation of the user's domain access level.
Uses the `get_domains_display` function to determine whether the user has Uses the `get_domains_display` function to determine whether the user has
"Viewer, all" access (can view all domains) or "Viewer, limited" access. "Viewer, all" access (can view all domains) or "Viewer, limited" access.
Returns: Returns:
str: The display name of the user's domain permissions. str: The display name of the user's domain permissions.
""" """
return get_domains_display(self.roles, self.additional_permissions) return get_domains_display(self.roles, self.additional_permissions)
@property @property
def domain_requests_display(self): def domain_requests_display(self):
""" """
Returns a string representation of the user's access to domain requests. Returns a string representation of the user's access to domain requests.
Uses the `get_domain_requests_display` function to determine if the user Uses the `get_domain_requests_display` function to determine if the user
is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), is a "Creator" (can create and edit requests), a "Viewer" (can only view requests),
or has "No access" to domain requests. or has "No access" to domain requests.
Returns: Returns:
str: The display name of the user's domain request permissions. str: The display name of the user's domain request permissions.
""" """
return get_domain_requests_display(self.roles, self.additional_permissions) return get_domain_requests_display(self.roles, self.additional_permissions)
@property @property
def members_display(self): def members_display(self):
""" """
Returns a string representation of the user's access to managing members. Returns a string representation of the user's access to managing members.
Uses the `get_members_display` function to determine if the user is a Uses the `get_members_display` function to determine if the user is a
"Manager" (can edit members), a "Viewer" (can view members), or has "No access" "Manager" (can edit members), a "Viewer" (can view members), or has "No access"
to member management. to member management.
Returns: Returns:

View file

@ -201,41 +201,41 @@ class UserPortfolioPermission(TimeStampedModel):
str: The display name of the user's role. str: The display name of the user's role.
""" """
return get_role_display(self.roles) return get_role_display(self.roles)
@property @property
def domains_display(self): def domains_display(self):
""" """
Returns a string representation of the user's domain access level. Returns a string representation of the user's domain access level.
Uses the `get_domains_display` function to determine whether the user has Uses the `get_domains_display` function to determine whether the user has
"Viewer, all" access (can view all domains) or "Viewer, limited" access. "Viewer, all" access (can view all domains) or "Viewer, limited" access.
Returns: Returns:
str: The display name of the user's domain permissions. str: The display name of the user's domain permissions.
""" """
return get_domains_display(self.roles, self.additional_permissions) return get_domains_display(self.roles, self.additional_permissions)
@property @property
def domain_requests_display(self): def domain_requests_display(self):
""" """
Returns a string representation of the user's access to domain requests. Returns a string representation of the user's access to domain requests.
Uses the `get_domain_requests_display` function to determine if the user Uses the `get_domain_requests_display` function to determine if the user
is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), is a "Creator" (can create and edit requests), a "Viewer" (can only view requests),
or has "No access" to domain requests. or has "No access" to domain requests.
Returns: Returns:
str: The display name of the user's domain request permissions. str: The display name of the user's domain request permissions.
""" """
return get_domain_requests_display(self.roles, self.additional_permissions) return get_domain_requests_display(self.roles, self.additional_permissions)
@property @property
def members_display(self): def members_display(self):
""" """
Returns a string representation of the user's access to managing members. Returns a string representation of the user's access to managing members.
Uses the `get_members_display` function to determine if the user is a Uses the `get_members_display` function to determine if the user is a
"Manager" (can edit members), a "Viewer" (can view members), or has "No access" "Manager" (can edit members), a "Viewer" (can view members), or has "No access"
to member management. to member management.
Returns: Returns:

View file

@ -82,6 +82,7 @@ class MemberPermissionDisplay(StrEnum):
VIEWER = "Viewer" VIEWER = "Viewer"
NONE = "None" NONE = "None"
def get_role_display(roles): def get_role_display(roles):
""" """
Returns a user-friendly display name for a given list of user roles. Returns a user-friendly display name for a given list of user roles.
@ -103,6 +104,7 @@ def get_role_display(roles):
else: else:
return "-" return "-"
def get_domains_display(roles, permissions): def get_domains_display(roles, permissions):
""" """
Determines the display name for a user's domain viewing permissions. Determines the display name for a user's domain viewing permissions.
@ -124,6 +126,7 @@ def get_domains_display(roles, permissions):
else: else:
return "Viewer, limited" return "Viewer, limited"
def get_domain_requests_display(roles, permissions): def get_domain_requests_display(roles, permissions):
""" """
Determines the display name for a user's domain request permissions. Determines the display name for a user's domain request permissions.
@ -148,6 +151,7 @@ def get_domain_requests_display(roles, permissions):
else: else:
return "No access" return "No access"
def get_members_display(roles, permissions): def get_members_display(roles, permissions):
""" """
Determines the display name for a user's member management permissions. Determines the display name for a user's member management permissions.
@ -172,6 +176,7 @@ def get_members_display(roles, permissions):
else: else:
return "No access" return "No access"
def validate_user_portfolio_permission(user_portfolio_permission): def validate_user_portfolio_permission(user_portfolio_permission):
""" """
Validates a UserPortfolioPermission instance. Located in portfolio_helper to avoid circular imports Validates a UserPortfolioPermission instance. Located in portfolio_helper to avoid circular imports

View file

@ -16,6 +16,7 @@ from registrar.utility.email_invitations import (
send_portfolio_admin_addition_emails, send_portfolio_admin_addition_emails,
send_portfolio_admin_removal_emails, send_portfolio_admin_removal_emails,
send_portfolio_invitation_email, send_portfolio_invitation_email,
send_portfolio_member_permission_update_email,
) )
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
@ -522,7 +523,6 @@ class PortfolioInvitationEmailTests(unittest.TestCase):
"registrar.utility.email_invitations._get_requestor_email", "registrar.utility.email_invitations._get_requestor_email",
side_effect=MissingEmailError("Requestor has no email"), side_effect=MissingEmailError("Requestor has no email"),
) )
@less_console_noise_decorator
def test_send_portfolio_invitation_email_missing_requestor_email(self, mock_get_email): def test_send_portfolio_invitation_email_missing_requestor_email(self, mock_get_email):
"""Test when requestor has no email""" """Test when requestor has no email"""
is_admin_invitation = False is_admin_invitation = False
@ -888,3 +888,77 @@ class SendPortfolioAdminRemovalEmailsTests(unittest.TestCase):
mock_get_requestor_email.assert_called_once_with(self.requestor, portfolio=self.portfolio) mock_get_requestor_email.assert_called_once_with(self.requestor, portfolio=self.portfolio)
mock_send_removal_emails.assert_called_once_with(self.email, self.requestor.email, self.portfolio) mock_send_removal_emails.assert_called_once_with(self.email, self.requestor.email, self.portfolio)
self.assertFalse(result) self.assertFalse(result)
class TestSendPortfolioMemberPermissionUpdateEmail(unittest.TestCase):
"""Unit tests for send_portfolio_member_permission_update_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_update_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_update.txt",
"emails/portfolio_update_subject.txt",
to_address="user@example.com",
context={
"requested_user": permissions.user,
"portfolio": permissions.portfolio,
"requestor_email": "requestor@example.com",
"permissions": permissions,
},
)
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_update_email(requestor, permissions)
# Assertions
mock_logger.warning.assert_called_once_with(
"Could not send email organization member update 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_update_email(requestor, permissions)
# Assertions
mock_logger.warning.assert_not_called() # Function should fail before logging email failure

View file

@ -225,6 +225,7 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i
) )
return all_admin_emails_sent return all_admin_emails_sent
def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission): def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission):
""" """
Sends an email notification to a portfolio member when their permissions are updated. Sends an email notification to a portfolio member when their permissions are updated.
@ -234,7 +235,7 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo
Args: Args:
requestor (User): The user initiating the permission update. requestor (User): The user initiating the permission update.
permissions (UserPortfolioPermission): The updated permissions object containing the affected user permissions (UserPortfolioPermission): The updated permissions object containing the affected user
and the portfolio details. and the portfolio details.
Returns: Returns:
@ -254,18 +255,19 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo
"portfolio": permissions.portfolio, "portfolio": permissions.portfolio,
"requestor_email": requestor_email, "requestor_email": requestor_email,
"permissions": permissions, "permissions": permissions,
} },
) )
except EmailSendingError as err: except EmailSendingError:
logger.warning( logger.warning(
"Could not send email organization member update notification to %s " "for portfolio: %s", "Could not send email organization member update notification to %s " "for portfolio: %s",
permissions.user.email, permissions.user.email,
permissions.portfolio.organization_name, permissions.portfolio.organization_name,
exc_info=True, exc_info=True,
) )
return False return False
return True return True
def send_portfolio_admin_addition_emails(email: str, requestor, portfolio: Portfolio): 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 Notifies all portfolio admins of the provided portfolio of a newly invited portfolio admin

View file

@ -215,8 +215,7 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
try: try:
if form.is_change(): if form.is_change():
if not send_portfolio_member_permission_update_email( if not send_portfolio_member_permission_update_email(
requestor=request.user, requestor=request.user, permissions=form.instance
permissions=form.instance
): ):
messages.warning(self.request, f"Could not send email notification to {user.email}.") messages.warning(self.request, f"Could not send email notification to {user.email}.")
if form.is_change_from_member_to_admin(): if form.is_change_from_member_to_admin():