1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-06 13:38:13 -05:00

[PM-15621] WIP, still have failing tests.

This commit is contained in:
Jimmy Vo 2025-03-07 11:00:48 -05:00
parent b57545811e
commit c8e816b4b6
No known key found for this signature in database
GPG Key ID: 7CB834D6F4FFCA11
8 changed files with 355 additions and 133 deletions

View File

@ -2,6 +2,7 @@
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Utilities;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
@ -564,20 +565,22 @@ public class OrganizationUsersController : Controller
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("{id}/delete-account")] [HttpDelete("{id}/delete-account")]
[HttpPost("{id}/delete-account")] [HttpPost("{id}/delete-account")]
public async Task DeleteAccount(Guid orgId, Guid id) public async Task<IActionResult> DeleteAccount(Guid orgId, Guid id)
{ {
if (!await _currentContext.ManageUsers(orgId)) if (!await _currentContext.ManageUsers(orgId))
{ {
throw new NotFoundException(); return NotFound();
} }
var currentUser = await _userService.GetUserByPrincipalAsync(User); var currentUser = await _userService.GetUserByPrincipalAsync(User);
if (currentUser == null) if (currentUser == null)
{ {
throw new UnauthorizedAccessException(); return Unauthorized();
} }
await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); var deletionResult = await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
return deletionResult.result.MapToActionResult();
} }
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
@ -599,7 +602,7 @@ public class OrganizationUsersController : Controller
var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id); var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r => return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.result)));
} }
[HttpPatch("{id}/revoke")] [HttpPatch("{id}/revoke")]

View File

@ -3,6 +3,7 @@ using Bit.Api.Models.Response;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Commands;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -203,6 +204,17 @@ public class OrganizationUserBulkResponseModel : ResponseModel
Id = id; Id = id;
Error = error; Error = error;
} }
public OrganizationUserBulkResponseModel(Guid id, CommandResult result,
string obj = "OrganizationBulkConfirmResponseModel") : base(obj)
{
Id = id;
if (result is Failure)
{
Error = result.ErrorMessages.ToString();
}
}
public Guid Id { get; set; } public Guid Id { get; set; }
public string Error { get; set; } public string Error { get; set; }
} }

View File

@ -4,12 +4,14 @@ using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Commands;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services; using Bit.Core.Tools.Services;
using Bit.Core.Validators;
#nullable enable #nullable enable
@ -24,7 +26,6 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IReferenceEventService _referenceEventService; private readonly IReferenceEventService _referenceEventService;
private readonly IPushNotificationService _pushService; private readonly IPushNotificationService _pushService;
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
@ -35,7 +36,6 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IUserRepository userRepository, IUserRepository userRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IReferenceEventService referenceEventService, IReferenceEventService referenceEventService,
IPushNotificationService pushService, IPushNotificationService pushService,
IProviderUserRepository providerUserRepository IProviderUserRepository providerUserRepository
@ -47,77 +47,67 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_userRepository = userRepository; _userRepository = userRepository;
_currentContext = currentContext; _currentContext = currentContext;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_referenceEventService = referenceEventService; _referenceEventService = referenceEventService;
_pushService = pushService; _pushService = pushService;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
} }
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) public async Task<(Guid OrganizationUserId, CommandResult result)> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{ {
var result = await InternalDeleteManyUsersAsync(organizationId, new[] { organizationUserId }, deletingUserId); var result = await InternalDeleteManyUsersAsync(organizationId, new[] { organizationUserId }, deletingUserId);
var exception = result.Single().exception; return result.FirstOrDefault();
if (exception != null)
{
throw exception;
}
} }
public async Task<IEnumerable<(Guid OrganizationUserId, string? ErrorMessage)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId) public async Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId)
{ {
var userDeletionResults = await InternalDeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId); return await InternalDeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);
return userDeletionResults
.Select(result => (result.OrganizationUserId, result.exception?.Message))
.ToList();
} }
private async Task<IEnumerable<(Guid OrganizationUserId, OrganizationUser? orgUser, User? user, Exception? exception)>> InternalDeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId) private async Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> InternalDeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId)
{ {
var orgUsers = await _organizationUserRepository.GetManyAsync(orgUserIds); var orgUsers = await _organizationUserRepository.GetManyAsync(orgUserIds);
var users = await GetUsersAsync(orgUsers); var users = await GetUsersAsync(orgUsers);
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds); var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds);
var userDeletionResults = new List<(Guid OrganizationUserId, OrganizationUser? orgUser, User? user, Exception? exception)>();
var userDeletionRequests = new List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)>();
foreach (var orgUserId in orgUserIds) foreach (var orgUserId in orgUserIds)
{ {
OrganizationUser? orgUser = null; var orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId);
User? user = null; if (orgUser == null || orgUser.OrganizationId != organizationId)
try
{ {
orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId); userDeletionRequests.Add((orgUserId, new BadRequestFailure("Member not found."), null, null));
if (orgUser == null || orgUser.OrganizationId != organizationId) continue;
{
throw new NotFoundException("Member not found.");
}
user = users.FirstOrDefault(u => u.Id == orgUser.UserId);
if (user == null)
{
throw new NotFoundException("Member not found.");
}
await ValidateAsync(organizationId, orgUser, user, deletingUserId, managementStatus);
await CancelPremiumAsync(user);
userDeletionResults.Add((orgUserId, orgUser, user, null));
} }
catch (Exception exception)
var user = users.FirstOrDefault(u => u.Id == orgUser.UserId);
if (user == null)
{ {
userDeletionResults.Add((orgUserId, orgUser, user, exception)); userDeletionRequests.Add((orgUserId, new BadRequestFailure("Member not found."), orgUser, null));
continue;
} }
var result = await ValidateAsync(organizationId, orgUser, user, deletingUserId, managementStatus);
if (result is not Success)
{
userDeletionRequests.Add((orgUserId, result, orgUser, user));
continue;
}
await CancelPremiumAsync(user);
userDeletionRequests.Add((orgUserId, new Success(), orgUser, user));
} }
await HandleUserDeletionsAsync(userDeletionResults); await HandleUserDeletionsAsync(userDeletionRequests);
await LogDeletedOrganizationUsersAsync(userDeletionResults); await LogDeletedOrganizationUsersAsync(userDeletionRequests);
return userDeletionResults; return userDeletionRequests
.Select(request => (request.OrganizationUserId, request.result))
.ToList();
} }
private async Task<IEnumerable<User>> GetUsersAsync(ICollection<OrganizationUser> orgUsers) private async Task<IEnumerable<User>> GetUsersAsync(ICollection<OrganizationUser> orgUsers)
@ -130,84 +120,124 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
return await _userRepository.GetManyAsync(userIds); return await _userRepository.GetManyAsync(userIds);
} }
private async Task ValidateAsync(Guid organizationId, OrganizationUser orgUser, User user, Guid? deletingUserId, IDictionary<Guid, bool> managementStatus) private async Task<CommandResult> ValidateAsync(Guid organizationId, OrganizationUser orgUser, User user, Guid? deletingUserId, IDictionary<Guid, bool> managementStatus)
{ {
EnsureUserStatusIsNotInvited(orgUser); var result1 = EnsureUserStatusIsNotInvited(orgUser);
PreventSelfDeletion(orgUser, deletingUserId); if (result1 is not Success)
EnsureUserIsManagedByOrganization(orgUser, managementStatus); {
return result1;
}
await EnsureOnlyOwnersCanDeleteOwnersAsync(organizationId, orgUser, deletingUserId); var result2 = PreventSelfDeletion(orgUser, deletingUserId);
await EnsureUserIsNotSoleOrganizationOwnerAsync(user); if (result2 is not Success)
await EnsureUserIsNotSoleProviderOwnerAsync(user); {
return result2;
}
var validators = new[]
{
() => EnsureUserStatusIsNotInvited(orgUser),
() => PreventSelfDeletion(orgUser, deletingUserId),
() => EnsureUserIsManagedByOrganization(orgUser, managementStatus),
};
var result = CommandResultValidator.ExecuteValidators(validators);
if (result is not Success)
{
return result;
}
var asyncValidators = new[]
{
async () => await EnsureOnlyOwnersCanDeleteOwnersAsync(organizationId, orgUser, deletingUserId),
async () => await EnsureUserIsNotSoleOrganizationOwnerAsync(user),
async () => await EnsureUserIsNotSoleProviderOwnerAsync(user)
};
var asyncResult = await CommandResultValidator.ExecuteValidatorAsync(asyncValidators);
if (asyncResult is not Success)
{
return asyncResult;
}
return new Success();
} }
private static void EnsureUserStatusIsNotInvited(OrganizationUser orgUser) private static CommandResult EnsureUserStatusIsNotInvited(OrganizationUser orgUser)
{ {
if (!orgUser.UserId.HasValue || orgUser.Status == OrganizationUserStatusType.Invited) if (!orgUser.UserId.HasValue || orgUser.Status == OrganizationUserStatusType.Invited)
{ {
throw new BadRequestException("You cannot delete a member with Invited status."); return new BadRequestFailure("You cannot delete a member with Invited status.");
} }
return new Success();
} }
private static void PreventSelfDeletion(OrganizationUser orgUser, Guid? deletingUserId) private static CommandResult PreventSelfDeletion(OrganizationUser orgUser, Guid? deletingUserId)
{ {
if (!orgUser.UserId.HasValue || !deletingUserId.HasValue) if (!orgUser.UserId.HasValue || !deletingUserId.HasValue)
{ {
return; return new Success();
} }
if (orgUser.UserId.Value == deletingUserId.Value) if (orgUser.UserId.Value == deletingUserId.Value)
{ {
throw new BadRequestException("You cannot delete yourself."); return new BadRequestFailure("You cannot delete yourself.");
} }
return new Success();
} }
private async Task EnsureOnlyOwnersCanDeleteOwnersAsync(Guid organizationId, OrganizationUser orgUser, Guid? deletingUserId) private async Task<CommandResult> EnsureOnlyOwnersCanDeleteOwnersAsync(Guid organizationId, OrganizationUser orgUser, Guid? deletingUserId)
{ {
if (orgUser.Type != OrganizationUserType.Owner) if (orgUser.Type != OrganizationUserType.Owner)
{ {
return; return new Success();
} }
if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(organizationId)) if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(organizationId))
{ {
throw new BadRequestException("Only owners can delete other owners."); return new BadRequestFailure("Only owners can delete other owners.");
} }
return new Success();
} }
private static void EnsureUserIsManagedByOrganization(OrganizationUser orgUser, IDictionary<Guid, bool> managementStatus) private static CommandResult EnsureUserIsManagedByOrganization(OrganizationUser orgUser, IDictionary<Guid, bool> managementStatus)
{ {
if (!managementStatus.TryGetValue(orgUser.Id, out var isManaged) || !isManaged) if (!managementStatus.TryGetValue(orgUser.Id, out var isManaged) || !isManaged)
{ {
throw new BadRequestException("Member is not managed by the organization."); return new BadRequestFailure("Member is not managed by the organization.");
} }
return new Success();
} }
private async Task EnsureUserIsNotSoleOrganizationOwnerAsync(User user) private async Task<CommandResult> EnsureUserIsNotSoleOrganizationOwnerAsync(User user)
{ {
var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id); var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id);
if (onlyOwnerCount > 0) if (onlyOwnerCount > 0)
{ {
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user."); return new BadRequestFailure("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
} }
return new Success();
} }
private async Task EnsureUserIsNotSoleProviderOwnerAsync(User user) private async Task<CommandResult> EnsureUserIsNotSoleProviderOwnerAsync(User user)
{ {
var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id); var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id);
if (onlyOwnerProviderCount > 0) if (onlyOwnerProviderCount > 0)
{ {
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user."); return new BadRequestFailure("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.");
} }
return new Success();
} }
private async Task LogDeletedOrganizationUsersAsync( private async Task LogDeletedOrganizationUsersAsync(
List<(Guid OrganizationUserId, OrganizationUser? orgUser, User? user, Exception? exception)> results) List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)> userDeletionRequests)
{ {
var eventDate = DateTime.UtcNow; var eventDate = DateTime.UtcNow;
var events = results var events = userDeletionRequests
.Where(result => .Where(request =>
result.exception == null request.result is Success)
&& result.orgUser != null) .Select(request => (request.orgUser!, (EventType)EventType.OrganizationUser_Deleted, (DateTime?)eventDate))
.Select(result => (result.orgUser!, (EventType)EventType.OrganizationUser_Deleted, (DateTime?)eventDate))
.ToList(); .ToList();
if (events.Any()) if (events.Any())
@ -215,13 +245,13 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
await _eventService.LogOrganizationUserEventsAsync(events); await _eventService.LogOrganizationUserEventsAsync(events);
} }
} }
private async Task HandleUserDeletionsAsync(List<(Guid OrganizationUserId, OrganizationUser? orgUser, User? user, Exception? exception)> userDeletionResults)
private async Task HandleUserDeletionsAsync(List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)> userDeletionRequests)
{ {
var usersToDelete = userDeletionResults var usersToDelete = userDeletionRequests
.Where(result => .Where(request =>
result.exception == null request.result is Success)
&& result.user != null) .Select(request => request.user!);
.Select(i => i.user!);
if (usersToDelete.Any()) if (usersToDelete.Any())
{ {

View File

@ -1,5 +1,7 @@
#nullable enable #nullable enable
using Bit.Core.Models.Commands;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IDeleteManagedOrganizationUserAccountCommand public interface IDeleteManagedOrganizationUserAccountCommand
@ -7,7 +9,7 @@ public interface IDeleteManagedOrganizationUserAccountCommand
/// <summary> /// <summary>
/// Removes a user from an organization and deletes all of their associated user data. /// Removes a user from an organization and deletes all of their associated user data.
/// </summary> /// </summary>
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); Task<(Guid OrganizationUserId, CommandResult result)> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
/// <summary> /// <summary>
/// Removes multiple users from an organization and deletes all of their associated user data. /// Removes multiple users from an organization and deletes all of their associated user data.
@ -15,5 +17,5 @@ public interface IDeleteManagedOrganizationUserAccountCommand
/// <returns> /// <returns>
/// An error message for each user that could not be removed, otherwise null. /// An error message for each user that could not be removed, otherwise null.
/// </returns> /// </returns>
Task<IEnumerable<(Guid OrganizationUserId, string? ErrorMessage)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId); Task<IEnumerable<(Guid OrganizationUserId, CommandResult result)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId);
} }

View File

@ -0,0 +1,36 @@
using Bit.Core.Models.Commands;
namespace Bit.Core.Validators;
public static class CommandResultValidator
{
public static CommandResult ExecuteValidators(Func<CommandResult>[] validators)
{
foreach (var validator in validators)
{
var result = validator();
if (result is not Success)
{
return result;
}
}
return new Success();
}
public static async Task<CommandResult> ExecuteValidatorAsync(Func<Task<CommandResult>>[] validators)
{
foreach (var validator in validators)
{
var result = await validator();
if (result is not Success)
{
return result;
}
}
return new Success();
}
}

View File

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

View File

@ -4,7 +4,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Models.Commands;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
@ -53,7 +53,7 @@ public class DeleteManagedOrganizationUserAccountCommandTests
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task DeleteUserAsync_WhenError_ShouldThrowException( public async Task DeleteUserAsync_WhenError_ShouldReturnFailure(
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user, Guid organizationId, SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user, Guid organizationId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser) [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
{ {
@ -66,7 +66,8 @@ public class DeleteManagedOrganizationUserAccountCommandTests
.Returns(new List<OrganizationUser> { }); .Returns(new List<OrganizationUser> { });
// Act // Act
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteUserAsync(organizationId, orgUser.Id, null)); // Test is not ready
await sutProvider.Sut.DeleteUserAsync(organizationId, orgUser.Id, null);
// Assert // Assert
} }
@ -101,7 +102,7 @@ public class DeleteManagedOrganizationUserAccountCommandTests
// Assert // Assert
Assert.Equal(2, results.Count()); Assert.Equal(2, results.Count());
Assert.All(results, r => Assert.Null(r.Item2)); Assert.All(results, r => Assert.IsType<Success>(r.Item2));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyAsync(userIds); await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyAsync(userIds);
await sutProvider.GetDependency<IUserRepository>().Received(1).DeleteManyAsync(Arg.Is<IEnumerable<User>>(users => users.Any(u => u.Id == user1.Id) && users.Any(u => u.Id == user2.Id))); await sutProvider.GetDependency<IUserRepository>().Received(1).DeleteManyAsync(Arg.Is<IEnumerable<User>>(users => users.Any(u => u.Id == user1.Id) && users.Any(u => u.Id == user2.Id)));
@ -125,10 +126,11 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Single(result); Assert.Single(result);
var userId = result.First().Item1; var userId = result.First().Item1;
var errorMessage = result.First().Item2;
Assert.Equal(orgUserId, userId); Assert.Equal(orgUserId, userId);
Assert.Contains("Member not found.", errorMessage);
var commandResult = result.First().Item2;
AssertErrorMessages("Member not found.", commandResult);
await sutProvider.GetDependency<IUserRepository>() await sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.DeleteManyAsync(default); .DeleteManyAsync(default);
@ -160,10 +162,11 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Single(result); Assert.Single(result);
var userId = result.First().Item1; var userId = result.First().Item1;
var errorMessage = result.First().Item2;
Assert.Equal(orgUser.Id, userId); Assert.Equal(orgUser.Id, userId);
Assert.Contains("You cannot delete yourself.", errorMessage);
var commandResult = result.First().Item2;
AssertErrorMessages("You cannot delete yourself.", commandResult);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>()); await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0) await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>()); .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
@ -193,9 +196,8 @@ public class DeleteManagedOrganizationUserAccountCommandTests
// Assert // Assert
Assert.Single(result); Assert.Single(result);
var userId = result.First().Item1; var userId = result.First().Item1;
var errorMessage = result.First().Item2;
Assert.Equal(orgUser.Id, userId); Assert.Equal(orgUser.Id, userId);
Assert.Contains("You cannot delete a member with Invited status.", errorMessage); AssertErrorMessages("You cannot delete a member with Invited status.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>()); await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0) await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>()); .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
@ -235,9 +237,10 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Single(result); Assert.Single(result);
var userId = result.First().Item1; var userId = result.First().Item1;
var errorMessage = result.First().Item2;
Assert.Equal(orgUser.Id, userId); Assert.Equal(orgUser.Id, userId);
Assert.Contains("Only owners can delete other owners.", errorMessage);
var commandResult = result.First().Item2;
AssertErrorMessages("Only owners can delete other owners.", commandResult);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>()); await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0) await sutProvider.GetDependency<IEventService>().Received(0)
@ -272,10 +275,11 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Single(result); Assert.Single(result);
var userId = result.First().Item1; var userId = result.First().Item1;
var errorMessage = result.First().Item2;
Assert.Equal(orgUser.Id, userId); Assert.Equal(orgUser.Id, userId);
Assert.Contains("Member is not managed by the organization.", errorMessage);
var commandResult = result.First().Item2;
AssertErrorMessages("Member is not managed by the organization.", commandResult);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>()); await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0) await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>()); .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
@ -316,10 +320,11 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Single(result); Assert.Single(result);
var userId = result.First().Item1; var userId = result.First().Item1;
var errorMessage = result.First().Item2;
Assert.Equal(orgUser.Id, userId); Assert.Equal(orgUser.Id, userId);
Assert.Contains("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.", errorMessage);
var commandResult = result.First().Item2;
AssertErrorMessages("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.", commandResult);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>()); await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0) await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>()); .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
@ -365,10 +370,11 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Single(result); Assert.Single(result);
var userId = result.First().Item1; var userId = result.First().Item1;
var errorMessage = result.First().Item2;
Assert.Equal(orgUser.Id, userId); Assert.Equal(orgUser.Id, userId);
Assert.Contains("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.", errorMessage);
var commandResult = result.First().Item2;
AssertErrorMessages("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.", commandResult);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>()); await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0) await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>()); .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
@ -410,15 +416,18 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Equal(3, results.Count()); Assert.Equal(3, results.Count());
var orgUser1ErrorMessage = results.First(r => r.Item1 == orgUser1.Id).Item2; var orgUser1ErrorMessage = results.First(r => r.Item1 == orgUser1.Id).Item2;
var orgUser2ErrorMessage = results.First(r => r.Item1 == orgUser2.Id).Item2;
var orgUser3ErrorMessage = results.First(r => r.Item1 == orgUser3.Id).Item2;
Assert.Null(orgUser1ErrorMessage); Assert.Null(orgUser1ErrorMessage);
Assert.Equal("Member not found.", orgUser2ErrorMessage);
Assert.Equal("Member is not managed by the organization.", orgUser3ErrorMessage); var orgUser2CommandResult = results.First(r => r.Item1 == orgUser2.Id).Item2;
AssertErrorMessages("Member not found.", orgUser2CommandResult);
var orgUser3CommandResult = results.First(r => r.Item1 == orgUser3.Id).Item2;
AssertErrorMessages("Member is not managed by the organization.", orgUser3CommandResult);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync( await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events => Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1)); events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));
} }
private static void AssertErrorMessages(string expectedErrorMessage, CommandResult commandResult) => Assert.Contains([expectedErrorMessage], ((Failure)commandResult).ErrorMessages.ToArray());
} }

View File

@ -0,0 +1,130 @@
using Bit.Core.Models.Commands;
using Bit.Core.Validators;
using Xunit;
namespace Bit.Core.Test.Validators;
public class CommandResultValidatorTests
{
[Fact]
public void ExecuteValidators_AllSuccess_ReturnsSuccess()
{
// Arrange
var validators = new Func<CommandResult>[]
{
() => new Success(),
() => new Success(),
() => new Success()
};
// Act
var result = CommandResultValidator.ExecuteValidators(validators);
// Assert
Assert.IsType<Success>(result);
}
public static IEnumerable<object[]> TestCases()
{
yield return new object[]
{
new Func<CommandResult>[]
{
() => new Failure("First failure"),
() => new Success(),
() => new Failure("Second failure"),
}
};
yield return new object[]
{
new Func<CommandResult>[]
{
() => new Success(),
() => new Failure("First failure"),
() => new Failure("Second failure"),
}
};
yield return new object[]
{
new Func<CommandResult>[]
{
() => new Success(),
() => new Success(),
() => new Failure("First failure"),
}
};
}
[Theory]
[MemberData(nameof(TestCases))]
public void ExecuteValidators_WhenValidatorFails_ReturnsFirstFailure(Func<CommandResult>[] validators)
{
// Act
var result = CommandResultValidator.ExecuteValidators(validators);
// Assert
Assert.IsType<Failure>(result);
Assert.Equal(["First failure"], ((Failure)result).ErrorMessages);
}
[Fact]
public async Task ExecuteValidatorAsync_AllSuccess_ReturnsSuccess()
{
// Arrange
var validators = new Func<Task<CommandResult>>[]
{
async () => await Task.FromResult(new Success()),
async () => await Task.FromResult(new Success()),
async () => await Task.FromResult(new Success())
};
// Act
var result = await CommandResultValidator.ExecuteValidatorAsync(validators);
// Assert
Assert.IsType<Success>(result);
}
public static IEnumerable<object[]> AsyncTestCases()
{
yield return new object[]
{
new Func<Task<CommandResult>>[]
{
async () => await Task.FromResult(new Failure("First failure")),
async () => await Task.FromResult(new Success()),
async () => await Task.FromResult(new Failure("Second failure")),
}
};
yield return new object[]
{
new Func<Task<CommandResult>>[]
{
async () => await Task.FromResult(new Success()),
async () => await Task.FromResult(new Failure("First failure")),
async () => await Task.FromResult(new Failure("Second failure")),
}
};
yield return new object[]
{
new Func<Task<CommandResult>>[]
{
async () => await Task.FromResult(new Success()),
async () => await Task.FromResult(new Success()),
async () => await Task.FromResult(new Failure("First failure")),
}
};
}
[Theory]
[MemberData(nameof(AsyncTestCases))]
public async Task ExecuteValidatorAsync_WhenValidatorFails_ReturnsFirstFailure(Func<Task<CommandResult>>[] validators)
{
// Act
var result = await CommandResultValidator.ExecuteValidatorAsync(validators);
// Assert
Assert.IsType<Failure>(result);
Assert.Equal(["First failure"], ((Failure)result).ErrorMessages);
}
}