-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(organization-invites): handle duplicate invites (#26404)
- Loading branch information
1 parent
2bd9f57
commit 0f78df2
Showing
2 changed files
with
248 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
from unittest.mock import ANY, patch | ||
|
||
from django.core import mail | ||
from freezegun import freeze_time | ||
from rest_framework import status | ||
|
||
from ee.models.explicit_team_membership import ExplicitTeamMembership | ||
|
@@ -156,18 +157,18 @@ def test_add_organization_invite_with_email_on_instance_but_send_email_prop_fals | |
# Assert invite email is not sent | ||
self.assertEqual(len(mail.outbox), 0) | ||
|
||
def test_can_create_invites_for_the_same_email_multiple_times(self): | ||
def test_create_invites_for_the_same_email_multiple_times_deletes_older_invites(self): | ||
email = "[email protected]" | ||
count = OrganizationInvite.objects.count() | ||
|
||
for _ in range(0, 2): | ||
for _ in range(0, 3): | ||
response = self.client.post("/api/organizations/@current/invites/", {"target_email": email}) | ||
self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||
obj = OrganizationInvite.objects.get(id=response.json()["id"]) | ||
self.assertEqual(obj.target_email, email) | ||
self.assertEqual(obj.created_by, self.user) | ||
|
||
self.assertEqual(OrganizationInvite.objects.count(), count + 2) | ||
self.assertEqual(OrganizationInvite.objects.count(), count + 1) | ||
|
||
def test_can_specify_membership_level_in_invite(self): | ||
email = "[email protected]" | ||
|
@@ -508,3 +509,148 @@ def test_delete_organization_invite_if_plain_member(self): | |
self.assertEqual(response.content, b"") # Empty response | ||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) | ||
self.assertFalse(OrganizationInvite.objects.exists()) | ||
|
||
# Combine pending invites | ||
|
||
def test_combine_pending_invites_combines_levels_and_project_access(self): | ||
email = "[email protected]" | ||
private_team_1 = Team.objects.create(organization=self.organization, name="Private Team 1", access_control=True) | ||
private_team_2 = Team.objects.create(organization=self.organization, name="Private Team 2", access_control=True) | ||
|
||
ExplicitTeamMembership.objects.create( | ||
team=private_team_1, | ||
parent_membership=self.organization_membership, | ||
level=ExplicitTeamMembership.Level.ADMIN, | ||
) | ||
ExplicitTeamMembership.objects.create( | ||
team=private_team_2, | ||
parent_membership=self.organization_membership, | ||
level=ExplicitTeamMembership.Level.ADMIN, | ||
) | ||
|
||
# Create first invite with member access to team 1 | ||
first_invite = self.client.post( | ||
"/api/organizations/@current/invites/", | ||
{ | ||
"target_email": email, | ||
"level": OrganizationMembership.Level.MEMBER, | ||
"private_project_access": [{"id": private_team_1.id, "level": ExplicitTeamMembership.Level.MEMBER}], | ||
}, | ||
).json() | ||
|
||
# Create second invite with admin access to team 2 | ||
second_invite = self.client.post( | ||
"/api/organizations/@current/invites/", | ||
{ | ||
"target_email": email, | ||
"level": OrganizationMembership.Level.ADMIN, | ||
"private_project_access": [{"id": private_team_2.id, "level": ExplicitTeamMembership.Level.ADMIN}], | ||
}, | ||
).json() | ||
|
||
# Create third invite combining previous invites | ||
response = self.client.post( | ||
"/api/organizations/@current/invites/", | ||
{ | ||
"target_email": email, | ||
"level": OrganizationMembership.Level.MEMBER, | ||
"private_project_access": [{"id": private_team_1.id, "level": ExplicitTeamMembership.Level.ADMIN}], | ||
"combine_pending_invites": True, | ||
}, | ||
) | ||
|
||
self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||
combined_invite = response.json() | ||
|
||
# Check that previous invites are deleted | ||
self.assertFalse(OrganizationInvite.objects.filter(id=first_invite["id"]).exists()) | ||
self.assertFalse(OrganizationInvite.objects.filter(id=second_invite["id"]).exists()) | ||
|
||
# Check that the new invite has the highest level (ADMIN) | ||
self.assertEqual(combined_invite["level"], OrganizationMembership.Level.ADMIN) | ||
|
||
# Check that private project access is combined with highest levels | ||
expected_access = [ | ||
{"id": private_team_1.id, "level": ExplicitTeamMembership.Level.ADMIN}, | ||
{"id": private_team_2.id, "level": ExplicitTeamMembership.Level.ADMIN}, | ||
] | ||
self.assertEqual(len(combined_invite["private_project_access"]), 2) | ||
for access in expected_access: | ||
self.assertIn(access, combined_invite["private_project_access"]) | ||
|
||
def test_combine_pending_invites_with_no_existing_invites(self): | ||
email = "[email protected]" | ||
response = self.client.post( | ||
"/api/organizations/@current/invites/", | ||
{ | ||
"target_email": email, | ||
"level": OrganizationMembership.Level.MEMBER, | ||
"combine_pending_invites": True, | ||
}, | ||
) | ||
|
||
self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||
invite = response.json() | ||
self.assertEqual(invite["level"], OrganizationMembership.Level.MEMBER) | ||
self.assertEqual(invite["target_email"], email) | ||
self.assertEqual(invite["private_project_access"], []) | ||
|
||
@freeze_time("2024-01-10") | ||
def test_combine_pending_invites_with_expired_invites(self): | ||
email = "[email protected]" | ||
|
||
# Create an expired invite | ||
with freeze_time("2023-01-05"): | ||
OrganizationInvite.objects.create( | ||
organization=self.organization, | ||
target_email=email, | ||
level=OrganizationMembership.Level.ADMIN, | ||
) | ||
|
||
response = self.client.post( | ||
"/api/organizations/@current/invites/", | ||
{ | ||
"target_email": email, | ||
"level": OrganizationMembership.Level.MEMBER, | ||
"combine_pending_invites": True, | ||
}, | ||
) | ||
|
||
self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||
invite = response.json() | ||
|
||
# Check that the new invite uses its own level, not the expired invite's level | ||
self.assertEqual(invite["level"], OrganizationMembership.Level.MEMBER) | ||
self.assertEqual(invite["target_email"], email) | ||
self.assertEqual(invite["private_project_access"], []) | ||
|
||
def test_combine_pending_invites_false_expires_existing_invites(self): | ||
email = "[email protected]" | ||
|
||
# Create first invite | ||
first_invite = self.client.post( | ||
"/api/organizations/@current/invites/", | ||
{ | ||
"target_email": email, | ||
"level": OrganizationMembership.Level.ADMIN, | ||
}, | ||
).json() | ||
|
||
# Create second invite with combine_pending_invites=False | ||
response = self.client.post( | ||
"/api/organizations/@current/invites/", | ||
{ | ||
"target_email": email, | ||
"level": OrganizationMembership.Level.MEMBER, | ||
"combine_pending_invites": False, | ||
}, | ||
) | ||
|
||
self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||
new_invite = response.json() | ||
|
||
# Check that previous invite is deleted | ||
self.assertFalse(OrganizationInvite.objects.filter(id=first_invite["id"]).exists()) | ||
|
||
# Check that new invite uses its own level | ||
self.assertEqual(new_invite["level"], OrganizationMembership.Level.MEMBER) |