diff --git a/src/Core/Auth/Enums/EmergencyAccessStatusType.cs b/src/Core/Auth/Enums/EmergencyAccessStatusType.cs index 7faaa11752..d817d6a950 100644 --- a/src/Core/Auth/Enums/EmergencyAccessStatusType.cs +++ b/src/Core/Auth/Enums/EmergencyAccessStatusType.cs @@ -2,9 +2,24 @@ public enum EmergencyAccessStatusType : byte { + /// + /// The user has been invited to be an emergency contact. + /// Invited = 0, + /// + /// The invited user, "grantee", has accepted the request to be an emergency contact. + /// Accepted = 1, + /// + /// The inviting user, "grantor", has approved the grantee's acceptance. + /// Confirmed = 2, + /// + /// The grantee has initiated the recovery process. + /// RecoveryInitiated = 3, + /// + /// The grantee has excercised their emergency access. + /// RecoveryApproved = 4, } diff --git a/src/Core/Auth/Services/IEmergencyAccessService.cs b/src/Core/Auth/Services/IEmergencyAccessService.cs index 2c94632510..6dd17151e6 100644 --- a/src/Core/Auth/Services/IEmergencyAccessService.cs +++ b/src/Core/Auth/Services/IEmergencyAccessService.cs @@ -3,6 +3,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Vault.Models.Data; @@ -20,6 +21,15 @@ public interface IEmergencyAccessService Task InitiateAsync(Guid id, User initiatingUser); Task ApproveAsync(Guid id, User approvingUser); Task RejectAsync(Guid id, User rejectingUser); + /// + /// This request is made by the Grantee user to fetch the policies for the Grantor User. + /// The Grantor User has to be the owner of the organization. + /// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user + /// are returned. + /// + /// EmergencyAccess.Id being acted on + /// User making the request, this is the Grantee + /// null if the GrantorUser is not an organization owner; A list of policies otherwise. Task> GetPoliciesAsync(Guid id, User requestingUser); Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser); Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key); diff --git a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs index dda16e29fe..2418830ea7 100644 --- a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; @@ -16,7 +15,6 @@ using Bit.Core.Tokens; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Services; -using Microsoft.AspNetCore.Identity; namespace Bit.Core.Auth.Services; @@ -31,8 +29,6 @@ public class EmergencyAccessService : IEmergencyAccessService private readonly IMailService _mailService; private readonly IUserService _userService; private readonly GlobalSettings _globalSettings; - private readonly IPasswordHasher _passwordHasher; - private readonly IOrganizationService _organizationService; private readonly IDataProtectorTokenFactory _dataProtectorTokenizer; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; @@ -45,9 +41,7 @@ public class EmergencyAccessService : IEmergencyAccessService ICipherService cipherService, IMailService mailService, IUserService userService, - IPasswordHasher passwordHasher, GlobalSettings globalSettings, - IOrganizationService organizationService, IDataProtectorTokenFactory dataProtectorTokenizer, IRemoveOrganizationUserCommand removeOrganizationUserCommand) { @@ -59,9 +53,7 @@ public class EmergencyAccessService : IEmergencyAccessService _cipherService = cipherService; _mailService = mailService; _userService = userService; - _passwordHasher = passwordHasher; _globalSettings = globalSettings; - _organizationService = organizationService; _dataProtectorTokenizer = dataProtectorTokenizer; _removeOrganizationUserCommand = removeOrganizationUserCommand; } @@ -126,7 +118,12 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Emergency Access not valid."); } - if (!_dataProtectorTokenizer.TryUnprotect(token, out var data) && data.IsValid(emergencyAccessId, user.Email)) + if (!_dataProtectorTokenizer.TryUnprotect(token, out var data)) + { + throw new BadRequestException("Invalid token."); + } + + if (!data.IsValid(emergencyAccessId, user.Email)) { throw new BadRequestException("Invalid token."); } @@ -140,6 +137,8 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Invitation already accepted."); } + // TODO PM-21687 + // Might not be reachable since the Tokenable.IsValid() does an email comparison if (string.IsNullOrWhiteSpace(emergencyAccess.Email) || !emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) { @@ -163,6 +162,8 @@ public class EmergencyAccessService : IEmergencyAccessService public async Task DeleteAsync(Guid emergencyAccessId, Guid grantorId) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); + // TODO PM-19438/PM-21687 + // Not sure why the GrantorId and the GranteeId are supposed to be the same? if (emergencyAccess == null || (emergencyAccess.GrantorId != grantorId && emergencyAccess.GranteeId != grantorId)) { throw new BadRequestException("Emergency Access not valid."); @@ -171,9 +172,9 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.DeleteAsync(emergencyAccess); } - public async Task ConfirmUserAsync(Guid emergencyAcccessId, string key, Guid confirmingUserId) + public async Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid confirmingUserId) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAcccessId); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted || emergencyAccess.GrantorId != confirmingUserId) { @@ -224,7 +225,6 @@ public class EmergencyAccessService : IEmergencyAccessService public async Task InitiateAsync(Guid id, User initiatingUser) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); - if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id || emergencyAccess.Status != EmergencyAccessStatusType.Confirmed) { @@ -285,6 +285,9 @@ public class EmergencyAccessService : IEmergencyAccessService public async Task> GetPoliciesAsync(Guid id, User requestingUser) { + // TODO PM-21687 + // Should we look up policies here or just verify the EmergencyAccess is correct + // and handle policy logic else where? Should this be a query/Command? var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) @@ -295,7 +298,9 @@ public class EmergencyAccessService : IEmergencyAccessService var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId); var grantorOrganizations = await _organizationUserRepository.GetManyByUserAsync(grantor.Id); - var isOrganizationOwner = grantorOrganizations.Any(organization => organization.Type == OrganizationUserType.Owner); + var isOrganizationOwner = grantorOrganizations + .Any(organization => organization.Type == OrganizationUserType.Owner); + var policies = isOrganizationOwner ? await _policyRepository.GetManyByUserIdAsync(grantor.Id) : null; return policies; @@ -311,7 +316,8 @@ public class EmergencyAccessService : IEmergencyAccessService } var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId); - + // TODO PM-21687 + // Redundant check of the EmergencyAccessType -> checked in IsValidRequest() ln 308 if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector) { throw new BadRequestException("You cannot takeover an account that is using Key Connector."); @@ -336,7 +342,9 @@ public class EmergencyAccessService : IEmergencyAccessService grantor.LastPasswordChangeDate = grantor.RevisionDate; grantor.Key = key; // Disable TwoFactor providers since they will otherwise block logins - grantor.SetTwoFactorProviders(new Dictionary()); + grantor.SetTwoFactorProviders([]); + // Disable New Device Verification since it will otherwise block logins + grantor.VerifyDevices = false; await _userRepository.ReplaceAsync(grantor); // Remove grantor from all organizations unless Owner @@ -421,12 +429,22 @@ public class EmergencyAccessService : IEmergencyAccessService await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token); } - private string NameOrEmail(User user) + private static string NameOrEmail(User user) { return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name; } - private bool IsValidRequest(EmergencyAccess availableAccess, User requestingUser, EmergencyAccessType requestedAccessType) + + /* + * Checks if EmergencyAccess Object is null + * Checks the requesting user is the same as the granteeUser (So we are checking for proper grantee action) + * Status _must_ equal RecoveryApproved (This means the grantor has invited, the grantee has accepted, and the grantor has approved so the shared key exists but hasn't been exercised yet) + * request type must equal the type of access requested (View or Takeover) + */ + private static bool IsValidRequest( + EmergencyAccess availableAccess, + User requestingUser, + EmergencyAccessType requestedAccessType) { return availableAccess != null && availableAccess.GranteeId == requestingUser.Id && diff --git a/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs b/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs index 6c2352ca00..006515aafd 100644 --- a/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs +++ b/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs @@ -1,11 +1,17 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Services; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Tokens; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -17,27 +23,21 @@ namespace Bit.Core.Test.Auth.Services; public class EmergencyAccessServiceTests { [Theory, BitAutoData] - public async Task SaveAsync_PremiumCannotUpdate( - SutProvider sutProvider, User savingUser) + public async Task InviteAsync_UserWithOutPremium_ThrowsBadRequest( + SutProvider sutProvider, User invitingUser, string email, int waitTime) { - savingUser.Premium = false; - var emergencyAccess = new EmergencyAccess - { - Type = EmergencyAccessType.Takeover, - GrantorId = savingUser.Id, - }; - - sutProvider.GetDependency().GetUserByIdAsync(savingUser.Id).Returns(savingUser); + sutProvider.GetDependency().CanAccessPremium(invitingUser).Returns(false); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime)); Assert.Contains("Not a premium user.", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().CreateAsync(default); } [Theory, BitAutoData] - public async Task InviteAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task InviteAsync_UserWithKeyConnector_ThrowsBadRequest( SutProvider sutProvider, User invitingUser, string email, int waitTime) { invitingUser.UsesKeyConnector = true; @@ -47,11 +47,461 @@ public class EmergencyAccessServiceTests () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime)); Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().CreateAsync(default); + } + + [Theory] + [BitAutoData(EmergencyAccessType.Takeover)] + [BitAutoData(EmergencyAccessType.View)] + public async Task InviteAsync_ReturnsEmergencyAccessObject( + EmergencyAccessType accessType, SutProvider sutProvider, User invitingUser, string email, int waitTime) + { + sutProvider.GetDependency().CanAccessPremium(invitingUser).Returns(true); + + var result = await sutProvider.Sut.InviteAsync(invitingUser, email, accessType, waitTime); + + Assert.NotNull(result); + Assert.Equal(accessType, result.Type); + Assert.Equal(invitingUser.Id, result.GrantorId); + Assert.Equal(email, result.Email); + Assert.Equal(EmergencyAccessStatusType.Invited, result.Status); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + sutProvider.GetDependency>() + .Received(1) + .Protect(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessInviteEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] - public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task GetAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, User user) + { + EmergencyAccessDetails emergencyAccess = null; + sutProvider.GetDependency() + .GetDetailsByIdGrantorIdAsync(Arg.Any(), Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetAsync(new Guid(), user.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + EmergencyAccess emergencyAccess = null; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_InvitingUserIdNotGrantorUserId_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Invited, + GrantorId = Guid.NewGuid(), + Type = EmergencyAccessType.Takeover, + }; ; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task ResendInviteAsync_EmergencyAccessStatusInvalid_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = statusType, + GrantorId = invitingUser.Id, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_SendsInviteAsync( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Invited, + GrantorId = invitingUser.Id, + Type = EmergencyAccessType.Takeover, + }; ; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + await sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId); + sutProvider.GetDependency>() + .Received(1) + .Protect(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUser.Name, Arg.Any()); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, User acceptingUser, string token) + { + EmergencyAccess emergencyAccess = null; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(new Guid(), acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_CannotUnprotectToken_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invalid token.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_TokenDataInvalid_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + EmergencyAccess wrongEmergencyAccess, + string token) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(wrongEmergencyAccess, 1); + return true; + }); + + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invalid token.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_AcceptedStatus_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invitation already accepted. You will receive an email when the grantor confirms you as an emergency access contact.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_NotInvitedStatus_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Confirmed; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invitation already accepted.", exception.Message); + } + + [Theory(Skip = "Code not reachable, Tokenable checks email match in IsValid()"), BitAutoData] + public async Task AcceptUserAsync_EmergencyAccessEmailDoesNotMatch_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("User email does not match invite.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_ReplaceEmergencyAccess_SendsEmail_Success( + SutProvider sutProvider, + User acceptingUser, + User invitingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetUserByIdAsync(Arg.Any()) + .Returns(invitingUser); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency()); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Accepted)); + + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessAcceptedEmailAsync(acceptingUser.Email, invitingUser.Email); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessGrantorIdNotEqual_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GrantorId = Guid.NewGuid(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessGranteeIdNotEqual_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GranteeId = Guid.NewGuid(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessIsDeleted_Success( + SutProvider sutProvider, + User user, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GranteeId = user.Id; + emergencyAccess.GrantorId = user.Id; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + await sutProvider.Sut.DeleteAsync(emergencyAccess.Id, user.Id); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessStatusIsNotAccepted_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessGrantorIdNotEqualToConfirmingUserId_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( SutProvider sutProvider, User confirmingUser, string key) { confirmingUser.UsesKeyConnector = true; @@ -62,8 +512,13 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(confirmingUser.Id).Returns(confirmingUser); - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(confirmingUser.Id) + .Returns(confirmingUser); + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ConfirmUserAsync(new Guid(), key, confirmingUser.Id)); @@ -73,29 +528,210 @@ public class EmergencyAccessServiceTests } [Theory, BitAutoData] - public async Task SaveAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task ConfirmUserAsync_ConfirmsAndReplacesEmergencyAccess_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser, + User granteeUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantorUser.Id) + .Returns(grantorUser); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GranteeId.Value) + .Returns(granteeUser); + + await sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Confirmed)); + + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessConfirmedEmailAsync(grantorUser.Name, granteeUser.Email); + } + + [Theory, BitAutoData] + public async Task SaveAsync_PremiumCannotUpdate_ThrowsBadRequest( SutProvider sutProvider, User savingUser) { - savingUser.UsesKeyConnector = true; var emergencyAccess = new EmergencyAccess { Type = EmergencyAccessType.Takeover, GrantorId = savingUser.Id, }; - var userService = sutProvider.GetDependency(); - userService.GetUserByIdAsync(savingUser.Id).Returns(savingUser); - userService.CanAccessPremium(savingUser).Returns(true); + sutProvider.GetDependency() + .CanAccessPremium(savingUser) + .Returns(false); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + Assert.Contains("Not a premium user.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task SaveAsync_EmergencyAccessGrantorIdNotEqualToSavingUserId_ThrowsBadRequest( + SutProvider sutProvider, User savingUser) + { + savingUser.Premium = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = new Guid(), + }; + + sutProvider.GetDependency() + .GetUserByIdAsync(savingUser.Id) + .Returns(savingUser); + sutProvider.GetDependency() + .CanAccessPremium(savingUser) + .Returns(true); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task SaveAsync_GrantorUserWithKeyConnectorCannotTakeover_ThrowsBadRequest( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser)); + Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); } [Theory, BitAutoData] - public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task SaveAsync_GrantorUserWithKeyConnectorCanView_SavesEmergencyAccess( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.View, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + await sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task SaveAsync_ValidRequest_SavesEmergencyAccess( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = false; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + await sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessNull_ThrowBadRequest( + SutProvider sutProvider, User initiatingUser) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessGranteeIdNotEqual_ThrowBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User initiatingUser) + { + emergencyAccess.GranteeId = new Guid(); + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessStatusIsNotConfirmed_ThrowBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User initiatingUser) + { + emergencyAccess.GranteeId = initiatingUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( SutProvider sutProvider, User initiatingUser, User grantor) { grantor.UsesKeyConnector = true; @@ -107,40 +743,711 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); } [Theory, BitAutoData] - public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover( - SutProvider sutProvider, User requestingUser, User grantor) + public async Task InitiateAsync_UserWithKeyConnectorCanView_Success( + SutProvider sutProvider, User initiatingUser, User grantor) + { + grantor.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Confirmed, + GranteeId = initiatingUser.Id, + GrantorId = grantor.Id, + Type = EmergencyAccessType.View, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + await sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated)); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_RequestIsCorrect_Success( + SutProvider sutProvider, User initiatingUser, User grantor) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Confirmed, + GranteeId = initiatingUser.Id, + GrantorId = grantor.Id, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + await sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated)); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_EmergencyAccessNull_ThrowsBadrequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(new Guid(), null)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_EmergencyAccessGrantorIdNotEquatToApproving_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task ApproveAsync_EmergencyAccessStatusNotRecoveryInitiated_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser, + User granteeUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(granteeUser); + + await sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryApproved)); + } + + [Theory, BitAutoData] + public async Task RejectAsync_EmergencyAccessIdNull_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RejectAsync_EmergencyAccessGrantorIdNotEqualToRequestUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + public async Task RejectAsync_EmergencyAccessStatusNotValid_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task RejectAsync_Success( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser, + User GranteeUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(GranteeUser); + + await sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Confirmed)); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_RequestNotValidEmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(default, default)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task GetPoliciesAsync_RequestNotValidStatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_RequestNotValidType_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task GetPoliciesAsync_OrganizationUserTypeNotOwner_ReturnsNull( + OrganizationUserType userType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser grantorOrganizationUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + grantorOrganizationUser.UserId = grantorUser.Id; + grantorOrganizationUser.Type = userType; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([grantorOrganizationUser]); + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_OrganizationUserEmpty_ReturnsNull( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([]); + + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_ReturnsNotNull( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser grantorOrganizationUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + grantorOrganizationUser.UserId = grantorUser.Id; + grantorOrganizationUser.Type = OrganizationUserType.Owner; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([grantorOrganizationUser]); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(grantorUser.Id) + .Returns([]); + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.NotNull(result); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_EmergencyAccessIsNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task TakeoverAsync_RequestNotValid_StatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_TypeIsView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( + SutProvider sutProvider, + User granteeUser, + User grantor) { grantor.UsesKeyConnector = true; var emergencyAccess = new EmergencyAccess { GrantorId = grantor.Id, - GranteeId = requestingUser.Id, + GranteeId = granteeUser.Id, Status = EmergencyAccessStatusType.RecoveryApproved, Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.TakeoverAsync(new Guid(), requestingUser)); + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message); } [Theory, BitAutoData] - public async Task PasswordAsync_Disables_2FA_Providers_On_The_Grantor( + public async Task TakeoverAsync_Success_ReturnsEmergencyAccessAndGrantorUser( + SutProvider sutProvider, + User granteeUser, + User grantor) + { + grantor.UsesKeyConnector = false; + var emergencyAccess = new EmergencyAccess + { + GrantorId = grantor.Id, + GranteeId = granteeUser.Id, + Status = EmergencyAccessStatusType.RecoveryApproved, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + var result = await sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser); + + Assert.Equal(result.Item1, emergencyAccess); + Assert.Equal(result.Item2, grantor); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_EmergencyAccessIsNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(default, default, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task PasswordAsync_RequestNotValid_StatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_TypeIsView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_NonOrgUser_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task PasswordAsync_OrgUser_NotOrganizationOwner_RemovedFromOrganization_Success( + OrganizationUserType userType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser organizationUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + organizationUser.UserId = grantorUser.Id; + organizationUser.Type = userType; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([organizationUser]); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + await sutProvider.GetDependency() + .Received(1) + .RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_OrgUser_IsOrganizationOwner_NotRemovedFromOrganization_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser organizationUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + organizationUser.UserId = grantorUser.Id; + organizationUser.Type = OrganizationUserType.Owner; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([organizationUser]); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + await sutProvider.GetDependency() + .Received(0) + .RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_Disables_NewDeviceVerification_And_TwoFactorProviders_On_The_Grantor( SutProvider sutProvider, User requestingUser, User grantor) { grantor.UsesKeyConnector = true; @@ -160,12 +1467,49 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); await sutProvider.Sut.PasswordAsync(Guid.NewGuid(), requestingUser, "blablahash", "blablakey"); Assert.Empty(grantor.GetTwoFactorProviders()); + Assert.False(grantor.VerifyDevices); await sutProvider.GetDependency().Received().ReplaceAsync(grantor); } + + [Theory, BitAutoData] + public async Task ViewAsync_EmergencyAccessTypeNotView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ViewAsync(emergencyAccess.Id, granteeUser)); + } + + [Theory, BitAutoData] + public async Task GetAttachmentDownloadAsync_EmergencyAccessTypeNotView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetAttachmentDownloadAsync(emergencyAccess.Id, default, default, granteeUser)); + } }