manage.get.gov/src/registrar/tests/test_views_portfolio.py

4755 lines
216 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.urls import reverse
from api.tests.common import less_console_noise_decorator
from registrar.config import settings
from registrar.models import Portfolio, SeniorOfficial
from unittest.mock import MagicMock, patch
from django_webtest import WebTest # type: ignore
from django.core.handlers.wsgi import WSGIRequest
from registrar.models import (
DomainRequest,
Domain,
DomainInformation,
UserDomainRole,
User,
Suborganization,
AllowedEmail,
)
from registrar.models.domain_invitation import DomainInvitation
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user_group import UserGroup
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from registrar.tests.test_views import TestWithUser
from registrar.utility.email import EmailSendingError
from registrar.utility.errors import MissingEmailError
from .common import MockEppLib, MockSESClient, completed_domain_request, create_test_user, create_user
from waffle.testutils import override_flag
from django.contrib.sessions.middleware import SessionMiddleware
import boto3_mocking # type: ignore
from django.test import Client
import logging
import json
logger = logging.getLogger(__name__)
class TestPortfolio(WebTest):
def setUp(self):
super().setUp()
self.client = Client()
self.user = create_test_user()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
def tearDown(self):
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
UserDomainRole.objects.all().delete()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
Domain.objects.all().delete()
User.objects.all().delete()
super().tearDown()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_portfolio_senior_official(self):
"""Tests that the senior official page on portfolio contains the content we expect"""
self.app.set_user(self.user.username)
so = SeniorOfficial.objects.create(
first_name="Saturn", last_name="Enceladus", title="Planet/Moon", email="spacedivision@igorville.com"
)
self.portfolio.senior_official = so
self.portfolio.save()
self.portfolio.refresh_from_db()
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[UserPortfolioPermissionChoices.VIEW_PORTFOLIO],
)
so_portfolio_page = self.app.get(reverse("senior-official"))
# Assert that we're on the right page
self.assertContains(so_portfolio_page, "Senior official")
self.assertContains(so_portfolio_page, "Saturn Enceladus")
self.assertContains(so_portfolio_page, "Planet/Moon")
self.assertContains(so_portfolio_page, "spacedivision@igorville.com")
self.assertNotContains(so_portfolio_page, "Save")
self.portfolio.delete()
so.delete()
@less_console_noise_decorator
def test_middleware_does_not_redirect_if_no_permission(self):
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=[]
)
self.user.portfolio = self.portfolio
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home"))
# Assert that we're on the right page
self.assertNotContains(portfolio_page, self.portfolio.organization_name)
@less_console_noise_decorator
def test_middleware_does_not_redirect_if_no_portfolio(self):
"""Test that user with no assigned portfolio is not redirected when attempting to access home"""
self.app.set_user(self.user.username)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home"))
# Assert that we're on the right page
self.assertNotContains(portfolio_page, self.portfolio.organization_name)
@less_console_noise_decorator
def test_middleware_redirects_to_portfolio_no_domains_page(self):
"""Test that user with a portfolio and VIEW_PORTFOLIO is redirected to the no domains page"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[UserPortfolioPermissionChoices.VIEW_PORTFOLIO],
)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home")).follow()
# Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
self.assertContains(portfolio_page, "You arent managing any domains")
@less_console_noise_decorator
def test_middleware_redirects_to_portfolio_domains_page(self):
"""Test that user with a portfolio, VIEW_PORTFOLIO, VIEW_ALL_DOMAINS
is redirected to portfolio domains page"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
],
)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home")).follow()
# Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
@less_console_noise_decorator
def test_portfolio_domains_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is denied access to portfolio domain view"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=[]
)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
response = self.app.get(reverse("domains"), status=403)
# Assert the response is a 403 Forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
def test_portfolio_domain_requests_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is denied access to portfolio domain view"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=[]
)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
response = self.app.get(reverse("domain-requests"), status=403)
# Assert the response is a 403 Forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
def test_portfolio_organization_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is not allowed access to portfolio organization page"""
self.app.set_user(self.user.username)
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=[]
)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
response = self.app.get(reverse("organization"), status=403)
# Assert the response is a 403 Forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
def test_portfolio_organization_page_read_only(self):
"""Test that user with a portfolio can access the portfolio organization page, read only"""
self.app.set_user(self.user.username)
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[UserPortfolioPermissionChoices.VIEW_PORTFOLIO],
)
self.portfolio.city = "Los Angeles"
self.portfolio.save()
with override_flag("organization_feature", active=True):
response = self.app.get(reverse("organization"))
# Assert the response is a 200
self.assertEqual(response.status_code, 200)
# The label for Federal agency will always be a h4
self.assertContains(response, '<h4 class="margin-bottom-05">Organization name</h4>')
# The read only label for city will be a h4
self.assertContains(response, '<h4 class="margin-bottom-05">City</h4>')
self.assertNotContains(response, 'for="id_city"')
self.assertContains(response, '<p class="margin-top-0">Los Angeles</p>')
@less_console_noise_decorator
def test_portfolio_organization_page_edit_access(self):
"""Test that user with a portfolio can access the portfolio organization page, read only"""
self.app.set_user(self.user.username)
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
],
)
self.portfolio.city = "Los Angeles"
self.portfolio.save()
with override_flag("organization_feature", active=True):
response = self.app.get(reverse("organization"))
# Assert the response is a 200
self.assertEqual(response.status_code, 200)
# The label for Federal agency will always be a h4
self.assertContains(response, '<h4 class="margin-bottom-05">Organization name</h4>')
# The read only label for city will be a h4
self.assertNotContains(response, '<h4 class="margin-bottom-05">City</h4>')
self.assertNotContains(response, '<p class="margin-top-0">Los Angeles</p>')
self.assertContains(response, 'for="id_city"')
@less_console_noise_decorator
@override_flag("organization_requests", active=True)
def test_accessible_pages_when_user_does_not_have_permission(self):
"""Tests which pages are accessible when user does not have portfolio permissions"""
self.app.set_user(self.user.username)
portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
]
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home")).follow()
# Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
self.assertContains(portfolio_page, reverse("domains"))
self.assertContains(portfolio_page, reverse("domain-requests"))
# removing non-basic portfolio perms, which should remove domains
# and domain requests from nav
portfolio_permission.additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
portfolio_permission.save()
portfolio_permission.refresh_from_db()
# Members should be redirected to the readonly domains page
portfolio_page = self.app.get(reverse("home")).follow()
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
self.assertContains(portfolio_page, "You arent managing any domains")
self.assertNotContains(portfolio_page, reverse("domains"))
self.assertNotContains(portfolio_page, reverse("domain-requests"))
# The organization page should still be accessible
org_page = self.app.get(reverse("organization"))
self.assertContains(org_page, self.portfolio.organization_name)
self.assertContains(org_page, "<h1>Organization</h1>")
# Both domain pages should not be accessible
domain_page = self.app.get(reverse("domains"), expect_errors=True)
self.assertEquals(domain_page.status_code, 403)
domain_request_page = self.app.get(reverse("domain-requests"), expect_errors=True)
self.assertEquals(domain_request_page.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_requests", active=True)
def test_accessible_pages_when_user_does_not_have_role(self):
"""Test that admin / memmber roles are associated with the right access"""
self.app.set_user(self.user.username)
roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=roles
)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home")).follow()
# Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
self.assertContains(portfolio_page, reverse("domains"))
self.assertContains(portfolio_page, reverse("domain-requests"))
# removing non-basic portfolio role, which should remove domains
# and domain requests from nav
portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
portfolio_permission.save()
portfolio_permission.refresh_from_db()
# Members should be redirected to the readonly domains page
portfolio_page = self.app.get(reverse("home")).follow()
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
self.assertContains(portfolio_page, "You arent managing any domains")
self.assertNotContains(portfolio_page, reverse("domains"))
self.assertNotContains(portfolio_page, reverse("domain-requests"))
# The organization page should still be accessible
org_page = self.app.get(reverse("organization"))
self.assertContains(org_page, self.portfolio.organization_name)
self.assertContains(org_page, "<h1>Organization</h1>")
# Both domain pages should not be accessible
domain_page = self.app.get(reverse("domains"), expect_errors=True)
self.assertEquals(domain_page.status_code, 403)
domain_request_page = self.app.get(reverse("domain-requests"), expect_errors=True)
self.assertEquals(domain_request_page.status_code, 403)
@less_console_noise_decorator
def test_portfolio_org_name(self):
"""Can load portfolio's org name page."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
]
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
page = self.app.get(reverse("organization"))
self.assertContains(page, "The name of your organization will be publicly listed as the domain registrant.")
@less_console_noise_decorator
def test_domain_org_name_address_content(self):
"""Org name and address information appears on the page."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
]
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
self.portfolio.organization_name = "Hotel California"
self.portfolio.save()
page = self.app.get(reverse("organization"))
# Once in the sidenav, once in the main nav
self.assertContains(page, "Hotel California", count=2)
self.assertContains(page, "Non-Federal Agency")
@less_console_noise_decorator
def test_domain_org_name_address_form(self):
"""Submitting changes works on the org name address page."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
]
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
self.portfolio.address_line1 = "1600 Penn Ave"
self.portfolio.save()
portfolio_org_name_page = self.app.get(reverse("organization"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
portfolio_org_name_page.form["address_line1"] = "6 Downing st"
portfolio_org_name_page.form["city"] = "London"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result_page = portfolio_org_name_page.form.submit()
self.assertEqual(success_result_page.status_code, 200)
self.assertContains(success_result_page, "6 Downing st")
self.assertContains(success_result_page, "London")
@less_console_noise_decorator
def test_portfolio_in_session_when_organization_feature_active(self):
"""When organization_feature flag is true and user has a portfolio,
the portfolio should be set in session."""
self.client.force_login(self.user)
roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=roles)
with override_flag("organization_feature", active=True):
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertEqual(session["portfolio"], self.portfolio, "Portfolio session variable has the wrong value.")
@less_console_noise_decorator
def test_portfolio_in_session_is_none_when_organization_feature_inactive(self):
"""When organization_feature flag is false and user has a portfolio,
the portfolio should be set to None in session.
This test also satisfies the condition when multiple_portfolios flag
is false and user has a portfolio, so won't add a redundant test for that."""
self.client.force_login(self.user)
roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=roles)
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertIsNone(session["portfolio"])
@less_console_noise_decorator
def test_portfolio_in_session_is_none_when_organization_feature_active_and_no_portfolio(self):
"""When organization_feature flag is true and user does not have a portfolio,
the portfolio should be set to None in session."""
self.client.force_login(self.user)
with override_flag("organization_feature", active=True):
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertIsNone(session["portfolio"])
@less_console_noise_decorator
def test_portfolio_in_session_when_multiple_portfolios_active(self):
"""When multiple_portfolios flag is true and user has a portfolio,
the portfolio should be set in session."""
self.client.force_login(self.user)
roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=roles)
with override_flag("organization_feature", active=True), override_flag("multiple_portfolios", active=True):
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertEqual(session["portfolio"], self.portfolio, "Portfolio session variable has the wrong value.")
@less_console_noise_decorator
def test_portfolio_in_session_is_none_when_multiple_portfolios_active_and_no_portfolio(self):
"""When multiple_portfolios flag is true and user does not have a portfolio,
the portfolio should be set to None in session."""
self.client.force_login(self.user)
with override_flag("multiple_portfolios", active=True):
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertIsNone(session["portfolio"])
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_org_member_can_only_see_domains_with_appropriate_permissions(self):
"""A user with the role organization_member should not have access to the domains page
if they do not have the right permissions.
"""
permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
# A default organization member should not be able to see any domains
self.client.force_login(self.user)
response = self.client.get(reverse("home"), follow=True)
self.assertFalse(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "You aren")
# Test the domains page - this user should not have access
response = self.client.get(reverse("domains"))
self.assertEqual(response.status_code, 403)
# Ensure that this user can see domains with the right permissions
permission.additional_permissions = [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS]
permission.save()
permission.refresh_from_db()
# Test the domains page - this user should have access
response = self.client.get(reverse("domains"))
self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Domain name")
# Test the managed domains permission
permission.additional_permissions = [UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS]
permission.save()
permission.refresh_from_db()
# Test the domains page - this user should have access
response = self.client.get(reverse("domains"))
self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Domain name")
permission.delete()
def check_widescreen_is_loaded(self, page_to_check):
"""Tests if class modifiers for widescreen mode are appropriately loaded into the DOM
for the given page"""
self.client.force_login(self.user)
# Ensure that this user can see domains with the right permissions
permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
permission.additional_permissions = [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS]
permission.save()
permission.refresh_from_db()
response = self.client.get(reverse(page_to_check))
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for widescreen modifier
self.assertContains(response, "--widescreen")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_widescreen_css_org_model(self):
"""Tests if class modifiers for widescreen mode are appropriately
loaded into the DOM for org model pages"""
self.check_widescreen_is_loaded("domains")
@less_console_noise_decorator
@override_flag("organization_feature", active=False)
def test_widescreen_css_non_org_model(self):
"""Tests if class modifiers for widescreen mode are appropriately
loaded into the DOM for non-org model pages"""
self.check_widescreen_is_loaded("home")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=False)
def test_organization_requests_waffle_flag_off_hides_nav_link_and_restricts_permission(self):
"""Setting the organization_requests waffle off hides the nav link and restricts access to the requests page"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertNotContains(home, "Domain requests")
domain_requests = self.app.get(reverse("domain-requests"), expect_errors=True)
self.assertEqual(domain_requests.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_organization_requests_waffle_flag_on_shows_nav_link_and_allows_permission(self):
"""Setting the organization_requests waffle on shows the nav link and allows access to the requests page"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertContains(home, "Domain requests")
domain_requests = self.app.get(reverse("domain-requests"))
self.assertEqual(domain_requests.status_code, 200)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=False)
def test_organization_members_waffle_flag_off_hides_nav_link(self):
"""Setting the organization_members waffle off hides the nav link"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertNotContains(home, "Members")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_organization_members_waffle_flag_on_shows_nav_link(self):
"""Setting the organization_members waffle on shows the nav link"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertContains(home, "Members")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_cannot_view_members_table(self):
"""Test that user without proper permission is denied access to members view."""
# Users can only view the members table if they have
# Portfolio Permission "view_members" selected.
# NOTE: Admins, by default, DO have permission
# to view/edit members.
# Scenarios to test include;
# (1) - User is not admin and can view portfolio, but not the members table
# (1) - User is admin and can view portfolio, as well as the members table
# --- non-admin
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
],
)
# Verify that the user cannot access the members page
# This will redirect the user to the members page.
self.client.force_login(self.user)
response = self.client.get(reverse("members"), follow=True)
# Assert the response is a 403 Forbidden
self.assertEqual(response.status_code, 403)
# --- admin
UserPortfolioPermission.objects.filter(user=self.user, portfolio=self.portfolio).update(
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
# Admins should have access to this page by default
response = self.client.get(reverse("members"), follow=True)
self.assertEqual(response.status_code, 200)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_can_view_members_table(self):
"""Test that user with proper permission is able to access members view"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# Verify that the user can access the members page
# This will redirect the user to the members page.
self.client.force_login(self.user)
response = self.client.get(reverse("members"), follow=True)
# Make sure the page loaded
self.assertEqual(response.status_code, 200)
# ---- Useful debugging stub to see what "assertContains" is finding
# pattern = r'Members'
# matches = re.findall(pattern, response.content.decode('utf-8'))
# for match in matches:
# TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"{match}")
# Make sure the page loaded
self.assertContains(response, "Members")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_can_manage_members(self):
"""Test that user with proper permission is able to manage members"""
user = self.user
self.app.set_user(user.username)
# give user permissions to view AND manage members
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Give user permissions to modify user objects in the DB
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
# Add the user to the group
user.groups.set([group])
# Verify that the user can access the members page
# This will redirect the user to the members page.
self.client.force_login(self.user)
response = self.client.get(reverse("members"), follow=True)
# Make sure the page loaded
self.assertEqual(response.status_code, 200)
# Verify that manage settings are sent in the dynamic HTML
self.client.force_login(self.user)
response = self.client.get(reverse("get_portfolio_members_json") + f"?portfolio={self.portfolio.pk}")
self.assertContains(response, '"action_label": "Manage"')
self.assertContains(response, '"svg_icon": "settings"')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_view_only_members(self):
"""Test that user with view only permission settings can only
view members (not manage them)"""
user = self.user
self.app.set_user(user.username)
# give user permissions to view AND manage members
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# Give user permissions to modify user objects in the DB
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
# Add the user to the group
user.groups.set([group])
# Verify that the user can access the members page
# This will redirect the user to the members page.
self.client.force_login(self.user)
response = self.client.get(reverse("members"), follow=True)
# Make sure the page loaded
self.assertEqual(response.status_code, 200)
# Verify that view-only settings are sent in the dynamic HTML
response = self.client.get(reverse("get_portfolio_members_json") + f"?portfolio={self.portfolio.pk}")
self.assertContains(response, '"action_label": "View"')
self.assertContains(response, '"svg_icon": "visibility"')
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_members_admin_detection(self):
"""Test that user with proper permission is able to manage members"""
user = self.user
self.app.set_user(user.username)
# give user permissions to view AND manage members
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Give user permissions to modify user objects in the DB
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
# Add the user to the group
user.groups.set([group])
# Verify that the user can access the members page
# This will redirect the user to the members page.
self.client.force_login(self.user)
response = self.client.get(reverse("members"), follow=True)
# Make sure the page loaded
self.assertEqual(response.status_code, 200)
# Verify that admin info is sent in the dynamic HTML
response = self.client.get(reverse("get_portfolio_members_json") + f"?portfolio={self.portfolio.pk}")
# TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"{response.content}")
self.assertContains(response, '"is_admin": true')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_cannot_view_member_page_when_flag_is_off(self):
"""Test that user cannot access the member page when waffle flag is off"""
# Verify that the user cannot access the member page
self.client.force_login(self.user)
response = self.client.get(reverse("member", kwargs={"pk": 1}), follow=True)
# Make sure the page is denied
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_cannot_view_member_page_when_user_has_no_permission(self):
"""Test that user cannot access the member page without proper permission"""
# give user base permissions
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
# Verify that the user cannot access the member page
self.client.force_login(self.user)
response = self.client.get(reverse("member", kwargs={"pk": 1}), follow=True)
# Make sure the page is denied
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_can_view_member_page_when_user_has_view_members(self):
"""Test that user can access the member page with view_members permission"""
# Arrange
# give user permissions to view members
permission_obj, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# Verify the page can be accessed
self.client.force_login(self.user)
response = self.client.get(reverse("member", kwargs={"pk": permission_obj.pk}), follow=True)
self.assertEqual(response.status_code, 200)
# Assert text within the page is correct
self.assertContains(response, "First Last")
self.assertContains(response, self.user.email)
self.assertContains(response, "Basic")
self.assertContains(response, "No access")
self.assertContains(response, "Viewer")
self.assertContains(response, "This member does not manage any domains.")
# Assert buttons and links within the page are correct
self.assertNotContains(response, "usa-button--more-actions") # test that 3 dot is not present
self.assertNotContains(response, "sprite.svg#edit") # test that Edit link is not present
self.assertNotContains(response, "sprite.svg#settings") # test that Manage link is not present
self.assertContains(response, "sprite.svg#visibility") # test that View link is present
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_can_view_member_page_when_user_has_edit_members(self):
"""Test that user can access the member page with edit_members permission"""
# Arrange
# give user admin role, which includes edit_members
permission_obj, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
# Verify the page can be accessed
self.client.force_login(self.user)
response = self.client.get(reverse("member", kwargs={"pk": permission_obj.pk}), follow=True)
self.assertEqual(response.status_code, 200)
# Assert text within the page is correct
self.assertContains(response, "First Last")
self.assertContains(response, self.user.email)
self.assertContains(response, "Admin")
self.assertContains(response, "Creator")
self.assertContains(response, "Manager")
self.assertContains(response, "This member does not manage any domains.")
# Assert buttons and links within the page are correct
self.assertContains(response, "wrapper-delete-action") # test that 3 dot is present
self.assertContains(response, "sprite.svg#edit") # test that Edit link is present
self.assertContains(response, "sprite.svg#edit") # test that Manage link is present
self.assertNotContains(response, "sprite.svg#visibility") # test that View link is not present
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_cannot_view_invitedmember_page_when_flag_is_off(self):
"""Test that user cannot access the invitedmember page when waffle flag is off"""
# Verify that the user cannot access the member page
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember", kwargs={"pk": 1}), follow=True)
# Make sure the page is denied
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_cannot_view_invitedmember_page_when_user_has_no_permission(self):
"""Test that user cannot access the invitedmember page without proper permission"""
# give user base permissions
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
# Verify that the user cannot access the member page
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember", kwargs={"pk": 1}), follow=True)
# Make sure the page is denied
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_can_view_invitedmember_page_when_user_has_view_members(self):
"""Test that user can access the invitedmember page with view_members permission"""
# Arrange
# give user permissions to view members
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
email="info@example.com",
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# Verify the page can be accessed
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember", kwargs={"pk": portfolio_invitation.pk}), follow=True)
self.assertEqual(response.status_code, 200)
# Assert text within the page is correct
self.assertContains(response, "Invited")
self.assertContains(response, portfolio_invitation.email)
self.assertContains(response, "Basic")
self.assertContains(response, "No access")
self.assertContains(response, "Viewer")
self.assertContains(response, "This member does not manage any domains.")
# Assert buttons and links within the page are correct
self.assertNotContains(response, "usa-button--more-actions") # test that 3 dot is not present
self.assertNotContains(response, "sprite.svg#edit") # test that Edit link is not present
self.assertNotContains(response, "sprite.svg#settings") # test that Manage link is not present
self.assertContains(response, "sprite.svg#visibility") # test that View link is present
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_can_view_invitedmember_page_when_user_has_edit_members(self):
"""Test that user can access the invitedmember page with org admin role"""
# Arrange
# give user admin role
permission_obj, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
email="info@example.com",
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
# Verify the page can be accessed
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember", kwargs={"pk": portfolio_invitation.pk}), follow=True)
self.assertEqual(response.status_code, 200)
# Assert text within the page is correct
self.assertContains(response, "Invited")
self.assertContains(response, portfolio_invitation.email)
self.assertContains(response, "Admin")
self.assertContains(response, "Viewer")
self.assertContains(response, "Creator")
self.assertContains(response, "Manager")
self.assertContains(response, "This member does not manage any domains.")
# Assert buttons and links within the page are correct
self.assertContains(response, "wrapper-delete-action") # test that 3 dot is present
self.assertContains(response, "sprite.svg#edit") # test that Edit link is present
self.assertContains(response, "sprite.svg#edit") # test that Manage link is present
self.assertNotContains(response, "sprite.svg#visibility") # test that View link is not present
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_portfolio_domain_requests_page_when_user_has_no_permissions(self):
"""Test the no requests page"""
UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
self.client.force_login(self.user)
# create and submit a domain request
domain_request = completed_domain_request(user=self.user)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
domain_request.submit()
domain_request.save()
requests_page = self.client.get(reverse("no-portfolio-requests"), follow=True)
self.assertContains(requests_page, "You dont have access to domain requests.")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_no_permissions(self):
"""Test the nav contains a link to the no requests page
Also test that members link not present"""
UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
self.client.force_login(self.user)
# create and submit a domain request
domain_request = completed_domain_request(user=self.user)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
domain_request.submit()
domain_request.save()
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
# link to no requests
self.assertContains(portfolio_landing_page, "no-organization-requests/")
# dropdown
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
# link to requests
self.assertNotContains(portfolio_landing_page, 'href="/requests/')
# link to create request
self.assertNotContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertNotContains(portfolio_landing_page, 'href="/members/')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_all_permissions(self):
"""Test the nav contains a dropdown with a link to create and another link to view requests
Also test for the existence of the Create a new request btn on the requests page
Also test for the existence of the members link"""
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
self.client.force_login(self.user)
# create and submit a domain request
domain_request = completed_domain_request(user=self.user)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
domain_request.submit()
domain_request.save()
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
# link to no requests
self.assertNotContains(portfolio_landing_page, "no-organization-requests/")
# dropdown
self.assertContains(portfolio_landing_page, "basic-nav-section-two")
# link to requests
self.assertContains(portfolio_landing_page, 'href="/requests/')
# link to create
self.assertContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertContains(portfolio_landing_page, 'href="/members/')
requests_page = self.client.get(reverse("domain-requests"))
# create new request btn
self.assertContains(requests_page, "Start a new domain request")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_view_but_not_edit_permissions(self):
"""Test the nav contains a simple link to view requests
Also test for the existence of the Create a new request btn on the requests page
Also test for the existence of members link"""
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
self.client.force_login(self.user)
# create and submit a domain request
domain_request = completed_domain_request(user=self.user)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
domain_request.submit()
domain_request.save()
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
# link to no requests
self.assertNotContains(portfolio_landing_page, "no-organization-requests/")
# dropdown
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
# link to requests
self.assertContains(portfolio_landing_page, 'href="/requests/')
# link to create
self.assertNotContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertContains(portfolio_landing_page, 'href="/members/')
requests_page = self.client.get(reverse("domain-requests"))
# create new request btn
self.assertNotContains(requests_page, "Start a new domain request")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_organization_requests_additional_column(self):
"""The requests table has a column for created at"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertContains(home, "Domain requests")
domain_requests = self.app.get(reverse("domain-requests"))
self.assertEqual(domain_requests.status_code, 200)
self.assertContains(domain_requests, "Created by")
@less_console_noise_decorator
def test_no_org_requests_no_additional_column(self):
"""The requests table does not have a column for created at"""
self.app.set_user(self.user.username)
home = self.app.get(reverse("home"))
self.assertContains(home, "Domain requests")
self.assertNotContains(home, "Created by")
@less_console_noise_decorator
def test_portfolio_cache_updates_when_modified(self):
"""Test that the portfolio in session updates when the portfolio is modified"""
self.client.force_login(self.user)
roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=roles)
with override_flag("organization_feature", active=True):
# Initial request to set the portfolio in session
response = self.client.get(reverse("home"), follow=True)
portfolio = self.client.session.get("portfolio")
self.assertEqual(portfolio.organization_name, "Hotel California")
self.assertContains(response, "Hotel California")
# Modify the portfolio
self.portfolio.organization_name = "Updated Hotel California"
self.portfolio.save()
# Make another request
response = self.client.get(reverse("home"), follow=True)
# Check if the updated portfolio name is in the response
self.assertContains(response, "Updated Hotel California")
# Verify that the session contains the updated portfolio
portfolio = self.client.session.get("portfolio")
self.assertEqual(portfolio.organization_name, "Updated Hotel California")
@less_console_noise_decorator
def test_portfolio_cache_updates_when_flag_disabled_while_logged_in(self):
"""Test that the portfolio in session is set to None when the organization_feature flag is disabled"""
self.client.force_login(self.user)
roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=roles)
with override_flag("organization_feature", active=True):
# Initial request to set the portfolio in session
response = self.client.get(reverse("home"), follow=True)
portfolio = self.client.session.get("portfolio")
self.assertEqual(portfolio.organization_name, "Hotel California")
self.assertContains(response, "Hotel California")
# Disable the organization_feature flag
with override_flag("organization_feature", active=False):
# Make another request
response = self.client.get(reverse("home"))
self.assertIsNone(self.client.session.get("portfolio"))
self.assertNotContains(response, "Hotel California")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_org_user_can_delete_own_domain_request_with_permission(self):
"""Test that an org user with edit permission can delete their own DomainRequest with a deletable status."""
# Assign the user to a portfolio with edit permission
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
)
# Create a domain request with status WITHDRAWN
domain_request = completed_domain_request(
name="test-domain.gov",
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
portfolio=self.portfolio,
)
domain_request.creator = self.user
domain_request.save()
self.client.force_login(self.user)
# Perform delete
response = self.client.post(
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}), follow=True
)
# Check that the response is 200
self.assertEqual(response.status_code, 200)
# Check that the domain request no longer exists
self.assertFalse(DomainRequest.objects.filter(pk=domain_request.pk).exists())
domain_request.delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_delete_domain_request_as_org_user_without_permission_with_deletable_status(self):
"""Test that an org user without edit permission cant delete their DomainRequest even if status is deletable."""
# Assign the user to a portfolio without edit permission
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[],
)
# Create a domain request with status STARTED
domain_request = completed_domain_request(
name="test-domain.gov",
status=DomainRequest.DomainRequestStatus.STARTED,
portfolio=self.portfolio,
)
domain_request.creator = self.user
domain_request.save()
self.client.force_login(self.user)
# Attempt to delete
response = self.client.post(
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}), follow=True
)
# Check response is 403 Forbidden
self.assertEqual(response.status_code, 403)
# Check that the domain request still exists
self.assertTrue(DomainRequest.objects.filter(pk=domain_request.pk).exists())
domain_request.delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_org_user_cannot_delete_others_domain_requests(self):
"""Test that an org user with edit permission cannot delete DomainRequests they did not create."""
# Assign the user to a portfolio with edit permission
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
)
# Create another user and a domain request
other_user = User.objects.create(username="other_user")
domain_request = completed_domain_request(
name="test-domain.gov",
status=DomainRequest.DomainRequestStatus.STARTED,
portfolio=self.portfolio,
)
domain_request.creator = other_user
domain_request.save()
self.client.force_login(self.user)
# Perform delete as self.user
response = self.client.post(
reverse("domain-request-delete", kwargs={"domain_request_pk": domain_request.pk}), follow=True
)
# Check response is 403 Forbidden
self.assertEqual(response.status_code, 403)
# Check that the domain request still exists
self.assertTrue(DomainRequest.objects.filter(pk=domain_request.pk).exists())
domain_request.delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_members_table_contains_hidden_permissions_js_hook(self):
# In the members_table.html we use data-has-edit-permission as a boolean
# to indicate if a user has permission to edit members in the specific portfolio
# 1. User w/ edit permission. This permission is included in Organization admin role
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
# Create a member under same portfolio
member_email = "a_member@example.com"
member, _ = User.objects.get_or_create(username="a_member", email=member_email)
UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
# I log in as the User so I can see the Members Table
self.client.force_login(self.user)
# Specifically go to the Member Table page
response = self.client.get(reverse("members"))
self.assertContains(response, 'data-has-edit-permission="True"')
# 2. User w/o edit permission.
permission = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio)
# Update to basic member with view members permission
permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
permission.additional_permissions = [
UserPortfolioPermissionChoices.VIEW_MEMBERS,
]
# Save the updated permissions list
permission.save()
# Re-fetch the page to check for updated permissions
response = self.client.get(reverse("members"))
self.assertContains(response, 'data-has-edit-permission="False"')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_page_has_kebab_wrapper_for_member_if_user_has_edit_permission(self):
"""Test that the kebab wrapper displays for a member with edit permissions"""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Create a member under same portfolio
member_email = "a_member@example.com"
member, _ = User.objects.get_or_create(
username="a_member", email=member_email, first_name="First", last_name="Last"
)
upp, _ = UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
# I log in as the User so I can see the Manage Member page
self.client.force_login(self.user)
# Specifically go to the Manage Member page
response = self.client.get(reverse("member", args=[upp.id]), follow=True)
self.assertEqual(response.status_code, 200)
# Check for email AND member type (which here is just member)
self.assertContains(response, f'data-member-email="{member_email}"')
self.assertContains(response, 'data-member-name="First Last"')
self.assertContains(response, 'data-member-type="member"')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_page_has_kebab_wrapper_for_invited_member_if_user_has_edit_permission(self):
"""Test that the kebab wrapper displays for an invitedmember with edit permissions"""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Invite a member under same portfolio
invited_member_email = "invited_member@example.com"
invitation = PortfolioInvitation.objects.create(
email=invited_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
# I log in as the User so I can see the Manage Member page
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember", args=[invitation.id]), follow=True)
self.assertEqual(response.status_code, 200)
# Assert the invited members email + invitedmember type
self.assertContains(response, f'data-member-name="{invited_member_email}"')
self.assertContains(response, 'data-member-type="invitedmember"')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_page_does_not_have_kebab_wrapper(self):
"""Test that the kebab does not display."""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# That creates a member with only view access
member_email = "member_with_view_access@example.com"
member, _ = User.objects.get_or_create(username="test_member_with_view_access", email=member_email)
upp, _ = UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# I log in as the Member with only view permissions to evaluate the pages behaviour
# when viewed by someone who doesn't have edit perms
self.client.force_login(member)
# Go to the Manage Member page
response = self.client.get(reverse("member", args=[upp.id]), follow=True)
self.assertEqual(response.status_code, 200)
# Assert that the kebab edit options are unavailable
self.assertNotContains(response, 'data-member-type="member"')
self.assertNotContains(response, 'data-member-type="invitedmember"')
self.assertNotContains(response, f'data-member-name="{member_email}"')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_page_has_correct_form_wrapper(self):
"""Test that the manage members page the right form wrapper"""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# That creates a member
member_email = "a_member@example.com"
member, _ = User.objects.get_or_create(email=member_email)
upp, _ = UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
# Login as the User to see the Manage Member page
self.client.force_login(self.user)
# Specifically go to the Manage Member page
response = self.client.get(reverse("member", args=[upp.id]), follow=True)
# Check for a 200 response
self.assertEqual(response.status_code, 200)
# Check for form method + that its "post" and id "member-delete-form"
self.assertContains(response, "<form")
self.assertContains(response, 'method="post"')
self.assertContains(response, 'id="member-delete-form"')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_toggleable_alert_wrapper_exists_on_members_page(self):
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# That creates a member
member_email = "a_member@example.com"
member, _ = User.objects.get_or_create(email=member_email)
UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
# Login as the User to see the Members Table page
self.client.force_login(self.user)
# Specifically go to the Members Table page
response = self.client.get(reverse("members"))
# Assert that the toggleable alert ID exists
self.assertContains(response, '<div id="toggleable-alert"')
class TestPortfolioMemberDeleteView(WebTest):
def setUp(self):
super().setUp()
self.client = Client()
self.user = create_test_user()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
def tearDown(self):
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
UserDomainRole.objects.all().delete()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
Domain.objects.all().delete()
User.objects.all().delete()
super().tearDown()
@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")
@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(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# That creates a member
member_email = "a_member@example.com"
member, _ = User.objects.get_or_create(email=member_email)
upp, _ = UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
with patch.object(User, "get_active_requests_count_in_portfolio", return_value=1):
self.client.force_login(self.user)
# We check X_REQUESTED_WITH bc those return JSON responses
response = self.client.post(
reverse("member-delete", kwargs={"pk": upp.pk}), HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
self.assertEqual(response.status_code, 400) # Bad request due to active requests
support_url = "https://get.gov/contact/"
expected_error_message = (
"This member can't be removed from the organization because they have an active domain request. "
f"Please <a class='usa-link' href='{support_url}' target='_blank'>contact us</a> "
"to remove this member."
)
self.assertContains(response, expected_error_message, status_code=400)
# 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")
@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
admin_perm_user, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
with patch.object(User, "is_only_admin_of_portfolio", return_value=True):
self.client.force_login(self.user)
# We check X_REQUESTED_WITH bc those return JSON responses
response = self.client.post(
reverse("member-delete", kwargs={"pk": admin_perm_user.pk}), HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
self.assertEqual(response.status_code, 400)
expected_error_message = (
"There must be at least one admin in your organization. Give another member admin "
"permissions, make sure they log into the registrar, and then remove this member."
)
self.assertContains(response, expected_error_message, status_code=400)
# 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")
@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
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Creating a member that can be deleted (see patch)
member_email = "deleteable_member@example.com"
member, _ = User.objects.get_or_create(email=member_email)
# Set up the member in the portfolio
upp, _ = UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
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
):
# Attempt to delete
self.client.force_login(self.user)
response = self.client.post(
# We check X_REQUESTED_WITH bc those return JSON responses
reverse("member-delete", kwargs={"pk": upp.pk}),
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
# Check for a successful deletion
self.assertEqual(response.status_code, 200)
expected_success_message = f"You've removed {member.email} from the organization."
self.assertContains(response, expected_success_message, status_code=200)
# 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")
@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."""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Creating an admin that can be deleted (see patch)
member_email = "deleteable_admin@example.com"
member, _ = User.objects.get_or_create(email=member_email)
# Set up the member in the portfolio
upp, _ = UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
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(
User, "is_only_admin_of_portfolio", return_value=False
):
# Attempt to delete
self.client.force_login(self.user)
response = self.client.post(
# We check X_REQUESTED_WITH bc those return JSON responses
reverse("member-delete", kwargs={"pk": upp.pk}),
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
# Check for a successful deletion
self.assertEqual(response.status_code, 200)
expected_success_message = f"You've removed {member.email} from the organization."
self.assertContains(response, expected_success_message, status_code=200)
# 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
# Assert the email content
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)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_admin_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.
Email to removed member also fails to send."""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Creating an admin that can be deleted (see patch)
member_email = "deleteable_admin@example.com"
member, _ = User.objects.get_or_create(email=member_email)
# Set up the member in the portfolio
upp, _ = UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
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(
User, "is_only_admin_of_portfolio", return_value=False
):
# Attempt to delete
self.client.force_login(self.user)
response = self.client.post(
# We check X_REQUESTED_WITH bc those return JSON responses
reverse("member-delete", kwargs={"pk": upp.pk}),
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
# Check for a successful deletion
self.assertEqual(response.status_code, 200)
expected_success_message = f"You've removed {member.email} from the organization."
self.assertContains(response, expected_success_message, status_code=200)
# 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
# Assert the email content
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)
@override_flag("organization_members", active=True)
def test_portfolio_member_delete_view_manage_members_page_active_requests(self):
"""Error state when deleting a member with active requests on the Manage Members page"""
# I'm an admin user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Create a member with active requests
member_email = "member_with_active_request@example.com"
member, _ = User.objects.get_or_create(email=member_email)
upp, _ = UserPortfolioPermission.objects.get_or_create(
user=member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
with patch.object(User, "get_active_requests_count_in_portfolio", return_value=1):
with patch("django.contrib.messages.error") as mock_error:
self.client.force_login(self.user)
response = self.client.post(
reverse("member-delete", kwargs={"pk": upp.pk}),
)
# We don't want to do follow=True in response bc that does automatic redirection
# We want 302 bc indicates redirect
self.assertEqual(response.status_code, 302)
support_url = "https://get.gov/contact/"
expected_error_message = (
"This member can't be removed from the organization because they have an active domain request. "
f"Please <a class='usa-link' href='{support_url}' target='_blank'>contact us</a> "
"to remove this member."
)
args, kwargs = mock_error.call_args
# Check if first arg is a WSGIRequest, confirms request object passed correctly
# WSGIRequest protocol is basically the HTTPRequest but in Django form (ie POST '/member/1/delete')
self.assertIsInstance(args[0], WSGIRequest)
# Check that the error message matches the expected error message
self.assertEqual(args[1], expected_error_message)
# Location is used for a 3xx HTTP status code to indicate that the URL was redirected
# and then confirm that we're still on the Manage Members page
self.assertEqual(response.headers["Location"], reverse("member", kwargs={"pk": upp.pk}))
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_portfolio_member_delete_view_manage_members_page_only_admin(self):
"""Error state when trying to delete the only admin on the Manage Members page"""
# Create an admin with admin user perms
admin_perm_user, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Set them to be the only admin and attempt to delete
with patch.object(User, "is_only_admin_of_portfolio", return_value=True):
with patch("django.contrib.messages.error") as mock_error:
self.client.force_login(self.user)
response = self.client.post(
reverse("member-delete", kwargs={"pk": admin_perm_user.pk}),
)
self.assertEqual(response.status_code, 302)
expected_error_message = (
"There must be at least one admin in your organization. Give another member admin "
"permissions, make sure they log into the registrar, and then remove this member."
)
args, kwargs = mock_error.call_args
# Check if first arg is a WSGIRequest, confirms request object passed correctly
# WSGIRequest protocol is basically the HTTPRequest but in Django form (ie POST '/member/1/delete')
self.assertIsInstance(args[0], WSGIRequest)
# Check that the error message matches the expected error message
self.assertEqual(args[1], expected_error_message)
# Location is used for a 3xx HTTP status code to indicate that the URL was redirected
# and then confirm that we're still on the Manage Members page
self.assertEqual(response.headers["Location"], reverse("member", kwargs={"pk": admin_perm_user.pk}))
class TestPortfolioInvitedMemberDeleteView(WebTest):
def setUp(self):
super().setUp()
self.client = Client()
self.user = create_test_user()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
def tearDown(self):
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
UserDomainRole.objects.all().delete()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
Domain.objects.all().delete()
User.objects.all().delete()
super().tearDown()
@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")
@patch("registrar.views.portfolios.send_portfolio_invitation_remove_email")
def test_portfolio_member_delete_view_manage_members_page_invitedmember(
self, send_invited_member_removal, mock_send_removal_emails
):
"""Success state w/ deleting invited member on Manage Members page should redirect back to Members Table"""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Invite a member under same portfolio
invited_member_email = "invited_member@example.com"
invitation = PortfolioInvitation.objects.create(
email=invited_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
# Invited member removal email sent successfully
send_invited_member_removal.return_value = True
with patch("django.contrib.messages.success") as mock_success:
self.client.force_login(self.user)
response = self.client.post(
reverse("invitedmember-delete", kwargs={"pk": invitation.pk}),
)
self.assertEqual(response.status_code, 302)
expected_success_message = f"You've removed {invitation.email} from the organization."
args, kwargs = mock_success.call_args
# Check if first arg is a WSGIRequest, confirms request object passed correctly
# WSGIRequest protocol is basically the HTTPRequest but in Django form (ie POST '/member/1/delete')
self.assertIsInstance(args[0], WSGIRequest)
# Check that the error message matches the expected error message
self.assertEqual(args[1], expected_success_message)
# Location is used for a 3xx HTTP status code to indicate that the URL was redirected
# and then confirm that we're now on Members Table page
self.assertEqual(response.headers["Location"], reverse("members"))
# assert send_portfolio_admin_removal_emails not called since invitation
# is for a basic member
mock_send_removal_emails.assert_not_called()
# assert that send_portfolio_invitation_remove_email is called
send_invited_member_removal.assert_called_once()
# Get the arguments passed to send_portfolio_invitation_removal_email
_, called_kwargs = send_invited_member_removal.call_args
# Assert the email content
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["invitation"].email, invitation.email)
self.assertEqual(called_kwargs["invitation"].portfolio, invitation.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")
@patch("registrar.views.portfolios.send_portfolio_invitation_remove_email")
def test_portfolio_member_delete_view_manage_members_page_invitedadmin(
self, send_invited_member_email, mock_send_removal_emails
):
"""Success state w/ deleting invited admin on Manage Members page should redirect back to Members Table"""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
mock_send_removal_emails.return_value = True
send_invited_member_email.return_value = True
# Invite an admin under same portfolio
invited_member_email = "invited_member@example.com"
invitation = PortfolioInvitation.objects.create(
email=invited_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
with patch("django.contrib.messages.success") as mock_success:
self.client.force_login(self.user)
response = self.client.post(
reverse("invitedmember-delete", kwargs={"pk": invitation.pk}),
)
self.assertEqual(response.status_code, 302)
expected_success_message = f"You've removed {invitation.email} from the organization."
args, kwargs = mock_success.call_args
# Check if first arg is a WSGIRequest, confirms request object passed correctly
# WSGIRequest protocol is basically the HTTPRequest but in Django form (ie POST '/member/1/delete')
self.assertIsInstance(args[0], WSGIRequest)
# Check that the error message matches the expected error message
self.assertEqual(args[1], expected_success_message)
# Location is used for a 3xx HTTP status code to indicate that the URL was redirected
# and then confirm that we're now on Members Table page
self.assertEqual(response.headers["Location"], reverse("members"))
# assert send_portfolio_admin_removal_emails is called since invitation
# is for an admin
mock_send_removal_emails.assert_called_once()
# assert that send_portfolio_invitation_remove_email is called
send_invited_member_email.assert_called_once()
# Get the arguments passed to send_portfolio_admin_addition_emails
_, called_kwargs = mock_send_removal_emails.call_args
# Assert the email content
self.assertEqual(called_kwargs["email"], invited_member_email)
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
# Get the arguments passed to send_portfolio_invitation_remove_email
_, called_kwargs = send_invited_member_email.call_args
# Assert the email content
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["invitation"].email, invitation.email)
self.assertEqual(called_kwargs["invitation"].portfolio, invitation.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")
@patch("registrar.views.portfolios.send_portfolio_invitation_remove_email")
def test_portfolio_member_delete_view_manage_members_page_invitedadmin_email_fails(
self, send_invited_member_email, mock_send_removal_emails
):
"""Success state w/ deleting invited admin on Manage Members page should redirect back to Members Table"""
# I'm a user
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
mock_send_removal_emails.return_value = False
send_invited_member_email.return_value = False
# Invite an admin under same portfolio
invited_member_email = "invited_member@example.com"
invitation = PortfolioInvitation.objects.create(
email=invited_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
with patch("django.contrib.messages.success") as mock_success:
self.client.force_login(self.user)
response = self.client.post(
reverse("invitedmember-delete", kwargs={"pk": invitation.pk}),
)
self.assertEqual(response.status_code, 302)
expected_success_message = f"You've removed {invitation.email} from the organization."
args, kwargs = mock_success.call_args
# Check if first arg is a WSGIRequest, confirms request object passed correctly
# WSGIRequest protocol is basically the HTTPRequest but in Django form (ie POST '/member/1/delete')
self.assertIsInstance(args[0], WSGIRequest)
# Check that the error message matches the expected error message
self.assertEqual(args[1], expected_success_message)
# Location is used for a 3xx HTTP status code to indicate that the URL was redirected
# and then confirm that we're now on Members Table page
self.assertEqual(response.headers["Location"], reverse("members"))
# assert send_portfolio_admin_removal_emails is called since invitation
# is for an admin
mock_send_removal_emails.assert_called_once()
# assert that send_portfolio_invitation_remove_email is called
send_invited_member_email.assert_called_once()
# Get the arguments passed to send_portfolio_admin_addition_emails
_, called_kwargs = mock_send_removal_emails.call_args
# Assert the email content
self.assertEqual(called_kwargs["email"], invited_member_email)
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
# Get the arguments passed to send_portfolio_invitation_remove_email
_, called_kwargs = send_invited_member_email.call_args
# Assert the email content
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["invitation"].email, invitation.email)
self.assertEqual(called_kwargs["invitation"].portfolio, invitation.portfolio)
class TestPortfolioMemberDomainsView(TestWithUser, WebTest):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create test member
cls.user_member = User.objects.create(
username="test_member",
first_name="Second",
last_name="User",
email="second@example.com",
phone="8003112345",
title="Member",
)
# Create test user with no perms
cls.user_no_perms = User.objects.create(
username="test_user_no_perms",
first_name="No",
last_name="Permissions",
email="user_no_perms@example.com",
phone="8003112345",
title="No Permissions",
)
# Create Portfolio
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio")
# Assign permissions to the user making requests
cls.portfolio_permission = UserPortfolioPermission.objects.create(
user=cls.user,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
cls.permission = UserPortfolioPermission.objects.create(
user=cls.user_member,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
@classmethod
def tearDownClass(cls):
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
super().tearDownClass()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_authenticated(self):
"""Tests that the portfolio member domains view is accessible."""
self.client.force_login(self.user)
response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id}))
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.user_member.email)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_no_perms(self):
"""Tests that the portfolio member domains view is not accessible to user with no perms."""
self.client.force_login(self.user_no_perms)
response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id}))
# Make sure the request returns forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_unauthenticated(self):
"""Tests that the portfolio member domains view is not accessible when no authenticated user."""
self.client.logout()
response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id}))
# Make sure the request returns redirect to openid login
self.assertEqual(response.status_code, 302) # Redirect to openid login
self.assertIn("/openid/login", response.url)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_not_found(self):
"""Tests that the portfolio member domains view returns not found if user portfolio permission not found."""
self.client.force_login(self.user)
response = self.client.get(reverse("member-domains", kwargs={"pk": "0"}))
# Make sure the response is not found
self.assertEqual(response.status_code, 404)
class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_no_perms = User.objects.create(
username="test_user_no_perms",
first_name="No",
last_name="Permissions",
email="user_no_perms@example.com",
phone="8003112345",
title="No Permissions",
)
# Create Portfolio
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio")
# Add an invited member who has been invited to manage domains
cls.invited_member_email = "invited@example.com"
cls.invitation = PortfolioInvitation.objects.create(
email=cls.invited_member_email,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# Assign permissions to the user making requests
UserPortfolioPermission.objects.create(
user=cls.user,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
@classmethod
def tearDownClass(cls):
PortfolioInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
super().tearDownClass()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_invitedmember_domains_authenticated(self):
"""Tests that the portfolio invited member domains view is accessible."""
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id}))
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.invited_member_email)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_invitedmember_domains_no_perms(self):
"""Tests that the portfolio invited member domains view is not accessible to user with no perms."""
self.client.force_login(self.user_no_perms)
response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id}))
# Make sure the request returns forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_invitedmember_domains_unauthenticated(self):
"""Tests that the portfolio invited member domains view is not accessible when no authenticated user."""
self.client.logout()
response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id}))
# Make sure the request returns redirect to openid login
self.assertEqual(response.status_code, 302) # Redirect to openid login
self.assertIn("/openid/login", response.url)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_not_found(self):
"""Tests that the portfolio invited member domains view returns not found if user is not a member."""
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": "0"}))
# Make sure the response is not found
self.assertEqual(response.status_code, 404)
class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create Portfolio
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio")
# Create domains for testing
cls.domain1 = Domain.objects.create(name="1.gov")
cls.domain2 = Domain.objects.create(name="2.gov")
cls.domain3 = Domain.objects.create(name="3.gov")
@classmethod
def tearDownClass(cls):
super().tearDownClass()
Portfolio.objects.all().delete()
User.objects.all().delete()
Domain.objects.all().delete()
def setUp(self):
super().setUp()
# Create test member
self.user_member = User.objects.create(
username="test_member",
first_name="Second",
last_name="User",
email="second@example.com",
phone="8003112345",
title="Member",
)
# Create test user with no perms
self.user_no_perms = User.objects.create(
username="test_user_no_perms",
first_name="No",
last_name="Permissions",
email="user_no_perms@example.com",
phone="8003112345",
title="No Permissions",
)
# Assign permissions to the user making requests
self.portfolio_permission = UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Assign permissions to test member
self.permission = UserPortfolioPermission.objects.create(
user=self.user_member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Create url to be used in all tests
self.url = reverse("member-domains-edit", kwargs={"pk": self.portfolio_permission.pk})
def tearDown(self):
super().tearDown()
UserDomainRole.objects.all().delete()
DomainInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
PortfolioInvitation.objects.all().delete()
Portfolio.objects.exclude(id=self.portfolio.id).delete()
User.objects.exclude(id=self.user.id).delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_edit_authenticated(self):
"""Tests that the portfolio member domains edit view is accessible."""
self.client.force_login(self.user)
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.user_member.email)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_edit_no_perms(self):
"""Tests that the portfolio member domains edit view is not accessible to user with no perms."""
self.client.force_login(self.user_no_perms)
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
# Make sure the request returns forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_edit_unauthenticated(self):
"""Tests that the portfolio member domains edit view is not accessible when no authenticated user."""
self.client.logout()
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
# Make sure the request returns redirect to openid login
self.assertEqual(response.status_code, 302) # Redirect to openid login
self.assertIn("/openid/login", response.url)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_edit_not_found(self):
"""Tests that the portfolio member domains edit view returns not found if user
portfolio permission not found."""
self.client.force_login(self.user)
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": "0"}))
# Make sure the response is not found
self.assertEqual(response.status_code, 404)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_domain_invitation_email")
def test_post_with_valid_added_domains(self, mock_send_domain_email):
"""Test that domains can be successfully added."""
self.client.force_login(self.user)
data = {
"added_domains": json.dumps([self.domain1.id, self.domain2.id, self.domain3.id]), # Mock domain IDs
}
response = self.client.post(self.url, data)
# Check that the UserDomainRole objects were created
self.assertEqual(UserDomainRole.objects.filter(user=self.user, role=UserDomainRole.Roles.MANAGER).count(), 3)
# Check for a success message and a redirect
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
expected_domains = [self.domain1, self.domain2, self.domain3]
# Verify that the invitation email was sent
mock_send_domain_email.assert_called_once()
call_args = mock_send_domain_email.call_args.kwargs
self.assertEqual(call_args["email"], "info@example.com")
self.assertEqual(call_args["requestor"], self.user)
self.assertEqual(list(call_args["domains"]), list(expected_domains))
self.assertIsNone(call_args.get("is_member_of_different_org"))
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_domain_invitation_email")
def test_post_with_valid_removed_domains(self, mock_send_domain_email):
"""Test that domains can be successfully removed."""
self.client.force_login(self.user)
# Create some UserDomainRole objects
domains = [self.domain1, self.domain2, self.domain3]
UserDomainRole.objects.bulk_create([UserDomainRole(domain=domain, user=self.user) for domain in domains])
data = {
"removed_domains": json.dumps([self.domain1.id, self.domain2.id]),
}
response = self.client.post(self.url, data)
# Check that the UserDomainRole objects were deleted
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 1)
self.assertEqual(UserDomainRole.objects.filter(domain=self.domain3, user=self.user).count(), 1)
# Check for a success message and a redirect
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
# assert that send_domain_invitation_email is not called
mock_send_domain_email.assert_not_called()
UserDomainRole.objects.all().delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_post_with_invalid_added_domains_data(self):
"""Test that an error is returned for invalid added domains data."""
self.client.force_login(self.user)
data = {
"added_domains": "json-statham",
}
response = self.client.post(self.url, data)
# Check that no UserDomainRole objects were created
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
# Check for an error message and a redirect
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(
str(messages[0]), "Invalid data for added domains. If the issue persists, please contact help@get.gov."
)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_post_with_invalid_removed_domains_data(self):
"""Test that an error is returned for invalid removed domains data."""
self.client.force_login(self.user)
data = {
"removed_domains": "not-a-json",
}
response = self.client.post(self.url, data)
# Check that no UserDomainRole objects were deleted
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
# Check for an error message and a redirect
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(
str(messages[0]), "Invalid data for removed domains. If the issue persists, please contact help@get.gov."
)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_post_with_no_changes(self):
"""Test that success message is displayed when no changes are made."""
self.client.force_login(self.user)
response = self.client.post(self.url, {})
# Check that no UserDomainRole objects were created or deleted
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
# Check for an info message and a redirect
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_domain_invitation_email")
def test_post_when_send_domain_email_raises_exception(self, mock_send_domain_email):
"""Test attempt to add new domains when an EmailSendingError raised."""
self.client.force_login(self.user)
data = {
"added_domains": json.dumps([self.domain1.id, self.domain2.id, self.domain3.id]), # Mock domain IDs
}
mock_send_domain_email.side_effect = EmailSendingError("Failed to send email")
response = self.client.post(self.url, data)
# Check that the UserDomainRole objects were not created
self.assertEqual(UserDomainRole.objects.filter(user=self.user, role=UserDomainRole.Roles.MANAGER).count(), 0)
# Check for an error message and a redirect to edit form
self.assertRedirects(response, reverse("member-domains-edit", kwargs={"pk": self.portfolio_permission.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(
str(messages[0]),
"An unexpected error occurred: Failed to send email. If the issue persists, please contact help@get.gov.",
)
class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create Portfolio
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio")
# Create domains for testing
cls.domain1 = Domain.objects.create(name="1.gov")
cls.domain2 = Domain.objects.create(name="2.gov")
cls.domain3 = Domain.objects.create(name="3.gov")
@classmethod
def tearDownClass(cls):
super().tearDownClass()
Portfolio.objects.all().delete()
User.objects.all().delete()
Domain.objects.all().delete()
def setUp(self):
super().setUp()
# Add a user with no permissions
self.user_no_perms = User.objects.create(
username="test_user_no_perms",
first_name="No",
last_name="Permissions",
email="user_no_perms@example.com",
phone="8003112345",
title="No Permissions",
)
# Add an invited member who has been invited to manage domains
self.invited_member_email = "invited@example.com"
self.invitation = PortfolioInvitation.objects.create(
email=self.invited_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# Assign permissions to the user making requests
UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
self.url = reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.pk})
def tearDown(self):
super().tearDown()
Domain.objects.all().delete()
DomainInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
PortfolioInvitation.objects.all().delete()
Portfolio.objects.exclude(id=self.portfolio.id).delete()
User.objects.exclude(id=self.user.id).delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_invitedmember_domains_edit_authenticated(self):
"""Tests that the portfolio invited member domains edit view is accessible."""
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.invited_member_email)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_invitedmember_domains_edit_no_perms(self):
"""Tests that the portfolio invited member domains edit view is not accessible to user with no perms."""
self.client.force_login(self.user_no_perms)
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
# Make sure the request returns forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_invitedmember_domains_edit_unauthenticated(self):
"""Tests that the portfolio invited member domains edit view is not accessible when no authenticated user."""
self.client.logout()
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
# Make sure the request returns redirect to openid login
self.assertEqual(response.status_code, 302) # Redirect to openid login
self.assertIn("/openid/login", response.url)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_domains_edit_not_found(self):
"""Tests that the portfolio invited member domains edit view returns not found if user is not a member."""
self.client.force_login(self.user)
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": "0"}))
# Make sure the response is not found
self.assertEqual(response.status_code, 404)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_domain_invitation_email")
def test_post_with_valid_added_domains(self, mock_send_domain_email):
"""Test adding new domains successfully."""
self.client.force_login(self.user)
data = {
"added_domains": json.dumps([self.domain1.id, self.domain2.id, self.domain3.id]),
}
response = self.client.post(self.url, data)
# Check that the DomainInvitation objects were created
self.assertEqual(
DomainInvitation.objects.filter(
email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED
).count(),
3,
)
# Check for a success message and a redirect
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
expected_domains = [self.domain1, self.domain2, self.domain3]
# Verify that the invitation email was sent
mock_send_domain_email.assert_called_once()
call_args = mock_send_domain_email.call_args.kwargs
self.assertEqual(call_args["email"], "invited@example.com")
self.assertEqual(call_args["requestor"], self.user)
self.assertEqual(list(call_args["domains"]), list(expected_domains))
self.assertFalse(call_args.get("is_member_of_different_org"))
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_domain_invitation_email")
def test_post_with_existing_and_new_added_domains(self, _):
"""Test updating existing and adding new invitations."""
self.client.force_login(self.user)
# Create existing invitations
DomainInvitation.objects.bulk_create(
[
DomainInvitation(
domain=self.domain1,
email="invited@example.com",
status=DomainInvitation.DomainInvitationStatus.CANCELED,
),
DomainInvitation(
domain=self.domain2,
email="invited@example.com",
status=DomainInvitation.DomainInvitationStatus.INVITED,
),
]
)
data = {
"added_domains": json.dumps([self.domain1.id, self.domain2.id, self.domain3.id]),
}
response = self.client.post(self.url, data)
# Check that status for domain_id=1 was updated to INVITED
self.assertEqual(
DomainInvitation.objects.get(domain=self.domain1, email="invited@example.com").status,
DomainInvitation.DomainInvitationStatus.INVITED,
)
# Check that domain_id=3 was created as INVITED
self.assertTrue(
DomainInvitation.objects.filter(
domain=self.domain3, email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED
).exists()
)
# Check for a success message and a redirect
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_domain_invitation_email")
def test_post_with_valid_removed_domains(self, mock_send_domain_email):
"""Test removing domains successfully."""
self.client.force_login(self.user)
# Create existing invitations
DomainInvitation.objects.bulk_create(
[
DomainInvitation(
domain=self.domain1,
email="invited@example.com",
status=DomainInvitation.DomainInvitationStatus.INVITED,
),
DomainInvitation(
domain=self.domain2,
email="invited@example.com",
status=DomainInvitation.DomainInvitationStatus.INVITED,
),
]
)
data = {
"removed_domains": json.dumps([self.domain1.id]),
}
response = self.client.post(self.url, data)
# Check that the status for domain_id=1 was updated to CANCELED
self.assertEqual(
DomainInvitation.objects.get(domain=self.domain1, email="invited@example.com").status,
DomainInvitation.DomainInvitationStatus.CANCELED,
)
# Check that domain_id=2 remains INVITED
self.assertEqual(
DomainInvitation.objects.get(domain=self.domain2, email="invited@example.com").status,
DomainInvitation.DomainInvitationStatus.INVITED,
)
# Check for a success message and a redirect
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
# assert that send_domain_invitation_email is not called
mock_send_domain_email.assert_not_called()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_post_with_invalid_added_domains_data(self):
"""Test handling of invalid JSON for added domains."""
self.client.force_login(self.user)
data = {
"added_domains": "not-a-json",
}
response = self.client.post(self.url, data)
# Check that no DomainInvitation objects were created
self.assertEqual(DomainInvitation.objects.count(), 0)
# Check for an error message and a redirect
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(
str(messages[0]), "Invalid data for added domains. If the issue persists, please contact help@get.gov."
)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_post_with_invalid_removed_domains_data(self):
"""Test handling of invalid JSON for removed domains."""
self.client.force_login(self.user)
data = {
"removed_domains": "json-sudeikis",
}
response = self.client.post(self.url, data)
# Check that no DomainInvitation objects were updated
self.assertEqual(DomainInvitation.objects.count(), 0)
# Check for an error message and a redirect
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(
str(messages[0]), "Invalid data for removed domains. If the issue persists, please contact help@get.gov."
)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_post_with_no_changes(self):
"""Test the case where no changes are made."""
self.client.force_login(self.user)
response = self.client.post(self.url, {})
# Check that no DomainInvitation objects were created or updated
self.assertEqual(DomainInvitation.objects.count(), 0)
# Check for an info message and a redirect
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_domain_invitation_email")
def test_post_when_send_domain_email_raises_exception(self, mock_send_domain_email):
"""Test attempt to add new domains when an EmailSendingError raised."""
self.client.force_login(self.user)
data = {
"added_domains": json.dumps([self.domain1.id, self.domain2.id, self.domain3.id]),
}
mock_send_domain_email.side_effect = EmailSendingError("Failed to send email")
response = self.client.post(self.url, data)
# Check that the DomainInvitation objects were not created
self.assertEqual(
DomainInvitation.objects.filter(
email="invited@example.com", status=DomainInvitation.DomainInvitationStatus.INVITED
).count(),
0,
)
# Check for an error message and a redirect to edit form
self.assertRedirects(response, reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.pk}))
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 1)
self.assertEqual(
str(messages[0]),
"An unexpected error occurred: Failed to send email. If the issue persists, please contact help@get.gov.",
)
class TestRequestingEntity(WebTest):
"""The requesting entity page is a domain request form that only exists
within the context of a portfolio."""
def setUp(self):
super().setUp()
self.client = Client()
self.user = create_user()
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
self.portfolio_2, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel Alaska")
self.suborganization, _ = Suborganization.objects.get_or_create(
name="Rocky road",
portfolio=self.portfolio,
)
self.suborganization_2, _ = Suborganization.objects.get_or_create(
name="Vanilla",
portfolio=self.portfolio,
)
self.unrelated_suborganization, _ = Suborganization.objects.get_or_create(
name="Cold",
portfolio=self.portfolio_2,
)
self.portfolio_role = UserPortfolioPermission.objects.create(
portfolio=self.portfolio,
user=self.user,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
)
# Login the current user
self.app.set_user(self.user.username)
self.mock_client_class = MagicMock()
self.mock_client = self.mock_client_class.return_value
def tearDown(self):
UserDomainRole.objects.all().delete()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
Domain.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
Suborganization.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
super().tearDown()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_form_validates_duplicate_suborganization(self):
"""Tests that form validation prevents duplicate suborganization names within the same portfolio"""
# Create an existing suborganization
suborganization = Suborganization.objects.create(name="Existing Suborg", portfolio=self.portfolio)
# Start the domain request process
response = self.app.get(reverse("domain-request:start"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# Navigate past the intro page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
form = response.forms[0]
response = form.submit().follow()
# Fill out the requesting entity form
form = response.forms[0]
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = "True"
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = "True"
form["portfolio_requesting_entity-requested_suborganization"] = suborganization.name.lower()
form["portfolio_requesting_entity-suborganization_city"] = "Eggnog"
form["portfolio_requesting_entity-suborganization_state_territory"] = DomainRequest.StateTerritoryChoices.OHIO
# Submit form and verify error
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
response = form.submit()
self.assertContains(response, "This suborganization already exists")
# Test that a different name is allowed
form["portfolio_requesting_entity-requested_suborganization"] = "New Suborg"
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
response = form.submit().follow()
# Verify successful submission by checking we're on the next page
self.assertContains(response, "Current websites")
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@less_console_noise_decorator
def test_requesting_entity_page_new_request(self):
"""Tests that the requesting entity page loads correctly when a new request is started"""
response = self.app.get(reverse("domain-request:start"))
# Navigate past the intro page
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
intro_form = response.forms[0]
response = intro_form.submit().follow()
# Test the requesting entiy page
self.assertContains(response, "Who will use the domain youre requesting?")
self.assertContains(response, "Add suborganization information")
# We expect to see the portfolio name in two places:
# the header, and as one of the radio button options.
self.assertContains(response, self.portfolio.organization_name, count=3)
# We expect the dropdown list to contain the suborganizations that currently exist on this portfolio
self.assertContains(response, self.suborganization.name, count=1)
self.assertContains(response, self.suborganization_2.name, count=1)
# However, we should only see suborgs that are on the actual portfolio
self.assertNotContains(response, self.unrelated_suborganization.name)
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@less_console_noise_decorator
def test_requesting_entity_page_existing_suborg_submission(self):
"""Tests that you can submit a form on this page and set a suborg"""
response = self.app.get(reverse("domain-request:start"))
# Navigate past the intro page
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
form = response.forms[0]
response = form.submit().follow()
# Check that we're on the right page
self.assertContains(response, "Who will use the domain youre requesting?")
form = response.forms[0]
# Test selecting an existing suborg
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
form["portfolio_requesting_entity-sub_organization"] = f"{self.suborganization.id}"
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = False
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
response = form.submit().follow()
# Ensure that the post occurred successfully by checking that we're on the following page.
self.assertContains(response, "Current websites")
created_domain_request_exists = DomainRequest.objects.filter(
organization_name__isnull=True, sub_organization=self.suborganization
).exists()
self.assertTrue(created_domain_request_exists)
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@less_console_noise_decorator
def test_requesting_entity_page_new_suborg_submission(self):
"""Tests that you can submit a form on this page and set a new suborg"""
response = self.app.get(reverse("domain-request:start"))
# Navigate past the intro page
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
form = response.forms[0]
response = form.submit().follow()
# Check that we're on the right page
self.assertContains(response, "Who will use the domain youre requesting?")
form = response.forms[0]
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
form["portfolio_requesting_entity-sub_organization"] = "other"
form["portfolio_requesting_entity-requested_suborganization"] = "moon"
form["portfolio_requesting_entity-suborganization_city"] = "kepler"
form["portfolio_requesting_entity-suborganization_state_territory"] = "AL"
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
response = form.submit().follow()
# Ensure that the post occurred successfully by checking that we're on the following page.
self.assertContains(response, "Current websites")
created_domain_request_exists = DomainRequest.objects.filter(
organization_name__isnull=True,
sub_organization__isnull=True,
requested_suborganization="moon",
suborganization_city="kepler",
suborganization_state_territory=DomainRequest.StateTerritoryChoices.ALABAMA,
).exists()
self.assertTrue(created_domain_request_exists)
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@less_console_noise_decorator
def test_requesting_entity_page_organization_submission(self):
"""Tests submitting an organization on the requesting org form"""
response = self.app.get(reverse("domain-request:start"))
# Navigate past the intro page
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
form = response.forms[0]
response = form.submit().follow()
# Check that we're on the right page
self.assertContains(response, "Who will use the domain youre requesting?")
form = response.forms[0]
# Test selecting an existing suborg
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = False
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
response = form.submit().follow()
# Ensure that the post occurred successfully by checking that we're on the following page.
self.assertContains(response, "Current websites")
created_domain_request_exists = DomainRequest.objects.filter(
organization_name=self.portfolio.organization_name,
).exists()
self.assertTrue(created_domain_request_exists)
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@less_console_noise_decorator
def test_requesting_entity_page_errors(self):
"""Tests that we get the expected form errors on requesting entity"""
domain_request = completed_domain_request(user=self.user, portfolio=self.portfolio)
response = self.app.get(
reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk})
).follow()
form = response.forms[0]
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# For 2 the tests below, it is required to submit a form without submitting a value
# for the select/combobox. WebTest will not do this; by default, WebTest will submit
# the first choice in a select. So, need to manipulate the form to remove the
# particular select/combobox that will not be submitted, and then post the form.
form_action = f"/request/{domain_request.pk}/portfolio_requesting_entity/"
# Test missing suborganization selection
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = False
# remove sub_organization from the form submission
form_data = form.submit_fields()
form_data = [(key, value) for key, value in form_data if key != "portfolio_requesting_entity-sub_organization"]
response = self.app.post(form_action, dict(form_data))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
self.assertContains(response, "Suborganization is required.", status_code=200)
# Test missing custom suborganization details
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
form["portfolio_requesting_entity-sub_organization"] = "other"
# remove suborganization_state_territory from the form submission
form_data = form.submit_fields()
form_data = [
(key, value)
for key, value in form_data
if key != "portfolio_requesting_entity-suborganization_state_territory"
]
response = self.app.post(form_action, dict(form_data))
self.assertContains(response, "Enter the name of your suborganization.", status_code=200)
self.assertContains(response, "Enter the city where your suborganization is located.", status_code=200)
self.assertContains(
response,
"Select the state, territory, or military post where your suborganization is located.",
status_code=200,
)
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@boto3_mocking.patching
@less_console_noise_decorator
def test_requesting_entity_submission_email_sent(self):
"""Tests that an email is sent out on successful form submission"""
AllowedEmail.objects.create(email=self.user.email)
domain_request = completed_domain_request(
user=self.user,
# This is the additional details field
has_anything_else=True,
)
domain_request.portfolio = self.portfolio
domain_request.requested_suborganization = "moon"
domain_request.suborganization_city = "kepler"
domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA
domain_request.save()
domain_request.refresh_from_db()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertNotIn("Anything else", body)
self.assertIn("kepler, AL", body)
self.assertIn("Requesting entity:", body)
self.assertIn("Administrators from your organization:", body)
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@boto3_mocking.patching
@less_console_noise_decorator
def test_requesting_entity_viewonly(self):
"""Tests the review steps page on under our viewonly context"""
domain_request = completed_domain_request(
user=create_test_user(),
# This is the additional details field
has_anything_else=True,
)
domain_request.portfolio = self.portfolio
domain_request.requested_suborganization = "moon"
domain_request.suborganization_city = "kepler"
domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA
domain_request.save()
domain_request.refresh_from_db()
domain_request.submit()
response = self.app.get(
reverse("domain-request-status-viewonly", kwargs={"domain_request_pk": domain_request.pk})
)
self.assertContains(response, "Requesting entity")
self.assertContains(response, "moon")
self.assertContains(response, "kepler, AL")
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@boto3_mocking.patching
@less_console_noise_decorator
def test_requesting_entity_manage(self):
"""Tests the review steps page on under our manage context"""
domain_request = completed_domain_request(
user=self.user,
# This is the additional details field
has_anything_else=True,
)
domain_request.portfolio = self.portfolio
domain_request.requested_suborganization = "moon"
domain_request.suborganization_city = "kepler"
domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA
domain_request.save()
domain_request.refresh_from_db()
domain_request.submit()
response = self.app.get(reverse("domain-request-status", kwargs={"domain_request_pk": domain_request.pk}))
self.assertContains(response, "Requesting entity")
self.assertContains(response, "moon")
self.assertContains(response, "kepler, AL")
class TestPortfolioInviteNewMemberView(MockEppLib, WebTest):
def setUp(self):
super().setUp()
self.user = create_test_user()
# Create Portfolio
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
# Add an invited member who has been invited to manage domains
self.invited_member_email = "invited@example.com"
self.invitation = PortfolioInvitation.objects.create(
email=self.invited_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
self.new_member_email = "newmember@example.com"
AllowedEmail.objects.get_or_create(email=self.new_member_email)
# Assign permissions to the user making requests
UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
def tearDown(self):
PortfolioInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
AllowedEmail.objects.all().delete()
super().tearDown()
@boto3_mocking.patching
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_invite_for_new_users(self):
"""Tests the member invitation flow for new users."""
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
mock_client_class = MagicMock()
mock_client = mock_client_class.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client_class):
# Simulate submission of member invite for new user
final_response = self.client.post(
reverse("new-member"),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email,
},
)
# Ensure the final submission is successful
self.assertEqual(final_response.status_code, 302) # Redirects
# Validate Database Changes
# Validate that portfolio invitation was created but not retrieved
portfolio_invite = PortfolioInvitation.objects.filter(
email=self.new_member_email, portfolio=self.portfolio
).first()
self.assertIsNotNone(portfolio_invite)
self.assertEqual(portfolio_invite.email, self.new_member_email)
self.assertEqual(portfolio_invite.status, PortfolioInvitation.PortfolioInvitationStatus.INVITED)
# Check that an email was sent
self.assertTrue(mock_client.send_email.called)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_invitation_email")
def test_member_invite_for_previously_removed_user(self, mock_send_email):
"""Tests the member invitation flow for an existing member which was previously removed."""
self.client.force_login(self.user)
# invite, then retrieve an existing user, then remove the user from the portfolio
retrieved_member_email = "retrieved@example.com"
retrieved_user = User.objects.create(
username="retrieved_user",
first_name="Retrieved",
last_name="User",
email=retrieved_member_email,
phone="8003111234",
title="retrieved",
)
retrieved_invitation = PortfolioInvitation.objects.create(
email=retrieved_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
status=PortfolioInvitation.PortfolioInvitationStatus.INVITED,
)
retrieved_invitation.retrieve()
retrieved_invitation.save()
upp = UserPortfolioPermission.objects.filter(
user=retrieved_user,
portfolio=self.portfolio,
)
upp.delete()
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Simulate submission of member invite for previously retrieved/removed member
final_response = self.client.post(
reverse("new-member"),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": retrieved_member_email,
},
)
# Ensure the final submission is successful
self.assertEqual(final_response.status_code, 302) # Redirects
# Validate Database Changes
# Validate that portfolio invitation was created and retrieved
self.assertFalse(
PortfolioInvitation.objects.filter(
email=retrieved_member_email,
portfolio=self.portfolio,
status=PortfolioInvitation.PortfolioInvitationStatus.INVITED,
).exists()
)
# at least one retrieved invitation
self.assertTrue(
PortfolioInvitation.objects.filter(
email=retrieved_member_email,
portfolio=self.portfolio,
status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED,
).exists()
)
# Ensure exactly one UserPortfolioPermission exists for the retrieved user
self.assertEqual(
UserPortfolioPermission.objects.filter(user=retrieved_user, portfolio=self.portfolio).count(),
1,
"Expected exactly one UserPortfolioPermission for the retrieved user.",
)
@boto3_mocking.patching
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_member_invite_for_new_users_initial_ajax_call_passes(self):
"""Tests the member invitation flow for new users."""
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
mock_client_class = MagicMock()
mock_client = mock_client_class.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client_class):
# Simulate submission of member invite for new user
final_response = self.client.post(
reverse("new-member"),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email,
},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
# Ensure the prep ajax submission is successful
self.assertEqual(final_response.status_code, 200)
# Check that the response is a JSON response with is_valid
json_response = final_response.json()
self.assertIn("is_valid", json_response)
self.assertTrue(json_response["is_valid"])
# assert that portfolio invitation is not created
self.assertFalse(
PortfolioInvitation.objects.filter(email=self.new_member_email, portfolio=self.portfolio).exists(),
"Portfolio invitation should not be created when an Exception occurs.",
)
# Check that an email was not sent
self.assertFalse(mock_client.send_email.called)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_invitation_email")
def test_member_invite_for_previously_invited_member_initial_ajax_call_fails(self, mock_send_email):
"""Tests the initial ajax call in the member invitation flow for existing portfolio member."""
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
invite_count_before = PortfolioInvitation.objects.count()
# Simulate submission of member invite for user who has already been invited
response = self.client.post(
reverse("new-member"),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"email": self.invited_member_email,
},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 200)
# Check that the response is a JSON response with is_valid == False
json_response = response.json()
self.assertIn("is_valid", json_response)
self.assertFalse(json_response["is_valid"])
# Validate Database has not changed
invite_count_after = PortfolioInvitation.objects.count()
self.assertEqual(invite_count_after, invite_count_before)
# assert that send_portfolio_invitation_email is not called
mock_send_email.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_invitation_email")
def test_submit_new_member_raises_email_sending_error(self, mock_send_email):
"""Test when adding a new member and email_send method raises EmailSendingError."""
mock_send_email.side_effect = EmailSendingError("Failed to send email.")
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
form_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email,
}
# Act
with patch("django.contrib.messages.error") as mock_error:
response = self.client.post(reverse("new-member"), data=form_data)
# Assert
# assert that the send_portfolio_invitation_email called
mock_send_email.assert_called_once_with(
email=self.new_member_email,
requestor=self.user,
portfolio=self.portfolio,
is_admin_invitation=False,
)
# assert that response is a redirect to reverse("members")
self.assertRedirects(response, reverse("members"))
# assert that messages contains message, "Could not send email invitation"
mock_error.assert_called_once_with(response.wsgi_request, "Could not send organization invitation email.")
# assert that portfolio invitation is not created
self.assertFalse(
PortfolioInvitation.objects.filter(email=self.new_member_email, portfolio=self.portfolio).exists(),
"Portfolio invitation should not be created when an EmailSendingError occurs.",
)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_invitation_email")
def test_submit_new_member_raises_missing_email_error(self, mock_send_email):
"""Test when adding a new member and email_send method raises MissingEmailError."""
mock_send_email.side_effect = MissingEmailError()
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
form_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email,
}
# Act
with patch("django.contrib.messages.error") as mock_error:
response = self.client.post(reverse("new-member"), data=form_data)
# Assert
# assert that the send_portfolio_invitation_email called
mock_send_email.assert_called_once_with(
email=self.new_member_email, requestor=self.user, portfolio=self.portfolio, is_admin_invitation=False
)
# assert that response is a redirect to reverse("members")
self.assertRedirects(response, reverse("members"))
# assert that messages contains message, "Could not send email invitation"
mock_error.assert_called_once_with(
response.wsgi_request,
"Can't send invitation email. No email is associated with your user account.",
)
# assert that portfolio invitation is not created
self.assertFalse(
PortfolioInvitation.objects.filter(email=self.new_member_email, portfolio=self.portfolio).exists(),
"Portfolio invitation should not be created when a MissingEmailError occurs.",
)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_invitation_email")
def test_submit_new_member_raises_exception(self, mock_send_email):
"""Test when adding a new member and email_send method raises Exception."""
mock_send_email.side_effect = Exception("Generic exception")
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
form_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email,
}
# Act
with patch("django.contrib.messages.warning") as mock_warning:
response = self.client.post(reverse("new-member"), data=form_data)
# Assert
# assert that the send_portfolio_invitation_email called
mock_send_email.assert_called_once_with(
email=self.new_member_email,
requestor=self.user,
portfolio=self.portfolio,
is_admin_invitation=False,
)
# assert that response is a redirect to reverse("members")
self.assertRedirects(response, reverse("members"))
# assert that messages contains message, "Could not send email invitation"
mock_warning.assert_called_once_with(response.wsgi_request, "Could not send portfolio email invitation.")
# assert that portfolio invitation is not created
self.assertFalse(
PortfolioInvitation.objects.filter(email=self.new_member_email, portfolio=self.portfolio).exists(),
"Portfolio invitation should not be created when an Exception occurs.",
)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_invitation_email")
def test_member_invite_for_previously_invited_member(self, mock_send_email):
"""Tests the member invitation flow for existing portfolio member."""
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
invite_count_before = PortfolioInvitation.objects.count()
# Simulate submission of member invite for user who has already been invited
response = self.client.post(
reverse("new-member"),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"email": self.invited_member_email,
},
)
self.assertEqual(response.status_code, 200)
# verify messages
self.assertContains(
response,
(
"This user is already assigned to a portfolio invitation. "
"Based on current waffle flag settings, users cannot be assigned "
"to multiple portfolios."
),
)
# Validate Database has not changed
invite_count_after = PortfolioInvitation.objects.count()
self.assertEqual(invite_count_after, invite_count_before)
# assert that send_portfolio_invitation_email is not called
mock_send_email.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_invitation_email")
def test_member_invite_for_existing_member(self, mock_send_email):
"""Tests the member invitation flow for existing portfolio member."""
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
invite_count_before = PortfolioInvitation.objects.count()
# Simulate submission of member invite for user who has already been invited
response = self.client.post(
reverse("new-member"),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"email": self.user.email,
},
)
self.assertEqual(response.status_code, 200)
# Verify messages
self.assertContains(
response,
(
"This user is already assigned to a portfolio. "
"Based on current waffle flag settings, users cannot be "
"assigned to multiple portfolios."
),
)
# Validate Database has not changed
invite_count_after = PortfolioInvitation.objects.count()
self.assertEqual(invite_count_after, invite_count_before)
# assert that send_portfolio_invitation_email is not called
mock_send_email.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_invitation_email")
def test_member_invite_for_existing_user_who_is_not_a_member(self, mock_send_email):
"""Tests the member invitation flow for existing user who is not a portfolio member."""
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
new_user = User.objects.create(email="newuser@example.com")
# Simulate submission of member invite for the newly created user
response = self.client.post(
reverse("new-member"),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": "newuser@example.com",
},
)
self.assertEqual(response.status_code, 302)
# Validate Database Changes
# Validate that portfolio invitation was created and retrieved
portfolio_invite = PortfolioInvitation.objects.filter(
email="newuser@example.com", portfolio=self.portfolio
).first()
self.assertIsNotNone(portfolio_invite)
self.assertEqual(portfolio_invite.email, "newuser@example.com")
self.assertEqual(portfolio_invite.status, PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
# Validate UserPortfolioPermission
user_portfolio_permission = UserPortfolioPermission.objects.filter(
user=new_user, portfolio=self.portfolio
).first()
self.assertIsNotNone(user_portfolio_permission)
# assert that send_portfolio_invitation_email is called
mock_send_email.assert_called_once()
call_args = mock_send_email.call_args.kwargs
self.assertEqual(call_args["email"], "newuser@example.com")
self.assertEqual(call_args["requestor"], self.user)
self.assertIsNone(call_args.get("is_member_of_different_org"))
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_invitation_email")
def test_admin_invite_for_new_users(self, mock_send_email):
"""Tests the member invitation flow for new admin."""
self.client.force_login(self.user)
# Simulate a session to ensure continuity
session_id = self.client.session.session_key
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
mock_send_email.return_value = True
# Simulate submission of member invite for new admin
final_response = self.client.post(
reverse("new-member"),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email,
},
)
# Ensure the final submission is successful
self.assertEqual(final_response.status_code, 302) # Redirects
# Validate Database Changes
# Validate that portfolio invitation was created but not retrieved
portfolio_invite = PortfolioInvitation.objects.filter(
email=self.new_member_email, portfolio=self.portfolio
).first()
self.assertIsNotNone(portfolio_invite)
self.assertEqual(portfolio_invite.email, self.new_member_email)
self.assertEqual(portfolio_invite.status, PortfolioInvitation.PortfolioInvitationStatus.INVITED)
# Check that an email was sent
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"], self.new_member_email)
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
class TestPortfolioMemberEditView(WebTest):
"""Tests for the edit member page on portfolios"""
def setUp(self):
self.user = create_user()
# Create Portfolio
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
# Add an invited member who has been invited to manage domains
self.invited_member_email = "invited@example.com"
self.invitation = PortfolioInvitation.objects.create(
email=self.invited_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# Assign permissions to the user making requests
UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
def tearDown(self):
PortfolioInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
@patch("registrar.views.portfolios.send_portfolio_member_permission_update_email")
def test_edit_member_permissions_basic_to_admin(
self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails
):
"""Tests converting a basic member to admin with full permissions."""
self.client.force_login(self.user)
# Create a basic member to edit
basic_member = create_test_user()
basic_permission = UserPortfolioPermission.objects.create(
user=basic_member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
)
# return indicator that notification emails sent successfully
mock_send_addition_emails.return_value = True
mock_send_update_email.return_value = True
response = self.client.post(
reverse("member-permissions", kwargs={"pk": basic_permission.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
},
)
# Verify redirect and success message
self.assertEqual(response.status_code, 302)
# Verify database changes
basic_permission.refresh_from_db()
self.assertEqual(basic_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
# assert addition emails are sent to portfolio admins
mock_send_addition_emails.assert_called_once()
# assert removal emails are not sent
mock_send_removal_emails.assert_not_called()
# assert update email sent
mock_send_update_email.assert_called_once()
# Get the arguments passed to send_portfolio_admin_addition_emails
_, called_kwargs = mock_send_addition_emails.call_args
# Assert the notification email content
self.assertEqual(called_kwargs["email"], basic_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_update_email
_, called_kwargs = mock_send_update_email.call_args
# Assert the update notification email content
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["permissions"], basic_permission)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("django.contrib.messages.warning")
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
@patch("registrar.views.portfolios.send_portfolio_member_permission_update_email")
def test_edit_member_permissions_basic_to_admin_notification_fails(
self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails, mock_messages_warning
):
"""Tests converting a basic member to admin with full permissions.
Handle when notification emails fail to send."""
self.client.force_login(self.user)
# Create a basic member to edit
basic_member = create_test_user()
basic_permission = UserPortfolioPermission.objects.create(
user=basic_member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
)
# At least one notification email failed to send
mock_send_addition_emails.return_value = False
mock_send_update_email.return_value = False
response = self.client.post(
reverse("member-permissions", kwargs={"pk": basic_permission.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
},
)
# Verify redirect and success message
self.assertEqual(response.status_code, 302)
# Verify database changes
basic_permission.refresh_from_db()
self.assertEqual(basic_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
# assert addition emails are sent to portfolio admins
mock_send_addition_emails.assert_called_once()
# assert no removal emails are sent
mock_send_removal_emails.assert_not_called()
# assert update email sent
mock_send_update_email.assert_called_once()
# Get the arguments passed to send_portfolio_admin_addition_emails
_, called_kwargs = mock_send_addition_emails.call_args
# Assert the email content
self.assertEqual(called_kwargs["email"], basic_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_update_email
_, called_kwargs = mock_send_update_email.call_args
# Assert the update notification email content
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["permissions"], basic_permission)
# Assert that messages.warning is called twice
self.assertEqual(mock_messages_warning.call_count, 2)
# Extract the actual messages sent
warning_messages = [call_args[0][1] for call_args in mock_messages_warning.call_args_list]
# Check for the expected messages
self.assertIn("Could not send email notification to existing organization admins.", warning_messages)
self.assertIn(f"Could not send email notification to {basic_member.email}.", warning_messages)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
@patch("registrar.views.portfolios.send_portfolio_member_permission_update_email")
def test_edit_member_permissions_admin_to_admin(
self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails
):
"""Tests updating an admin without changing permissions."""
self.client.force_login(self.user)
# Create an admin member to edit
admin_member = create_test_user()
admin_permission = UserPortfolioPermission.objects.create(
user=admin_member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[],
)
response = self.client.post(
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
},
)
# Verify redirect and success message
self.assertEqual(response.status_code, 302)
# assert update, addition and removal emails are not sent to portfolio admins
mock_send_addition_emails.assert_not_called()
mock_send_removal_emails.assert_not_called()
mock_send_update_email.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_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
@patch("registrar.views.portfolios.send_portfolio_member_permission_update_email")
def test_edit_member_permissions_basic_to_basic(
self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails
):
"""Tests updating an admin without changing permissions."""
self.client.force_login(self.user)
# Create a basic member to edit
basic_member = create_test_user()
basic_permission = UserPortfolioPermission.objects.create(
user=basic_member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
)
mock_send_update_email.return_value = True
response = self.client.post(
reverse("member-permissions", kwargs={"pk": basic_permission.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
"member_permissions": "no_access",
"domain_request_permissions": "no_access",
},
)
# Verify redirect and success message
self.assertEqual(response.status_code, 302)
# assert addition and removal emails are not sent to portfolio admins
mock_send_addition_emails.assert_not_called()
mock_send_removal_emails.assert_not_called()
# assert update email is sent to updated member
mock_send_update_email.assert_called_once()
# Get the arguments passed to send_portfolio_member_permission_update_email
_, called_kwargs = mock_send_update_email.call_args
# Assert the email content
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["permissions"], basic_permission)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
@patch("registrar.views.portfolios.send_portfolio_member_permission_update_email")
def test_edit_member_permissions_admin_to_basic(
self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails
):
"""Tests converting an admin to basic member."""
self.client.force_login(self.user)
# Create an admin member to edit
admin_member = create_test_user()
admin_permission = UserPortfolioPermission.objects.create(
user=admin_member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
print(admin_permission)
mock_send_removal_emails.return_value = True
mock_send_update_email.return_value = True
response = self.client.post(
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
"member_permissions": "no_access",
"domain_request_permissions": "no_access",
},
)
# Verify redirect and success message
self.assertEqual(response.status_code, 302)
# Verify database changes
admin_permission.refresh_from_db()
self.assertEqual(admin_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_MEMBER])
# assert removal emails and update email are sent to portfolio admins
mock_send_update_email.assert_called_once()
mock_send_addition_emails.assert_not_called()
mock_send_removal_emails.assert_called_once()
# Get the arguments passed to send_portfolio_admin_removal_emails
_, called_kwargs = mock_send_removal_emails.call_args
# Assert the email content
self.assertEqual(called_kwargs["email"], admin_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_update_email
_, called_kwargs = mock_send_update_email.call_args
# Assert the email content
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["permissions"], admin_permission)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("django.contrib.messages.warning")
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
@patch("registrar.views.portfolios.send_portfolio_member_permission_update_email")
def test_edit_member_permissions_admin_to_basic_notification_fails(
self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails, mock_messages_warning
):
"""Tests converting an admin to basic member."""
self.client.force_login(self.user)
# Create an admin member to edit
admin_member = create_test_user()
admin_permission = UserPortfolioPermission.objects.create(
user=admin_member,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
)
# False return indicates that at least one notification email failed to send
mock_send_removal_emails.return_value = False
mock_send_update_email.return_value = False
response = self.client.post(
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
"member_permissions": "no_access",
"domain_request_permissions": "no_access",
},
)
# Verify redirect and success message
self.assertEqual(response.status_code, 302)
# Verify database changes
admin_permission.refresh_from_db()
self.assertEqual(admin_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_MEMBER])
# assert update email and removal emails are sent to portfolio admins
mock_send_addition_emails.assert_not_called()
mock_send_removal_emails.assert_called_once()
mock_send_update_email.assert_called_once()
# Get the arguments passed to send_portfolio_admin_removal_emails
_, called_kwargs = mock_send_removal_emails.call_args
# Assert the email content
self.assertEqual(called_kwargs["email"], admin_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_update_email
_, called_kwargs = mock_send_update_email.call_args
# Assert the email content
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["permissions"], admin_permission)
# Assert that messages.warning is called twice
self.assertEqual(mock_messages_warning.call_count, 2)
# Extract the actual messages sent
warning_messages = [call_args[0][1] for call_args in mock_messages_warning.call_args_list]
# Check for the expected messages
self.assertIn("Could not send email notification to existing organization admins.", warning_messages)
self.assertIn(f"Could not send email notification to {admin_member.email}.", warning_messages)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_edit_member_permissions_validation(self):
"""Tests form validation for required fields based on role."""
self.client.force_login(self.user)
member = create_test_user()
permission = UserPortfolioPermission.objects.create(
user=member, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
# Test missing required admin permissions
response = self.client.post(
reverse("member-permissions", kwargs={"pk": permission.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
# Missing required admin fields
},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.context["form"].errors["domain_request_permissions"][0],
"Domain request permission is required.",
)
self.assertEqual(response.context["form"].errors["member_permissions"][0], "Member permission is required.")
self.assertEqual(response.context["form"].errors["domain_permissions"][0], "Domain permission is required.")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_admin_removing_own_admin_role(self):
"""Tests an admin removing their own admin role redirects to home.
Removing the admin role will remove both view and edit members permissions.
Note: The user can remove the edit members permissions but as long as they
stay in admin role, they will at least still have view members permissions.
"""
self.client.force_login(self.user)
# Get the user's admin permission
admin_permission = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio)
response = self.client.post(
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
"member_permissions": "no_access",
"domain_request_permissions": "no_access",
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], reverse("home"))
class TestPortfolioInvitedMemberEditView(WebTest):
"""Tests for the edit invited member page on portfolios"""
def setUp(self):
self.user = create_user()
# Create Portfolio
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
# Add an invited member who has been invited to manage domains
self.invited_member_email = "invited@example.com"
self.invitation = PortfolioInvitation.objects.create(
email=self.invited_member_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
# Add an invited admin who has been invited to manage domains
self.invited_admin_email = "invitedadmin@example.com"
self.admin_invitation = PortfolioInvitation.objects.create(
email=self.invited_admin_email,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[],
)
# Assign permissions to the user making requests
UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
def tearDown(self):
PortfolioInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
def test_edit_invited_member_permissions_basic_to_admin(self, mock_send_removal_emails, mock_send_addition_emails):
"""Tests editing permissions for an invited (but not yet joined) member.
Update basic member to admin."""
self.client.force_login(self.user)
# email notifications send successfully
mock_send_addition_emails.return_value = True
# Test updating invitation permissions
response = self.client.post(
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
},
)
self.assertEqual(response.status_code, 302)
# Verify invitation was updated
updated_invitation = PortfolioInvitation.objects.get(pk=self.invitation.id)
self.assertEqual(updated_invitation.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
# Assert that addition emails are sent
mock_send_addition_emails.assert_called_once()
# Assert that removal emails are not sent
mock_send_removal_emails.assert_not_called()
# Get the arguments passed to send_portfolio_admin_addition_emails
_, called_kwargs = mock_send_addition_emails.call_args
# Assert the notification email content
self.assertEqual(called_kwargs["email"], self.invited_member_email)
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("django.contrib.messages.warning")
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
def test_edit_invited_member_permissions_basic_to_admin_notification_fails(
self, mock_send_removal_emails, mock_send_addition_emails, mock_messages_warning
):
"""Tests editing permissions for an invited (but not yet joined) member.
Update basic member to admin."""
self.client.force_login(self.user)
# at least one email notification not sent successfully
mock_send_addition_emails.return_value = False
# Test updating invitation permissions
response = self.client.post(
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
},
)
self.assertEqual(response.status_code, 302)
# Verify invitation was updated
updated_invitation = PortfolioInvitation.objects.get(pk=self.invitation.id)
self.assertEqual(updated_invitation.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
# Assert that addition emails are sent
mock_send_addition_emails.assert_called_once()
# Assert that removal emails are not sent
mock_send_removal_emails.assert_not_called()
# Get the arguments passed to send_portfolio_admin_addition_emails
_, called_kwargs = mock_send_addition_emails.call_args
# Assert the notification email content
self.assertEqual(called_kwargs["email"], self.invited_member_email)
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
# Assert warning message is called correctly
mock_messages_warning.assert_called_once()
warning_args, _ = mock_messages_warning.call_args
self.assertIsInstance(warning_args[0], WSGIRequest)
self.assertEqual(warning_args[1], "Could not send email notification to existing organization admins.")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
def test_edit_invited_member_permissions_admin_to_basic(self, mock_send_removal_emails, mock_send_addition_emails):
"""Tests editing permissions for an invited (but not yet joined) admin.
Update admin to basic member."""
self.client.force_login(self.user)
# email notifications send successfully
mock_send_addition_emails.return_value = True
# Test updating invitation permissions
response = self.client.post(
reverse("invitedmember-permissions", kwargs={"pk": self.admin_invitation.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
"member_permissions": "no_access",
"domain_request_permissions": "no_access",
},
)
self.assertEqual(response.status_code, 302)
# Verify invitation was updated
updated_invitation = PortfolioInvitation.objects.get(pk=self.admin_invitation.id)
self.assertEqual(updated_invitation.roles, [UserPortfolioRoleChoices.ORGANIZATION_MEMBER])
# Assert that addition emails are not sent
mock_send_addition_emails.assert_not_called()
# Assert that removal emails are sent
mock_send_removal_emails.assert_called_once()
# Get the arguments passed to send_portfolio_admin_removal_emails
_, called_kwargs = mock_send_removal_emails.call_args
# Assert the notification email content
self.assertEqual(called_kwargs["email"], self.invited_admin_email)
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("django.contrib.messages.warning")
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
def test_edit_invited_member_permissions_admin_to_basic_notification_fails(
self, mock_send_removal_emails, mock_send_addition_emails, mock_messages_warning
):
"""Tests editing permissions for an invited (but not yet joined) admin.
Update basic member to admin. At least one notification email fails."""
self.client.force_login(self.user)
# at least one email notification not sent successfully
mock_send_removal_emails.return_value = False
# Test updating invitation permissions
response = self.client.post(
reverse("invitedmember-permissions", kwargs={"pk": self.admin_invitation.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
"member_permissions": "no_access",
"domain_request_permissions": "no_access",
},
)
self.assertEqual(response.status_code, 302)
# Verify invitation was updated
updated_invitation = PortfolioInvitation.objects.get(pk=self.admin_invitation.id)
self.assertEqual(updated_invitation.roles, [UserPortfolioRoleChoices.ORGANIZATION_MEMBER])
# Assert that addition emails are not sent
mock_send_addition_emails.assert_not_called()
# Assert that removal emails are sent
mock_send_removal_emails.assert_called_once()
# Get the arguments passed to send_portfolio_admin_removal_emails
_, called_kwargs = mock_send_removal_emails.call_args
# Assert the notification email content
self.assertEqual(called_kwargs["email"], self.invited_admin_email)
self.assertEqual(called_kwargs["requestor"], self.user)
self.assertEqual(called_kwargs["portfolio"], self.portfolio)
# Assert warning message is called correctly
mock_messages_warning.assert_called_once()
warning_args, _ = mock_messages_warning.call_args
self.assertIsInstance(warning_args[0], WSGIRequest)
self.assertEqual(warning_args[1], "Could not send email notification to existing organization admins.")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
@patch("registrar.views.portfolios.send_portfolio_admin_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
def test_edit_invited_member_permissions_basic_to_basic(self, mock_send_removal_emails, mock_send_addition_emails):
"""Tests editing permissions for an invited (but not yet joined) member.
Update basic member without changing role."""
self.client.force_login(self.user)
# Test updating invitation permissions
response = self.client.post(
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
"member_permissions": "no_access",
"domain_request_permissions": "no_access",
},
)
self.assertEqual(response.status_code, 302)
# Assert that addition and removal emails are not sent
mock_send_addition_emails.assert_not_called()
mock_send_removal_emails.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_addition_emails")
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
def test_edit_invited_member_permissions_admin_to_admin(self, mock_send_removal_emails, mock_send_addition_emails):
"""Tests editing permissions for an invited (but not yet joined) admin.
Update admin member without changing role."""
self.client.force_login(self.user)
# Test updating invitation permissions
response = self.client.post(
reverse("invitedmember-permissions", kwargs={"pk": self.admin_invitation.id}),
{
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
},
)
self.assertEqual(response.status_code, 302)
# Assert that addition and removal emails are not sent
mock_send_addition_emails.assert_not_called()
mock_send_removal_emails.assert_not_called()