diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index 2ad0b27cb..ed2d65324 100644 --- a/src/Admin/Models/UserEditModel.cs +++ b/src/Admin/Models/UserEditModel.cs @@ -9,10 +9,7 @@ namespace Bit.Admin.Models; public class UserEditModel { - public UserEditModel() - { - - } + public UserEditModel() { } public UserEditModel( User user, @@ -21,10 +18,9 @@ public class UserEditModel BillingInfo billingInfo, BillingHistoryInfo billingHistoryInfo, GlobalSettings globalSettings, - bool? domainVerified - ) + bool? claimedAccount) { - User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified); + User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount); BillingInfo = billingInfo; BillingHistoryInfo = billingHistoryInfo; diff --git a/src/Admin/Models/UserViewModel.cs b/src/Admin/Models/UserViewModel.cs index 75c089ee5..7fddbc0f5 100644 --- a/src/Admin/Models/UserViewModel.cs +++ b/src/Admin/Models/UserViewModel.cs @@ -14,7 +14,7 @@ public class UserViewModel public bool Premium { get; } public short? MaxStorageGb { get; } public bool EmailVerified { get; } - public bool? DomainVerified { get; } + public bool? ClaimedAccount { get; } public bool TwoFactorEnabled { get; } public DateTime AccountRevisionDate { get; } public DateTime RevisionDate { get; } @@ -36,7 +36,7 @@ public class UserViewModel bool premium, short? maxStorageGb, bool emailVerified, - bool? domainVerified, + bool? claimedAccount, bool twoFactorEnabled, DateTime accountRevisionDate, DateTime revisionDate, @@ -58,7 +58,7 @@ public class UserViewModel Premium = premium; MaxStorageGb = maxStorageGb; EmailVerified = emailVerified; - DomainVerified = domainVerified; + ClaimedAccount = claimedAccount; TwoFactorEnabled = twoFactorEnabled; AccountRevisionDate = accountRevisionDate; RevisionDate = revisionDate; @@ -79,7 +79,7 @@ public class UserViewModel users.Select(user => MapViewModel(user, lookup, false)); public static UserViewModel MapViewModel(User user, - IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) => + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? claimedAccount) => new( user.Id, user.Name, @@ -89,7 +89,7 @@ public class UserViewModel user.Premium, user.MaxStorageGb, user.EmailVerified, - domainVerified, + claimedAccount, IsTwoFactorEnabled(user, lookup), user.AccountRevisionDate, user.RevisionDate, @@ -106,7 +106,7 @@ public class UserViewModel public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) => MapViewModel(user, isTwoFactorEnabled, Array.Empty(), false); - public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers, bool? domainVerified) => + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers, bool? claimedAccount) => new( user.Id, user.Name, @@ -116,7 +116,7 @@ public class UserViewModel user.Premium, user.MaxStorageGb, user.EmailVerified, - domainVerified, + claimedAccount, isTwoFactorEnabled, user.AccountRevisionDate, user.RevisionDate, diff --git a/src/Admin/Views/Users/_ViewInformation.cshtml b/src/Admin/Views/Users/_ViewInformation.cshtml index 00afcc19d..ae8bcb073 100644 --- a/src/Admin/Views/Users/_ViewInformation.cshtml +++ b/src/Admin/Views/Users/_ViewInformation.cshtml @@ -12,9 +12,10 @@
Email Verified
@(Model.EmailVerified ? "Yes" : "No")
- @if(Model.DomainVerified.HasValue){ -
Domain Verified
-
@(Model.DomainVerified.Value == true ? "Yes" : "No")
+ @if(Model.ClaimedAccount.HasValue) + { +
Claimed Account
+
@(Model.ClaimedAccount.Value ? "Yes" : "No")
}
Using 2FA
diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 3193962fa..12d11fbc1 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -539,7 +539,7 @@ public class OrganizationUsersController : Controller var userId = _userService.GetProperUserId(User); var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value); return new ListResponseModel(result.Select(r => - new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); + new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); } [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs index 583645a89..7c1cdf05f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs @@ -1,14 +1,53 @@ -using Bit.Core.Entities; -using Bit.Core.Enums; +using Bit.Core.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; public interface IRemoveOrganizationUserCommand { + /// + /// Removes a user from an organization. + /// + /// The ID of the organization. + /// The ID of the user to remove. + Task RemoveUserAsync(Guid organizationId, Guid userId); + + /// + /// Removes a user from an organization with a specified deleting user. + /// + /// The ID of the organization. + /// The ID of the organization user to remove. + /// The ID of the user performing the removal operation. Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); + /// + /// Removes a user from an organization using a system user. + /// + /// The ID of the organization. + /// The ID of the organization user to remove. + /// The system user performing the removal operation. Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser); - Task RemoveUserAsync(Guid organizationId, Guid userId); - Task>> RemoveUsersAsync(Guid organizationId, - IEnumerable organizationUserIds, Guid? deletingUserId); + + /// + /// Removes multiple users from an organization with a specified deleting user. + /// + /// The ID of the organization. + /// The collection of organization user IDs to remove. + /// The ID of the user performing the removal operation. + /// + /// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty. + /// + Task> RemoveUsersAsync( + Guid organizationId, IEnumerable organizationUserIds, Guid? deletingUserId); + + /// + /// Removes multiple users from an organization using a system user. + /// + /// The ID of the organization. + /// The collection of organization user IDs to remove. + /// The system user performing the removal operation. + /// + /// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty. + /// + Task> RemoveUsersAsync( + Guid organizationId, IEnumerable organizationUserIds, EventSystemUser eventSystemUser); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index e6d56ea87..fa027f8e4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -17,6 +17,16 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand private readonly IPushRegistrationService _pushRegistrationService; private readonly ICurrentContext _currentContext; 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( IDeviceRepository deviceRepository, @@ -25,7 +35,10 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand IPushNotificationService pushNotificationService, IPushRegistrationService pushRegistrationService, ICurrentContext currentContext, - IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, + IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, + IFeatureService featureService, + TimeProvider timeProvider) { _deviceRepository = deviceRepository; _organizationUserRepository = organizationUserRepository; @@ -34,14 +47,27 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand _pushRegistrationService = pushRegistrationService; _currentContext = currentContext; _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) { 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); } @@ -49,108 +75,79 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) { 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); } - public async Task RemoveUserAsync(Guid organizationId, Guid userId) + public async Task> RemoveUsersAsync( + Guid organizationId, IEnumerable organizationUserIds, Guid? deletingUserId) { - var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); - ValidateDeleteUser(organizationId, organizationUser); + var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId, eventSystemUser: null); - await RepositoryDeleteUserAsync(organizationUser, null); + 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, eventDate))); + } - await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage)); } - public async Task>> RemoveUsersAsync(Guid organizationId, - IEnumerable organizationUsersId, - Guid? deletingUserId) + public async Task> RemoveUsersAsync( + Guid organizationId, IEnumerable organizationUserIds, EventSystemUser eventSystemUser) { - var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); - var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) - .ToList(); + var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId: null, eventSystemUser); - if (!filteredUsers.Any()) + var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList(); + if (removedUsers.Any()) { - throw new BadRequestException("Users invalid."); + DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime; + await _eventService.LogOrganizationUserEventsAsync( + removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventSystemUser, eventDate))); } - if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - var deletingUserIsOwner = false; - if (deletingUserId.HasValue) - { - deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); - } - - var result = new List>(); - var deletedUserIds = new List(); - 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; + return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage)); } - private void ValidateDeleteUser(Guid organizationId, OrganizationUser orgUser) + private void ValidateRemoveUser(Guid organizationId, OrganizationUser orgUser) { 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) { - throw new BadRequestException("You cannot remove yourself."); + throw new BadRequestException(RemoveYourselfErrorMessage); } if (orgUser.Type == OrganizationUserType.Owner) { 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)) { - 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()); await _pushNotificationService.PushSyncOrgKeysAsync(userId); } + + private async Task> RemoveUsersInternalAsync( + Guid organizationId, IEnumerable 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; + } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 22cfc69d4..2548261cc 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -129,7 +129,6 @@ public static class FeatureFlagKeys public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor"; 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 NativeCreateAccountFlow = "native-create-account-flow"; public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs index e8a756c7c..9e5d91e09 100644 --- a/src/Core/Models/Commands/CommandResult.cs +++ b/src/Core/Models/Commands/CommandResult.cs @@ -8,5 +8,5 @@ public class CommandResult(IEnumerable errors) public bool HasErrors => ErrorMessages.Count > 0; public List ErrorMessages { get; } = errors.ToList(); - public CommandResult() : this((IEnumerable)[]) { } + public CommandResult() : this(Array.Empty()) { } } diff --git a/test/Admin.Test/Models/UserViewModelTests.cs b/test/Admin.Test/Models/UserViewModelTests.cs index fac5d5f0e..d015b9832 100644 --- a/test/Admin.Test/Models/UserViewModelTests.cs +++ b/test/Admin.Test/Models/UserViewModelTests.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Admin.Models; +using Bit.Admin.Models; using Bit.Core.Entities; using Bit.Core.Vault.Entities; using Bit.Test.Common.AutoFixture.Attributes; @@ -116,30 +114,26 @@ public class UserViewModelTests var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), verifiedDomain); - Assert.True(actual.DomainVerified); + Assert.True(actual.ClaimedAccount); } [Theory] [BitAutoData] public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user) { - var verifiedDomain = false; var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), verifiedDomain); - Assert.False(actual.DomainVerified); + Assert.False(actual.ClaimedAccount); } [Theory] [BitAutoData] public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user) { - var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), null); - Assert.Null(actual.DomainVerified); + Assert.Null(actual.ClaimedAccount); } - - } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs index 2d10ce626..61371b756 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs @@ -9,6 +9,7 @@ using Bit.Core.Services; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; @@ -18,38 +19,93 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; public class RemoveOrganizationUserCommandTests { [Theory, BitAutoData] - public async Task RemoveUser_Success( + public async Task RemoveUser_WithDeletingUserId_Success( [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - + // Arrange organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + + // Act await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); - await organizationUserRepository.Received(1).DeleteAsync(organizationUser); - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } - [Theory] - [BitAutoData] - public async Task RemoveUser_NotFound_ThrowsException(SutProvider sutProvider, + [Theory, BitAutoData] + public async Task RemoveUser_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, + SutProvider sutProvider) + { + // Arrange + organizationUser.OrganizationId = deletingUser.OrganizationId; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + + // Act + await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetUsersOrganizationManagementStatusAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id))); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + } + + [Theory, BitAutoData] + public async Task RemoveUser_WithDeletingUserId_NotFound_ThrowsException( + SutProvider sutProvider, Guid organizationId, Guid organizationUserId) { - await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); + // Act & Assert + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); } - [Theory] - [BitAutoData] - public async Task RemoveUser_MismatchingOrganizationId_ThrowsException( + [Theory, BitAutoData] + public async Task RemoveUser_WithDeletingUserId_MismatchingOrganizationId_ThrowsException( SutProvider sutProvider, Guid organizationId, Guid organizationUserId) { + // Arrange sutProvider.GetDependency() .GetByIdAsync(organizationUserId) .Returns(new OrganizationUser @@ -58,92 +114,231 @@ public class RemoveOrganizationUserCommandTests OrganizationId = Guid.NewGuid() }); - await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); + // Act & Assert + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); } [Theory, BitAutoData] - public async Task RemoveUser_InvalidUser_ThrowsException( - OrganizationUser organizationUser, OrganizationUser deletingUser, - SutProvider sutProvider) + public async Task RemoveUser_WithDeletingUserId_InvalidUser_ThrowsException( + OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + // Act & Assert var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId)); - Assert.Contains("User not found.", exception.Message); + () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, null)); + Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message); } [Theory, BitAutoData] - public async Task RemoveUser_RemoveYourself_ThrowsException(OrganizationUser deletingUser, SutProvider sutProvider) + public async Task RemoveUser_WithDeletingUserId_RemoveYourself_ThrowsException( + OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); + // Act & Assert var exception = await Assert.ThrowsAsync( () => 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] - public async Task RemoveUser_NonOwnerRemoveOwner_ThrowsException( + public async Task RemoveUser_WithDeletingUserId_NonOwnerRemoveOwner_ThrowsException( [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - + // Arrange organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - currentContext.OrganizationAdmin(deletingUser.OrganizationId).Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .OrganizationAdmin(organizationUser.OrganizationId) + .Returns(true); + + // Act & Assert var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); - Assert.Contains("Only owners can delete other owners.", exception.Message); + () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveOwnerByNonOwnerErrorMessage, exception.Message); } [Theory, BitAutoData] - public async Task RemoveUser_RemovingLastOwner_ThrowsException( + public async Task RemoveUser_WithDeletingUserId_RemovingLastOwner_ThrowsException( [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); - + // Arrange organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( - deletingUser.OrganizationId, - Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()) - .Returns(false); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(false); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + + // Act & Assert var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, null)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - hasConfirmedOwnersExceptQuery + () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + await sutProvider.GetDependency() .Received(1) .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(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 sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(orgUser.Id) + .Returns(orgUser); + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is>(i => i.Contains(orgUser.Id))) + .Returns(new Dictionary { { orgUser.Id, true } }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(orgUser.OrganizationId, orgUser.Id, deletingUserId)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveClaimedAccountErrorMessage, exception.Message); + await sutProvider.GetDependency() + .Received(1) + .GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is>(i => i.Contains(orgUser.Id))); + } + [Theory, BitAutoData] public async Task RemoveUser_WithEventSystemUser_Success( [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + EventSystemUser eventSystemUser, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + // Act + await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + await sutProvider.GetDependency() + .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 sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + // Act + await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); + } + + [Theory] + [BitAutoData] + public async Task RemoveUser_WithEventSystemUser_NotFound_ThrowsException( + SutProvider sutProvider, + Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) + { + // Act & Assert + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser)); + } + + [Theory] + [BitAutoData] + public async Task RemoveUser_WithEventSystemUser_MismatchingOrganizationId_ThrowsException( + SutProvider sutProvider, Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) + { + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationUserId) + .Returns(new OrganizationUser + { + Id = organizationUserId, + OrganizationId = Guid.NewGuid() + }); + + // Act & Assert + await Assert.ThrowsAsync(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, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(false); - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - - await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); - - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + await sutProvider.GetDependency() + .Received(1) + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), true); } [Theory, BitAutoData] @@ -170,23 +365,26 @@ public class RemoveOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RemoveUser_ByUserId_NotFound_ThrowsException(SutProvider sutProvider, - Guid organizationId, Guid userId) + public async Task RemoveUser_ByUserId_NotFound_ThrowsException( + SutProvider sutProvider, Guid organizationId, Guid userId) { + // Act & Assert await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId)); } [Theory, BitAutoData] - public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(OrganizationUser organizationUser, - SutProvider sutProvider) + public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException( + OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); + // Arrange + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + // Act & Assert var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value)); - Assert.Contains("User not found.", exception.Message); + Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message); } [Theory, BitAutoData] @@ -194,21 +392,22 @@ public class RemoveOrganizationUserCommandTests [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); - - organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); - hasConfirmedOwnersExceptQuery + // Arrange + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()) .Returns(false); + // Act & Assert var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - hasConfirmedOwnersExceptQuery + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + await sutProvider.GetDependency() .Received(1) .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, @@ -217,93 +416,371 @@ public class RemoveOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RemoveUsers_FilterInvalid_ThrowsException(OrganizationUser organizationUser, OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationUsers = new[] { organizationUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - - var exception = await Assert.ThrowsAsync( - () => 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 sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationUsers = new[] { deletingUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) - .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 sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) - .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 sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - 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( - () => 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( + public async Task RemoveUsers_WithDeletingUserId_Success( [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2, - SutProvider sutProvider) + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - + // Arrange + var sutProvider = SutProviderFactory(); + var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; var organizationUsers = new[] { orgUser1, orgUser2 }; var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) .Returns(true); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))) + .Returns(new Dictionary { { 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() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>(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().GetUtcNow().UtcDateTime; + orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))) + .Returns(new Dictionary { { 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() + .Received(1) + .GetUsersOrganizationManagementStatusAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>(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 sutProvider) + { + // Arrange + var organizationUsers = new[] { organizationUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => 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 sutProvider) + { + // Arrange + var organizationUsers = new[] { deletingUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .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 sutProvider) + { + // Arrange + orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .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 sutProvider) + { + // Arrange + orgUser.OrganizationId = deletingUser.OrganizationId; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetManyAsync(Arg.Is>(i => i.Contains(orgUser.Id))) + .Returns(new[] { orgUser }); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any>()) + .Returns(true); + + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is>(i => i.Contains(orgUser.Id))) + .Returns(new Dictionary { { orgUser.Id, true } }); + + // Act + var result = await sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUser.UserId); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventsAsync(Arg.Any>()); + 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 sutProvider) + { + // Arrange + var organizationUsers = new[] { orgUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner) + .Returns(organizationUsers); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => 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().GetUtcNow().UtcDateTime; + orgUser1.OrganizationId = orgUser2.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any>()) + .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() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>( + 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().GetUtcNow().UtcDateTime; + orgUser1.OrganizationId = orgUser2.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any>()) + .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() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>( + 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 sutProvider) + { + // Arrange + var organizationUsers = new[] { organizationUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => 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 sutProvider) + { + // Arrange + var organizationUsers = new[] { orgUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner) + .Returns(organizationUsers); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, eventSystemUser)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + } + + /// + /// Returns a new SutProvider with a FakeTimeProvider registered in the Sut. + /// + private static SutProvider SutProviderFactory() + { + return new SutProvider() + .WithFakeTimeProvider() + .Create(); } }