[PM-23768] Public API - add restore and revoke member endpoint (#6859)

* Add restore and revoke to public api

* Follow naming conventions

* Use POST instead of PUT

* hello claude

* Update test names

* Actually fix test names

* Add JsonConstructor attr

* Fix test
This commit is contained in:
Thomas Rittson
2026-01-28 04:11:15 +10:00
committed by GitHub
parent 898904a673
commit 80eec2df85
3 changed files with 206 additions and 8 deletions

View File

@@ -264,4 +264,138 @@ public class MembersControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
new Permissions { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true },
orgUser.GetPermissions());
}
[Fact]
public async Task Revoke_Member_Success()
{
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
_factory, _organization.Id, OrganizationUserType.User);
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/revoke", null);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var updatedUser = await _factory.GetService<IOrganizationUserRepository>()
.GetByIdAsync(orgUser.Id);
Assert.NotNull(updatedUser);
Assert.Equal(OrganizationUserStatusType.Revoked, updatedUser.Status);
}
[Fact]
public async Task Revoke_AlreadyRevoked_ReturnsBadRequest()
{
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
_factory, _organization.Id, OrganizationUserType.User);
var revokeResponse = await _client.PostAsync($"/public/members/{orgUser.Id}/revoke", null);
Assert.Equal(HttpStatusCode.OK, revokeResponse.StatusCode);
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/revoke", null);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var error = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();
Assert.Equal("Already revoked.", error?.Message);
}
[Fact]
public async Task Revoke_NotFound_ReturnsNotFound()
{
var response = await _client.PostAsync($"/public/members/{Guid.NewGuid()}/revoke", null);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Revoke_DifferentOrganization_ReturnsNotFound()
{
// Create a different organization
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(ownerEmail);
var (otherOrganization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Create a user in the other organization
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
_factory, otherOrganization.Id, OrganizationUserType.User);
// Re-authenticate with the original organization
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
// Try to revoke the user from the other organization
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/revoke", null);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Restore_Member_Success()
{
// Invite a user to revoke
var email = $"integration-test{Guid.NewGuid()}@example.com";
var inviteRequest = new MemberCreateRequestModel
{
Email = email,
Type = OrganizationUserType.User,
};
var inviteResponse = await _client.PostAsync("/public/members", JsonContent.Create(inviteRequest));
Assert.Equal(HttpStatusCode.OK, inviteResponse.StatusCode);
var invitedMember = await inviteResponse.Content.ReadFromJsonAsync<MemberResponseModel>();
Assert.NotNull(invitedMember);
// Revoke the invited user
var revokeResponse = await _client.PostAsync($"/public/members/{invitedMember.Id}/revoke", null);
Assert.Equal(HttpStatusCode.OK, revokeResponse.StatusCode);
// Restore the user
var response = await _client.PostAsync($"/public/members/{invitedMember.Id}/restore", null);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Verify user is restored to Invited state
var updatedUser = await _factory.GetService<IOrganizationUserRepository>()
.GetByIdAsync(invitedMember.Id);
Assert.NotNull(updatedUser);
Assert.Equal(OrganizationUserStatusType.Invited, updatedUser.Status);
}
[Fact]
public async Task Restore_AlreadyActive_ReturnsBadRequest()
{
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
_factory, _organization.Id, OrganizationUserType.User);
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/restore", null);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var error = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();
Assert.Equal("Already active.", error?.Message);
}
[Fact]
public async Task Restore_NotFound_ReturnsNotFound()
{
var response = await _client.PostAsync($"/public/members/{Guid.NewGuid()}/restore", null);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Restore_DifferentOrganization_ReturnsNotFound()
{
// Create a different organization
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(ownerEmail);
var (otherOrganization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Create a user in the other organization
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
_factory, otherOrganization.Id, OrganizationUserType.User);
// Re-authenticate with the original organization
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
// Try to restore the user from the other organization
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/restore", null);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}