Unit tests, get domains for invited member

This commit is contained in:
Rachid Mrad 2024-10-17 13:27:57 -04:00
parent c5de4b3a1d
commit 3d6417bb99
No known key found for this signature in database
3 changed files with 328 additions and 62 deletions

View file

@ -2043,11 +2043,11 @@ class MembersTable extends LoadTableBase {
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) { if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members."; permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members.";
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) { } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
permissionsHTML += "<p> class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members."; permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.";
} }
// if there are no additional permissions, display a no additional permissions message // if there are no additional permissions, display a no additional permissions message
if (!permissionsHTML) { if (!permissionsHTML) {
permissionsHTML += "<p><b>No additional permissions:</b> There are no additional permissions for this member.</p>"; permissionsHTML += "<p class='margin-top-1 p--blockquote'><b>No additional permissions:</b> There are no additional permissions for this member.</p>";
} }
// add permissions header in all cases // add permissions header in all cases
permissionsHTML = "<div class='desktop:grid-col-7'><h4 class='margin-y-0 text-primary'>Additional permissions for this member</h4>" + permissionsHTML + "</div>"; permissionsHTML = "<div class='desktop:grid-col-7'><h4 class='margin-y-0 text-primary'>Additional permissions for this member</h4>" + permissionsHTML + "</div>";
@ -2058,7 +2058,7 @@ class MembersTable extends LoadTableBase {
showMoreButton = ` showMoreButton = `
<button <button
type="button" type="button"
class="usa-button--show-more-button usa-button usa-button--unstyled display-block margin-top-2" class="usa-button--show-more-button usa-button usa-button--unstyled display-block margin-top-1"
data-for=${member_id} data-for=${member_id}
> >
<span>Expand</span> <span>Expand</span>

View file

@ -1,21 +1,25 @@
from django.urls import reverse from django.urls import reverse
from registrar.models.domain import Domain
from registrar.models.domain_information import DomainInformation
from registrar.models.domain_invitation import DomainInvitation
from registrar.models.portfolio import Portfolio from registrar.models.portfolio import Portfolio
from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user import User from registrar.models.user import User
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from .test_views import TestWithUser from registrar.tests.common import MockEppLib, create_test_user
from django_webtest import WebTest # type: ignore from django_webtest import WebTest # type: ignore
class GetPortfolioMembersJsonTest(TestWithUser, WebTest): class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.user = create_test_user()
# Create additional users # Create additional users
cls.user2 = User.objects.create( self.user2 = User.objects.create(
username="test_user2", username="test_user2",
first_name="Second", first_name="Second",
last_name="User", last_name="User",
@ -23,7 +27,7 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest):
phone="8003112345", phone="8003112345",
title="Member", title="Member",
) )
cls.user3 = User.objects.create( self.user3 = User.objects.create(
username="test_user3", username="test_user3",
first_name="Third", first_name="Third",
last_name="User", last_name="User",
@ -31,7 +35,7 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest):
phone="8003113456", phone="8003113456",
title="Member", title="Member",
) )
cls.user4 = User.objects.create( self.user4 = User.objects.create(
username="test_user4", username="test_user4",
first_name="Fourth", first_name="Fourth",
last_name="User", last_name="User",
@ -39,60 +43,66 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest):
phone="8003114567", phone="8003114567",
title="Admin", title="Admin",
) )
cls.email5 = "fifth@example.com" self.user5 = User.objects.create(
username="test_user5",
first_name="Fifth",
last_name="User",
email="fifth@example.com",
phone="8003114568",
title="Admin",
)
self.email6 = "fifth@example.com"
# Create Portfolio # Create Portfolio
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio") self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
# Assign permissions # Assign permissions
UserPortfolioPermission.objects.create(
user=cls.user,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
UserPortfolioPermission.objects.create(
user=cls.user2,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=cls.user3,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=cls.user4,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
PortfolioInvitation.objects.create(
email=cls.email5,
portfolio=cls.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
@classmethod self.app.set_user(self.user.username)
def tearDownClass(cls):
def tearDown(self):
UserDomainRole.objects.all().delete()
DomainInformation.objects.all().delete()
Domain.objects.all().delete()
PortfolioInvitation.objects.all().delete() PortfolioInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete() UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete() Portfolio.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
super().tearDownClass() super().tearDown()
def setUp(self):
super().setUp()
self.app.set_user(self.user.username)
def test_get_portfolio_members_json_authenticated(self): def test_get_portfolio_members_json_authenticated(self):
"""Test that portfolio members are returned properly for an authenticated user.""" """Test that portfolio members are returned properly for an authenticated user."""
"""Also tests that reposnse is 200 when no domains"""
UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
UserPortfolioPermission.objects.create(
user=self.user2,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=self.user3,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=self.user4,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
UserPortfolioPermission.objects.create(
user=self.user5,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id}) response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json data = response.json
@ -115,15 +125,207 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest):
self.user3.email, self.user3.email,
self.user4.email, self.user4.email,
self.user4.email, self.user4.email,
self.email5, self.user5.email,
} }
actual_emails = {member["email"] for member in data["members"]} actual_emails = {member["email"] for member in data["members"]}
self.assertEqual(expected_emails, actual_emails) self.assertEqual(expected_emails, actual_emails)
expected_roles = {
UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
}
# Convert each member's roles list to a frozenset
actual_roles = {role for member in data["members"] for role in member["roles"]}
self.assertEqual(expected_roles, actual_roles)
expected_additional_permissions = {
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
}
actual_additional_permissions = {permission for member in data["members"] for permission in member["permissions"]}
self.assertTrue(expected_additional_permissions.issubset(actual_additional_permissions))
def test_get_portfolio_invited_json_authenticated(self):
"""Test that portfolio invitees are returned properly for an authenticated user."""
"""Also tests that reposnse is 200 when no domains"""
PortfolioInvitation.objects.create(
email=self.email6,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id})
self.assertEqual(response.status_code, 200)
data = response.json
# Check pagination info
self.assertEqual(data["page"], 1)
self.assertEqual(data["num_pages"], 1)
self.assertEqual(data["total"], 1)
self.assertEqual(data["unfiltered_total"], 1)
# Check the number of members
self.assertEqual(len(data["members"]), 1)
# Check member fields
expected_emails = {
self.email6
}
actual_emails = {member["email"] for member in data["members"]}
self.assertEqual(expected_emails, actual_emails)
expected_roles = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
}
# Convert each member's roles list to a frozenset
actual_roles = {role for member in data["members"] for role in member["roles"]}
self.assertEqual(expected_roles, actual_roles)
expected_additional_permissions = {
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
}
actual_additional_permissions = {permission for member in data["members"] for permission in member["permissions"]}
self.assertTrue(expected_additional_permissions.issubset(actual_additional_permissions))
def test_get_portfolio_members_json_with_domains(self):
"""Test that portfolio members are returned properly for an authenticated user and the response includes
the domains that the member manages.."""
UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
UserPortfolioPermission.objects.create(
user=self.user2,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=self.user3,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=self.user4,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
domain = Domain.objects.create(
name="somedomain1.com",
)
DomainInformation.objects.create(
creator=self.user,
domain=domain,
portfolio=self.portfolio,
)
UserDomainRole.objects.create(
user=self.user,
domain=domain,
role=UserDomainRole.Roles.MANAGER,
)
response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id})
self.assertEqual(response.status_code, 200)
data = response.json
# Check if the domain appears in the response JSON
domain_names = [
domain_name
for member in data["members"]
for domain_name in member.get("domain_names", [])
]
self.assertIn("somedomain1.com", domain_names)
def test_get_portfolio_invited_json_with_domains(self):
"""Test that portfolio invited members are returned properly for an authenticated user and the response includes
the domains that the member manages.."""
PortfolioInvitation.objects.create(
email=self.email6,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
domain = Domain.objects.create(
name="somedomain1.com",
)
DomainInformation.objects.create(
creator=self.user,
domain=domain,
portfolio=self.portfolio,
)
DomainInvitation.objects.create(
email=self.email6,
domain=domain,
)
response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id})
self.assertEqual(response.status_code, 200)
data = response.json
# Check if the domain appears in the response JSON
domain_names = [
domain_name
for member in data["members"]
for domain_name in member.get("domain_names", [])
]
self.assertIn("somedomain1.com", domain_names)
def test_pagination(self): def test_pagination(self):
"""Test that pagination works properly when there are more members than page size.""" """Test that pagination works properly when there are more members than page size."""
UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
UserPortfolioPermission.objects.create(
user=self.user2,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=self.user3,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=self.user4,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
PortfolioInvitation.objects.create(
email=self.email6,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Create additional members to exceed page size of 10 # Create additional members to exceed page size of 10
for i in range(5, 15): for i in range(6, 16):
user, _ = User.objects.get_or_create( user, _ = User.objects.get_or_create(
username=f"test_user{i}", username=f"test_user{i}",
first_name=f"User{i}", first_name=f"User{i}",
@ -172,6 +374,40 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest):
def test_search(self): def test_search(self):
"""Test search functionality for portfolio members.""" """Test search functionality for portfolio members."""
UserPortfolioPermission.objects.create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
UserPortfolioPermission.objects.create(
user=self.user2,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=self.user3,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
)
UserPortfolioPermission.objects.create(
user=self.user4,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
PortfolioInvitation.objects.create(
email=self.email6,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
)
# Search by name # Search by name
response = self.app.get( response = self.app.get(
reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id, "search_term": "Second"} reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id, "search_term": "Second"}

View file

@ -1,13 +1,14 @@
from django.http import JsonResponse from django.http import JsonResponse
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Value, F, CharField, TextField, Q, Case, When from django.db.models import Value, F, CharField, TextField, Q, Case, When, OuterRef, Subquery
from django.db.models.functions import Concat, Coalesce from django.db.models.functions import Concat, Coalesce
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.aggregates import ArrayAgg
from django.urls import reverse from django.urls import reverse
from django.db.models.functions import Cast from django.db.models.functions import Cast
from registrar.models.domain_invitation import DomainInvitation
from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.user_portfolio_permission import UserPortfolioPermission
@ -89,7 +90,8 @@ def initial_permissions_search(portfolio):
# specify the output_field to ensure union has same column types # specify the output_field to ensure union has same column types
output_field=CharField() output_field=CharField()
), ),
distinct=True distinct=True,
filter=Q(user__permissions__domain__isnull=False)
), ),
source=Value("permission", output_field=CharField()), source=Value("permission", output_field=CharField()),
) )
@ -110,7 +112,22 @@ def initial_permissions_search(portfolio):
def initial_invitations_search(portfolio): def initial_invitations_search(portfolio):
"""Perform initial invitations search before applying any filters.""" """Perform initial invitations search and get related DomainInvitation data based on the email."""
# Get DomainInvitation query for matching email
domain_invitations = DomainInvitation.objects.filter(
email=OuterRef('email'),
domain__isnull=False
).annotate(
domain_info=Concat(
F('domain__id'),
Value(':'),
F('domain__name'),
output_field=CharField()
)
)
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
invitations = invitations.annotate( invitations = invitations.annotate(
first_name=Value(None, output_field=CharField()), first_name=Value(None, output_field=CharField()),
@ -118,7 +135,14 @@ def initial_invitations_search(portfolio):
email_display=F("email"), email_display=F("email"),
last_active=Value("Invited", output_field=TextField()), last_active=Value("Invited", output_field=TextField()),
member_display=F("email"), member_display=F("email"),
domain_info=Value([], output_field=ArrayField(TextField())), # ArrayAgg for multiple domain_invitations matched by email, filtered to exclude nulls
domain_info=Coalesce( # Use Coalesce to return an empty list if no domain invitations exist
ArrayAgg(
Subquery(domain_invitations.values('domain_info')),
distinct=True,
),
Value([], output_field=ArrayField(CharField())) # Ensure we return an empty list
),
source=Value("invitation", output_field=CharField()), source=Value("invitation", output_field=CharField()),
).values( ).values(
"id", "id",
@ -181,9 +205,15 @@ def serialize_members(request, portfolio, item, user):
"member_display": item.get("member_display", ""), "member_display": item.get("member_display", ""),
"roles": (item.get("roles") or []), "roles": (item.get("roles") or []),
"permissions": UserPortfolioPermission.get_portfolio_permissions(item.get("roles"), item.get("additional_permissions")), "permissions": UserPortfolioPermission.get_portfolio_permissions(item.get("roles"), item.get("additional_permissions")),
# split domain_info array values into ids to form urls, and names "domain_names": [
"domain_urls": [reverse("domain", kwargs={"pk": domain_info.split(":")[0]}) for domain_info in item.get("domain_info")], domain_info.split(":")[1] for domain_info in (item.get("domain_info") or [])
"domain_names": [domain_info.split(":")[1] for domain_info in item.get("domain_info")], if domain_info is not None # Prevent splitting None
],
"domain_urls": [
reverse("domain", kwargs={"pk": domain_info.split(":")[0]})
for domain_info in (item.get("domain_info") or [])
if domain_info is not None # Prevent splitting None
],
"is_admin": is_admin, "is_admin": is_admin,
"last_active": item.get("last_active", ""), "last_active": item.get("last_active", ""),
"action_url": action_url, "action_url": action_url,