1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-01 13:43:23 +01:00

Merge remote-tracking branch 'origin/main' into ac/pm-14245/remove-pm-13322-add-policy-definitions-from-codebase

This commit is contained in:
Thomas Rittson 2024-11-28 14:20:54 +10:00
commit 4822a8ebd6
No known key found for this signature in database
GPG Key ID: CDDDA03861C35E27
10 changed files with 823 additions and 254 deletions

View File

@ -9,10 +9,7 @@ namespace Bit.Admin.Models;
public class UserEditModel public class UserEditModel
{ {
public UserEditModel() public UserEditModel() { }
{
}
public UserEditModel( public UserEditModel(
User user, User user,
@ -21,10 +18,9 @@ public class UserEditModel
BillingInfo billingInfo, BillingInfo billingInfo,
BillingHistoryInfo billingHistoryInfo, BillingHistoryInfo billingHistoryInfo,
GlobalSettings globalSettings, GlobalSettings globalSettings,
bool? domainVerified bool? claimedAccount)
)
{ {
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified); User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount);
BillingInfo = billingInfo; BillingInfo = billingInfo;
BillingHistoryInfo = billingHistoryInfo; BillingHistoryInfo = billingHistoryInfo;

View File

@ -14,7 +14,7 @@ public class UserViewModel
public bool Premium { get; } public bool Premium { get; }
public short? MaxStorageGb { get; } public short? MaxStorageGb { get; }
public bool EmailVerified { get; } public bool EmailVerified { get; }
public bool? DomainVerified { get; } public bool? ClaimedAccount { get; }
public bool TwoFactorEnabled { get; } public bool TwoFactorEnabled { get; }
public DateTime AccountRevisionDate { get; } public DateTime AccountRevisionDate { get; }
public DateTime RevisionDate { get; } public DateTime RevisionDate { get; }
@ -36,7 +36,7 @@ public class UserViewModel
bool premium, bool premium,
short? maxStorageGb, short? maxStorageGb,
bool emailVerified, bool emailVerified,
bool? domainVerified, bool? claimedAccount,
bool twoFactorEnabled, bool twoFactorEnabled,
DateTime accountRevisionDate, DateTime accountRevisionDate,
DateTime revisionDate, DateTime revisionDate,
@ -58,7 +58,7 @@ public class UserViewModel
Premium = premium; Premium = premium;
MaxStorageGb = maxStorageGb; MaxStorageGb = maxStorageGb;
EmailVerified = emailVerified; EmailVerified = emailVerified;
DomainVerified = domainVerified; ClaimedAccount = claimedAccount;
TwoFactorEnabled = twoFactorEnabled; TwoFactorEnabled = twoFactorEnabled;
AccountRevisionDate = accountRevisionDate; AccountRevisionDate = accountRevisionDate;
RevisionDate = revisionDate; RevisionDate = revisionDate;
@ -79,7 +79,7 @@ public class UserViewModel
users.Select(user => MapViewModel(user, lookup, false)); users.Select(user => MapViewModel(user, lookup, false));
public static UserViewModel MapViewModel(User user, public static UserViewModel MapViewModel(User user,
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) => IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? claimedAccount) =>
new( new(
user.Id, user.Id,
user.Name, user.Name,
@ -89,7 +89,7 @@ public class UserViewModel
user.Premium, user.Premium,
user.MaxStorageGb, user.MaxStorageGb,
user.EmailVerified, user.EmailVerified,
domainVerified, claimedAccount,
IsTwoFactorEnabled(user, lookup), IsTwoFactorEnabled(user, lookup),
user.AccountRevisionDate, user.AccountRevisionDate,
user.RevisionDate, user.RevisionDate,
@ -106,7 +106,7 @@ public class UserViewModel
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) => public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false); MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false);
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? domainVerified) => public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? claimedAccount) =>
new( new(
user.Id, user.Id,
user.Name, user.Name,
@ -116,7 +116,7 @@ public class UserViewModel
user.Premium, user.Premium,
user.MaxStorageGb, user.MaxStorageGb,
user.EmailVerified, user.EmailVerified,
domainVerified, claimedAccount,
isTwoFactorEnabled, isTwoFactorEnabled,
user.AccountRevisionDate, user.AccountRevisionDate,
user.RevisionDate, user.RevisionDate,

View File

@ -12,9 +12,10 @@
<dt class="col-sm-4 col-lg-3">Email Verified</dt> <dt class="col-sm-4 col-lg-3">Email Verified</dt>
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd> <dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
@if(Model.DomainVerified.HasValue){ @if(Model.ClaimedAccount.HasValue)
<dt class="col-sm-4 col-lg-3">Domain Verified</dt> {
<dd class="col-sm-8 col-lg-9">@(Model.DomainVerified.Value == true ? "Yes" : "No")</dd> <dt class="col-sm-4 col-lg-3">Claimed Account</dt>
<dd class="col-sm-8 col-lg-9">@(Model.ClaimedAccount.Value ? "Yes" : "No")</dd>
} }
<dt class="col-sm-4 col-lg-3">Using 2FA</dt> <dt class="col-sm-4 col-lg-3">Using 2FA</dt>

View File

@ -539,7 +539,7 @@ public class OrganizationUsersController : Controller
var userId = _userService.GetProperUserId(User); var userId = _userService.GetProperUserId(User);
var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value); var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r => return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
} }
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]

View File

@ -1,14 +1,53 @@
using Bit.Core.Entities; using Bit.Core.Enums;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IRemoveOrganizationUserCommand public interface IRemoveOrganizationUserCommand
{ {
/// <summary>
/// Removes a user from an organization.
/// </summary>
/// <param name="organizationId">The ID of the organization.</param>
/// <param name="userId">The ID of the user to remove.</param>
Task RemoveUserAsync(Guid organizationId, Guid userId);
/// <summary>
/// Removes a user from an organization with a specified deleting user.
/// </summary>
/// <param name="organizationId">The ID of the organization.</param>
/// <param name="organizationUserId">The ID of the organization user to remove.</param>
/// <param name="deletingUserId">The ID of the user performing the removal operation.</param>
Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
/// <summary>
/// Removes a user from an organization using a system user.
/// </summary>
/// <param name="organizationId">The ID of the organization.</param>
/// <param name="organizationUserId">The ID of the organization user to remove.</param>
/// <param name="eventSystemUser">The system user performing the removal operation.</param>
Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser); Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser);
Task RemoveUserAsync(Guid organizationId, Guid userId);
Task<List<Tuple<OrganizationUser, string>>> RemoveUsersAsync(Guid organizationId, /// <summary>
IEnumerable<Guid> organizationUserIds, Guid? deletingUserId); /// Removes multiple users from an organization with a specified deleting user.
/// </summary>
/// <param name="organizationId">The ID of the organization.</param>
/// <param name="organizationUserIds">The collection of organization user IDs to remove.</param>
/// <param name="deletingUserId">The ID of the user performing the removal operation.</param>
/// <returns>
/// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty.
/// </returns>
Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
/// <summary>
/// Removes multiple users from an organization using a system user.
/// </summary>
/// <param name="organizationId">The ID of the organization.</param>
/// <param name="organizationUserIds">The collection of organization user IDs to remove.</param>
/// <param name="eventSystemUser">The system user performing the removal operation.</param>
/// <returns>
/// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty.
/// </returns>
Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
Guid organizationId, IEnumerable<Guid> organizationUserIds, EventSystemUser eventSystemUser);
} }

View File

@ -17,6 +17,16 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
private readonly IPushRegistrationService _pushRegistrationService; private readonly IPushRegistrationService _pushRegistrationService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
private readonly IFeatureService _featureService;
private readonly TimeProvider _timeProvider;
public const string UserNotFoundErrorMessage = "User not found.";
public const string UsersInvalidErrorMessage = "Users invalid.";
public const string RemoveYourselfErrorMessage = "You cannot remove yourself.";
public const string RemoveOwnerByNonOwnerErrorMessage = "Only owners can delete other owners.";
public const string RemoveLastConfirmedOwnerErrorMessage = "Organization must have at least one confirmed owner.";
public const string RemoveClaimedAccountErrorMessage = "Cannot remove member accounts claimed by the organization. To offboard a member, revoke or delete the account.";
public RemoveOrganizationUserCommand( public RemoveOrganizationUserCommand(
IDeviceRepository deviceRepository, IDeviceRepository deviceRepository,
@ -25,7 +35,10 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
IPushRegistrationService pushRegistrationService, IPushRegistrationService pushRegistrationService,
ICurrentContext currentContext, ICurrentContext currentContext,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
IFeatureService featureService,
TimeProvider timeProvider)
{ {
_deviceRepository = deviceRepository; _deviceRepository = deviceRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -34,14 +47,27 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
_pushRegistrationService = pushRegistrationService; _pushRegistrationService = pushRegistrationService;
_currentContext = currentContext; _currentContext = currentContext;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
_featureService = featureService;
_timeProvider = timeProvider;
}
public async Task RemoveUserAsync(Guid organizationId, Guid userId)
{
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
ValidateRemoveUser(organizationId, organizationUser);
await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser: null);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
} }
public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{ {
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
ValidateDeleteUser(organizationId, organizationUser); ValidateRemoveUser(organizationId, organizationUser);
await RepositoryDeleteUserAsync(organizationUser, deletingUserId); await RepositoryRemoveUserAsync(organizationUser, deletingUserId, eventSystemUser: null);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
} }
@ -49,108 +75,79 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)
{ {
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
ValidateDeleteUser(organizationId, organizationUser); ValidateRemoveUser(organizationId, organizationUser);
await RepositoryDeleteUserAsync(organizationUser, null); await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
} }
public async Task RemoveUserAsync(Guid organizationId, Guid userId) public async Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? deletingUserId)
{ {
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId, eventSystemUser: null);
ValidateDeleteUser(organizationId, organizationUser);
await RepositoryDeleteUserAsync(organizationUser, null); var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();
if (removedUsers.Any())
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); {
DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime;
await _eventService.LogOrganizationUserEventsAsync(
removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventDate)));
} }
public async Task<List<Tuple<OrganizationUser, string>>> RemoveUsersAsync(Guid organizationId, return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));
IEnumerable<Guid> organizationUsersId,
Guid? deletingUserId)
{
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
.ToList();
if (!filteredUsers.Any())
{
throw new BadRequestException("Users invalid.");
} }
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId)) public async Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
Guid organizationId, IEnumerable<Guid> organizationUserIds, EventSystemUser eventSystemUser)
{ {
throw new BadRequestException("Organization must have at least one confirmed owner."); var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId: null, eventSystemUser);
var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();
if (removedUsers.Any())
{
DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime;
await _eventService.LogOrganizationUserEventsAsync(
removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventSystemUser, eventDate)));
} }
var deletingUserIsOwner = false; return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));
if (deletingUserId.HasValue)
{
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
} }
var result = new List<Tuple<OrganizationUser, string>>(); private void ValidateRemoveUser(Guid organizationId, OrganizationUser orgUser)
var deletedUserIds = new List<Guid>();
foreach (var orgUser in filteredUsers)
{
try
{
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)
{
throw new BadRequestException("You cannot remove yourself.");
}
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)
{
throw new BadRequestException("Only owners can delete other owners.");
}
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
if (orgUser.UserId.HasValue)
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
}
result.Add(Tuple.Create(orgUser, ""));
deletedUserIds.Add(orgUser.Id);
}
catch (BadRequestException e)
{
result.Add(Tuple.Create(orgUser, e.Message));
}
await _organizationUserRepository.DeleteManyAsync(deletedUserIds);
}
return result;
}
private void ValidateDeleteUser(Guid organizationId, OrganizationUser orgUser)
{ {
if (orgUser == null || orgUser.OrganizationId != organizationId) if (orgUser == null || orgUser.OrganizationId != organizationId)
{ {
throw new NotFoundException("User not found."); throw new NotFoundException(UserNotFoundErrorMessage);
} }
} }
private async Task RepositoryDeleteUserAsync(OrganizationUser orgUser, Guid? deletingUserId) private async Task RepositoryRemoveUserAsync(OrganizationUser orgUser, Guid? deletingUserId, EventSystemUser? eventSystemUser)
{ {
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value) if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)
{ {
throw new BadRequestException("You cannot remove yourself."); throw new BadRequestException(RemoveYourselfErrorMessage);
} }
if (orgUser.Type == OrganizationUserType.Owner) if (orgUser.Type == OrganizationUserType.Owner)
{ {
if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(orgUser.OrganizationId)) if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(orgUser.OrganizationId))
{ {
throw new BadRequestException("Only owners can delete other owners."); throw new BadRequestException(RemoveOwnerByNonOwnerErrorMessage);
} }
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, new[] { orgUser.Id }, includeProvider: true)) if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, new[] { orgUser.Id }, includeProvider: true))
{ {
throw new BadRequestException("Organization must have at least one confirmed owner."); throw new BadRequestException(RemoveLastConfirmedOwnerErrorMessage);
}
}
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null)
{
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });
if (managementStatus.TryGetValue(orgUser.Id, out var isManaged) && isManaged)
{
throw new BadRequestException(RemoveClaimedAccountErrorMessage);
} }
} }
@ -177,4 +174,70 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
organizationId.ToString()); organizationId.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(userId); await _pushNotificationService.PushSyncOrgKeysAsync(userId);
} }
private async Task<IEnumerable<(OrganizationUser OrganizationUser, string ErrorMessage)>> RemoveUsersInternalAsync(
Guid organizationId, IEnumerable<Guid> organizationUsersId, Guid? deletingUserId, EventSystemUser? eventSystemUser)
{
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId).ToList();
if (!filteredUsers.Any())
{
throw new BadRequestException(UsersInvalidErrorMessage);
}
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
{
throw new BadRequestException(RemoveLastConfirmedOwnerErrorMessage);
}
var deletingUserIsOwner = false;
if (deletingUserId.HasValue)
{
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
}
var managementStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null
? await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, filteredUsers.Select(u => u.Id))
: filteredUsers.ToDictionary(u => u.Id, u => false);
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();
foreach (var orgUser in filteredUsers)
{
try
{
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)
{
throw new BadRequestException(RemoveYourselfErrorMessage);
}
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)
{
throw new BadRequestException(RemoveOwnerByNonOwnerErrorMessage);
}
if (managementStatus.TryGetValue(orgUser.Id, out var isManaged) && isManaged)
{
throw new BadRequestException(RemoveClaimedAccountErrorMessage);
}
result.Add((orgUser, string.Empty));
}
catch (BadRequestException e)
{
result.Add((orgUser, e.Message));
}
}
var organizationUsersToRemove = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();
if (organizationUsersToRemove.Any())
{
await _organizationUserRepository.DeleteManyAsync(organizationUsersToRemove.Select(ou => ou.Id));
foreach (var orgUser in organizationUsersToRemove.Where(ou => ou.UserId.HasValue))
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
}
}
return result;
}
} }

View File

@ -129,7 +129,6 @@ public static class FeatureFlagKeys
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor"; public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2"; public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
public const string MembersTwoFAQueryOptimization = "ac-1698-members-two-fa-query-optimization";
public const string NativeCarouselFlow = "native-carousel-flow"; public const string NativeCarouselFlow = "native-carousel-flow";
public const string NativeCreateAccountFlow = "native-create-account-flow"; public const string NativeCreateAccountFlow = "native-create-account-flow";
public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; public const string AccountDeprovisioning = "pm-10308-account-deprovisioning";

View File

@ -8,5 +8,5 @@ public class CommandResult(IEnumerable<string> errors)
public bool HasErrors => ErrorMessages.Count > 0; public bool HasErrors => ErrorMessages.Count > 0;
public List<string> ErrorMessages { get; } = errors.ToList(); public List<string> ErrorMessages { get; } = errors.ToList();
public CommandResult() : this((IEnumerable<string>)[]) { } public CommandResult() : this(Array.Empty<string>()) { }
} }

View File

@ -1,6 +1,4 @@
#nullable enable using Bit.Admin.Models;
using Bit.Admin.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -116,30 +114,26 @@ public class UserViewModelTests
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain); var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);
Assert.True(actual.DomainVerified); Assert.True(actual.ClaimedAccount);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user) public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user)
{ {
var verifiedDomain = false; var verifiedDomain = false;
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain); var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);
Assert.False(actual.DomainVerified); Assert.False(actual.ClaimedAccount);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user) public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user)
{ {
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), null); var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), null);
Assert.Null(actual.DomainVerified); Assert.Null(actual.ClaimedAccount);
} }
} }

View File

@ -9,6 +9,7 @@ using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Time.Testing;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@ -18,38 +19,93 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
public class RemoveOrganizationUserCommandTests public class RemoveOrganizationUserCommandTests
{ {
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUser_Success( public async Task RemoveUser_WithDeletingUserId_Success(
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider) SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
var currentContext = sutProvider.GetDependency<ICurrentContext>();
organizationUser.OrganizationId = deletingUser.OrganizationId; organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(deletingUser.Id)
.Returns(deletingUser);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(deletingUser.OrganizationId)
.Returns(true);
// Act
await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId);
await organizationUserRepository.Received(1).DeleteAsync(organizationUser); // Assert
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.DidNotReceiveWithAnyArgs()
.GetUsersOrganizationManagementStatusAsync(default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteAsync(organizationUser);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
} }
[Theory] [Theory, BitAutoData]
[BitAutoData] public async Task RemoveUser_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success(
public async Task RemoveUser_NotFound_ThrowsException(SutProvider<RemoveOrganizationUserCommand> sutProvider, [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
organizationUser.OrganizationId = deletingUser.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(deletingUser.Id)
.Returns(deletingUser);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(deletingUser.OrganizationId)
.Returns(true);
// Act
await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId);
// Assert
await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.Received(1)
.GetUsersOrganizationManagementStatusAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)));
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteAsync(organizationUser);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
}
[Theory, BitAutoData]
public async Task RemoveUser_WithDeletingUserId_NotFound_ThrowsException(
SutProvider<RemoveOrganizationUserCommand> sutProvider,
Guid organizationId, Guid organizationUserId) Guid organizationId, Guid organizationUserId)
{ {
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); // Act & Assert
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null));
} }
[Theory] [Theory, BitAutoData]
[BitAutoData] public async Task RemoveUser_WithDeletingUserId_MismatchingOrganizationId_ThrowsException(
public async Task RemoveUser_MismatchingOrganizationId_ThrowsException(
SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId) SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
{ {
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId) .GetByIdAsync(organizationUserId)
.Returns(new OrganizationUser .Returns(new OrganizationUser
@ -58,92 +114,231 @@ public class RemoveOrganizationUserCommandTests
OrganizationId = Guid.NewGuid() OrganizationId = Guid.NewGuid()
}); });
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); // Act & Assert
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUser_InvalidUser_ThrowsException( public async Task RemoveUser_WithDeletingUserId_InvalidUser_ThrowsException(
OrganizationUser organizationUser, OrganizationUser deletingUser, OrganizationUser organizationUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); .GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>( var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId)); () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, null));
Assert.Contains("User not found.", exception.Message); Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUser_RemoveYourself_ThrowsException(OrganizationUser deletingUser, SutProvider<RemoveOrganizationUserCommand> sutProvider) public async Task RemoveUser_WithDeletingUserId_RemoveYourself_ThrowsException(
OrganizationUser deletingUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); .GetByIdAsync(deletingUser.Id)
.Returns(deletingUser);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId)); () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId));
Assert.Contains("You cannot remove yourself.", exception.Message); Assert.Contains(RemoveOrganizationUserCommand.RemoveYourselfErrorMessage, exception.Message);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUser_NonOwnerRemoveOwner_ThrowsException( public async Task RemoveUser_WithDeletingUserId_NonOwnerRemoveOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,
[OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider) SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
var currentContext = sutProvider.GetDependency<ICurrentContext>();
organizationUser.OrganizationId = deletingUser.OrganizationId; organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
currentContext.OrganizationAdmin(deletingUser.OrganizationId).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationAdmin(organizationUser.OrganizationId)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId));
Assert.Contains("Only owners can delete other owners.", exception.Message); Assert.Contains(RemoveOrganizationUserCommand.RemoveOwnerByNonOwnerErrorMessage, exception.Message);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUser_RemovingLastOwner_ThrowsException( public async Task RemoveUser_WithDeletingUserId_RemovingLastOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,
OrganizationUser deletingUser, OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider) SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>();
organizationUser.OrganizationId = deletingUser.OrganizationId; organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
deletingUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), Arg.Any<bool>())
.Returns(false);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>())
.Returns(false);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(deletingUser.OrganizationId)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, null)); () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId));
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);
hasConfirmedOwnersExceptQuery await sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.Received(1) .Received(1)
.HasConfirmedOwnersExceptAsync( .HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId, organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), true); Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), true);
} }
[Theory, BitAutoData]
public async Task RemoveUserAsync_WithDeletingUserId_WithAccountDeprovisioningEnabled_WhenUserIsManaged_ThrowsException(
[OrganizationUser(status: OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser,
Guid deletingUserId,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUser.Id)
.Returns(orgUser);
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)))
.Returns(new Dictionary<Guid, bool> { { orgUser.Id, true } });
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(orgUser.OrganizationId, orgUser.Id, deletingUserId));
Assert.Contains(RemoveOrganizationUserCommand.RemoveClaimedAccountErrorMessage, exception.Message);
await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.Received(1)
.GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)));
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUser_WithEventSystemUser_Success( public async Task RemoveUser_WithEventSystemUser_Success(
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
EventSystemUser eventSystemUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
// Act
await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser);
// Assert
await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.DidNotReceiveWithAnyArgs()
.GetUsersOrganizationManagementStatusAsync(default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteAsync(organizationUser);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
}
[Theory, BitAutoData]
public async Task RemoveUser_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success(
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
EventSystemUser eventSystemUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
// Act
await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser);
// Assert
await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.DidNotReceiveWithAnyArgs()
.GetUsersOrganizationManagementStatusAsync(default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteAsync(organizationUser);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
}
[Theory]
[BitAutoData]
public async Task RemoveUser_WithEventSystemUser_NotFound_ThrowsException(
SutProvider<RemoveOrganizationUserCommand> sutProvider,
Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)
{
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser));
}
[Theory]
[BitAutoData]
public async Task RemoveUser_WithEventSystemUser_MismatchingOrganizationId_ThrowsException(
SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns(new OrganizationUser
{
Id = organizationUserId,
OrganizationId = Guid.NewGuid()
});
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser));
}
[Theory, BitAutoData]
public async Task RemoveUser_WithEventSystemUser_RemovingLastOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,
EventSystemUser eventSystemUser, EventSystemUser eventSystemUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider) SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>())
.Returns(false);
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); // Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser));
Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); await sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.Received(1)
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), true);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -170,23 +365,26 @@ public class RemoveOrganizationUserCommandTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUser_ByUserId_NotFound_ThrowsException(SutProvider<RemoveOrganizationUserCommand> sutProvider, public async Task RemoveUser_ByUserId_NotFound_ThrowsException(
Guid organizationId, Guid userId) SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid userId)
{ {
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId)); await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(OrganizationUser organizationUser, public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(
SutProvider<RemoveOrganizationUserCommand> sutProvider) OrganizationUser organizationUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)
.Returns(organizationUser);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>( var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value)); () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value));
Assert.Contains("User not found.", exception.Message); Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -194,21 +392,22 @@ public class RemoveOrganizationUserCommandTests
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider) SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>(); sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)
organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); .Returns(organizationUser);
hasConfirmedOwnersExceptQuery sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync( .HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId, organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>()) Arg.Any<bool>())
.Returns(false); .Returns(false);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value)); () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value));
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);
hasConfirmedOwnersExceptQuery await sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.Received(1) .Received(1)
.HasConfirmedOwnersExceptAsync( .HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId, organizationUser.OrganizationId,
@ -217,93 +416,371 @@ public class RemoveOrganizationUserCommandTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task RemoveUsers_FilterInvalid_ThrowsException(OrganizationUser organizationUser, OrganizationUser deletingUser, public async Task RemoveUsers_WithDeletingUserId_Success(
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationUsers = new[] { organizationUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId));
Assert.Contains("Users invalid.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUsers_RemoveYourself_ThrowsException(
OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationUsers = new[] { deletingUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
Assert.Contains("You cannot remove yourself.", result[0].Item2);
}
[Theory, BitAutoData]
public async Task RemoveUsers_NonOwnerRemoveOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
var organizationUsers = new[] { orgUser1 };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
Assert.Contains("Only owners can delete other owners.", result[0].Item2);
}
[Theory, BitAutoData]
public async Task RemoveUsers_LastOwner_ThrowsException(
[OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationUsers = new[] { orgUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null));
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUsers_Success(
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2, [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2)
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{ {
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); // Arrange
var currentContext = sutProvider.GetDependency<ICurrentContext>(); var sutProvider = SutProviderFactory();
var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
var organizationUsers = new[] { orgUser1, orgUser2 }; var organizationUsers = new[] { orgUser1, orgUser2 };
var organizationUserIds = organizationUsers.Select(u => u.Id); var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(deletingUser.Id)
.Returns(deletingUser);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>() sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>()) .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true); .Returns(true);
currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(deletingUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.GetUsersOrganizationManagementStatusAsync(
deletingUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)))
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, false }, { orgUser2.Id, false } });
await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); // Act
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
// Assert
Assert.Equal(2, result.Count());
Assert.All(result, r => Assert.Empty(r.ErrorMessage));
await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.DidNotReceiveWithAnyArgs()
.GetUsersOrganizationManagementStatusAsync(default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, DateTime? DateTime)>>(i =>
i.First().OrganizationUser.Id == orgUser1.Id
&& i.Last().OrganizationUser.Id == orgUser2.Id
&& i.All(u => u.DateTime == eventDate)));
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success(
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2)
{
// Arrange
var sutProvider = SutProviderFactory();
var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
var organizationUsers = new[] { orgUser1, orgUser2 };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(deletingUser.Id)
.Returns(deletingUser);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(deletingUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.GetUsersOrganizationManagementStatusAsync(
deletingUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)))
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, false }, { orgUser2.Id, false } });
// Act
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
// Assert
Assert.Equal(2, result.Count());
Assert.All(result, r => Assert.Empty(r.ErrorMessage));
await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.Received(1)
.GetUsersOrganizationManagementStatusAsync(
deletingUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, DateTime? DateTime)>>(i =>
i.First().OrganizationUser.Id == orgUser1.Id
&& i.Last().OrganizationUser.Id == orgUser2.Id
&& i.All(u => u.DateTime == eventDate)));
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithDeletingUserId_WithMismatchingOrganizationId_ThrowsException(OrganizationUser organizationUser,
OrganizationUser deletingUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
var organizationUsers = new[] { organizationUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId));
Assert.Contains(RemoveOrganizationUserCommand.UsersInvalidErrorMessage, exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithDeletingUserId_RemoveYourself_ThrowsException(
OrganizationUser deletingUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
var organizationUsers = new[] { deletingUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
// Act
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
// Assert
Assert.Contains(RemoveOrganizationUserCommand.RemoveYourselfErrorMessage, result.First().ErrorMessage);
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithDeletingUserId_NonOwnerRemoveOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2,
[OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
var organizationUsers = new[] { orgUser1, orgUser2 };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
// Act
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
// Assert
Assert.Contains(RemoveOrganizationUserCommand.RemoveOwnerByNonOwnerErrorMessage, result.First().ErrorMessage);
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithDeletingUserId_RemovingManagedUser_WithAccountDeprovisioningEnabled_ThrowsException(
[OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser,
OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
orgUser.OrganizationId = deletingUser.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)))
.Returns(new[] { orgUser });
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)))
.Returns(new Dictionary<Guid, bool> { { orgUser.Id, true } });
// Act
var result = await sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUser.UserId);
// Assert
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteManyAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, DateTime? DateTime)>>());
Assert.Contains(RemoveOrganizationUserCommand.RemoveClaimedAccountErrorMessage, result.First().ErrorMessage);
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithDeletingUserId_LastOwner_ThrowsException(
[OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
var organizationUsers = new[] { orgUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner)
.Returns(organizationUsers);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null));
Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithEventSystemUser_Success(
EventSystemUser eventSystemUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1,
OrganizationUser orgUser2)
{
// Arrange
var sutProvider = SutProviderFactory();
var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;
orgUser1.OrganizationId = orgUser2.OrganizationId;
var organizationUsers = new[] { orgUser1, orgUser2 };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
// Act
var result = await sutProvider.Sut.RemoveUsersAsync(orgUser1.OrganizationId, organizationUserIds, eventSystemUser);
// Assert
Assert.Equal(2, result.Count());
Assert.All(result, r => Assert.Empty(r.ErrorMessage));
await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.DidNotReceiveWithAnyArgs()
.GetUsersOrganizationManagementStatusAsync(default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, EventSystemUser EventSystemUser, DateTime? DateTime)>>(
i => i.First().OrganizationUser.Id == orgUser1.Id
&& i.Last().OrganizationUser.Id == orgUser2.Id
&& i.All(u => u.EventSystemUser == eventSystemUser
&& u.DateTime == eventDate)));
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success(
EventSystemUser eventSystemUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1,
OrganizationUser orgUser2)
{
// Arrange
var sutProvider = SutProviderFactory();
var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;
orgUser1.OrganizationId = orgUser2.OrganizationId;
var organizationUsers = new[] { orgUser1, orgUser2 };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
// Act
var result = await sutProvider.Sut.RemoveUsersAsync(orgUser1.OrganizationId, organizationUserIds, eventSystemUser);
// Assert
Assert.Equal(2, result.Count());
Assert.All(result, r => Assert.Empty(r.ErrorMessage));
await sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
.DidNotReceiveWithAnyArgs()
.GetUsersOrganizationManagementStatusAsync(default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, EventSystemUser EventSystemUser, DateTime? DateTime)>>(
i => i.First().OrganizationUser.Id == orgUser1.Id
&& i.Last().OrganizationUser.Id == orgUser2.Id
&& i.All(u => u.EventSystemUser == eventSystemUser
&& u.DateTime == eventDate)));
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithEventSystemUser_WithMismatchingOrganizationId_ThrowsException(
EventSystemUser eventSystemUser,
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
var organizationUsers = new[] { organizationUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUsersAsync(Guid.NewGuid(), organizationUserIds, eventSystemUser));
Assert.Contains(RemoveOrganizationUserCommand.UsersInvalidErrorMessage, exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUsers_WithEventSystemUser_LastOwner_ThrowsException(
[OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
EventSystemUser eventSystemUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
// Arrange
var organizationUsers = new[] { orgUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(default)
.ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner)
.Returns(organizationUsers);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, eventSystemUser));
Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);
}
/// <summary>
/// Returns a new SutProvider with a FakeTimeProvider registered in the Sut.
/// </summary>
private static SutProvider<RemoveOrganizationUserCommand> SutProviderFactory()
{
return new SutProvider<RemoveOrganizationUserCommand>()
.WithFakeTimeProvider()
.Create();
} }
} }