mirror of
https://github.com/bitwarden/server.git
synced 2024-11-27 13:05:23 +01:00
[PM-10316] Add Command to Remove User and Delete Data for Organization-Managed Users (#4726)
* Add HasVerifiedDomainsAsync method to IOrganizationDomainService * Add GetManagedUserIdsByOrganizationIdAsync method to IOrganizationUserRepository and the corresponding queries * Fix case on the sproc OrganizationUser_ReadManagedIdsByOrganizationId parameter * Update the EF query to use the Email from the User table * dotnet format * Fix IOrganizationDomainService.HasVerifiedDomainsAsync by checking that domains have been Verified and add unit tests * Rename IOrganizationUserRepository.GetManagedUserIdsByOrganizationAsync * Fix domain queries * Add OrganizationUserRepository integration tests * Add summary to IOrganizationDomainService.HasVerifiedDomainsAsync * chore: Rename IOrganizationUserRepository.GetManagedUserIdsByOrganizationAsync to GetManyIdsManagedByOrganizationIdAsync * Add IsManagedByAnyOrganizationAsync method to IUserRepository * Add integration tests for UserRepository.IsManagedByAnyOrganizationAsync * Refactor to IUserService.IsManagedByAnyOrganizationAsync and IOrganizationService.GetUsersOrganizationManagementStatusAsync * chore: Refactor IsManagedByAnyOrganizationAsync method in UserService * Refactor IOrganizationService.GetUsersOrganizationManagementStatusAsync to return IDictionary<Guid, bool> * Extract IOrganizationService.GetUsersOrganizationManagementStatusAsync into a query * Update comments in OrganizationDomainService to use proper capitalization * Move OrganizationDomainService to AdminConsole ownership and update namespace * feat: Add support for organization domains in enterprise plans * feat: Add HasOrganizationDomains property to OrganizationAbility class * refactor: Update GetOrganizationUsersManagementStatusQuery to use IApplicationCacheService * Remove HasOrganizationDomains and use UseSso to check if Organization can have Verified Domains * Refactor UserService.IsManagedByAnyOrganizationAsync to simply check the UseSso flag * Add new event types for organization user deletion and voluntary departure * Add DeleteManagedOrganizationUserAccountCommand to remove user and delete account * Refactor DeleteManagedOrganizationUserAccountCommand to use orgUser.Id instead of orgUser.UserId.Value * Add DeleteManagedOrganizationUserAccountCommandTests * Remove duplicate sql migration script * Update DeleteManagedOrganizationUserAccountCommand methods to cover all existing checks on OrganizationService * Add unit tests for all user checks * Refactor DeleteManagedOrganizationUserAccountCommand * Set nullable enable annotation on DeleteManagedOrganizationUserAccountCommand * Fix possible null reference * Refactor DeleteManagedOrganizationUserAccountCommand.cs for improved event logging * Use UserRepository.GetByIdAsync instead of UserService.GetUserByIdAsync * Refactor DeleteManagedOrganizationUserAccountCommand.cs for improved error messages * Refactor DeleteManagedOrganizationUserAccountCommand.cs for improved event logging, error handling and reduce database calls * Rename unit tests to correctly describe expected outcome
This commit is contained in:
parent
42f6112c55
commit
6514b342fc
@ -46,7 +46,7 @@ public enum EventType : int
|
|||||||
OrganizationUser_Invited = 1500,
|
OrganizationUser_Invited = 1500,
|
||||||
OrganizationUser_Confirmed = 1501,
|
OrganizationUser_Confirmed = 1501,
|
||||||
OrganizationUser_Updated = 1502,
|
OrganizationUser_Updated = 1502,
|
||||||
OrganizationUser_Removed = 1503,
|
OrganizationUser_Removed = 1503, // Organization user data was deleted
|
||||||
OrganizationUser_UpdatedGroups = 1504,
|
OrganizationUser_UpdatedGroups = 1504,
|
||||||
OrganizationUser_UnlinkedSso = 1505,
|
OrganizationUser_UnlinkedSso = 1505,
|
||||||
OrganizationUser_ResetPassword_Enroll = 1506,
|
OrganizationUser_ResetPassword_Enroll = 1506,
|
||||||
@ -58,6 +58,8 @@ public enum EventType : int
|
|||||||
OrganizationUser_Restored = 1512,
|
OrganizationUser_Restored = 1512,
|
||||||
OrganizationUser_ApprovedAuthRequest = 1513,
|
OrganizationUser_ApprovedAuthRequest = 1513,
|
||||||
OrganizationUser_RejectedAuthRequest = 1514,
|
OrganizationUser_RejectedAuthRequest = 1514,
|
||||||
|
OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted
|
||||||
|
OrganizationUser_Left = 1516, // User voluntarily left the organization
|
||||||
|
|
||||||
Organization_Updated = 1600,
|
Organization_Updated = 1600,
|
||||||
Organization_PurgedVault = 1601,
|
Organization_PurgedVault = 1601,
|
||||||
|
@ -0,0 +1,160 @@
|
|||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
|
||||||
|
public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganizationUserAccountCommand
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IEventService _eventService;
|
||||||
|
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
public DeleteManagedOrganizationUserAccountCommand(
|
||||||
|
IUserService userService,
|
||||||
|
IEventService eventService,
|
||||||
|
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IOrganizationService organizationService)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_eventService = eventService;
|
||||||
|
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_organizationService = organizationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||||
|
{
|
||||||
|
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||||
|
if (organizationUser == null || organizationUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Member not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, new[] { organizationUserId });
|
||||||
|
var hasOtherConfirmedOwners = await _organizationService.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true);
|
||||||
|
|
||||||
|
await ValidateDeleteUserAsync(organizationId, organizationUser, deletingUserId, managementStatus, hasOtherConfirmedOwners);
|
||||||
|
|
||||||
|
var user = await _userRepository.GetByIdAsync(organizationUser.UserId!.Value);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Member not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userService.DeleteAsync(user);
|
||||||
|
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<(Guid OrganizationUserId, string? ErrorMessage)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId)
|
||||||
|
{
|
||||||
|
var orgUsers = await _organizationUserRepository.GetManyAsync(orgUserIds);
|
||||||
|
var userIds = orgUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId!.Value).ToList();
|
||||||
|
var users = await _userRepository.GetManyAsync(userIds);
|
||||||
|
|
||||||
|
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds);
|
||||||
|
var hasOtherConfirmedOwners = await _organizationService.HasConfirmedOwnersExceptAsync(organizationId, orgUserIds, includeProvider: true);
|
||||||
|
|
||||||
|
var results = new List<(Guid OrganizationUserId, string? ErrorMessage)>();
|
||||||
|
foreach (var orgUserId in orgUserIds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Member not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await ValidateDeleteUserAsync(organizationId, orgUser, deletingUserId, managementStatus, hasOtherConfirmedOwners);
|
||||||
|
|
||||||
|
var user = users.FirstOrDefault(u => u.Id == orgUser.UserId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Member not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userService.DeleteAsync(user);
|
||||||
|
results.Add((orgUserId, string.Empty));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results.Add((orgUserId, ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await LogDeletedOrganizationUsersAsync(orgUsers, results);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateDeleteUserAsync(Guid organizationId, OrganizationUser orgUser, Guid? deletingUserId, IDictionary<Guid, bool> managementStatus, bool hasOtherConfirmedOwners)
|
||||||
|
{
|
||||||
|
if (!orgUser.UserId.HasValue || orgUser.Status == OrganizationUserStatusType.Invited)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot delete a member with Invited status.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletingUserId.HasValue && orgUser.UserId.Value == deletingUserId.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot delete yourself.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orgUser.Type == OrganizationUserType.Owner)
|
||||||
|
{
|
||||||
|
if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(organizationId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Only owners can delete other owners.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasOtherConfirmedOwners)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!managementStatus.TryGetValue(orgUser.Id, out var isManaged) || !isManaged)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Member is not managed by the organization.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LogDeletedOrganizationUsersAsync(
|
||||||
|
IEnumerable<OrganizationUser> orgUsers,
|
||||||
|
IEnumerable<(Guid OrgUserId, string? ErrorMessage)> results)
|
||||||
|
{
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<(OrganizationUser OrgUser, EventType Event, DateTime? EventDate)>();
|
||||||
|
|
||||||
|
foreach (var (orgUserId, errorMessage) in results)
|
||||||
|
{
|
||||||
|
var orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId);
|
||||||
|
// If the user was not found or there was an error, we skip logging the event
|
||||||
|
if (orgUser == null || !string.IsNullOrEmpty(errorMessage))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.Add((orgUser, EventType.OrganizationUser_Deleted, eventDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.Any())
|
||||||
|
{
|
||||||
|
await _eventService.LogOrganizationUserEventsAsync(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
public interface IDeleteManagedOrganizationUserAccountCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a user from an organization and deletes all of their associated user data.
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes multiple users from an organization and deletes all of their associated user data.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// An error message for each user that could not be removed, otherwise null.
|
||||||
|
/// </returns>
|
||||||
|
Task<IEnumerable<(Guid OrganizationUserId, string? ErrorMessage)>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid? deletingUserId);
|
||||||
|
}
|
@ -91,6 +91,7 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IRemoveOrganizationUserCommand, RemoveOrganizationUserCommand>();
|
services.AddScoped<IRemoveOrganizationUserCommand, RemoveOrganizationUserCommand>();
|
||||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||||
|
services.AddScoped<IDeleteManagedOrganizationUserAccountCommand, DeleteManagedOrganizationUserAccountCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
||||||
|
@ -0,0 +1,492 @@
|
|||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class DeleteManagedOrganizationUserAccountCommandTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteUserAsync_WithValidUser_DeletesUserAndLogsEvent(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user, Guid deletingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetByIdAsync(user.Id)
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByIdAsync(organizationUser.Id)
|
||||||
|
.Returns(organizationUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
|
||||||
|
.GetUsersOrganizationManagementStatusAsync(
|
||||||
|
organizationUser.OrganizationId,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
|
||||||
|
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationService>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(
|
||||||
|
organizationUser.OrganizationId,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
|
||||||
|
includeProvider: Arg.Any<bool>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user);
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteUserAsync_WithUserNotFound_ThrowsException(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider,
|
||||||
|
Guid organizationId, Guid organizationUserId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByIdAsync(organizationUserId)
|
||||||
|
.Returns((OrganizationUser?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
|
sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Member not found.", exception.Message);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteUserAsync_DeletingYourself_ThrowsException(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider,
|
||||||
|
User user,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser,
|
||||||
|
Guid deletingUserId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = user.Id = deletingUserId;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByIdAsync(organizationUser.Id)
|
||||||
|
.Returns(organizationUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("You cannot delete yourself.", exception.Message);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteUserAsync_WhenUserIsInvited_ThrowsException(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByIdAsync(organizationUser.Id)
|
||||||
|
.Returns(organizationUser);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("You cannot delete a member with Invited status.", exception.Message);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteUserAsync_DeletingOwnerWhenNotOwner_ThrowsException(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
|
||||||
|
Guid deletingUserId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByIdAsync(organizationUser.Id)
|
||||||
|
.Returns(organizationUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.OrganizationOwner(organizationUser.OrganizationId)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Only owners can delete other owners.", exception.Message);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteUserAsync_DeletingLastConfirmedOwner_ThrowsException(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
|
||||||
|
Guid deletingUserId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByIdAsync(organizationUser.Id)
|
||||||
|
.Returns(organizationUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.OrganizationOwner(organizationUser.OrganizationId)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationService>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(
|
||||||
|
organizationUser.OrganizationId,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
|
||||||
|
includeProvider: Arg.Any<bool>())
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteUserAsync_WithUserNotManaged_ThrowsException(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByIdAsync(organizationUser.Id)
|
||||||
|
.Returns(organizationUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
|
||||||
|
.GetUsersOrganizationManagementStatusAsync(organizationUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, false } });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Member is not managed by the organization.", exception.Message);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user1, User user2, Guid organizationId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
|
||||||
|
orgUser1.UserId = user1.Id;
|
||||||
|
orgUser2.UserId = user2.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new List<OrganizationUser> { orgUser1, orgUser2 });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id)))
|
||||||
|
.Returns(new[] { user1, user2 });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
|
||||||
|
.GetUsersOrganizationManagementStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUser1.Id, orgUser2.Id }, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, results.Count());
|
||||||
|
Assert.All(results, r => Assert.Empty(r.Item2));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user1);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user2);
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
|
||||||
|
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1
|
||||||
|
&& events.Count(e => e.Item1.Id == orgUser2.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteManyUsersAsync_WhenUserNotFound_ReturnsErrorMessage(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid orgUserId)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUserId }, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(orgUserId, result.First().Item1);
|
||||||
|
Assert.Contains("Member not found.", result.First().Item2);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteManyUsersAsync_WhenDeletingYourself_ReturnsErrorMessage(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider,
|
||||||
|
User user, [OrganizationUser] OrganizationUser orgUser, Guid deletingUserId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.UserId = user.Id = deletingUserId;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new List<OrganizationUser> { orgUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||||
|
.Returns(new[] { user });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||||
|
Assert.Contains("You cannot delete yourself.", result.First().Item2);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteManyUsersAsync_WhenUserIsInvited_ReturnsErrorMessage(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.UserId = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new List<OrganizationUser> { orgUser });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||||
|
Assert.Contains("You cannot delete a member with Invited status.", result.First().Item2);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteManyUsersAsync_WhenDeletingOwnerAsNonOwner_ReturnsErrorMessage(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
|
||||||
|
Guid deletingUserId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new List<OrganizationUser> { orgUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
|
||||||
|
.Returns(new[] { user });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.OrganizationOwner(orgUser.OrganizationId)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||||
|
Assert.Contains("Only owners can delete other owners.", result.First().Item2);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteManyUsersAsync_WhenDeletingLastOwner_ReturnsErrorMessage(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
|
||||||
|
Guid deletingUserId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new List<OrganizationUser> { orgUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
|
||||||
|
.Returns(new[] { user });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.OrganizationOwner(orgUser.OrganizationId)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationService>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any<IEnumerable<Guid>>(), true)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||||
|
Assert.Contains("Organization must have at least one confirmed owner.", result.First().Item2);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteManyUsersAsync_WhenUserNotManaged_ReturnsErrorMessage(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new List<OrganizationUser> { orgUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.UserId.Value)))
|
||||||
|
.Returns(new[] { user });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
|
||||||
|
.GetUsersOrganizationManagementStatusAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new Dictionary<Guid, bool> { { orgUser.Id, false } });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(orgUser.Id, result.First().Item1);
|
||||||
|
Assert.Contains("Member is not managed by the organization.", result.First().Item2);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(0)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteManyUsersAsync_MixedValidAndInvalidUsers_ReturnsAppropriateResults(
|
||||||
|
SutProvider<DeleteManagedOrganizationUserAccountCommand> sutProvider, User user1, User user3,
|
||||||
|
Guid organizationId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser2,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser3)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser1.UserId = user1.Id;
|
||||||
|
orgUser2.UserId = null;
|
||||||
|
orgUser3.UserId = user3.Id;
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organizationId;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new List<OrganizationUser> { orgUser1, orgUser2, orgUser3 });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user3.Id)))
|
||||||
|
.Returns(new[] { user1, user3 });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
|
||||||
|
.GetUsersOrganizationManagementStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser3.Id, false } });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUser1.Id, orgUser2.Id, orgUser3.Id }, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, results.Count());
|
||||||
|
Assert.Empty(results.First(r => r.Item1 == orgUser1.Id).Item2);
|
||||||
|
Assert.Equal("You cannot delete a member with Invited status.", results.First(r => r.Item1 == orgUser2.Id).Item2);
|
||||||
|
Assert.Equal("Member is not managed by the organization.", results.First(r => r.Item1 == orgUser3.Id).Item2);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user1);
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
|
||||||
|
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user