You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
693 lines
29 KiB
693 lines
29 KiB
using Bit.Core.AdminConsole.Entities; |
|
using Bit.Core.AdminConsole.Enums; |
|
using Bit.Core.AdminConsole.Services; |
|
using Bit.Core.Auth.Models.Business.Tokenables; |
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; |
|
using Bit.Core.Billing.Enums; |
|
using Bit.Core.Entities; |
|
using Bit.Core.Enums; |
|
using Bit.Core.Exceptions; |
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers; |
|
using Bit.Core.OrganizationFeatures.OrganizationUsers; |
|
using Bit.Core.Repositories; |
|
using Bit.Core.Services; |
|
using Bit.Core.Settings; |
|
using Bit.Core.Test.AutoFixture.OrganizationFixtures; |
|
using Bit.Core.Tokens; |
|
using Bit.Core.Utilities; |
|
using Bit.Test.Common.AutoFixture; |
|
using Bit.Test.Common.AutoFixture.Attributes; |
|
using Bit.Test.Common.Fakes; |
|
using Microsoft.AspNetCore.DataProtection; |
|
using NSubstitute; |
|
using Xunit; |
|
|
|
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers; |
|
|
|
// Note: test names follow MethodName_StateUnderTest_ExpectedBehavior pattern. |
|
[SutProviderCustomize] |
|
public class AcceptOrgUserCommandTests |
|
{ |
|
private readonly IUserService _userService = Substitute.For<IUserService>(); |
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>(); |
|
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>(); |
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(); |
|
|
|
// Base AcceptOrgUserAsync method tests ---------------------------------------------------------------------------- |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUser_InvitedUserToSingleOrg_AcceptsOrgUser( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
|
|
// Act |
|
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService); |
|
|
|
// Assert |
|
// Verify returned org user details |
|
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user); |
|
|
|
// Verify org repository called with updated orgUser |
|
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync( |
|
Arg.Is<OrganizationUser>(ou => ou.Id == orgUser.Id && ou.Status == OrganizationUserStatusType.Accepted)); |
|
|
|
// Verify emails sent to admin |
|
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationAcceptedEmailAsync( |
|
Arg.Is<Organization>(o => o.Id == org.Id), |
|
Arg.Is<string>(e => e == user.Email), |
|
Arg.Is<IEnumerable<string>>(a => a.Contains(adminUserDetails.Email)) |
|
); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUser_OrgUserStatusIsRevoked_ReturnsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Common setup |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
|
|
// Revoke user status |
|
orgUser.Status = OrganizationUserStatusType.Revoked; |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => |
|
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); |
|
|
|
Assert.Equal("Your organization access has been revoked.", exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData(OrganizationUserStatusType.Accepted)] |
|
[BitAutoData(OrganizationUserStatusType.Confirmed)] |
|
public async Task AcceptOrgUser_OrgUserStatusIsNotInvited_ThrowsBadRequest( |
|
OrganizationUserStatusType orgUserStatus, |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
|
|
// Set status to something other than invited |
|
orgUser.Status = orgUserStatus; |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => |
|
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); |
|
|
|
Assert.Equal("Already accepted.", exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUser_UserJoiningOrgWithSingleOrgPolicyWhileInAnotherOrg_ThrowsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
|
|
// Make user part of another org |
|
var otherOrgUser = new OrganizationUser { UserId = user.Id, OrganizationId = Guid.NewGuid() }; // random org ID |
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetManyByUserAsync(user.Id) |
|
.Returns(Task.FromResult<ICollection<OrganizationUser>>(new List<OrganizationUser> { otherOrgUser })); |
|
|
|
// Make organization they are trying to join have the single org policy |
|
var singleOrgPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId }; |
|
sutProvider.GetDependency<IPolicyService>() |
|
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg, OrganizationUserStatusType.Invited) |
|
.Returns(Task.FromResult<ICollection<OrganizationUserPolicyDetails>>( |
|
new List<OrganizationUserPolicyDetails> { singleOrgPolicy })); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => |
|
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); |
|
|
|
Assert.Equal("You may not join this organization until you leave or remove all other organizations.", |
|
exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
|
|
// Mock that user is part of an org that has the single org policy |
|
sutProvider.GetDependency<IPolicyService>() |
|
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg) |
|
.Returns(true); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => |
|
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); |
|
|
|
Assert.Equal( |
|
"You cannot join this organization because you are a member of another organization which forbids it", |
|
exception.Message); |
|
} |
|
|
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
|
|
// User doesn't have 2FA enabled |
|
_twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); |
|
|
|
// Organization they are trying to join requires 2FA |
|
var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId }; |
|
sutProvider.GetDependency<IPolicyService>() |
|
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication, |
|
OrganizationUserStatusType.Invited) |
|
.Returns(Task.FromResult<ICollection<OrganizationUserPolicyDetails>>( |
|
new List<OrganizationUserPolicyDetails> { twoFactorPolicy })); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => |
|
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); |
|
|
|
Assert.Equal("You cannot join this organization until you enable two-step login on your user account.", |
|
exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData(OrganizationUserType.Admin)] |
|
[BitAutoData(OrganizationUserType.Owner)] |
|
public async Task AcceptOrgUser_AdminOfFreePlanTryingToJoinSecondFreeOrg_ThrowsBadRequest( |
|
OrganizationUserType userType, |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
org.PlanType = PlanType.Free; |
|
orgUser.Type = userType; |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetCountByFreeOrganizationAdminUserAsync(user.Id) |
|
.Returns(1); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => |
|
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); |
|
|
|
Assert.Equal("You can only be an admin of one free organization.", exception.Message); |
|
} |
|
|
|
// AcceptOrgUserByOrgIdAsync tests -------------------------------------------------------------------------------- |
|
|
|
[Theory] |
|
[EphemeralDataProtectionAutoData] |
|
public async Task AcceptOrgUserByToken_OldToken_AcceptsUserAndVerifiesEmail( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser); |
|
|
|
var oldToken = CreateOldToken(sutProvider, orgUser); |
|
|
|
// Act |
|
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, oldToken, _userService); |
|
|
|
// Assert |
|
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user); |
|
|
|
// Verify user email verified logic |
|
Assert.True(user.EmailVerified); |
|
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync( |
|
Arg.Is<User>(u => u.Id == user.Id && u.Email == user.Email && user.EmailVerified == true)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByToken_NewToken_AcceptsUserAndVerifiesEmail( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order |
|
// to avoid resetting mocks |
|
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); |
|
sutProvider.Create(); |
|
|
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser); |
|
|
|
// Must come after common mocks as they mutate the org user. |
|
// Mock tokenable factory to return a token that expires in 5 days |
|
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser) |
|
{ |
|
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) |
|
}); |
|
|
|
var newToken = CreateNewToken(orgUser); |
|
|
|
// Act |
|
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService); |
|
|
|
// Assert |
|
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user); |
|
|
|
// Verify user email verified logic |
|
Assert.True(user.EmailVerified); |
|
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync( |
|
Arg.Is<User>(u => u.Id == user.Id && u.Email == user.Email && user.EmailVerified == true)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByToken_NullOrgUser_ThrowsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Guid orgUserId) |
|
{ |
|
// Arrange |
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByIdAsync(orgUserId).Returns((OrganizationUser)null); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUserId, user, "token", _userService)); |
|
|
|
Assert.Equal("User invalid.", exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByToken_GenericInvalidToken_ThrowsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, OrganizationUser orgUser) |
|
{ |
|
// Arrange |
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByIdAsync(orgUser.Id) |
|
.Returns(Task.FromResult(orgUser)); |
|
|
|
var invalidToken = "invalidToken"; |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, invalidToken, _userService)); |
|
|
|
Assert.Equal("Invalid token.", exception.Message); |
|
} |
|
|
|
[Theory] |
|
[EphemeralDataProtectionAutoData] |
|
public async Task AcceptOrgUserByToken_ExpiredOldToken_ThrowsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser); |
|
|
|
// As the old token simply set a timestamp which was later compared against the |
|
// OrganizationInviteExpirationHours global setting to determine if it was expired or not, |
|
// we can simply set the expiration to 24 hours ago to simulate an expired token. |
|
sutProvider.GetDependency<IGlobalSettings>().OrganizationInviteExpirationHours.Returns(-24); |
|
|
|
var oldToken = CreateOldToken(sutProvider, orgUser); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, oldToken, _userService)); |
|
|
|
Assert.Equal("Invalid token.", exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByToken_ExpiredNewToken_ThrowsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, OrganizationUser orgUser) |
|
{ |
|
// Arrange |
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order |
|
// to avoid resetting mocks |
|
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); |
|
sutProvider.Create(); |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByIdAsync(orgUser.Id) |
|
.Returns(Task.FromResult(orgUser)); |
|
|
|
// Must come after common mocks as they mutate the org user. |
|
// Mock tokenable factory to return a token that expired yesterday |
|
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser) |
|
{ |
|
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(-1)) |
|
}); |
|
|
|
var newToken = CreateNewToken(orgUser); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService)); |
|
|
|
Assert.Equal("Invalid token.", exception.Message); |
|
|
|
} |
|
|
|
[Theory] |
|
[BitAutoData(OrganizationUserStatusType.Accepted, |
|
"Invitation already accepted. You will receive an email when your organization membership is confirmed.")] |
|
[BitAutoData(OrganizationUserStatusType.Confirmed, |
|
"You are already part of this organization.")] |
|
public async Task AcceptOrgUserByToken_UserAlreadyInOrg_ThrowsBadRequest( |
|
OrganizationUserStatusType statusType, |
|
string expectedErrorMessage, |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, OrganizationUser orgUser) |
|
{ |
|
// Arrange |
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order |
|
// to avoid resetting mocks |
|
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); |
|
sutProvider.Create(); |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByIdAsync(orgUser.Id) |
|
.Returns(Task.FromResult(orgUser)); |
|
|
|
// Indicate that a user with the given email already exists in the organization |
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetCountByOrganizationAsync(orgUser.OrganizationId, user.Email, true) |
|
.Returns(1); |
|
|
|
orgUser.Status = statusType; |
|
|
|
// Must come after common mocks as they mutate the org user. |
|
// Mock tokenable factory to return valid, new token that expires in 5 days |
|
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser) |
|
{ |
|
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) |
|
}); |
|
|
|
var newToken = CreateNewToken(orgUser); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService)); |
|
|
|
Assert.Equal(expectedErrorMessage, exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByToken_EmailMismatch_ThrowsBadRequest( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, OrganizationUser orgUser) |
|
{ |
|
// Arrange |
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order |
|
// to avoid resetting mocks |
|
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); |
|
sutProvider.Create(); |
|
|
|
SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser); |
|
|
|
// Modify the orgUser's email to be different from the user's email to simulate the mismatch |
|
orgUser.Email = "mismatchedEmail@example.com"; |
|
|
|
// Must come after common mocks as they mutate the org user. |
|
// Mock tokenable factory to return a token that expires in 5 days |
|
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser) |
|
{ |
|
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) |
|
}); |
|
|
|
var newToken = CreateNewToken(orgUser); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService)); |
|
|
|
Assert.Equal("User email does not match invite.", exception.Message); |
|
} |
|
|
|
|
|
// AcceptOrgUserByOrgSsoIdAsync ----------------------------------------------------------------------------------- |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByOrgSsoIdAsync_ValidData_AcceptsOrgUser( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
|
|
sutProvider.GetDependency<IOrganizationRepository>() |
|
.GetByIdentifierAsync(org.Identifier) |
|
.Returns(org); |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByOrganizationAsync(org.Id, user.Id) |
|
.Returns(orgUser); |
|
|
|
// Act |
|
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(org.Identifier, user, _userService); |
|
|
|
// Assert |
|
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByOrgSsoIdAsync_InvalidOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider, |
|
string orgSsoIdentifier, User user) |
|
{ |
|
// Arrange |
|
|
|
sutProvider.GetDependency<IOrganizationRepository>() |
|
.GetByIdentifierAsync(orgSsoIdentifier) |
|
.Returns((Organization)null); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(orgSsoIdentifier, user, _userService)); |
|
|
|
Assert.Equal("Organization invalid.", exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByOrgSsoIdAsync_UserNotInOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider, |
|
Organization org, User user) |
|
{ |
|
// Arrange |
|
sutProvider.GetDependency<IOrganizationRepository>() |
|
.GetByIdentifierAsync(org.Identifier) |
|
.Returns(org); |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByOrganizationAsync(org.Id, user.Id) |
|
.Returns((OrganizationUser)null); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(org.Identifier, user, _userService)); |
|
|
|
Assert.Equal("User not found within organization.", exception.Message); |
|
} |
|
|
|
// AcceptOrgUserByOrgIdAsync --------------------------------------------------------------------------------------- |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByOrgId_ValidData_AcceptsOrgUser( |
|
SutProvider<AcceptOrgUserCommand> sutProvider, |
|
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); |
|
|
|
sutProvider.GetDependency<IOrganizationRepository>() |
|
.GetByIdAsync(org.Id) |
|
.Returns(org); |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByOrganizationAsync(org.Id, user.Id) |
|
.Returns(orgUser); |
|
|
|
// Act |
|
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByOrgIdAsync(org.Id, user, _userService); |
|
|
|
// Assert |
|
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByOrgId_InvalidOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider, |
|
Guid orgId, User user) |
|
{ |
|
// Arrange |
|
|
|
sutProvider.GetDependency<IOrganizationRepository>() |
|
.GetByIdAsync(orgId) |
|
.Returns((Organization)null); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByOrgIdAsync(orgId, user, _userService)); |
|
|
|
Assert.Equal("Organization invalid.", exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task AcceptOrgUserByOrgId_UserNotInOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider, |
|
Organization org, User user) |
|
{ |
|
// Arrange |
|
sutProvider.GetDependency<IOrganizationRepository>() |
|
.GetByIdAsync(org.Id) |
|
.Returns(org); |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByOrganizationAsync(org.Id, user.Id) |
|
.Returns((OrganizationUser)null); |
|
|
|
// Act & Assert |
|
var exception = await Assert.ThrowsAsync<BadRequestException>( |
|
() => sutProvider.Sut.AcceptOrgUserByOrgIdAsync(org.Id, user, _userService)); |
|
|
|
Assert.Equal("User not found within organization.", exception.Message); |
|
} |
|
|
|
// Private helpers ------------------------------------------------------------------------------------------------- |
|
|
|
/// <summary> |
|
/// Asserts that the given org user is in the expected state after a successful AcceptOrgUserAsync call. |
|
/// For use in happy path tests. |
|
/// </summary> |
|
private void AssertValidAcceptedOrgUser(OrganizationUser resultOrgUser, OrganizationUser expectedOrgUser, User user) |
|
{ |
|
Assert.NotNull(resultOrgUser); |
|
Assert.Equal(OrganizationUserStatusType.Accepted, resultOrgUser.Status); |
|
Assert.Equal(expectedOrgUser, resultOrgUser); |
|
Assert.Equal(expectedOrgUser.Id, resultOrgUser.Id); |
|
Assert.Null(resultOrgUser.Email); |
|
Assert.Equal(user.Id, resultOrgUser.UserId); |
|
|
|
|
|
} |
|
|
|
private void SetupCommonAcceptOrgUserByTokenMocks(SutProvider<AcceptOrgUserCommand> sutProvider, User user, OrganizationUser orgUser) |
|
{ |
|
sutProvider.GetDependency<IGlobalSettings>().OrganizationInviteExpirationHours.Returns(24); |
|
user.EmailVerified = false; |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetByIdAsync(orgUser.Id) |
|
.Returns(Task.FromResult(orgUser)); |
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetCountByOrganizationAsync(orgUser.OrganizationId, user.Email, true) |
|
.Returns(0); |
|
} |
|
|
|
/// <summary> |
|
/// Sets up common mock behavior for the AcceptOrgUserAsync tests. |
|
/// This method initializes: |
|
/// - The invited user's email, status, type, and organization ID. |
|
/// - Ensures the user is not part of any other organizations. |
|
/// - Confirms the target organization doesn't have a single org policy. |
|
/// - Ensures the user doesn't belong to an organization with a single org policy. |
|
/// - Assumes the user doesn't have 2FA enabled and the organization doesn't require it. |
|
/// - Provides mock data for an admin to validate email functionality. |
|
/// - Returns the corresponding organization for the given org ID. |
|
/// </summary> |
|
private void SetupCommonAcceptOrgUserMocks(SutProvider<AcceptOrgUserCommand> sutProvider, User user, |
|
Organization org, |
|
OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) |
|
{ |
|
// Arrange |
|
orgUser.Email = user.Email; |
|
orgUser.Status = OrganizationUserStatusType.Invited; |
|
orgUser.Type = OrganizationUserType.User; |
|
orgUser.OrganizationId = org.Id; |
|
|
|
// User is not part of any other orgs |
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetManyByUserAsync(user.Id) |
|
.Returns( |
|
Task.FromResult<ICollection<OrganizationUser>>(new List<OrganizationUser>()) |
|
); |
|
|
|
// Org they are trying to join does not have single org policy |
|
sutProvider.GetDependency<IPolicyService>() |
|
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg, OrganizationUserStatusType.Invited) |
|
.Returns( |
|
Task.FromResult<ICollection<OrganizationUserPolicyDetails>>( |
|
new List<OrganizationUserPolicyDetails>() |
|
) |
|
); |
|
|
|
// User is not part of any organization that applies the single org policy |
|
sutProvider.GetDependency<IPolicyService>() |
|
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg) |
|
.Returns(false); |
|
|
|
// User doesn't have 2FA enabled |
|
_twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); |
|
|
|
// Org does not require 2FA |
|
sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(user.Id, |
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited) |
|
.Returns(Task.FromResult<ICollection<OrganizationUserPolicyDetails>>( |
|
new List<OrganizationUserPolicyDetails>())); |
|
|
|
// Provide at least 1 admin to test email functionality |
|
sutProvider.GetDependency<IOrganizationUserRepository>() |
|
.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin) |
|
.Returns(Task.FromResult<IEnumerable<OrganizationUserUserDetails>>( |
|
new List<OrganizationUserUserDetails>() { adminUserDetails } |
|
)); |
|
|
|
// Return org |
|
sutProvider.GetDependency<IOrganizationRepository>() |
|
.GetByIdAsync(org.Id) |
|
.Returns(Task.FromResult(org)); |
|
} |
|
|
|
|
|
private string CreateOldToken(SutProvider<AcceptOrgUserCommand> sutProvider, |
|
OrganizationUser organizationUser) |
|
{ |
|
var dataProtector = sutProvider.GetDependency<IDataProtectionProvider>() |
|
.CreateProtector("OrganizationServiceDataProtector"); |
|
|
|
// Token matching the format used in OrganizationService.InviteUserAsync |
|
var oldToken = dataProtector.Protect( |
|
$"OrganizationUserInvite {organizationUser.Id} {organizationUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); |
|
|
|
return oldToken; |
|
} |
|
|
|
private string CreateNewToken(OrganizationUser orgUser) |
|
{ |
|
var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser); |
|
var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable); |
|
|
|
return protectedToken; |
|
} |
|
}
|
|
|