mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 08:32:50 -05:00
[AC-1751] AC Team code ownership moves: OrganizationUser (part 1) (#3487)
* Move OrganizationUser domain to AC Team ownership * Namespaces will be updated in a separate commit
This commit is contained in:
218
test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs
Normal file
218
test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs
Normal file
@ -0,0 +1,218 @@
|
||||
using System.Text.Json;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Kernel;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
|
||||
public class OrganizationCustomization : ICustomization
|
||||
{
|
||||
public bool UseGroups { get; set; }
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var maxCollections = (short)new Random().Next(10, short.MaxValue);
|
||||
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.Id, organizationId)
|
||||
.With(o => o.MaxCollections, maxCollections)
|
||||
.With(o => o.UseGroups, UseGroups));
|
||||
|
||||
fixture.Customize<Collection>(composer =>
|
||||
composer
|
||||
.With(c => c.OrganizationId, organizationId)
|
||||
.Without(o => o.CreationDate)
|
||||
.Without(o => o.RevisionDate));
|
||||
|
||||
fixture.Customize<Group>(composer => composer.With(g => g.OrganizationId, organizationId));
|
||||
}
|
||||
}
|
||||
|
||||
internal class OrganizationBuilder : ISpecimenBuilder
|
||||
{
|
||||
public object Create(object request, ISpecimenContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var type = request as Type;
|
||||
if (type == null || type != typeof(Organization))
|
||||
{
|
||||
return new NoSpecimen();
|
||||
}
|
||||
|
||||
var fixture = new Fixture();
|
||||
var providers = fixture.Create<Dictionary<TwoFactorProviderType, TwoFactorProvider>>();
|
||||
var organization = new Fixture().WithAutoNSubstitutions().Create<Organization>();
|
||||
organization.SetTwoFactorProviders(providers);
|
||||
return organization;
|
||||
}
|
||||
}
|
||||
|
||||
internal class PaidOrganization : ICustomization
|
||||
{
|
||||
public PlanType CheckedPlanType { get; set; }
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var validUpgradePlans = StaticStore.Plans.Where(p => p.Type != PlanType.Free && p.LegacyYear == null).OrderBy(p => p.UpgradeSortOrder).Select(p => p.Type).ToList();
|
||||
var lowestActivePaidPlan = validUpgradePlans.First();
|
||||
CheckedPlanType = CheckedPlanType.Equals(PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType;
|
||||
validUpgradePlans.Remove(lowestActivePaidPlan);
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.PlanType, CheckedPlanType));
|
||||
fixture.Customize<OrganizationUpgrade>(composer => composer
|
||||
.With(ou => ou.Plan, validUpgradePlans.First()));
|
||||
}
|
||||
}
|
||||
|
||||
internal class FreeOrganization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.PlanType, PlanType.Free));
|
||||
}
|
||||
}
|
||||
|
||||
internal class FreeOrganizationUpgrade : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.PlanType, PlanType.Free));
|
||||
|
||||
var plansToIgnore = new List<PlanType> { PlanType.Free, PlanType.Custom };
|
||||
var selectedPlan = StaticStore.Plans.Last(p => !plansToIgnore.Contains(p.Type) && !p.Disabled);
|
||||
|
||||
fixture.Customize<OrganizationUpgrade>(composer => composer
|
||||
.With(ou => ou.Plan, selectedPlan.Type)
|
||||
.With(ou => ou.PremiumAccessAddon, selectedPlan.PasswordManager.HasPremiumAccessOption));
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.Without(o => o.GatewaySubscriptionId));
|
||||
}
|
||||
}
|
||||
|
||||
internal class OrganizationInvite : ICustomization
|
||||
{
|
||||
public OrganizationUserType InviteeUserType { get; set; }
|
||||
public OrganizationUserType InvitorUserType { get; set; }
|
||||
public string PermissionsBlob { get; set; }
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
PermissionsBlob = PermissionsBlob ?? JsonSerializer.Serialize(new Permissions(), new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.Id, organizationId)
|
||||
.With(o => o.Seats, (short)100));
|
||||
fixture.Customize<OrganizationUser>(composer => composer
|
||||
.With(ou => ou.OrganizationId, organizationId)
|
||||
.With(ou => ou.Type, InvitorUserType)
|
||||
.With(ou => ou.Permissions, PermissionsBlob));
|
||||
fixture.Customize<OrganizationUserInvite>(composer => composer
|
||||
.With(oi => oi.Type, InviteeUserType));
|
||||
}
|
||||
}
|
||||
|
||||
public class SecretsManagerOrganizationCustomization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
const PlanType planType = PlanType.EnterpriseAnnually;
|
||||
var organizationId = Guid.NewGuid();
|
||||
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.Id, organizationId)
|
||||
.With(o => o.UseSecretsManager, true)
|
||||
.With(o => o.SecretsManagerBeta, false)
|
||||
.With(o => o.PlanType, planType)
|
||||
.With(o => o.Plan, StaticStore.GetPlan(planType).Name)
|
||||
.With(o => o.MaxAutoscaleSmSeats, (int?)null)
|
||||
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null));
|
||||
}
|
||||
}
|
||||
|
||||
internal class OrganizationCustomizeAttribute : BitCustomizeAttribute
|
||||
{
|
||||
public bool UseGroups { get; set; }
|
||||
public override ICustomization GetCustomization() => new OrganizationCustomization() { UseGroups = UseGroups };
|
||||
}
|
||||
|
||||
internal class PaidOrganizationCustomizeAttribute : BitCustomizeAttribute
|
||||
{
|
||||
public PlanType CheckedPlanType { get; set; } = PlanType.FamiliesAnnually;
|
||||
public override ICustomization GetCustomization() => new PaidOrganization() { CheckedPlanType = CheckedPlanType };
|
||||
}
|
||||
|
||||
internal class FreeOrganizationCustomizeAttribute : BitCustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization() => new FreeOrganization();
|
||||
}
|
||||
|
||||
internal class FreeOrganizationUpgradeCustomize : BitCustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization() => new FreeOrganizationUpgrade();
|
||||
}
|
||||
|
||||
internal class OrganizationInviteCustomizeAttribute : BitCustomizeAttribute
|
||||
{
|
||||
public OrganizationUserType InviteeUserType { get; set; } = OrganizationUserType.Owner;
|
||||
public OrganizationUserType InvitorUserType { get; set; } = OrganizationUserType.Owner;
|
||||
public string PermissionsBlob { get; set; }
|
||||
|
||||
public override ICustomization GetCustomization() => new OrganizationInvite
|
||||
{
|
||||
InviteeUserType = InviteeUserType,
|
||||
InvitorUserType = InvitorUserType,
|
||||
PermissionsBlob = PermissionsBlob,
|
||||
};
|
||||
}
|
||||
|
||||
internal class SecretsManagerOrganizationCustomizeAttribute : BitCustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization() =>
|
||||
new SecretsManagerOrganizationCustomization();
|
||||
}
|
||||
|
||||
internal class EphemeralDataProtectionCustomization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customizations.Add(new EphemeralDataProtectionProviderBuilder());
|
||||
}
|
||||
|
||||
private class EphemeralDataProtectionProviderBuilder : ISpecimenBuilder
|
||||
{
|
||||
public object Create(object request, ISpecimenContext context)
|
||||
{
|
||||
var type = request as Type;
|
||||
if (type == null || type != typeof(IDataProtectionProvider))
|
||||
{
|
||||
return new NoSpecimen();
|
||||
}
|
||||
|
||||
return new EphemeralDataProtectionProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class EphemeralDataProtectionAutoDataAttribute : CustomAutoDataAttribute
|
||||
{
|
||||
public EphemeralDataProtectionAutoDataAttribute() : base(new SutProviderCustomization(), new EphemeralDataProtectionCustomization())
|
||||
{ }
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
|
||||
public class OrganizationUserCustomization : ICustomization
|
||||
{
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
|
||||
public OrganizationUserCustomization(OrganizationUserStatusType status, OrganizationUserType type)
|
||||
{
|
||||
Status = status;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<OrganizationUser>(composer => composer
|
||||
.With(o => o.Type, Type)
|
||||
.With(o => o.Status, Status));
|
||||
}
|
||||
}
|
||||
|
||||
public class OrganizationUserAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly OrganizationUserStatusType _status;
|
||||
private readonly OrganizationUserType _type;
|
||||
|
||||
public OrganizationUserAttribute(
|
||||
OrganizationUserStatusType status = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationUserType type = OrganizationUserType.User)
|
||||
{
|
||||
_status = status;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new OrganizationUserCustomization(_status, _type);
|
||||
}
|
||||
}
|
@ -0,0 +1,667 @@
|
||||
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.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 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
|
||||
_userService.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);
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
_userService.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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user