tests on admin and forms

This commit is contained in:
Rachid Mrad 2024-12-20 20:08:18 -05:00
parent d27352b599
commit 8326bbbca5
No known key found for this signature in database
3 changed files with 451 additions and 3 deletions

View file

@ -1492,7 +1492,7 @@ class PortfolioInvitationAdmin(ListHeaderAdmin):
Emails sent to requested user / email. Emails sent to requested user / email.
When exceptions are raised, return without saving model. When exceptions are raised, return without saving model.
""" """
if not change: # Only send email if this is a new PortfolioInvitation(creation) if not change: # Only send email if this is a new PortfolioInvitation (creation)
portfolio = obj.portfolio portfolio = obj.portfolio
requested_email = obj.email requested_email = obj.email
requestor = request.user requestor = request.user
@ -1537,7 +1537,7 @@ class PortfolioInvitationAdmin(ListHeaderAdmin):
) )
else: else:
logger.warning("Could not send email invitation (Other Exception)", obj.portfolio, exc_info=True) logger.warning("Could not send email invitation (Other Exception)", exc_info=True)
messages.error(request, "Could not send email invitation. Portfolio invitation not saved.") messages.error(request, "Could not send email invitation. Portfolio invitation not saved.")
def response_add(self, request, obj, post_url_continue=None): def response_add(self, request, obj, post_url_continue=None):

View file

@ -2,6 +2,8 @@ from datetime import datetime
from django.utils import timezone from django.utils import timezone
from django.test import TestCase, RequestFactory, Client from django.test import TestCase, RequestFactory, Client
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from registrar.utility.email import EmailSendingError
from registrar.utility.errors import MissingEmailError
from waffle.testutils import override_flag from waffle.testutils import override_flag
from django_webtest import WebTest # type: ignore from django_webtest import WebTest # type: ignore
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
@ -277,6 +279,29 @@ class TestUserPortfolioPermissionAdmin(TestCase):
# Should return the forbidden permissions for member role # Should return the forbidden permissions for member role
self.assertEqual(member_only_permissions, set(member_forbidden)) self.assertEqual(member_only_permissions, set(member_forbidden))
@less_console_noise_decorator
def test_has_change_form_description(self):
"""Tests if this model has a model description on the change form view"""
self.client.force_login(self.superuser)
user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.superuser, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
response = self.client.get(
"/admin/registrar/userportfoliopermission/{}/change/".format(user_portfolio_permission.pk),
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(
response,
"If you add someone to a portfolio here, it will not trigger an invitation email.",
)
class TestPortfolioInvitationAdmin(TestCase): class TestPortfolioInvitationAdmin(TestCase):
"""Tests for the PortfolioInvitationAdmin class as super user """Tests for the PortfolioInvitationAdmin class as super user
@ -432,6 +457,30 @@ class TestPortfolioInvitationAdmin(TestCase):
) )
self.assertContains(response, "Show more") self.assertContains(response, "Show more")
@less_console_noise_decorator
def test_has_change_form_description(self):
"""Tests if this model has a model description on the change form view"""
self.client.force_login(self.superuser)
invitation, _ = PortfolioInvitation.objects.get_or_create(
email=self.superuser.email, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
response = self.client.get(
"/admin/registrar/portfolioinvitation/{}/change/".format(invitation.pk),
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(
response,
"If you add someone to a portfolio here, it will trigger an invitation email when you click",
)
@less_console_noise_decorator
def test_get_filters(self): def test_get_filters(self):
"""Ensures that our filters are displaying correctly""" """Ensures that our filters are displaying correctly"""
with less_console_noise(): with less_console_noise():
@ -456,6 +505,211 @@ class TestPortfolioInvitationAdmin(TestCase):
self.assertContains(response, invited_html, count=1) self.assertContains(response, invited_html, count=1)
self.assertContains(response, retrieved_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)
# 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
admin_instance.save_model(request, portfolio_invitation, None, None)
# Assert that send_portfolio_invitation_email is not 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_warning.assert_called_once_with(request, "james.gordon@gotham.gov has been invited.")
@less_console_noise_decorator
@patch("registrar.admin.send_portfolio_invitation_email")
@patch("django.contrib.messages.warning") # Mock the `messages.warning` call
def test_save_does_not_send_email_if_requested_user_exists(self, mock_messages_warning, mock_send_email):
"""On save_model, an email is NOT sent if an the requested email belongs to an existing user.
It also throws a warning."""
self.client.force_login(self.superuser)
# Create an instance of the admin class
admin_instance = PortfolioInvitationAdmin(PortfolioInvitation, admin_site=None)
# Mock the UserPortfolioPermission query to simulate the invitation already existing
existing_user = create_user()
UserPortfolioPermission.objects.create(user=existing_user, portfolio=self.portfolio)
# Create a PortfolioInvitation instance
portfolio_invitation = PortfolioInvitation(
email=existing_user.email,
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
admin_instance.save_model(request, portfolio_invitation, None, None)
# Assert that send_portfolio_invitation_email is not called
mock_send_email.assert_not_called()
# 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.error") # Mock the `messages.error` call
def test_save_exception_email_sending_error(self, mock_messages_error, mock_send_email):
"""Handle EmailSendingError correctly when sending the portfolio invitation fails."""
self.client.force_login(self.superuser)
# Mock the email sending function to raise EmailSendingError
mock_send_email.side_effect = EmailSendingError("Email service unavailable")
# 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
admin_instance.save_model(request, portfolio_invitation, None, None)
# 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."
)
@less_console_noise_decorator
@patch("registrar.admin.send_portfolio_invitation_email")
@patch("django.contrib.messages.error") # Mock the `messages.error` call
def test_save_exception_missing_email_error(self, mock_messages_error, mock_send_email):
"""Handle MissingEmailError correctly when no email exists for the requestor."""
self.client.force_login(self.superuser)
# Mock the email sending function to raise MissingEmailError
mock_send_email.side_effect = MissingEmailError("Email-less Rachid")
# 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
admin_instance.save_model(request, portfolio_invitation, None, None)
# Assert that messages.error was called with the correct message
mock_messages_error.assert_called_once_with(
request,
"Can't send invitation email. No email is associated with the account for 'Email-less Rachid'.",
)
@less_console_noise_decorator
@patch("registrar.admin.send_portfolio_invitation_email")
@patch("django.contrib.messages.error") # 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)
# Mock the email sending function to raise a generic exception
mock_send_email.side_effect = Exception("Unexpected error")
# 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
admin_instance.save_model(request, portfolio_invitation, None, None)
# 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."
)
# @patch("django.contrib.messages.get_messages")
# def test_form_rerenders_if_errors_or_warnings(self, mock_get_messages):
# """Ensure the form is re-rendered when errors or warnings are present."""
# self.client.force_login(self.superuser)
# # Mock the presence of an error message
# mock_message = Mock()
# mock_message.level_tag = "error"
# mock_message.message = "Simulated error message"
# mock_get_messages.return_value = [mock_message]
# # Create an instance of the admin class
# admin_instance = PortfolioInvitationAdmin(PortfolioInvitation, admin_site=AdminSite())
# # 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
# # Trigger response_add
# response = admin_instance.response_add(request, portfolio_invitation)
# # Assert that the response status code is 200 (indicating the form was re-rendered)
# self.assertEqual(response.status_code, 200, msg="Expected form to re-render due to errors.")
# # Assert that the mocked error message is included in the response
# self.assertContains(response, "Simulated error message", msg_prefix="Expected error message not found.")
class TestHostAdmin(TestCase): class TestHostAdmin(TestCase):
"""Tests for the HostAdmin class as super user """Tests for the HostAdmin class as super user

View file

@ -18,7 +18,13 @@ from registrar.forms.domain_request_wizard import (
AboutYourOrganizationForm, AboutYourOrganizationForm,
) )
from registrar.forms.domain import ContactForm from registrar.forms.domain import ContactForm
from registrar.tests.common import MockEppLib from registrar.forms.portfolio import BasePortfolioMemberForm, PortfolioInvitedMemberForm, PortfolioMemberForm, PortfolioNewMemberForm
from registrar.models.portfolio import Portfolio
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user import User
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from registrar.tests.common import MockEppLib, create_user
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -408,3 +414,191 @@ class TestContactForm(TestCase):
def test_contact_form_email_invalid2(self): def test_contact_form_email_invalid2(self):
form = ContactForm(data={"email": "@"}) form = ContactForm(data={"email": "@"})
self.assertEqual(form.errors["email"], ["Enter a valid email address."]) self.assertEqual(form.errors["email"], ["Enter a valid email address."])
class TestBasePortfolioMemberForms(TestCase):
"""We test on the child forms instead of BasePortfolioMemberForm becasue the base form
is a model form with no model bound."""
def setUp(self):
super().setUp()
self.user = create_user()
self.portfolio, _ = Portfolio.objects.get_or_create(creator_id=self.user.id, organization_name="Hotel California")
def tearDown(self):
super().tearDown()
Portfolio.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
PortfolioInvitation.objects.all().delete()
User.objects.all().delete()
def _assert_form_is_valid(self, form_class, data, instance=None):
if instance != None:
form = form_class(data=data, instance=instance)
else:
print('no instance')
form = form_class(data=data)
self.assertTrue(form.is_valid(), f"Form {form_class.__name__} failed validation with data: {data}")
return form
def _assert_form_has_error(self, form_class, data, field_name):
form = form_class(data=data)
self.assertFalse(form.is_valid())
self.assertIn(field_name, form.errors)
def _assert_initial_data(self, form_class, instance, expected_initial_data):
"""Helper to check if the instance data is correctly mapped to the initial form values."""
form = form_class(instance=instance)
for field, expected_value in expected_initial_data.items():
self.assertEqual(form.initial[field], expected_value)
def _assert_permission_mapping(self, form_class, data, expected_permissions):
"""Helper to check if permissions are correctly handled and mapped."""
form = self._assert_form_is_valid(form_class, data)
cleaned_data = form.cleaned_data
for permission in expected_permissions:
self.assertIn(permission, cleaned_data["additional_permissions"])
def test_required_field_for_admin(self):
"""Test that required fields are validated for an admin role."""
data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
"domain_request_permission_admin": "", # Simulate missing field
"member_permission_admin": "", # Simulate missing field
}
# Check required fields for all forms
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_admin")
self._assert_form_has_error(PortfolioMemberForm, data, "member_permission_admin")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permission_admin")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permission_admin")
self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permission_admin")
self._assert_form_has_error(PortfolioNewMemberForm, data, "member_permission_admin")
def test_required_field_for_member(self):
"""Test that required fields are validated for a member role."""
data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": "", # Simulate missing field
}
# Check required fields for all forms
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_member")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permission_member")
self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permission_member")
def test_clean_validates_required_fields_for_role(self):
"""Test that the `clean` method validates the correct fields for each role.
For PortfolioMemberForm and PortfolioInvitedMemberForm, we pass an object as the instance to the form.
For UserPortfolioPermissionChoices, we add a portfolio and an email to the POST data.
These things are handled in the views."""
user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(portfolio=self.portfolio, user=self.user)
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(portfolio=self.portfolio, email="hi@ho")
data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
"domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS.value,
}
# Check form validity for all forms
form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission)
cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value])
self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS])
form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation)
cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value])
self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS])
data = {
"email": "hi@ho.com",
"portfolio": self.portfolio.id,
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
"domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS.value,
}
form = self._assert_form_is_valid(PortfolioNewMemberForm, data)
cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value])
self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS])
def test_clean_member_permission_edgecase(self):
"""Test that the clean method correctly handles the special "no_access" value for members.
We'll need to add a portfolio, which in the app is handled by the view post."""
user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(portfolio=self.portfolio, user=self.user)
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(portfolio=self.portfolio, email="hi@ho")
data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": "no_access", # Simulate no access permission
}
form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission)
cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["domain_request_permission_member"], None)
form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation)
cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["domain_request_permission_member"], None)
def test_map_instance_to_initial_admin_role(self):
"""Test that instance data is correctly mapped to the initial form values for an admin role."""
user_portfolio_permission = UserPortfolioPermission(
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MEMBERS],
)
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
portfolio=self.portfolio,
email="hi@ho",
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MEMBERS],
)
expected_initial_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
"domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
"member_permission_admin": UserPortfolioPermissionChoices.VIEW_MEMBERS,
}
self._assert_initial_data(PortfolioMemberForm, user_portfolio_permission, expected_initial_data)
self._assert_initial_data(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data)
def test_map_instance_to_initial_member_role(self):
"""Test that instance data is correctly mapped to the initial form values for a member role."""
user_portfolio_permission = UserPortfolioPermission(
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
)
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
portfolio=self.portfolio,
email="hi@ho",
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
)
expected_initial_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
}
self._assert_initial_data(PortfolioMemberForm, user_portfolio_permission, expected_initial_data)
self._assert_initial_data(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data)
def test_invalid_data_for_admin(self):
"""Test invalid form submission for an admin role with missing permissions."""
data = {
"email": "hi@ho.com",
"portfolio": self.portfolio.id,
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
"domain_request_permission_admin": "", # Missing field
"member_permission_admin": "", # Missing field
}
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_admin")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permission_admin")