mirror of
https://github.com/bitwarden/server
synced 2025-12-11 05:43:35 +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:
@@ -11,6 +11,7 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
@@ -23,6 +24,7 @@ using Bit.Core.Billing.Pricing;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
@@ -59,6 +61,7 @@ public class OrganizationUsersController : Controller
|
|||||||
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
private readonly IDeleteClaimedOrganizationUserAccountCommand _deleteClaimedOrganizationUserAccountCommand;
|
private readonly IDeleteClaimedOrganizationUserAccountCommand _deleteClaimedOrganizationUserAccountCommand;
|
||||||
|
private readonly IDeleteClaimedOrganizationUserAccountCommandvNext _deleteClaimedOrganizationUserAccountCommandvNext;
|
||||||
private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;
|
private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;
|
||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
@@ -87,6 +90,7 @@ public class OrganizationUsersController : Controller
|
|||||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
IDeleteClaimedOrganizationUserAccountCommand deleteClaimedOrganizationUserAccountCommand,
|
IDeleteClaimedOrganizationUserAccountCommand deleteClaimedOrganizationUserAccountCommand,
|
||||||
|
IDeleteClaimedOrganizationUserAccountCommandvNext deleteClaimedOrganizationUserAccountCommandvNext,
|
||||||
IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery,
|
IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
@@ -115,6 +119,7 @@ public class OrganizationUsersController : Controller
|
|||||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
_deleteClaimedOrganizationUserAccountCommand = deleteClaimedOrganizationUserAccountCommand;
|
_deleteClaimedOrganizationUserAccountCommand = deleteClaimedOrganizationUserAccountCommand;
|
||||||
|
_deleteClaimedOrganizationUserAccountCommandvNext = deleteClaimedOrganizationUserAccountCommandvNext;
|
||||||
_getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;
|
_getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;
|
||||||
_policyRequirementQuery = policyRequirementQuery;
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
@@ -536,6 +541,12 @@ public class OrganizationUsersController : Controller
|
|||||||
[Authorize<ManageUsersRequirement>]
|
[Authorize<ManageUsersRequirement>]
|
||||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||||
{
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor))
|
||||||
|
{
|
||||||
|
await DeleteAccountvNext(orgId, id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var currentUser = await _userService.GetUserByPrincipalAsync(User);
|
var currentUser = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (currentUser == null)
|
if (currentUser == null)
|
||||||
{
|
{
|
||||||
@@ -553,10 +564,33 @@ public class OrganizationUsersController : Controller
|
|||||||
await DeleteAccount(orgId, id);
|
await DeleteAccount(orgId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IResult> DeleteAccountvNext(Guid orgId, Guid id)
|
||||||
|
{
|
||||||
|
var currentUserId = _userService.GetProperUserId(User);
|
||||||
|
if (currentUserId == null)
|
||||||
|
{
|
||||||
|
return TypedResults.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandResult = await _deleteClaimedOrganizationUserAccountCommandvNext.DeleteUserAsync(orgId, id, currentUserId.Value);
|
||||||
|
|
||||||
|
return commandResult.Result.Match<IResult>(
|
||||||
|
error => error is NotFoundError
|
||||||
|
? TypedResults.NotFound(new ErrorResponseModel(error.Message))
|
||||||
|
: TypedResults.BadRequest(new ErrorResponseModel(error.Message)),
|
||||||
|
TypedResults.Ok
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpDelete("delete-account")]
|
[HttpDelete("delete-account")]
|
||||||
[Authorize<ManageUsersRequirement>]
|
[Authorize<ManageUsersRequirement>]
|
||||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
{
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor))
|
||||||
|
{
|
||||||
|
return await BulkDeleteAccountvNext(orgId, model);
|
||||||
|
}
|
||||||
|
|
||||||
var currentUser = await _userService.GetUserByPrincipalAsync(User);
|
var currentUser = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (currentUser == null)
|
if (currentUser == null)
|
||||||
{
|
{
|
||||||
@@ -577,6 +611,24 @@ public class OrganizationUsersController : Controller
|
|||||||
return await BulkDeleteAccount(orgId, model);
|
return await BulkDeleteAccount(orgId, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccountvNext(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
|
{
|
||||||
|
var currentUserId = _userService.GetProperUserId(User);
|
||||||
|
if (currentUserId == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _deleteClaimedOrganizationUserAccountCommandvNext.DeleteManyUsersAsync(orgId, model.Ids, currentUserId.Value);
|
||||||
|
|
||||||
|
var responses = result.Select(r => r.Result.Match(
|
||||||
|
error => new OrganizationUserBulkResponseModel(r.Id, error.Message),
|
||||||
|
_ => new OrganizationUserBulkResponseModel(r.Id, string.Empty)
|
||||||
|
));
|
||||||
|
|
||||||
|
return new ListResponseModel<OrganizationUserBulkResponseModel>(responses);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("{id}/revoke")]
|
[HttpPut("{id}/revoke")]
|
||||||
[Authorize<ManageUsersRequirement>]
|
[Authorize<ManageUsersRequirement>]
|
||||||
public async Task RevokeAsync(Guid orgId, Guid id)
|
public async Task RevokeAsync(Guid orgId, Guid id)
|
||||||
|
|||||||
@@ -236,8 +236,8 @@ public class OrganizationUserPublicKeyResponseModel : ResponseModel
|
|||||||
|
|
||||||
public class OrganizationUserBulkResponseModel : ResponseModel
|
public class OrganizationUserBulkResponseModel : ResponseModel
|
||||||
{
|
{
|
||||||
public OrganizationUserBulkResponseModel(Guid id, string error,
|
public OrganizationUserBulkResponseModel(Guid id, string error)
|
||||||
string obj = "OrganizationBulkConfirmResponseModel") : base(obj)
|
: base("OrganizationBulkConfirmResponseModel")
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Error = error;
|
Error = error;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of a command.
|
||||||
|
/// This is a <see cref="OneOf{Error, T}"/> type that contains an Error if the command execution failed, or the result of the command if it succeeded.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the successful result. If there is no successful result (void), use <see cref="BulkCommandResult"/>.</typeparam>
|
||||||
|
|
||||||
|
public class CommandResult<T>(OneOf<Error, T> result) : OneOfBase<Error, T>(result)
|
||||||
|
{
|
||||||
|
public bool IsError => IsT0;
|
||||||
|
public bool IsSuccess => IsT1;
|
||||||
|
public Error AsError => AsT0;
|
||||||
|
public T AsSuccess => AsT1;
|
||||||
|
|
||||||
|
public static implicit operator CommandResult<T>(T value) => new(value);
|
||||||
|
public static implicit operator CommandResult<T>(Error error) => new(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of a command where successful execution returns no value (void).
|
||||||
|
/// See <see cref="CommandResult{T}"/> for more information.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandResult(OneOf<Error, None> result) : CommandResult<None>(result)
|
||||||
|
{
|
||||||
|
public static implicit operator CommandResult(None none) => new(none);
|
||||||
|
public static implicit operator CommandResult(Error error) => new(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A wrapper for <see cref="CommandResult{T}"/> with an ID, to identify the result in bulk operations.
|
||||||
|
/// </summary>
|
||||||
|
public record BulkCommandResult<T>(Guid Id, CommandResult<T> Result);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A wrapper for <see cref="CommandResult"/> with an ID, to identify the result in bulk operations.
|
||||||
|
/// </summary>
|
||||||
|
public record BulkCommandResult(Guid Id, CommandResult Result);
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
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 Microsoft.Extensions.Logging;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
|
|
||||||
|
public class DeleteClaimedOrganizationUserAccountCommandvNext(
|
||||||
|
IUserService userService,
|
||||||
|
IEventService eventService,
|
||||||
|
IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IPushNotificationService pushService,
|
||||||
|
ILogger<DeleteClaimedOrganizationUserAccountCommandvNext> logger,
|
||||||
|
IDeleteClaimedOrganizationUserAccountValidatorvNext deleteClaimedOrganizationUserAccountValidatorvNext)
|
||||||
|
: IDeleteClaimedOrganizationUserAccountCommandvNext
|
||||||
|
{
|
||||||
|
public async Task<BulkCommandResult> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId)
|
||||||
|
{
|
||||||
|
var result = await DeleteManyUsersAsync(organizationId, [organizationUserId], deletingUserId);
|
||||||
|
return result.Single();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BulkCommandResult>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid deletingUserId)
|
||||||
|
{
|
||||||
|
orgUserIds = orgUserIds.ToList();
|
||||||
|
var orgUsers = await organizationUserRepository.GetManyAsync(orgUserIds);
|
||||||
|
var users = await GetUsersAsync(orgUsers);
|
||||||
|
var claimedStatuses = await getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, orgUserIds);
|
||||||
|
|
||||||
|
var internalRequests = CreateInternalRequests(organizationId, deletingUserId, orgUserIds, orgUsers, users, claimedStatuses);
|
||||||
|
var validationResults = (await deleteClaimedOrganizationUserAccountValidatorvNext.ValidateAsync(internalRequests)).ToList();
|
||||||
|
|
||||||
|
var validRequests = validationResults.ValidRequests();
|
||||||
|
await CancelPremiumsAsync(validRequests);
|
||||||
|
await HandleUserDeletionsAsync(validRequests);
|
||||||
|
await LogDeletedOrganizationUsersAsync(validRequests);
|
||||||
|
|
||||||
|
return validationResults.Select(v => v.Match(
|
||||||
|
error => new BulkCommandResult(v.Request.OrganizationUserId, error),
|
||||||
|
_ => new BulkCommandResult(v.Request.OrganizationUserId, new None())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<DeleteUserValidationRequest> CreateInternalRequests(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid deletingUserId,
|
||||||
|
IEnumerable<Guid> orgUserIds,
|
||||||
|
ICollection<OrganizationUser> orgUsers,
|
||||||
|
IEnumerable<User> users,
|
||||||
|
IDictionary<Guid, bool> claimedStatuses)
|
||||||
|
{
|
||||||
|
foreach (var orgUserId in orgUserIds)
|
||||||
|
{
|
||||||
|
var orgUser = orgUsers.FirstOrDefault(orgUser => orgUser.Id == orgUserId);
|
||||||
|
var user = users.FirstOrDefault(user => user.Id == orgUser?.UserId);
|
||||||
|
claimedStatuses.TryGetValue(orgUserId, out var isClaimed);
|
||||||
|
|
||||||
|
yield return new DeleteUserValidationRequest
|
||||||
|
{
|
||||||
|
User = user,
|
||||||
|
OrganizationUserId = orgUserId,
|
||||||
|
OrganizationUser = orgUser,
|
||||||
|
IsClaimed = isClaimed,
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
DeletingUserId = deletingUserId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<User>> GetUsersAsync(ICollection<OrganizationUser> orgUsers)
|
||||||
|
{
|
||||||
|
var userIds = orgUsers
|
||||||
|
.Where(orgUser => orgUser.UserId.HasValue)
|
||||||
|
.Select(orgUser => orgUser.UserId!.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return await userRepository.GetManyAsync(userIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LogDeletedOrganizationUsersAsync(IEnumerable<DeleteUserValidationRequest> requests)
|
||||||
|
{
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var events = requests
|
||||||
|
.Select(request => (request.OrganizationUser!, EventType.OrganizationUser_Deleted, (DateTime?)eventDate))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (events.Count != 0)
|
||||||
|
{
|
||||||
|
await eventService.LogOrganizationUserEventsAsync(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleUserDeletionsAsync(IEnumerable<DeleteUserValidationRequest> requests)
|
||||||
|
{
|
||||||
|
var users = requests
|
||||||
|
.Select(request => request.User!)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (users.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await userRepository.DeleteManyAsync(users);
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
await pushService.PushLogOutAsync(user.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CancelPremiumsAsync(IEnumerable<DeleteUserValidationRequest> requests)
|
||||||
|
{
|
||||||
|
var users = requests.Select(request => request.User!);
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await userService.CancelPremiumAsync(user);
|
||||||
|
}
|
||||||
|
catch (GatewayException exception)
|
||||||
|
{
|
||||||
|
logger.LogWarning(exception, "Failed to cancel premium subscription for {userId}.", user.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext.ValidationResultHelpers;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
|
|
||||||
|
public class DeleteClaimedOrganizationUserAccountValidatorvNext(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IProviderUserRepository providerUserRepository) : IDeleteClaimedOrganizationUserAccountValidatorvNext
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<ValidationResult<DeleteUserValidationRequest>>> ValidateAsync(IEnumerable<DeleteUserValidationRequest> requests)
|
||||||
|
{
|
||||||
|
var tasks = requests.Select(ValidateAsync);
|
||||||
|
var results = await Task.WhenAll(tasks);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ValidationResult<DeleteUserValidationRequest>> ValidateAsync(DeleteUserValidationRequest request)
|
||||||
|
{
|
||||||
|
// Ensure user exists
|
||||||
|
if (request.User == null || request.OrganizationUser == null)
|
||||||
|
{
|
||||||
|
return Invalid(request, new UserNotFoundError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete invited users
|
||||||
|
if (request.OrganizationUser.Status == OrganizationUserStatusType.Invited)
|
||||||
|
{
|
||||||
|
return Invalid(request, new InvalidUserStatusError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete yourself
|
||||||
|
if (request.OrganizationUser.UserId == request.DeletingUserId)
|
||||||
|
{
|
||||||
|
return Invalid(request, new CannotDeleteYourselfError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can only delete a claimed user
|
||||||
|
if (!request.IsClaimed)
|
||||||
|
{
|
||||||
|
return Invalid(request, new UserNotClaimedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete an owner unless you are an owner or provider
|
||||||
|
if (request.OrganizationUser.Type == OrganizationUserType.Owner &&
|
||||||
|
!await currentContext.OrganizationOwner(request.OrganizationId))
|
||||||
|
{
|
||||||
|
return Invalid(request, new CannotDeleteOwnersError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete a user who is the sole owner of an organization
|
||||||
|
var onlyOwnerCount = await organizationUserRepository.GetCountByOnlyOwnerAsync(request.User.Id);
|
||||||
|
if (onlyOwnerCount > 0)
|
||||||
|
{
|
||||||
|
return Invalid(request, new SoleOwnerError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete a user who is the sole member of a provider
|
||||||
|
var onlyOwnerProviderCount = await providerUserRepository.GetCountByOnlyOwnerAsync(request.User.Id);
|
||||||
|
if (onlyOwnerProviderCount > 0)
|
||||||
|
{
|
||||||
|
return Invalid(request, new SoleProviderError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom users cannot delete admins
|
||||||
|
if (request.OrganizationUser.Type == OrganizationUserType.Admin && await currentContext.OrganizationCustom(request.OrganizationId))
|
||||||
|
{
|
||||||
|
return Invalid(request, new CannotDeleteAdminsError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Valid(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
|
|
||||||
|
public class DeleteUserValidationRequest
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; init; }
|
||||||
|
public Guid OrganizationUserId { get; init; }
|
||||||
|
public OrganizationUser? OrganizationUser { get; init; }
|
||||||
|
public User? User { get; init; }
|
||||||
|
public Guid DeletingUserId { get; init; }
|
||||||
|
public bool IsClaimed { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly typed error containing a reason that an action failed.
|
||||||
|
/// This is used for business logic validation and other expected errors, not exceptions.
|
||||||
|
/// </summary>
|
||||||
|
public abstract record Error(string Message);
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="Error"/> type that maps to a NotFoundResult at the api layer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Message"></param>
|
||||||
|
public abstract record NotFoundError(string Message) : Error(Message);
|
||||||
|
|
||||||
|
public record UserNotFoundError() : NotFoundError("Invalid user.");
|
||||||
|
public record UserNotClaimedError() : Error("Member is not claimed by the organization.");
|
||||||
|
public record InvalidUserStatusError() : Error("You cannot delete a member with Invited status.");
|
||||||
|
public record CannotDeleteYourselfError() : Error("You cannot delete yourself.");
|
||||||
|
public record CannotDeleteOwnersError() : Error("Only owners can delete other owners.");
|
||||||
|
public record SoleOwnerError() : Error("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
|
||||||
|
public record SoleProviderError() : Error("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.");
|
||||||
|
public record CannotDeleteAdminsError() : Error("Custom users can not delete admins.");
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
|
|
||||||
|
public interface IDeleteClaimedOrganizationUserAccountCommandvNext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a user from an organization and deletes all of their associated user data.
|
||||||
|
/// </summary>
|
||||||
|
Task<BulkCommandResult> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes multiple users from an organization and deletes all of their associated user data.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// An error message for each user that could not be removed, otherwise null.
|
||||||
|
/// </returns>
|
||||||
|
Task<IEnumerable<BulkCommandResult>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid deletingUserId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
|
|
||||||
|
public interface IDeleteClaimedOrganizationUserAccountValidatorvNext
|
||||||
|
{
|
||||||
|
Task<IEnumerable<ValidationResult<DeleteUserValidationRequest>>> ValidateAsync(IEnumerable<DeleteUserValidationRequest> requests);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of validating a request.
|
||||||
|
/// This is for use within the Core layer, e.g. validating a command request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request that has been validated.</param>
|
||||||
|
/// <param name="error">A <see cref="OneOf{Error, None}"/> type that contains an Error if validation failed.</param>
|
||||||
|
/// <typeparam name="TRequest">The request type.</typeparam>
|
||||||
|
public class ValidationResult<TRequest>(TRequest request, OneOf<Error, None> error) : OneOfBase<Error, None>(error)
|
||||||
|
{
|
||||||
|
public TRequest Request { get; } = request;
|
||||||
|
|
||||||
|
public bool IsError => IsT0;
|
||||||
|
public bool IsValid => IsT1;
|
||||||
|
public Error AsError => AsT0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ValidationResultHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a successful <see cref="ValidationResult{TRequest}"/> with no error set.
|
||||||
|
/// </summary>
|
||||||
|
public static ValidationResult<T> Valid<T>(T request) => new(request, new None());
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a failed <see cref="ValidationResult{TRequest}"/> with the specified error.
|
||||||
|
/// </summary>
|
||||||
|
public static ValidationResult<T> Invalid<T>(T request, Error error) => new(request, error);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts successfully validated requests from a sequence of <see cref="ValidationResult{TRequest}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static List<T> ValidRequests<T>(this IEnumerable<ValidationResult<T>> results) =>
|
||||||
|
results
|
||||||
|
.Where(r => r.IsValid)
|
||||||
|
.Select(r => r.Request)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
@@ -134,6 +134,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal";
|
public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal";
|
||||||
public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service";
|
public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service";
|
||||||
public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors";
|
public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors";
|
||||||
|
public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command";
|
||||||
|
|
||||||
/* Auth Team */
|
/* Auth Team */
|
||||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||||
@@ -133,6 +134,10 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
|
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
|
||||||
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||||
|
|
||||||
|
// vNext implementations (feature flagged)
|
||||||
|
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommandvNext, DeleteClaimedOrganizationUserAccountCommandvNext>();
|
||||||
|
services.AddScoped<IDeleteClaimedOrganizationUserAccountValidatorvNext, DeleteClaimedOrganizationUserAccountValidatorvNext>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
using Bit.Api.IntegrationTest.Helpers;
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@@ -30,6 +33,10 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
|||||||
featureService
|
featureService
|
||||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
|
featureService
|
||||||
|
.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor)
|
||||||
|
.Returns(true);
|
||||||
});
|
});
|
||||||
_client = _factory.CreateClient();
|
_client = _factory.CreateClient();
|
||||||
_loginHelper = new LoginHelper(_factory, _client);
|
_loginHelper = new LoginHelper(_factory, _client);
|
||||||
@@ -42,6 +49,91 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
|||||||
private Organization _organization = null!;
|
private Organization _organization = null!;
|
||||||
private string _ownerEmail = 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]
|
[Theory]
|
||||||
[InlineData(OrganizationUserType.User)]
|
[InlineData(OrganizationUserType.User)]
|
||||||
[InlineData(OrganizationUserType.Custom)]
|
[InlineData(OrganizationUserType.Custom)]
|
||||||
@@ -57,11 +149,36 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
|||||||
Ids = new List<Guid> { Guid.NewGuid() }
|
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);
|
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]
|
[Theory]
|
||||||
[InlineData(OrganizationUserType.User)]
|
[InlineData(OrganizationUserType.User)]
|
||||||
[InlineData(OrganizationUserType.Custom)]
|
[InlineData(OrganizationUserType.Custom)]
|
||||||
@@ -74,7 +191,7 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
|||||||
|
|
||||||
var userToRemove = Guid.NewGuid();
|
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);
|
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,27 +330,6 @@ public class OrganizationUsersControllerTests
|
|||||||
sutProvider.Sut.DeleteAccount(orgId, id));
|
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]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
||||||
|
|||||||
@@ -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?)>));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user