1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-15621] Refactor delete claimed user command (#6221)

- create vNext command
- restructure command to simplify logic
- move validation to a separate class
- implement result types using OneOf library and demo
  their use here
This commit is contained in:
Thomas Rittson
2025-09-11 13:58:32 +10:00
committed by GitHub
parent bd1745a50d
commit 2c860df34b
16 changed files with 1502 additions and 25 deletions

View File

@@ -1,10 +1,13 @@
using System.Net;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
@@ -30,6 +33,10 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
featureService
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
featureService
.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor)
.Returns(true);
});
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
@@ -42,6 +49,91 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
private Organization _organization = null!;
private string _ownerEmail = null!;
[Fact]
public async Task BulkDeleteAccount_Success()
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
await _loginHelper.LoginAsync(userEmail);
var (_, orgUserToDelete) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
var userRepository = _factory.GetService<IUserRepository>();
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
Assert.NotNull(orgUserToDelete.UserId);
Assert.NotNull(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));
Assert.NotNull(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));
var request = new OrganizationUserBulkRequestModel
{
Ids = [orgUserToDelete.Id]
};
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request);
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Single(content.Data, r => r.Id == orgUserToDelete.Id && r.Error == string.Empty);
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.Null(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));
Assert.Null(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));
}
[Fact]
public async Task BulkDeleteAccount_MixedResults()
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Admin);
await _loginHelper.LoginAsync(userEmail);
// Can delete users
var (_, validOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
// Cannot delete owners
var (_, invalidOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Owner);
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
var userRepository = _factory.GetService<IUserRepository>();
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
Assert.NotNull(validOrgUser.UserId);
Assert.NotNull(invalidOrgUser.UserId);
var arrangedUsers =
await userRepository.GetManyAsync([validOrgUser.UserId.Value, invalidOrgUser.UserId.Value]);
Assert.Equal(2, arrangedUsers.Count());
var arrangedOrgUsers =
await organizationUserRepository.GetManyAsync([validOrgUser.Id, invalidOrgUser.Id]);
Assert.Equal(2, arrangedOrgUsers.Count);
var request = new OrganizationUserBulkRequestModel
{
Ids = [validOrgUser.Id, invalidOrgUser.Id]
};
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request);
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
var debug = await httpResponse.Content.ReadAsStringAsync();
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Equal(2, content.Data.Count());
Assert.Contains(content.Data, r => r.Id == validOrgUser.Id && r.Error == string.Empty);
Assert.Contains(content.Data, r =>
r.Id == invalidOrgUser.Id &&
string.Equals(r.Error, new CannotDeleteOwnersError().Message, StringComparison.Ordinal));
var actualUsers =
await userRepository.GetManyAsync([validOrgUser.UserId.Value, invalidOrgUser.UserId.Value]);
Assert.Single(actualUsers, u => u.Id == invalidOrgUser.UserId.Value);
var actualOrgUsers =
await organizationUserRepository.GetManyAsync([validOrgUser.Id, invalidOrgUser.Id]);
Assert.Single(actualOrgUsers, ou => ou.Id == invalidOrgUser.Id);
}
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
@@ -57,11 +149,36 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
Ids = new List<Guid> { Guid.NewGuid() }
};
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/remove", request);
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request);
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
[Fact]
public async Task DeleteAccount_Success()
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
await _loginHelper.LoginAsync(userEmail);
var (_, orgUserToDelete) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
var userRepository = _factory.GetService<IUserRepository>();
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
Assert.NotNull(orgUserToDelete.UserId);
Assert.NotNull(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));
Assert.NotNull(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));
var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{orgUserToDelete.Id}/delete-account");
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.Null(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));
Assert.Null(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));
}
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
@@ -74,7 +191,7 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
var userToRemove = Guid.NewGuid();
var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{userToRemove}");
var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{userToRemove}/delete-account");
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}

View File

@@ -330,27 +330,6 @@ public class OrganizationUsersControllerTests
sutProvider.Sut.DeleteAccount(orgId, id));
}
[Theory]
[BitAutoData]
public async Task BulkDeleteAccount_WhenUserCanManageUsers_Success(
Guid orgId, OrganizationUserBulkRequestModel model, User currentUser,
List<(Guid, string)> deleteResults, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountCommand>()
.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id)
.Returns(deleteResults);
var response = await sutProvider.Sut.BulkDeleteAccount(orgId, model);
Assert.Equal(deleteResults.Count, response.Data.Count());
Assert.True(response.Data.All(r => deleteResults.Any(res => res.Item1 == r.Id && res.Item2 == r.Error)));
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountCommand>()
.Received(1)
.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
}
[Theory]
[BitAutoData]
public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(

View File

@@ -0,0 +1,467 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountCommandvNextTests
{
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
organizationUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResult = CreateSuccessfulValidationResult(request);
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { organizationUser },
[user],
organizationId,
new Dictionary<Guid, bool> { { organizationUser.Id, true } });
SetupValidatorMock(sutProvider, [validationResult]);
var result = await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUser.Id, deletingUserId);
Assert.Equal(organizationUser.Id, result.Id);
Assert.True(result.Result.IsSuccess);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)));
await AssertSuccessfulUserOperations(sutProvider, [user], [organizationUser]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithEmptyUserIds_ReturnsEmptyResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
Guid organizationId,
Guid deletingUserId)
{
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [], deletingUserId);
Assert.Empty(results);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser] OrganizationUser orgUser2)
{
// Arrange
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
orgUser1.UserId = user1.Id;
orgUser2.UserId = user2.Id;
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser1.Id,
OrganizationUser = orgUser1,
User = user1,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser2.Id,
OrganizationUser = orgUser2,
User = user2,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResults = new[]
{
CreateSuccessfulValidationResult(request1),
CreateSuccessfulValidationResult(request2)
};
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { orgUser1, orgUser2 },
[user1, user2],
organizationId,
new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser1.Id, orgUser2.Id], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.All(resultsList, result => Assert.True(result.Result.IsSuccess));
await AssertSuccessfulUserOperations(sutProvider, [user1, user2], [orgUser1, orgUser2]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidationErrors_ReturnsErrorResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
Guid organizationId,
Guid orgUserId1,
Guid orgUserId2,
Guid deletingUserId)
{
// Arrange
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUserId1,
DeletingUserId = deletingUserId
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUserId2,
DeletingUserId = deletingUserId
};
var validationResults = new[]
{
CreateFailedValidationResult(request1, new UserNotClaimedError()),
CreateFailedValidationResult(request2, new InvalidUserStatusError())
};
SetupRepositoryMocks(sutProvider, [], [], organizationId, new Dictionary<Guid, bool>());
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserId1, orgUserId2], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.Equal(orgUserId1, resultsList[0].Id);
Assert.True(resultsList[0].Result.IsError);
Assert.IsType<UserNotClaimedError>(resultsList[0].Result.AsError);
Assert.Equal(orgUserId2, resultsList[1].Id);
Assert.True(resultsList[1].Result.IsError);
Assert.IsType<InvalidUserStatusError>(resultsList[1].Result.AsError);
await AssertNoUserOperations(sutProvider);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithMixedValidationResults_HandlesPartialSuccessCorrectly(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User validUser,
Guid organizationId,
Guid validOrgUserId,
Guid invalidOrgUserId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser validOrgUser)
{
validOrgUser.Id = validOrgUserId;
validOrgUser.UserId = validUser.Id;
validOrgUser.OrganizationId = organizationId;
var validRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = validOrgUserId,
OrganizationUser = validOrgUser,
User = validUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var invalidRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = invalidOrgUserId,
DeletingUserId = deletingUserId
};
var validationResults = new[]
{
CreateSuccessfulValidationResult(validRequest),
CreateFailedValidationResult(invalidRequest, new UserNotFoundError())
};
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { validOrgUser },
[validUser],
organizationId,
new Dictionary<Guid, bool> { { validOrgUserId, true } });
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [validOrgUserId, invalidOrgUserId], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
var validResult = resultsList.First(r => r.Id == validOrgUserId);
var invalidResult = resultsList.First(r => r.Id == invalidOrgUserId);
Assert.True(validResult.Result.IsSuccess);
Assert.True(invalidResult.Result.IsError);
Assert.IsType<UserNotFoundError>(invalidResult.Result.AsError);
await AssertSuccessfulUserOperations(sutProvider, [validUser], [validOrgUser]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_CancelPremiumsAsync_HandlesGatewayExceptionAndLogsWarning(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser)
{
orgUser.UserId = user.Id;
orgUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser.Id,
OrganizationUser = orgUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResult = CreateSuccessfulValidationResult(request);
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { orgUser },
[user],
organizationId,
new Dictionary<Guid, bool> { { orgUser.Id, true } });
SetupValidatorMock(sutProvider, [validationResult]);
var gatewayException = new GatewayException("Payment gateway error");
sutProvider.GetDependency<IUserService>()
.CancelPremiumAsync(user)
.ThrowsAsync(gatewayException);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser.Id], deletingUserId);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList.First().Result.IsSuccess);
await sutProvider.GetDependency<IUserService>().Received(1).CancelPremiumAsync(user);
await AssertSuccessfulUserOperations(sutProvider, [user], [orgUser]);
sutProvider.GetDependency<ILogger<DeleteClaimedOrganizationUserAccountCommandvNext>>()
.Received(1)
.Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString()!.Contains($"Failed to cancel premium subscription for {user.Id}")),
gatewayException,
Arg.Any<Func<object, Exception?, string>>());
}
[Theory]
[BitAutoData]
public async Task CreateInternalRequests_CreatesCorrectRequestsForAllUsers(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser] OrganizationUser orgUser2)
{
orgUser1.UserId = user1.Id;
orgUser2.UserId = user2.Id;
var orgUserIds = new[] { orgUser1.Id, orgUser2.Id };
var orgUsers = new List<OrganizationUser> { orgUser1, orgUser2 };
var users = new[] { user1, user2 };
var claimedStatuses = new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, false } };
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(orgUsers);
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id)))
.Returns(users);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(claimedStatuses);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(callInfo =>
{
var requests = callInfo.Arg<IEnumerable<DeleteUserValidationRequest>>();
return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError()));
});
// Act
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);
// Assert
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.Received(1)
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
requests.Count() == 2 &&
requests.Any(r => r.OrganizationUserId == orgUser1.Id &&
r.OrganizationId == organizationId &&
r.OrganizationUser == orgUser1 &&
r.User == user1 &&
r.DeletingUserId == deletingUserId &&
r.IsClaimed == true) &&
requests.Any(r => r.OrganizationUserId == orgUser2.Id &&
r.OrganizationId == organizationId &&
r.OrganizationUser == orgUser2 &&
r.User == user2 &&
r.DeletingUserId == deletingUserId &&
r.IsClaimed == false)));
}
[Theory]
[BitAutoData]
public async Task GetUsersAsync_WithNullUserIds_ReturnsEmptyCollection(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUserWithoutUserId)
{
orgUserWithoutUserId.UserId = null; // Intentionally setting to null for test case
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUserWithoutUserId });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()))
.Returns([]);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(callInfo =>
{
var requests = callInfo.Arg<IEnumerable<DeleteUserValidationRequest>>();
return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError()));
});
// Act
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserWithoutUserId.Id], deletingUserId);
// Assert
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.Received(1)
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
requests.Count() == 1 &&
requests.Single().User == null));
await sutProvider.GetDependency<IUserRepository>().Received(1)
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()));
}
private static ValidationResult<DeleteUserValidationRequest> CreateSuccessfulValidationResult(
DeleteUserValidationRequest request) =>
ValidationResultHelpers.Valid(request);
private static ValidationResult<DeleteUserValidationRequest> CreateFailedValidationResult(
DeleteUserValidationRequest request,
Error error) =>
ValidationResultHelpers.Invalid(request, error);
private static void SetupRepositoryMocks(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
ICollection<OrganizationUser> orgUsers,
IEnumerable<User> users,
Guid organizationId,
Dictionary<Guid, bool> claimedStatuses)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(orgUsers);
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(users);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(claimedStatuses);
}
private static void SetupValidatorMock(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
IEnumerable<ValidationResult<DeleteUserValidationRequest>> validationResults)
{
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(validationResults);
}
private static async Task AssertSuccessfulUserOperations(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
IEnumerable<User> expectedUsers,
IEnumerable<OrganizationUser> expectedOrgUsers)
{
var userList = expectedUsers.ToList();
var orgUserList = expectedOrgUsers.ToList();
await sutProvider.GetDependency<IUserRepository>().Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<User>>(users =>
userList.All(expectedUser => users.Any(u => u.Id == expectedUser.Id))));
foreach (var user in userList)
{
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(user.Id);
}
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
orgUserList.All(expectedOrgUser =>
events.Any(e => e.Item1.Id == expectedOrgUser.Id && e.Item2 == EventType.OrganizationUser_Deleted))));
}
private static async Task AssertNoUserOperations(SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider)
{
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().DeleteManyAsync(default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushLogOutAsync(default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventsAsync(default(IEnumerable<(OrganizationUser, EventType, DateTime?)>));
}
}

View File

@@ -0,0 +1,503 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
organizationUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
Assert.Equal(request, resultsList[0].Request);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithMultipleValidRequests_ReturnsAllValidResults(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2)
{
orgUser1.UserId = user1.Id;
orgUser1.OrganizationId = organizationId;
orgUser2.UserId = user2.Id;
orgUser2.OrganizationId = organizationId;
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser1.Id,
OrganizationUser = orgUser1,
User = user1,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser2.Id,
OrganizationUser = orgUser2,
User = user2,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user1.Id);
SetupMocks(sutProvider, organizationId, user2.Id);
var results = await sutProvider.Sut.ValidateAsync([request1, request2]);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.All(resultsList, result => Assert.True(result.IsValid));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithNullUser_ReturnsUserNotFoundError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = null,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotFoundError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId)
{
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = Guid.NewGuid(),
OrganizationUser = null,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotFoundError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithInvitedUser_ReturnsInvalidUserStatusError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<InvalidUserStatusError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WhenDeletingYourself_ReturnsCannotDeleteYourselfError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = user.Id,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteYourselfError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithUnclaimedUser_ReturnsUserNotClaimedError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = false
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotClaimedError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsNotOwner_ReturnsCannotDeleteOwnersError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteOwnersError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsOwner_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithSoleOwnerOfOrganization_ReturnsSoleOwnerError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(1);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<SoleOwnerError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithSoleProviderOwner_ReturnsSoleProviderError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
sutProvider.GetDependency<IProviderUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(1);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<SoleProviderError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_CustomUserDeletingAdmin_ReturnsCannotDeleteAdminsError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Custom);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteAdminsError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_AdminDeletingAdmin_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithMixedValidAndInvalidRequests_ReturnsCorrespondingResults(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User validUser,
User invalidUser,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser validOrgUser,
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser invalidOrgUser)
{
validOrgUser.UserId = validUser.Id;
invalidOrgUser.UserId = invalidUser.Id;
var validRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = validOrgUser.Id,
OrganizationUser = validOrgUser,
User = validUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var invalidRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = invalidOrgUser.Id,
OrganizationUser = invalidOrgUser,
User = invalidUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, validUser.Id);
var results = await sutProvider.Sut.ValidateAsync([validRequest, invalidRequest]);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
var validResult = resultsList.First(r => r.Request == validRequest);
var invalidResult = resultsList.First(r => r.Request == invalidRequest);
Assert.True(validResult.IsValid);
Assert.True(invalidResult.IsError);
Assert.IsType<InvalidUserStatusError>(invalidResult.AsError);
}
private static void SetupMocks(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
Guid organizationId,
Guid userId,
OrganizationUserType currentUserType = OrganizationUserType.Owner)
{
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(currentUserType == OrganizationUserType.Owner);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationAdmin(organizationId)
.Returns(currentUserType is OrganizationUserType.Owner or OrganizationUserType.Admin);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationCustom(organizationId)
.Returns(currentUserType is OrganizationUserType.Custom);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(userId)
.Returns(0);
sutProvider.GetDependency<IProviderUserRepository>()
.GetCountByOnlyOwnerAsync(userId)
.Returns(0);
}
}