From b3e89d87c0765e5c2e024081b9448b94a6f87415 Mon Sep 17 00:00:00 2001 From: Rui Tome Date: Thu, 17 Oct 2024 12:19:34 +0100 Subject: [PATCH] Implement UserLeaveAsync in IRemoveOrganizationUserCommand and refactor OrganizationsController to use it --- .../Controllers/OrganizationsController.cs | 2 +- .../IRemoveOrganizationUserCommand.cs | 6 + .../RemoveOrganizationUserCommand.cs | 36 +- .../OrganizationsControllerTests.cs | 4 +- .../RemoveOrganizationUserCommandTests.cs | 384 +++++++++++++++--- 5 files changed, 369 insertions(+), 63 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 6bcf75b35c..b7009022fc 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -248,7 +248,7 @@ public class OrganizationsController : Controller throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); } - await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id); + await _removeOrganizationUserCommand.UserLeaveAsync(id, user.Id); } [HttpDelete("{id}")] diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs index 583645a890..f4b8d48fdf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs @@ -11,4 +11,10 @@ public interface IRemoveOrganizationUserCommand Task RemoveUserAsync(Guid organizationId, Guid userId); Task>> RemoveUsersAsync(Guid organizationId, IEnumerable organizationUserIds, Guid? deletingUserId); + /// + /// User leaves organization. + /// + /// Organization to leave. + /// User to leave. + Task UserLeaveAsync(Guid organizationId, Guid userId); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index 09444306e6..9a81eb5fb4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -91,7 +91,8 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand } var result = new List>(); - var deletedUserIds = new List(); + var usersToDelete = new List(); + foreach (var orgUser in filteredUsers) { try @@ -106,26 +107,43 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand throw new BadRequestException("Only owners can delete other owners."); } + usersToDelete.Add(orgUser); + result.Add(Tuple.Create(orgUser, "")); + } + catch (BadRequestException e) + { + result.Add(Tuple.Create(orgUser, e.Message)); + } + } + + if (usersToDelete.Any()) + { + await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id)); + + foreach (var orgUser in usersToDelete) + { 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; } + public async Task UserLeaveAsync(Guid organizationId, Guid userId) + { + var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + ValidateDeleteUser(organizationId, organizationUser); + + await RepositoryDeleteUserAsync(organizationUser, null); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left); + } + private void ValidateDeleteUser(Guid organizationId, OrganizationUser orgUser) { if (orgUser == null || orgUser.OrganizationId != organizationId) diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 25227fec7b..9fb4d4d142 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -129,7 +129,7 @@ public class OrganizationsControllerTests : IDisposable Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.", exception.Message); - await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default); + await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().UserLeaveAsync(default, default); } [Theory] @@ -160,7 +160,7 @@ public class OrganizationsControllerTests : IDisposable await _sut.Leave(orgId); - await _removeOrganizationUserCommand.Received(1).RemoveUserAsync(orgId, user.Id); + await _removeOrganizationUserCommand.Received(1).UserLeaveAsync(orgId, user.Id); } [Theory, AutoData] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs index 2d10ce626b..2eb740fd83 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs @@ -23,18 +23,25 @@ public class RemoveOrganizationUserCommandTests [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - organizationUser.OrganizationId = deletingUser.OrganizationId; + + var organizationUserRepository = sutProvider.GetDependency(); organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); + + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); 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); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } [Theory] @@ -43,6 +50,14 @@ public class RemoveOrganizationUserCommandTests Guid organizationId, Guid organizationUserId) { await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory] @@ -59,6 +74,14 @@ public class RemoveOrganizationUserCommandTests }); await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -66,25 +89,45 @@ public class RemoveOrganizationUserCommandTests OrganizationUser organizationUser, OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); + organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId)); + Assert.Contains("User not found.", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] public async Task RemoveUser_RemoveYourself_ThrowsException(OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId)); + Assert.Contains("You cannot remove yourself.", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -93,16 +136,26 @@ public class RemoveOrganizationUserCommandTests [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - 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(deletingUser.OrganizationId) + .Returns(true); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); Assert.Contains("Only owners can delete other owners.", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -111,24 +164,35 @@ public class RemoveOrganizationUserCommandTests OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( - deletingUser.OrganizationId, - Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()) + + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()) .Returns(false); 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.GetDependency() .Received(1) .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(i => i.Contains(organizationUser.Id)), true); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -137,13 +201,19 @@ public class RemoveOrganizationUserCommandTests EventSystemUser eventSystemUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + sutProvider.GetDependency() + .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); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); } [Theory, BitAutoData] @@ -151,9 +221,7 @@ public class RemoveOrganizationUserCommandTests [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository + sutProvider.GetDependency() .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) .Returns(organizationUser); @@ -166,7 +234,13 @@ public class RemoveOrganizationUserCommandTests await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } [Theory, BitAutoData] @@ -174,19 +248,35 @@ public class RemoveOrganizationUserCommandTests Guid organizationId, Guid userId) { await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value)); Assert.Contains("User not found.", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -194,11 +284,10 @@ 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 + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(i => i.Contains(organizationUser.Id)), @@ -207,27 +296,47 @@ public class RemoveOrganizationUserCommandTests 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 + + _ = sutProvider.GetDependency() .Received(1) .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [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); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); + Assert.Contains("Users invalid.", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -235,16 +344,26 @@ public class RemoveOrganizationUserCommandTests 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() + .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); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -265,7 +384,16 @@ public class RemoveOrganizationUserCommandTests .Returns(true); var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + Assert.Contains("Only owners can delete other owners.", result[0].Item2); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -282,7 +410,50 @@ public class RemoveOrganizationUserCommandTests var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null)); + Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_OneErrorOneSuccess( + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser, + SutProvider sutProvider) + { + orgUser.OrganizationId = deletingUser.OrganizationId; + + var organizationUsers = new[] { deletingUser, orgUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + + Assert.Contains("You cannot remove yourself.", result[0].Item2); + Assert.Contains("", result[1].Item2); + + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(ids => ids.Count() == 1 && ids.Contains(orgUser.Id))); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); } [Theory, BitAutoData] @@ -291,19 +462,130 @@ public class RemoveOrganizationUserCommandTests [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - 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() .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) .Returns(true); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(ids => ids.Count() == 2 && ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id))); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(Arg.Is(u => u.Id == orgUser1.Id), EventType.OrganizationUser_Removed); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(Arg.Is(u => u.Id == orgUser2.Id), EventType.OrganizationUser_Removed); + } + + [Theory, BitAutoData] + public async Task UserLeave_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + 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(true); + + await sutProvider.Sut.UserLeaveAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left); + } + + [Theory, BitAutoData] + public async Task UserLeave_NotFound_ThrowsException(SutProvider sutProvider, + Guid organizationId, Guid userId) + { + await Assert.ThrowsAsync(async () => await sutProvider.Sut.UserLeaveAsync(organizationId, userId)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); + } + + [Theory, BitAutoData] + public async Task UserLeave_InvalidUser_ThrowsException(OrganizationUser organizationUser, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UserLeaveAsync(Guid.NewGuid(), organizationUser.UserId.Value)); + + Assert.Contains("User not found.", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); + } + + [Theory, BitAutoData] + public async Task UserLeave_RemovingLastOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + 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); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UserLeaveAsync(organizationUser.OrganizationId, organizationUser.UserId.Value)); + + Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); + _ = sutProvider.GetDependency() + .Received(1) + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } }