mirror of
https://github.com/bitwarden/server
synced 2025-12-27 05:33:17 +00:00
Merge branch 'main' into arch/seeder-api
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Kernel;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
@@ -9,7 +11,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
@@ -20,12 +22,24 @@ public class OrganizationCustomization : ICustomization
|
||||
{
|
||||
public bool UseGroups { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
|
||||
public OrganizationCustomization()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public OrganizationCustomization(bool useAutomaticUserConfirmation, PlanType planType)
|
||||
{
|
||||
UseAutomaticUserConfirmation = useAutomaticUserConfirmation;
|
||||
PlanType = planType;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var maxCollections = (short)new Random().Next(10, short.MaxValue);
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == PlanType);
|
||||
var plan = MockPlans.Plans.FirstOrDefault(p => p.Type == PlanType);
|
||||
var seats = (short)new Random().Next(plan.PasswordManager.BaseSeats, plan.PasswordManager.MaxSeats ?? short.MaxValue);
|
||||
var smSeats = plan.SupportsSecretsManager
|
||||
? (short?)new Random().Next(plan.SecretsManager.BaseSeats, plan.SecretsManager.MaxSeats ?? short.MaxValue)
|
||||
@@ -37,7 +51,8 @@ public class OrganizationCustomization : ICustomization
|
||||
.With(o => o.UseGroups, UseGroups)
|
||||
.With(o => o.PlanType, PlanType)
|
||||
.With(o => o.Seats, seats)
|
||||
.With(o => o.SmSeats, smSeats));
|
||||
.With(o => o.SmSeats, smSeats)
|
||||
.With(o => o.UseAutomaticUserConfirmation, UseAutomaticUserConfirmation));
|
||||
|
||||
fixture.Customize<Collection>(composer =>
|
||||
composer
|
||||
@@ -77,7 +92,7 @@ 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 validUpgradePlans = MockPlans.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);
|
||||
@@ -105,7 +120,7 @@ internal class FreeOrganizationUpgrade : ICustomization
|
||||
.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);
|
||||
var selectedPlan = MockPlans.Plans.Last(p => !plansToIgnore.Contains(p.Type) && !p.Disabled);
|
||||
|
||||
fixture.Customize<OrganizationUpgrade>(composer => composer
|
||||
.With(ou => ou.Plan, selectedPlan.Type)
|
||||
@@ -153,7 +168,7 @@ public class SecretsManagerOrganizationCustomization : ICustomization
|
||||
.With(o => o.Id, organizationId)
|
||||
.With(o => o.UseSecretsManager, true)
|
||||
.With(o => o.PlanType, planType)
|
||||
.With(o => o.Plan, StaticStore.GetPlan(planType).Name)
|
||||
.With(o => o.Plan, MockPlans.Get(planType).Name)
|
||||
.With(o => o.MaxAutoscaleSmSeats, (int?)null)
|
||||
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null));
|
||||
}
|
||||
@@ -277,3 +292,9 @@ internal class EphemeralDataProtectionAutoDataAttribute : CustomAutoDataAttribut
|
||||
public EphemeralDataProtectionAutoDataAttribute() : base(new SutProviderCustomization(), new EphemeralDataProtectionCustomization())
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class OrganizationAttribute(bool useAutomaticUserConfirmation = false, PlanType planType = PlanType.Free) : CustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter) =>
|
||||
new OrganizationCustomization(useAutomaticUserConfirmation, planType);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
@@ -21,7 +21,21 @@ public class IntegrationTemplateContextTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user)
|
||||
public void DateIso8601_ReturnsIso8601FormattedDate(EventMessage eventMessage)
|
||||
{
|
||||
var testDate = new DateTime(2025, 10, 27, 13, 30, 0, DateTimeKind.Utc);
|
||||
eventMessage.Date = testDate;
|
||||
var sut = new IntegrationTemplateContext(eventMessage);
|
||||
|
||||
var result = sut.DateIso8601;
|
||||
|
||||
Assert.Equal("2025-10-27T13:30:00.0000000Z", result);
|
||||
// Verify it's valid ISO 8601
|
||||
Assert.True(DateTime.TryParse(result, out _));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails user)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
|
||||
|
||||
@@ -37,7 +51,7 @@ public class IntegrationTemplateContextTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, User user)
|
||||
public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails user)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
|
||||
|
||||
@@ -53,7 +67,23 @@ public class IntegrationTemplateContextTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, User actingUser)
|
||||
public void UserType_WhenUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails user)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
|
||||
|
||||
Assert.Equal(user.Type, sut.UserType);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void UserType_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { User = null };
|
||||
|
||||
Assert.Null(sut.UserType);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
|
||||
|
||||
@@ -69,7 +99,7 @@ public class IntegrationTemplateContextTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, User actingUser)
|
||||
public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
|
||||
|
||||
@@ -84,6 +114,22 @@ public class IntegrationTemplateContextTests
|
||||
Assert.Null(sut.ActingUserEmail);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ActingUserType_WhenActingUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
|
||||
|
||||
Assert.Equal(actingUser.Type, sut.ActingUserType);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ActingUserType_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };
|
||||
|
||||
Assert.Null(sut.ActingUserType);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization)
|
||||
{
|
||||
@@ -99,4 +145,20 @@ public class IntegrationTemplateContextTests
|
||||
|
||||
Assert.Null(sut.OrganizationName);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void GroupName_WhenGroupIsSet_ReturnsName(EventMessage eventMessage, Group group)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { Group = group };
|
||||
|
||||
Assert.Equal(group.Name, sut.GroupName);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void GroupName_WhenGroupIsNull_ReturnsNull(EventMessage eventMessage)
|
||||
{
|
||||
var sut = new IntegrationTemplateContext(eventMessage) { Group = null };
|
||||
|
||||
Assert.Null(sut.GroupName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -191,6 +192,37 @@ public class VerifyOrganizationDomainCommandTests
|
||||
x.PerformedBy.UserId == userId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_WhenPolicyValidatorsRefactorFlagEnabled_UsesVNextSavePolicyCommand(
|
||||
OrganizationDomain domain, Guid userId, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(domain.DomainName, domain.Txt)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)
|
||||
.Returns(true);
|
||||
|
||||
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Is<SavePolicyModel>(m =>
|
||||
m.PolicyUpdate.Type == PolicyType.SingleOrg &&
|
||||
m.PolicyUpdate.OrganizationId == domain.OrganizationId &&
|
||||
m.PolicyUpdate.Enabled &&
|
||||
m.PerformedBy is StandardUser &&
|
||||
m.PerformedBy.UserId == userId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled(
|
||||
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
|
||||
@@ -0,0 +1,696 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = null,
|
||||
OrganizationUserId = Guid.NewGuid(),
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserNotFoundError>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNullUserId_ReturnsUserNotFoundError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = null;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserNotFoundError>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNullOrganization_ReturnsOrganizationNotFoundError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = null,
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationNotFound>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithValidAcceptedUser_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true, planType: PlanType.EnterpriseAnnually)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(request, result.Request);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithMismatchedOrganizationId_ReturnsOrganizationUserIdIsInvalidError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = Guid.NewGuid(); // Different from organization.Id
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationUserIdIsInvalid>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||
public async Task ValidateAsync_WithNotAcceptedStatus_ReturnsUserIsNotAcceptedError(
|
||||
OrganizationUserStatusType statusType,
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.Status = statusType;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserIsNotAccepted>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
public async Task ValidateAsync_WithNonUserType_ReturnsUserIsNotUserTypeError(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.Type = userType;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserIsNotUserType>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserWithout2FA_And2FARequired_ReturnsError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var twoFactorPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, false)]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(userId)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement([twoFactorPolicyDetails]));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserDoesNotHaveTwoFactorEnabled>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserWith2FA_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserWithout2FA_And2FANotRequired_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, false)]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(userId)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement([])); // No 2FA policy
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithSingleOrgPolicyOnThisOrg_ReturnsError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var singleOrgPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
PolicyType = PolicyType.SingleOrg
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([singleOrgPolicyDetails]));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationEnforcesSingleOrgPolicy>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithSingleOrgPolicyOnOtherOrg_ReturnsError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var otherOrgId = Guid.NewGuid(); // Different org
|
||||
var singleOrgPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = otherOrgId,
|
||||
PolicyType = PolicyType.SingleOrg,
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([singleOrgPolicyDetails]));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OtherOrganizationEnforcesSingleOrgPolicy>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInSingleOrg_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]); // Single org
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithNoSingleOrgPolicy_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
autoConfirmPolicy.Type = PolicyType.AutomaticUserConfirmation;
|
||||
autoConfirmPolicy.Enabled = true;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([]));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithAutoConfirmPolicyDisabled_ReturnsAutoConfirmPolicyNotEnabledError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns((Policy)null);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<AutomaticallyConfirmUsersPolicyIsNotEnabled>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithOrganizationUseAutomaticUserConfirmationDisabled_ReturnsAutoConfirmPolicyNotEnabledError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: false)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<AutomaticallyConfirmUsersPolicyIsNotEnabled>(result.AsError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AutomaticallyConfirmUsersCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithValidRequest_ConfirmsUserSuccessfully(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key));
|
||||
|
||||
await AssertSuccessfulOperationsAsync(sutProvider, organizationUser, organization, user, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithInvalidUserOrgId_ReturnsOrganizationUserIdIsInvalidError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = Guid.NewGuid(); // User belongs to another organization
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, false, new OrganizationUserIdIsInvalid());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationUserIdIsInvalid>(result.AsError);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceive()
|
||||
.ConfirmOrganizationUserAsync(Arg.Any<AcceptedOrganizationUserToConfirm>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenAlreadyConfirmed_ReturnsNoneSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
// Return false to indicate the user is already confirmed
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(x =>
|
||||
x.OrganizationUserId == organizationUser.Id && x.Key == request.Key))
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(x =>
|
||||
x.OrganizationUserId == organizationUser.Id && x.Key == request.Key));
|
||||
|
||||
// Verify no side effects occurred
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceive()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceive()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectionEnabled_CreatesDefaultCollection(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName, // Non-empty to trigger creation
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, true); // Policy requires collection
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().ConfirmOrganizationUserAsync(
|
||||
Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(
|
||||
Arg.Is<Collection>(c =>
|
||||
c.OrganizationId == organization.Id &&
|
||||
c.Name == defaultCollectionName &&
|
||||
c.Type == CollectionType.DefaultUserCollection),
|
||||
Arg.Is<IEnumerable<CollectionAccessSelection>>(groups => groups == null),
|
||||
Arg.Is<IEnumerable<CollectionAccessSelection>>(access =>
|
||||
access.FirstOrDefault(x => x.Id == organizationUser.Id && x.Manage) != null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectionDisabled_DoesNotCreateCollection(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = string.Empty, // Empty, so the collection won't be created
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, false); // Policy doesn't require
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateAsync(Arg.Any<Collection>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenCreateDefaultCollectionFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName, // Non-empty to trigger creation
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)).Returns(true);
|
||||
|
||||
var collectionException = new Exception("Collection creation failed");
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.CreateAsync(Arg.Any<Collection>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>())
|
||||
.ThrowsAsync(collectionException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if collection creation fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to create default collection for user")),
|
||||
collectionException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenEventLogFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
var eventException = new Exception("Event logging failed");
|
||||
sutProvider.GetDependency<IEventService>()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(),
|
||||
EventType.OrganizationUser_AutomaticallyConfirmed,
|
||||
Arg.Any<DateTime?>())
|
||||
.ThrowsAsync(eventException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if event log fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to log OrganizationUser_AutomaticallyConfirmed event")),
|
||||
eventException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenSendEmailFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
var emailException = new Exception("Email sending failed");
|
||||
sutProvider.GetDependency<IMailService>()
|
||||
.SendOrganizationConfirmedEmailAsync(organization.Name, user.Email, organizationUser.AccessSecretsManager)
|
||||
.ThrowsAsync(emailException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if email fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to send OrganizationUserConfirmed")),
|
||||
emailException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenUserNotFoundForEmail_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
// Return null when retrieving user for email
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns((User)null!);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if user not found for email
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenDeleteDeviceRegistrationFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName,
|
||||
Device device)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
device.UserId = user.Id;
|
||||
device.PushToken = "test-push-token";
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDeviceRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<Device> { device });
|
||||
|
||||
var deviceException = new Exception("Device registration deletion failed");
|
||||
sutProvider.GetDependency<IPushRegistrationService>()
|
||||
.DeleteUserRegistrationOrganizationAsync(Arg.Any<IEnumerable<string>>(), organization.Id.ToString())
|
||||
.ThrowsAsync(deviceException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if device registration deletion fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to delete device registration")),
|
||||
deviceException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenPushSyncOrgKeysFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
var pushException = new Exception("Push sync failed");
|
||||
sutProvider.GetDependency<IPushNotificationService>()
|
||||
.PushSyncOrgKeysAsync(user.Id)
|
||||
.ThrowsAsync(pushException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if push sync fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to push organization keys")),
|
||||
pushException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithDevicesWithoutPushToken_FiltersCorrectly(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName,
|
||||
Device deviceWithToken,
|
||||
Device deviceWithoutToken)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
deviceWithToken.UserId = user.Id;
|
||||
deviceWithToken.PushToken = "test-token";
|
||||
deviceWithoutToken.UserId = user.Id;
|
||||
deviceWithoutToken.PushToken = null;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDeviceRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<Device> { deviceWithToken, deviceWithoutToken });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<IPushRegistrationService>()
|
||||
.Received(1)
|
||||
.DeleteUserRegistrationOrganizationAsync(
|
||||
Arg.Is<IEnumerable<string>>(devices =>
|
||||
devices.Count(d => deviceWithToken.Id.ToString() == d) == 1),
|
||||
organization.Id.ToString());
|
||||
}
|
||||
|
||||
private static void SetupRepositoryMocks(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
Organization organization,
|
||||
User user)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IDeviceRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<Device>());
|
||||
}
|
||||
|
||||
private static void SetupValidatorMock(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
AutomaticallyConfirmOrganizationUserRequest originalRequest,
|
||||
OrganizationUser organizationUser,
|
||||
Organization organization,
|
||||
bool isValid,
|
||||
Error? error = null)
|
||||
{
|
||||
var validationRequest = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = originalRequest.PerformedBy,
|
||||
DefaultUserCollectionName = originalRequest.DefaultUserCollectionName,
|
||||
OrganizationUserId = originalRequest.OrganizationUserId,
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationId = originalRequest.OrganizationId,
|
||||
Organization = organization,
|
||||
Key = originalRequest.Key
|
||||
};
|
||||
|
||||
var validationResult = isValid
|
||||
? ValidationResultHelpers.Valid(validationRequest)
|
||||
: ValidationResultHelpers.Invalid(validationRequest, error ?? new UserIsNotAccepted());
|
||||
|
||||
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUsersValidator>()
|
||||
.ValidateAsync(Arg.Any<AutomaticallyConfirmOrganizationUserValidationRequest>())
|
||||
.Returns(validationResult);
|
||||
}
|
||||
|
||||
private static void SetupPolicyRequirementMock(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
bool requiresDefaultCollection)
|
||||
{
|
||||
var policyDetails = requiresDefaultCollection
|
||||
? new List<PolicyDetails> { new() { OrganizationId = organizationId } }
|
||||
: new List<PolicyDetails>();
|
||||
|
||||
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
|
||||
requiresDefaultCollection ? OrganizationDataOwnershipState.Enabled : OrganizationDataOwnershipState.Disabled,
|
||||
policyDetails);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
|
||||
.Returns(policyRequirement);
|
||||
}
|
||||
|
||||
private static async Task AssertSuccessfulOperationsAsync(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
Organization organization,
|
||||
User user,
|
||||
string key)
|
||||
{
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(
|
||||
Arg.Is<OrganizationUser>(x => x.Id == organizationUser.Id),
|
||||
EventType.OrganizationUser_AutomaticallyConfirmed,
|
||||
Arg.Any<DateTime?>());
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationConfirmedEmailAsync(
|
||||
organization.Name,
|
||||
user.Email,
|
||||
organizationUser.AccessSecretsManager);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(user.Id);
|
||||
|
||||
await sutProvider.GetDependency<IPushRegistrationService>()
|
||||
.Received(1)
|
||||
.DeleteUserRegistrationOrganizationAsync(
|
||||
Arg.Any<IEnumerable<string>>(),
|
||||
organization.Id.ToString());
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,8 @@ public class ConfirmOrganizationUserCommandTests
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2025, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2025, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
@@ -13,7 +13,6 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
@@ -22,6 +21,7 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
@@ -29,6 +29,7 @@ using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
using static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteUserOrganizationValidationRequestHelpers;
|
||||
using Enterprise2019Plan = Bit.Core.Test.Billing.Mocks.Plans.Enterprise2019Plan;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
@@ -5,7 +5,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
|
||||
@@ -10,7 +10,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -23,11 +23,12 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2025)]
|
||||
public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
|
||||
{
|
||||
signup.Plan = planType;
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
var plan = MockPlans.Get(signup.Plan);
|
||||
|
||||
signup.AdditionalSeats = 0;
|
||||
signup.PaymentMethodType = PaymentMethodType.Card;
|
||||
@@ -36,7 +37,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
signup.IsFromSecretsManagerTrial = false;
|
||||
signup.IsFromProvider = false;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));
|
||||
|
||||
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
|
||||
|
||||
@@ -65,6 +66,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2025)]
|
||||
public async Task SignUp_AssignsOwnerToDefaultCollection
|
||||
(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
|
||||
{
|
||||
@@ -75,7 +77,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
signup.UseSecretsManager = false;
|
||||
signup.IsFromProvider = false;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));
|
||||
|
||||
// Extract orgUserId when created
|
||||
Guid? orgUserId = null;
|
||||
@@ -110,7 +112,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
{
|
||||
signup.Plan = planType;
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
var plan = MockPlans.Get(signup.Plan);
|
||||
|
||||
signup.UseSecretsManager = true;
|
||||
signup.AdditionalSeats = 15;
|
||||
@@ -121,7 +123,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
signup.IsFromSecretsManagerTrial = false;
|
||||
signup.IsFromProvider = false;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));
|
||||
|
||||
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
|
||||
|
||||
@@ -162,7 +164,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
signup.PremiumAccessAddon = false;
|
||||
signup.IsFromProvider = true;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpOrganizationAsync(signup));
|
||||
Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message);
|
||||
@@ -182,7 +184,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
signup.AdditionalStorageGb = 0;
|
||||
signup.IsFromProvider = false;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
|
||||
@@ -202,7 +204,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
signup.AdditionalServiceAccounts = 10;
|
||||
signup.IsFromProvider = false;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
|
||||
@@ -222,7 +224,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
signup.AdditionalServiceAccounts = -10;
|
||||
signup.IsFromProvider = false;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
|
||||
@@ -242,7 +244,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
Owner = new User { Id = Guid.NewGuid() }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id)
|
||||
|
||||
@@ -10,7 +10,7 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -36,7 +36,7 @@ public class ProviderClientOrganizationSignUpCommandTests
|
||||
signup.AdditionalSeats = 15;
|
||||
signup.CollectionName = collectionName;
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
var plan = MockPlans.Get(signup.Plan);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(signup.Plan)
|
||||
.Returns(plan);
|
||||
@@ -112,7 +112,7 @@ public class ProviderClientOrganizationSignUpCommandTests
|
||||
signup.Plan = PlanType.TeamsMonthly;
|
||||
signup.AdditionalSeats = -5;
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
var plan = MockPlans.Get(signup.Plan);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(signup.Plan)
|
||||
.Returns(plan);
|
||||
@@ -132,7 +132,7 @@ public class ProviderClientOrganizationSignUpCommandTests
|
||||
{
|
||||
signup.Plan = planType;
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
var plan = MockPlans.Get(signup.Plan);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(signup.Plan)
|
||||
.Returns(plan);
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
|
||||
using Bit.Core.Billing.Organizations.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationUpdateCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenValidOrganization_UpdatesOrganization(
|
||||
Guid organizationId,
|
||||
string name,
|
||||
string billingEmail,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.GatewayCustomerId = null; // No Stripe customer, so no billing update
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = name,
|
||||
BillingEmail = billingEmail
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(name, result.Name);
|
||||
Assert.Equal(billingEmail.ToLowerInvariant().Trim(), result.BillingEmail);
|
||||
|
||||
await organizationRepository
|
||||
.Received(1)
|
||||
.GetByIdAsync(Arg.Is<Guid>(id => id == organizationId));
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenOrganizationNotFound_ThrowsNotFoundException(
|
||||
Guid organizationId,
|
||||
string name,
|
||||
string billingEmail,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = name,
|
||||
BillingEmail = billingEmail
|
||||
};
|
||||
|
||||
// Act/Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData((string)null)]
|
||||
public async Task UpdateAsync_WhenGatewayCustomerIdIsNullOrEmpty_SkipsBillingUpdate(
|
||||
string gatewayCustomerId,
|
||||
Guid organizationId,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.Name = "Old Name";
|
||||
organization.GatewayCustomerId = gatewayCustomerId;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = "New Name",
|
||||
BillingEmail = organization.BillingEmail
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal("New Name", result.Name);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenKeysProvided_AndNotAlreadySet_SetsKeys(
|
||||
Guid organizationId,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.PublicKey = null;
|
||||
organization.PrivateKey = null;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = organization.Name,
|
||||
BillingEmail = organization.BillingEmail,
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(publicKey, result.PublicKey);
|
||||
Assert.Equal(encryptedPrivateKey, result.PrivateKey);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenKeysProvided_AndAlreadySet_DoesNotOverwriteKeys(
|
||||
Guid organizationId,
|
||||
string newPublicKey,
|
||||
string newEncryptedPrivateKey,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
var existingPublicKey = organization.PublicKey;
|
||||
var existingPrivateKey = organization.PrivateKey;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = organization.Name,
|
||||
BillingEmail = organization.BillingEmail,
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newEncryptedPrivateKey
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(existingPublicKey, result.PublicKey);
|
||||
Assert.Equal(existingPrivateKey, result.PrivateKey);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_UpdatingNameOnly_UpdatesNameAndNotBillingEmail(
|
||||
Guid organizationId,
|
||||
string newName,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.Name = "Old Name";
|
||||
var originalBillingEmail = organization.BillingEmail;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = newName,
|
||||
BillingEmail = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(newName, result.Name);
|
||||
Assert.Equal(originalBillingEmail, result.BillingEmail);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.Received(1)
|
||||
.UpdateOrganizationNameAndEmail(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_UpdatingBillingEmailOnly_UpdatesBillingEmailAndNotName(
|
||||
Guid organizationId,
|
||||
string newBillingEmail,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.BillingEmail = "old@example.com";
|
||||
var originalName = organization.Name;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = null,
|
||||
BillingEmail = newBillingEmail
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(originalName, result.Name);
|
||||
Assert.Equal(newBillingEmail.ToLowerInvariant().Trim(), result.BillingEmail);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.Received(1)
|
||||
.UpdateOrganizationNameAndEmail(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenNoChanges_PreservesBothFields(
|
||||
Guid organizationId,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
var originalName = organization.Name;
|
||||
var originalBillingEmail = organization.BillingEmail;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = null,
|
||||
BillingEmail = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(originalName, result.Name);
|
||||
Assert.Equal(originalBillingEmail, result.BillingEmail);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_SelfHosted_OnlyUpdatesKeysNotOrganizationDetails(
|
||||
Guid organizationId,
|
||||
string newName,
|
||||
string newBillingEmail,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
globalSettings.SelfHosted.Returns(true);
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.Name = "Original Name";
|
||||
organization.BillingEmail = "original@example.com";
|
||||
organization.PublicKey = null;
|
||||
organization.PrivateKey = null;
|
||||
|
||||
organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = newName, // Should be ignored
|
||||
BillingEmail = newBillingEmail, // Should be ignored
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Original Name", result.Name); // Not changed
|
||||
Assert.Equal("original@example.com", result.BillingEmail); // Not changed
|
||||
Assert.Equal(publicKey, result.PublicKey); // Changed
|
||||
Assert.Equal(encryptedPrivateKey, result.PrivateKey); // Changed
|
||||
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
|
||||
@@ -0,0 +1,628 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AutomaticUserConfirmationPolicyEventHandlerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_SingleOrgNotEnabled_ReturnsError(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns((Policy?)null);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Single organization policy must be enabled", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_SingleOrgPolicyDisabled_ReturnsError(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Single organization policy must be enabled", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_UsersNotCompliantWithSingleOrg_ReturnsError(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
Guid nonCompliantUserId,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var orgUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = nonCompliantUserId,
|
||||
Email = "user@example.com"
|
||||
};
|
||||
|
||||
var otherOrgUser = new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
UserId = nonCompliantUserId,
|
||||
Status = OrganizationUserStatusType.Confirmed
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([orgUser]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([otherOrgUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_UserWithInvitedStatusInOtherOrg_ValidationPasses(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
Guid userId,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var orgUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = userId,
|
||||
Email = "test@email.com"
|
||||
};
|
||||
|
||||
var otherOrgUser = new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
UserId = null, // invited users do not have a user id
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
Email = orgUser.Email
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([orgUser]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_ProviderUsersExist_ReturnsError(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var providerUser = new ProviderUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ProviderId = Guid.NewGuid(),
|
||||
UserId = Guid.NewGuid(),
|
||||
Status = ProviderUserStatusType.Confirmed
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([providerUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Provider user type", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_AllValidationsPassed_ReturnsEmptyString(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var orgUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = Guid.NewGuid(),
|
||||
Email = "user@example.com"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([orgUser]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_PolicyAlreadyEnabled_ReturnsEmptyString(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, currentPolicy);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.DidNotReceive()
|
||||
.GetByOrganizationIdTypeAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_DisablingPolicy_ReturnsEmptyString(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, currentPolicy);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.DidNotReceive()
|
||||
.GetByOrganizationIdTypeAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_IncludesOwnersAndAdmins_InComplianceCheck(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
Guid nonCompliantOwnerId,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var ownerUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = nonCompliantOwnerId,
|
||||
Email = "owner@example.com"
|
||||
};
|
||||
|
||||
var otherOrgUser = new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
UserId = nonCompliantOwnerId,
|
||||
Status = OrganizationUserStatusType.Confirmed
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([ownerUser]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([otherOrgUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_InvitedUsersExcluded_FromComplianceCheck(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var invitedUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
UserId = Guid.NewGuid(),
|
||||
Email = "invited@example.com"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([invitedUser]);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_RevokedUsersExcluded_FromComplianceCheck(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var revokedUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Revoked,
|
||||
UserId = Guid.NewGuid(),
|
||||
Email = "revoked@example.com"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([revokedUser]);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_AcceptedUsersIncluded_InComplianceCheck(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
Guid nonCompliantUserId,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var acceptedUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
UserId = nonCompliantUserId,
|
||||
Email = "accepted@example.com"
|
||||
};
|
||||
|
||||
var otherOrgUser = new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
UserId = nonCompliantUserId,
|
||||
Status = OrganizationUserStatusType.Confirmed
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([acceptedUser]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([otherOrgUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_EmptyOrganization_ReturnsEmptyString(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_WithSavePolicyModel_CallsValidateWithPolicyUpdate(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
singleOrgPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SingleOrg)
|
||||
.Returns(singleOrgPolicy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_EnablingPolicy_SetsUseAutomaticUserConfirmationToTrue(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
Organization organization,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Id = policyUpdate.OrganizationId;
|
||||
organization.UseAutomaticUserConfirmation = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(Arg.Is<Organization>(o =>
|
||||
o.Id == organization.Id &&
|
||||
o.UseAutomaticUserConfirmation == true &&
|
||||
o.RevisionDate > DateTime.MinValue));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_DisablingPolicy_SetsUseAutomaticUserConfirmationToFalse(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation, false)] PolicyUpdate policyUpdate,
|
||||
Organization organization,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Id = policyUpdate.OrganizationId;
|
||||
organization.UseAutomaticUserConfirmation = true;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(Arg.Is<Organization>(o =>
|
||||
o.Id == organization.Id &&
|
||||
o.UseAutomaticUserConfirmation == false &&
|
||||
o.RevisionDate > DateTime.MinValue));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_OrganizationNotFound_DoesNotThrowException(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns((Organization?)null);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertAsync(Arg.Any<Organization>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecutePreUpsertSideEffectAsync_CallsOnSaveSideEffectsAsync(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,
|
||||
Organization organization,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Id = policyUpdate.OrganizationId;
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, currentPolicy);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(Arg.Is<Organization>(o =>
|
||||
o.Id == organization.Id &&
|
||||
o.UseAutomaticUserConfirmation == policyUpdate.Enabled));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_UpdatesRevisionDate(
|
||||
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
|
||||
Organization organization,
|
||||
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Id = policyUpdate.OrganizationId;
|
||||
var originalRevisionDate = DateTime.UtcNow.AddDays(-1);
|
||||
organization.RevisionDate = originalRevisionDate;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(Arg.Is<Organization>(o =>
|
||||
o.Id == organization.Id &&
|
||||
o.RevisionDate > originalRevisionDate));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class BlockClaimedDomainAccountCreationPolicyValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_NoVerifiedDomains_ValidationError(
|
||||
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
|
||||
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
|
||||
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("You must claim at least one domain to turn on this policy", result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EnablingPolicy_HasVerifiedDomains_Success(
|
||||
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
|
||||
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
|
||||
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_DisablingPolicy_NoValidation(
|
||||
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate,
|
||||
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
|
||||
.DidNotReceive()
|
||||
.HasVerifiedDomainsAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_WithSavePolicyModel_EnablingPolicy_NoVerifiedDomains_ValidationError(
|
||||
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
|
||||
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
|
||||
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("You must claim at least one domain to turn on this policy", result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_WithSavePolicyModel_EnablingPolicy_HasVerifiedDomains_Success(
|
||||
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
|
||||
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
|
||||
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_NoValidation(
|
||||
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate,
|
||||
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);
|
||||
|
||||
// Assert
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
|
||||
.DidNotReceive()
|
||||
.HasVerifiedDomainsAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_FeatureFlagDisabled_ReturnsError(
|
||||
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
|
||||
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("This feature is not enabled", result);
|
||||
await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
|
||||
.DidNotReceive()
|
||||
.HasVerifiedDomainsAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Type_ReturnsBlockClaimedDomainAccountCreation()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null, null);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Equal(PolicyType.BlockClaimedDomainAccountCreation, validator.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiredPolicies_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null, null);
|
||||
|
||||
// Act
|
||||
var requiredPolicies = validator.RequiredPolicies.ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(requiredPolicies);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public class FreeFamiliesForEnterprisePolicyValidatorTests
|
||||
.GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organizationSponsorships);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
|
||||
|
||||
@@ -120,7 +120,7 @@ public class FreeFamiliesForEnterprisePolicyValidatorTests
|
||||
.GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organizationSponsorships);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(false);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -58,7 +58,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -84,7 +84,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -110,7 +110,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
var collectionRepository = Substitute.For<ICollectionRepository>();
|
||||
|
||||
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -199,7 +199,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
var collectionRepository = Substitute.For<ICollectionRepository>();
|
||||
|
||||
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -238,7 +238,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, metadata);
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, metadata);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -286,7 +286,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(false);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -312,7 +312,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -338,7 +338,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -364,7 +364,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
var collectionRepository = Substitute.For<ICollectionRepository>();
|
||||
|
||||
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -404,7 +404,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
var collectionRepository = Substitute.For<ICollectionRepository>();
|
||||
|
||||
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
@@ -436,7 +436,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, metadata);
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, metadata);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
@@ -88,7 +88,7 @@ public class RequireSsoPolicyValidatorTests
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
|
||||
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -109,7 +109,7 @@ public class RequireSsoPolicyValidatorTests
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
|
||||
Assert.Contains("Trusted device encryption is on", result, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -129,7 +129,7 @@ public class RequireSsoPolicyValidatorTests
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
|
||||
@@ -94,7 +94,7 @@ public class ResetPasswordPolicyValidatorTests
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
|
||||
Assert.Contains("Trusted device encryption is on and requires this policy.", result, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -118,7 +118,7 @@ public class ResetPasswordPolicyValidatorTests
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
|
||||
@@ -162,7 +162,7 @@ public class SingleOrgPolicyValidatorTests
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
|
||||
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -186,7 +186,7 @@ public class SingleOrgPolicyValidatorTests
|
||||
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
@@ -256,7 +256,7 @@ public class SingleOrgPolicyValidatorTests
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
|
||||
.Returns(new CommandResult());
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
(orgUserDetailUserWithout2Fa, false),
|
||||
});
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy));
|
||||
@@ -228,7 +228,7 @@ public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
|
||||
.Returns(new CommandResult());
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
|
||||
|
||||
@@ -288,7 +288,7 @@ public class SavePolicyCommandTests
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory();
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
@@ -332,7 +332,7 @@ public class SavePolicyCommandTests
|
||||
|
||||
|
||||
var sutProvider = SutProviderFactory();
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
|
||||
@@ -33,7 +33,7 @@ public class VNextSavePolicyCommandTests
|
||||
fakePolicyValidationEvent
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var newPolicy = new Policy
|
||||
{
|
||||
@@ -77,7 +77,7 @@ public class VNextSavePolicyCommandTests
|
||||
fakePolicyValidationEvent
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
@@ -117,7 +117,7 @@ public class VNextSavePolicyCommandTests
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory();
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
|
||||
@@ -137,7 +137,7 @@ public class VNextSavePolicyCommandTests
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory();
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
|
||||
@@ -167,7 +167,7 @@ public class VNextSavePolicyCommandTests
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var requireSsoPolicy = new Policy
|
||||
{
|
||||
@@ -202,7 +202,7 @@ public class VNextSavePolicyCommandTests
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var requireSsoPolicy = new Policy
|
||||
{
|
||||
@@ -237,7 +237,7 @@ public class VNextSavePolicyCommandTests
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var requireSsoPolicy = new Policy
|
||||
{
|
||||
@@ -271,7 +271,7 @@ public class VNextSavePolicyCommandTests
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
@@ -302,7 +302,7 @@ public class VNextSavePolicyCommandTests
|
||||
new FakeVaultTimeoutDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
@@ -331,7 +331,7 @@ public class VNextSavePolicyCommandTests
|
||||
new FakeSingleOrgDependencyEvent()
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
@@ -356,7 +356,7 @@ public class VNextSavePolicyCommandTests
|
||||
fakePolicyValidationEvent
|
||||
]);
|
||||
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
var singleOrgPolicy = new Policy
|
||||
{
|
||||
|
||||
@@ -38,6 +38,20 @@ public class EventIntegrationEventWriteServiceTests
|
||||
organizationId: Arg.Is<string>(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateManyAsync_EmptyList_DoesNothing()
|
||||
{
|
||||
await Subject.CreateManyAsync([]);
|
||||
await _eventIntegrationPublisher.DidNotReceiveWithAnyArgs().PublishEventAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_DisposesEventIntegrationPublisher()
|
||||
{
|
||||
await Subject.DisposeAsync();
|
||||
await _eventIntegrationPublisher.Received(1).DisposeAsync();
|
||||
}
|
||||
|
||||
private static bool AssertJsonStringsMatch(EventMessage expected, string body)
|
||||
{
|
||||
var actual = JsonSerializer.Deserialize<EventMessage>(body);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -13,6 +14,7 @@ using Bit.Test.Common.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
@@ -20,9 +22,10 @@ namespace Bit.Core.Test.Services;
|
||||
public class EventIntegrationHandlerTests
|
||||
{
|
||||
private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
private const string _templateWithGroup = "Group: #GroupName#";
|
||||
private const string _templateWithOrganization = "Org: #OrganizationName#";
|
||||
private const string _templateWithUser = "#UserName#, #UserEmail#";
|
||||
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#";
|
||||
private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#";
|
||||
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#";
|
||||
private static readonly Guid _organizationId = Guid.NewGuid();
|
||||
private static readonly Uri _uri = new Uri("https://localhost");
|
||||
private static readonly Uri _uri2 = new Uri("https://example.com");
|
||||
@@ -45,7 +48,7 @@ public class EventIntegrationHandlerTests
|
||||
.Create();
|
||||
}
|
||||
|
||||
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> expectedMessage(string template)
|
||||
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> ExpectedMessage(string template)
|
||||
{
|
||||
return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
@@ -105,11 +108,237 @@ public class EventIntegrationHandlerTests
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
|
||||
config.Template = _templateBase;
|
||||
config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { });
|
||||
config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup());
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_ActingUserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.ActingUserId ??= Guid.NewGuid();
|
||||
|
||||
cache.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
).Returns(actingUser);
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
|
||||
|
||||
await cache.Received(1).GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
|
||||
Assert.Equal(actingUser, context.ActingUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_ActingUserIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.ActingUserId = null;
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
Assert.Null(context.ActingUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_ActingUserOrganizationIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId = null;
|
||||
eventMessage.ActingUserId ??= Guid.NewGuid();
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
Assert.Null(context.ActingUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.GroupId ??= Guid.NewGuid();
|
||||
|
||||
cache.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
|
||||
).Returns(group);
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
|
||||
|
||||
await cache.Received(1).GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
|
||||
);
|
||||
Assert.Equal(group, context.Group);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_GroupIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
eventMessage.GroupId = null;
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
|
||||
);
|
||||
Assert.Null(context.Group);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
|
||||
cache.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
|
||||
).Returns(organization);
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
|
||||
|
||||
await cache.Received(1).GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
|
||||
);
|
||||
Assert.Equal(organization, context.Organization);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_OrganizationIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId = null;
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
|
||||
);
|
||||
Assert.Null(context.Organization);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.UserId ??= Guid.NewGuid();
|
||||
|
||||
cache.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
).Returns(userDetails);
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
|
||||
|
||||
await cache.Received(1).GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
|
||||
Assert.Equal(userDetails, context.User);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_UserIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId = null;
|
||||
eventMessage.UserId ??= Guid.NewGuid();
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
|
||||
Assert.Null(context.User);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_OrganizationUserIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.UserId = null;
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
|
||||
Assert.Null(context.User);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.ActingUserId ??= Guid.NewGuid();
|
||||
eventMessage.GroupId ??= Guid.NewGuid();
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.UserId ??= Guid.NewGuid();
|
||||
|
||||
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateBase);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
|
||||
);
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
|
||||
);
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
@@ -120,6 +349,16 @@ public class EventIntegrationHandlerTests
|
||||
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_NoOrganizationId_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||
eventMessage.OrganizationId = null;
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||
{
|
||||
@@ -128,15 +367,16 @@ public class EventIntegrationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -147,7 +387,7 @@ public class EventIntegrationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
@@ -157,72 +397,9 @@ public class EventIntegrationHandlerTests
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var user = Substitute.For<User>();
|
||||
user.Email = "test@example.com";
|
||||
user.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}");
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||
var organization = Substitute.For<Organization>();
|
||||
organization.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(organization);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}");
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var user = Substitute.For<User>();
|
||||
user.Email = "test@example.com";
|
||||
user.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}");
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -246,7 +423,7 @@ public class EventIntegrationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
|
||||
@@ -288,7 +465,7 @@ public class EventIntegrationHandlerTests
|
||||
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
@@ -306,7 +483,7 @@ public class EventIntegrationHandlerTests
|
||||
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class EventRouteServiceTests
|
||||
{
|
||||
private readonly IEventWriteService _broadcastEventWriteService = Substitute.For<IEventWriteService>();
|
||||
private readonly IEventWriteService _storageEventWriteService = Substitute.For<IEventWriteService>();
|
||||
private readonly IFeatureService _featureService = Substitute.For<IFeatureService>();
|
||||
private readonly EventRouteService Subject;
|
||||
|
||||
public EventRouteServiceTests()
|
||||
{
|
||||
Subject = new EventRouteService(_broadcastEventWriteService, _storageEventWriteService, _featureService);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_FlagDisabled_EventSentToStorageService(EventMessage eventMessage)
|
||||
{
|
||||
_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(false);
|
||||
|
||||
await Subject.CreateAsync(eventMessage);
|
||||
|
||||
await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<EventMessage>());
|
||||
await _storageEventWriteService.Received(1).CreateAsync(eventMessage);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_FlagEnabled_EventSentToBroadcastService(EventMessage eventMessage)
|
||||
{
|
||||
_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(true);
|
||||
|
||||
await Subject.CreateAsync(eventMessage);
|
||||
|
||||
await _broadcastEventWriteService.Received(1).CreateAsync(eventMessage);
|
||||
await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<EventMessage>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateManyAsync_FlagDisabled_EventsSentToStorageService(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(false);
|
||||
|
||||
await Subject.CreateManyAsync(eventMessages);
|
||||
|
||||
await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any<IEnumerable<EventMessage>>());
|
||||
await _storageEventWriteService.Received(1).CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateManyAsync_FlagEnabled_EventsSentToBroadcastService(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(true);
|
||||
|
||||
await Subject.CreateManyAsync(eventMessages);
|
||||
|
||||
await _broadcastEventWriteService.Received(1).CreateManyAsync(eventMessages);
|
||||
await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any<IEnumerable<EventMessage>>());
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,35 @@ public class IntegrationFilterServiceTests
|
||||
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EvaluateFilterGroup_EqualsUserIdString_Matches(EventMessage eventMessage)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
eventMessage.UserId = userId;
|
||||
|
||||
var group = new IntegrationFilterGroup
|
||||
{
|
||||
AndOperator = true,
|
||||
Rules =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Property = "UserId",
|
||||
Operation = IntegrationFilterOperation.Equals,
|
||||
Value = userId.ToString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||
Assert.True(result);
|
||||
|
||||
var jsonGroup = JsonSerializer.Serialize(group);
|
||||
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||
Assert.NotNull(roundtrippedGroup);
|
||||
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EvaluateFilterGroup_EqualsUserId_DoesNotMatch(EventMessage eventMessage)
|
||||
{
|
||||
@@ -281,6 +310,45 @@ public class IntegrationFilterServiceTests
|
||||
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EvaluateFilterGroup_NestedGroups_AnyMatch(EventMessage eventMessage)
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var collectionId = Guid.NewGuid();
|
||||
eventMessage.UserId = id;
|
||||
eventMessage.CollectionId = collectionId;
|
||||
|
||||
var nestedGroup = new IntegrationFilterGroup
|
||||
{
|
||||
AndOperator = false,
|
||||
Rules =
|
||||
[
|
||||
new() { Property = "UserId", Operation = IntegrationFilterOperation.Equals, Value = id },
|
||||
new()
|
||||
{
|
||||
Property = "CollectionId",
|
||||
Operation = IntegrationFilterOperation.In,
|
||||
Value = new Guid?[] { Guid.NewGuid() }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var topGroup = new IntegrationFilterGroup
|
||||
{
|
||||
AndOperator = false,
|
||||
Groups = [nestedGroup]
|
||||
};
|
||||
|
||||
var result = _service.EvaluateFilterGroup(topGroup, eventMessage);
|
||||
Assert.True(result);
|
||||
|
||||
var jsonGroup = JsonSerializer.Serialize(topGroup);
|
||||
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||
Assert.NotNull(roundtrippedGroup);
|
||||
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EvaluateFilterGroup_UnknownProperty_ReturnsFalse(EventMessage eventMessage)
|
||||
{
|
||||
|
||||
@@ -21,8 +21,8 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Fakes;
|
||||
@@ -618,7 +618,7 @@ public class OrganizationServiceTests
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites);
|
||||
|
||||
@@ -666,7 +666,7 @@ public class OrganizationServiceTests
|
||||
.SendInvitesAsync(Arg.Any<SendInvitesRequest>()).ThrowsAsync<Exception>();
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
await Assert.ThrowsAsync<AggregateException>(async () =>
|
||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites));
|
||||
@@ -732,7 +732,7 @@ public class OrganizationServiceTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id,
|
||||
seatAdjustment, maxAutoscaleSeats));
|
||||
@@ -757,7 +757,7 @@ public class OrganizationServiceTests
|
||||
organization.SmSeats = 100;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
@@ -837,7 +837,7 @@ public class OrganizationServiceTests
|
||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||
public void ValidateSecretsManagerPlan_ThrowsException_WhenNoSecretsManagerSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var signup = new OrganizationUpgrade
|
||||
{
|
||||
UseSecretsManager = true,
|
||||
@@ -854,7 +854,7 @@ public class OrganizationServiceTests
|
||||
[BitAutoData(PlanType.Free)]
|
||||
public void ValidateSecretsManagerPlan_ThrowsException_WhenSubtractingSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var signup = new OrganizationUpgrade
|
||||
{
|
||||
UseSecretsManager = true,
|
||||
@@ -871,7 +871,7 @@ public class OrganizationServiceTests
|
||||
PlanType planType,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var signup = new OrganizationUpgrade
|
||||
{
|
||||
UseSecretsManager = true,
|
||||
@@ -890,7 +890,7 @@ public class OrganizationServiceTests
|
||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||
public void ValidateSecretsManagerPlan_ThrowsException_WhenMoreSeatsThanPasswordManagerSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var signup = new OrganizationUpgrade
|
||||
{
|
||||
UseSecretsManager = true,
|
||||
@@ -912,7 +912,7 @@ public class OrganizationServiceTests
|
||||
PlanType planType,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var signup = new OrganizationUpgrade
|
||||
{
|
||||
UseSecretsManager = true,
|
||||
@@ -930,7 +930,7 @@ public class OrganizationServiceTests
|
||||
PlanType planType,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var signup = new OrganizationUpgrade
|
||||
{
|
||||
UseSecretsManager = true,
|
||||
@@ -952,7 +952,7 @@ public class OrganizationServiceTests
|
||||
PlanType planType,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var signup = new OrganizationUpgrade
|
||||
{
|
||||
UseSecretsManager = true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Models.Slack;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -28,6 +29,9 @@ public class SlackIntegrationHandlerTests
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token);
|
||||
|
||||
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(new SlackSendMessageResponse() { Ok = true, Channel = _channelId });
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
@@ -39,4 +43,97 @@ public class SlackIntegrationHandlerTests
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("service_unavailable")]
|
||||
[InlineData("ratelimited")]
|
||||
[InlineData("rate_limited")]
|
||||
[InlineData("internal_error")]
|
||||
[InlineData("message_limit_exceeded")]
|
||||
public async Task HandleAsync_FailedRetryableRequest_ReturnsFailureWithRetryable(string error)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
|
||||
{
|
||||
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
|
||||
MessageId = "MessageId",
|
||||
RenderedTemplate = "Test Message"
|
||||
};
|
||||
|
||||
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(new SlackSendMessageResponse() { Ok = false, Channel = _channelId, Error = error });
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.NotNull(result.FailureReason);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("access_denied")]
|
||||
[InlineData("channel_not_found")]
|
||||
[InlineData("token_expired")]
|
||||
[InlineData("token_revoked")]
|
||||
public async Task HandleAsync_FailedNonRetryableRequest_ReturnsNonRetryableFailure(string error)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
|
||||
{
|
||||
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
|
||||
MessageId = "MessageId",
|
||||
RenderedTemplate = "Test Message"
|
||||
};
|
||||
|
||||
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(new SlackSendMessageResponse() { Ok = false, Channel = _channelId, Error = error });
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.NotNull(result.FailureReason);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_NullResponse_ReturnsNonRetryableFailure()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
|
||||
{
|
||||
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
|
||||
MessageId = "MessageId",
|
||||
RenderedTemplate = "Test Message"
|
||||
};
|
||||
|
||||
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns((SlackSendMessageResponse?)null);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal("Slack response was null", result.FailureReason);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,27 @@ public class SlackServiceTests
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelIdAsync_NoChannelFound_ReturnsEmptyResult()
|
||||
{
|
||||
var emptyResponse = JsonSerializer.Serialize(
|
||||
new
|
||||
{
|
||||
ok = true,
|
||||
channels = Array.Empty<string>(),
|
||||
response_metadata = new { next_cursor = "" }
|
||||
});
|
||||
|
||||
_handler.When(HttpMethod.Get)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(emptyResponse));
|
||||
|
||||
var sutProvider = GetSutProvider();
|
||||
var result = await sutProvider.Sut.GetChannelIdAsync(_token, "general");
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelIdAsync_ReturnsCorrectChannelId()
|
||||
{
|
||||
@@ -235,6 +256,32 @@ public class SlackServiceTests
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDmChannelByEmailAsync_ApiErrorUnparsableDmResponse_ReturnsEmptyString()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var email = "user@example.com";
|
||||
var userId = "U12345";
|
||||
|
||||
var userResponse = new
|
||||
{
|
||||
ok = true,
|
||||
user = new { id = userId }
|
||||
};
|
||||
|
||||
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
|
||||
|
||||
_handler.When("https://slack.com/api/conversations.open")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("NOT JSON"));
|
||||
|
||||
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
|
||||
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDmChannelByEmailAsync_ApiErrorUserResponse_ReturnsEmptyString()
|
||||
{
|
||||
@@ -244,7 +291,7 @@ public class SlackServiceTests
|
||||
var userResponse = new
|
||||
{
|
||||
ok = false,
|
||||
error = "An error occured"
|
||||
error = "An error occurred"
|
||||
};
|
||||
|
||||
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
|
||||
@@ -256,6 +303,21 @@ public class SlackServiceTests
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDmChannelByEmailAsync_ApiErrorUnparsableUserResponse_ReturnsEmptyString()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var email = "user@example.com";
|
||||
|
||||
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("Not JSON"));
|
||||
|
||||
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
|
||||
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRedirectUrl_ReturnsCorrectUrl()
|
||||
{
|
||||
@@ -341,18 +403,29 @@ public class SlackServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendSlackMessageByChannelId_Sends_Correct_Message()
|
||||
public async Task SendSlackMessageByChannelId_Success_ReturnsSuccessfulResponse()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelId = "C12345";
|
||||
var message = "Hello, Slack!";
|
||||
|
||||
var jsonResponse = JsonSerializer.Serialize(new
|
||||
{
|
||||
ok = true,
|
||||
channel = channelId,
|
||||
});
|
||||
|
||||
_handler.When(HttpMethod.Post)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(string.Empty));
|
||||
.WithContent(new StringContent(jsonResponse));
|
||||
|
||||
await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
|
||||
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
|
||||
|
||||
// Response was parsed correctly
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Ok);
|
||||
|
||||
// Request was sent correctly
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
@@ -365,4 +438,62 @@ public class SlackServiceTests
|
||||
Assert.Equal(message, json.RootElement.GetProperty("text").GetString() ?? string.Empty);
|
||||
Assert.Equal(channelId, json.RootElement.GetProperty("channel").GetString() ?? string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendSlackMessageByChannelId_Failure_ReturnsErrorResponse()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelId = "C12345";
|
||||
var message = "Hello, Slack!";
|
||||
|
||||
var jsonResponse = JsonSerializer.Serialize(new
|
||||
{
|
||||
ok = false,
|
||||
channel = channelId,
|
||||
error = "error"
|
||||
});
|
||||
|
||||
_handler.When(HttpMethod.Post)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(jsonResponse));
|
||||
|
||||
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
|
||||
|
||||
// Response was parsed correctly
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.Ok);
|
||||
Assert.NotNull(result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendSlackMessageByChannelIdAsync_InvalidJson_ReturnsNull()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelId = "C12345";
|
||||
var message = "Hello world!";
|
||||
|
||||
_handler.When(HttpMethod.Post)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("Not JSON"));
|
||||
|
||||
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendSlackMessageByChannelIdAsync_HttpServerError_ReturnsNull()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelId = "C12345";
|
||||
var message = "Hello world!";
|
||||
|
||||
_handler.When(HttpMethod.Post)
|
||||
.RespondWith(HttpStatusCode.InternalServerError)
|
||||
.WithContent(new StringContent(string.Empty));
|
||||
|
||||
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ public class IntegrationTemplateProcessorTests
|
||||
[Theory]
|
||||
[InlineData("User name is #UserName#")]
|
||||
[InlineData("Email: #UserEmail#")]
|
||||
[InlineData("User type = #UserType#")]
|
||||
public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template)
|
||||
{
|
||||
var result = IntegrationTemplateProcessor.TemplateRequiresUser(template);
|
||||
@@ -102,6 +103,7 @@ public class IntegrationTemplateProcessorTests
|
||||
[Theory]
|
||||
[InlineData("Acting user is #ActingUserName#")]
|
||||
[InlineData("Acting user's email is #ActingUserEmail#")]
|
||||
[InlineData("Acting user's type is #ActingUserType#")]
|
||||
public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template)
|
||||
{
|
||||
var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template);
|
||||
@@ -118,6 +120,25 @@ public class IntegrationTemplateProcessorTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Group name is #GroupName#!")]
|
||||
[InlineData("Group: #GroupName#")]
|
||||
public void TemplateRequiresGroup_ContainingKeys_ReturnsTrue(string template)
|
||||
{
|
||||
var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("#GroupId#")] // This is on the base class, not fetched, so should be false
|
||||
[InlineData("No Group Tokens")]
|
||||
[InlineData("")]
|
||||
public void TemplateRequiresGroup_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template)
|
||||
{
|
||||
var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Organization: #OrganizationName#")]
|
||||
[InlineData("Welcome to #OrganizationName#")]
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using Bit.Core.Auth.Attributes;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Attributes;
|
||||
|
||||
public class MarketingInitiativeValidationAttributeTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsValid_NullValue_ReturnsTrue()
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var actual = sut.IsValid(null);
|
||||
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MarketingInitiativeConstants.Premium)]
|
||||
public void IsValid_AcceptedValue_ReturnsTrue(string value)
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var actual = sut.IsValid(value);
|
||||
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("invalid")]
|
||||
[InlineData("")]
|
||||
[InlineData("Premium")] // case sensitive - capitalized
|
||||
[InlineData("PREMIUM")] // case sensitive - uppercase
|
||||
[InlineData("premium ")] // trailing space
|
||||
[InlineData(" premium")] // leading space
|
||||
public void IsValid_InvalidStringValue_ReturnsFalse(string value)
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var actual = sut.IsValid(value);
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(123)] // integer
|
||||
[InlineData(true)] // boolean
|
||||
[InlineData(45.67)] // double
|
||||
public void IsValid_NonStringValue_ReturnsFalse(object value)
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var actual = sut.IsValid(value);
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ErrorMessage_ContainsAcceptedValues()
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var errorMessage = sut.ErrorMessage;
|
||||
|
||||
Assert.NotNull(errorMessage);
|
||||
Assert.Contains("premium", errorMessage);
|
||||
Assert.Contains("Marketing initiative type must be one of:", errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Models.Api.Request.Accounts;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot tests to ensure the string constants in <see cref="MarketingInitiativeConstants"/> do not change unintentionally.
|
||||
/// If you intentionally change any of these values, please update the tests to reflect the new expected values.
|
||||
/// </summary>
|
||||
public class MarketingInitiativeConstantsSnapshotTests
|
||||
{
|
||||
[Fact]
|
||||
public void MarketingInitiativeConstants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("premium", MarketingInitiativeConstants.Premium);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -12,6 +14,7 @@ using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -364,4 +367,54 @@ public class SsoConfigServiceTests
|
||||
await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()
|
||||
.UpsertAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_Tde_WhenPolicyValidatorsRefactorEnabled_UsesVNextSavePolicyCommand(
|
||||
SutProvider<SsoConfigService> sutProvider, Organization organization)
|
||||
{
|
||||
var ssoConfig = new SsoConfig
|
||||
{
|
||||
Id = default,
|
||||
Data = new SsoConfigurationData
|
||||
{
|
||||
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,
|
||||
}.Serialize(),
|
||||
Enabled = true,
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
|
||||
|
||||
await sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Is<SavePolicyModel>(m =>
|
||||
m.PolicyUpdate.Type == PolicyType.SingleOrg &&
|
||||
m.PolicyUpdate.OrganizationId == organization.Id &&
|
||||
m.PolicyUpdate.Enabled &&
|
||||
m.PerformedBy is SystemUser));
|
||||
|
||||
await sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Is<SavePolicyModel>(m =>
|
||||
m.PolicyUpdate.Type == PolicyType.ResetPassword &&
|
||||
m.PolicyUpdate.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled &&
|
||||
m.PolicyUpdate.OrganizationId == organization.Id &&
|
||||
m.PolicyUpdate.Enabled &&
|
||||
m.PerformedBy is SystemUser));
|
||||
|
||||
await sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Is<SavePolicyModel>(m =>
|
||||
m.PolicyUpdate.Type == PolicyType.RequireSso &&
|
||||
m.PolicyUpdate.OrganizationId == organization.Id &&
|
||||
m.PolicyUpdate.Enabled &&
|
||||
m.PerformedBy is SystemUser));
|
||||
|
||||
await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()
|
||||
.UpsertAsync(default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
@@ -37,6 +38,12 @@ public class RegisterUserCommandTests
|
||||
public async Task RegisterUser_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
@@ -61,6 +68,12 @@ public class RegisterUserCommandTests
|
||||
public async Task RegisterUser_WhenCreateUserFails_ReturnsIdentityResultFailed(SutProvider<RegisterUserCommand> sutProvider, User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Failed());
|
||||
@@ -80,6 +93,120 @@ public class RegisterUserCommandTests
|
||||
.SendWelcomeEmailAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterSSOAutoProvisionedUserAsync tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
[Theory, BitAutoData]
|
||||
public async Task RegisterSSOAutoProvisionedUserAsync_Success(
|
||||
User user,
|
||||
Organization organization,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Id = Guid.NewGuid();
|
||||
organization.Id = Guid.NewGuid();
|
||||
organization.Name = "Test Organization";
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.CreateUserAsync(user);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RegisterSSOAutoProvisionedUserAsync_UserRegistrationFails_ReturnsFailedResult(
|
||||
User user,
|
||||
Organization organization,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = new IdentityError();
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Failed(expectedError));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Contains(expectedError, result.Errors);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationUserWelcomeEmailAsync(Arg.Any<User>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
public async Task RegisterSSOAutoProvisionedUserAsync_EnterpriseOrg_SendsOrganizationWelcomeEmail(
|
||||
PlanType planType,
|
||||
User user,
|
||||
Organization organization,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = planType;
|
||||
organization.Name = "Enterprise Org";
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((OrganizationUser)null);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationUserWelcomeEmailAsync(user, organization.Name);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RegisterSSOAutoProvisionedUserAsync_FeatureFlagDisabled_SendsLegacyWelcomeEmail(
|
||||
User user,
|
||||
Organization organization,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendWelcomeEmailAsync(user);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterUserWithOrganizationInviteToken tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
@@ -301,6 +428,138 @@ public class RegisterUserCommandTests
|
||||
Assert.Equal(expectedErrorMessage, exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromDifferentOrg_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
orgUser.Email = user.Email;
|
||||
orgUser.Id = orgUserId;
|
||||
var blockingOrganizationId = Guid.NewGuid(); // Different org that has the domain blocked
|
||||
orgUser.OrganizationId = Guid.NewGuid(); // The org they're trying to join
|
||||
|
||||
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
|
||||
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = orgInviteTokenable;
|
||||
return true;
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(orgUserId)
|
||||
.Returns(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
// Mock the new overload that excludes the organization - it should return true (domain IS blocked by another org)
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", orgUser.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromSameOrg_Succeeds(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@company-domain.com";
|
||||
user.ReferenceData = null;
|
||||
orgUser.Email = user.Email;
|
||||
orgUser.Id = orgUserId;
|
||||
// The organization owns the domain and is trying to invite the user
|
||||
orgUser.OrganizationId = Guid.NewGuid();
|
||||
|
||||
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
|
||||
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = orgInviteTokenable;
|
||||
return true;
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(orgUserId)
|
||||
.Returns(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
// Mock the new overload - it should return false (domain is NOT blocked by OTHER orgs)
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", orgUser.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user, masterPasswordHash)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
await sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.Received(1)
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", orgUser.OrganizationId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaOrganizationInviteToken_WithValidTokenButNullOrgUser_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@example.com";
|
||||
orgUser.Email = user.Email;
|
||||
orgUser.Id = orgUserId;
|
||||
|
||||
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
|
||||
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = orgInviteTokenable;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Mock GetByIdAsync to return null - simulating a deleted or non-existent organization user
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(orgUserId)
|
||||
.Returns((OrganizationUser)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
|
||||
Assert.Equal("Invalid organization user invitation.", exception.Message);
|
||||
|
||||
// Verify that GetByIdAsync was called
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.GetByIdAsync(orgUserId);
|
||||
|
||||
// Verify that user creation was never attempted
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.DidNotReceive()
|
||||
.CreateUserAsync(Arg.Any<User>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterUserViaEmailVerificationToken tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
@@ -310,6 +569,12 @@ public class RegisterUserCommandTests
|
||||
public async Task RegisterUserViaEmailVerificationToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(callInfo =>
|
||||
@@ -342,6 +607,12 @@ public class RegisterUserCommandTests
|
||||
public async Task RegisterUserViaEmailVerificationToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(callInfo =>
|
||||
@@ -380,6 +651,12 @@ public class RegisterUserCommandTests
|
||||
string orgSponsoredFreeFamilyPlanInviteToken)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
|
||||
.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)
|
||||
.Returns((true, new OrganizationSponsorship()));
|
||||
@@ -409,6 +686,12 @@ public class RegisterUserCommandTests
|
||||
string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
|
||||
.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)
|
||||
.Returns((false, new OrganizationSponsorship()));
|
||||
@@ -446,9 +729,14 @@ public class RegisterUserCommandTests
|
||||
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
emergencyAccess.Email = user.Email;
|
||||
emergencyAccess.Id = acceptEmergencyAccessId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
|
||||
.TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
@@ -482,9 +770,14 @@ public class RegisterUserCommandTests
|
||||
string masterPasswordHash, EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
emergencyAccess.Email = "wrong@email.com";
|
||||
emergencyAccess.Id = acceptEmergencyAccessId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
|
||||
.TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
@@ -525,6 +818,8 @@ public class RegisterUserCommandTests
|
||||
User user, string masterPasswordHash, Guid providerUserId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
// Start with plaintext
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
|
||||
@@ -547,6 +842,10 @@ public class RegisterUserCommandTests
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.OrganizationInviteExpirationHours.Returns(120); // 5 days
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user, masterPasswordHash)
|
||||
.Returns(IdentityResult.Success);
|
||||
@@ -576,6 +875,8 @@ public class RegisterUserCommandTests
|
||||
User user, string masterPasswordHash, Guid providerUserId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
// Start with plaintext
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
|
||||
@@ -598,6 +899,10 @@ public class RegisterUserCommandTests
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.OrganizationInviteExpirationHours.Returns(120); // 5 days
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
// Using sutProvider in the parameters of the function means that the constructor has already run for the
|
||||
// command so we have to recreate it in order for our mock overrides to be used.
|
||||
sutProvider.Create();
|
||||
@@ -646,5 +951,434 @@ public class RegisterUserCommandTests
|
||||
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Domain blocking tests (BlockClaimedDomainAccountCreation policy)
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUser_BlockedDomain_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
|
||||
.Returns(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUser(user));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
|
||||
// Verify user creation was never attempted
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.DidNotReceive()
|
||||
.CreateUserAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUser_AllowedDomain_Succeeds(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@allowed-domain.com";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowed-domain.com")
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterUser(user);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
await sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.Received(1)
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowed-domain.com");
|
||||
}
|
||||
|
||||
// SendWelcomeEmail tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
||||
[BitAutoData(PlanType.Free)]
|
||||
public async Task SendWelcomeEmail_FamilyOrg_SendsFamilyWelcomeEmail(
|
||||
PlanType planType,
|
||||
User user,
|
||||
Organization organization,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = planType;
|
||||
organization.Name = "Family Org";
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((OrganizationUser)null);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, organization.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaEmailVerificationToken_BlockedDomain_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
|
||||
string emailVerificationToken, bool receiveMarketingMaterials)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_BlockedDomain_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
|
||||
string orgSponsoredFreeFamilyPlanInviteToken)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
|
||||
.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)
|
||||
.Returns((true, new OrganizationSponsorship()));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_BlockedDomain_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
|
||||
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
emergencyAccess.Email = user.Email;
|
||||
emergencyAccess.Id = acceptEmergencyAccessId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
|
||||
.TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 10);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaProviderInviteToken_BlockedDomain_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, Guid providerUserId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
|
||||
// Start with plaintext
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
|
||||
|
||||
// Get the byte array of the plaintext
|
||||
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
|
||||
|
||||
// Base64 encode the byte array (this is passed to protector.protect(bytes))
|
||||
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
var mockDataProtector = Substitute.For<IDataProtector>();
|
||||
|
||||
// Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)
|
||||
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(mockDataProtector);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.OrganizationInviteExpirationHours.Returns(120); // 5 days
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
|
||||
.Returns(true);
|
||||
|
||||
// Using sutProvider in the parameters of the function means that the constructor has already run for the
|
||||
// command so we have to recreate it in order for our mock overrides to be used.
|
||||
sutProvider.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Invalid email format tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUser_InvalidEmailFormat_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "invalid-email-format";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUser(user));
|
||||
Assert.Equal("Invalid email address format.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaEmailVerificationToken_InvalidEmailFormat_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
|
||||
string emailVerificationToken, bool receiveMarketingMaterials)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "invalid-email-format";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));
|
||||
Assert.Equal("Invalid email address format.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendWelcomeEmail_OrganizationNull_SendsIndividualWelcomeEmail(
|
||||
User user,
|
||||
OrganizationUser orgUser,
|
||||
string orgInviteToken,
|
||||
string masterPasswordHash,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.ReferenceData = null;
|
||||
orgUser.Email = user.Email;
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user, masterPasswordHash)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(orgUser.Id)
|
||||
.Returns(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||
.Returns((Policy)null);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(orgUser.OrganizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
|
||||
.Returns(true);
|
||||
|
||||
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
|
||||
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = orgInviteTokenable;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUser.Id);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendIndividualUserWelcomeEmailAsync(user);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendWelcomeEmail_OrganizationDisplayNameNull_SendsIndividualWelcomeEmail(
|
||||
User user,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
Organization organization = new Organization
|
||||
{
|
||||
Name = null
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((OrganizationUser)null);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendIndividualUserWelcomeEmailAsync(user);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationWelcomeEmailDetailsAsync_HappyPath_ReturnsOrganizationWelcomeEmailDetails(
|
||||
Organization organization,
|
||||
User user,
|
||||
OrganizationUser orgUser,
|
||||
string masterPasswordHash,
|
||||
string orgInviteToken,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.ReferenceData = null;
|
||||
orgUser.Email = user.Email;
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user, masterPasswordHash)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(orgUser.Id)
|
||||
.Returns(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||
.Returns((Policy)null);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(orgUser.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
|
||||
.Returns(true);
|
||||
|
||||
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
|
||||
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = orgInviteTokenable;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.GetByIdAsync(orgUser.OrganizationId);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationUserWelcomeEmailAsync(user, organization.DisplayName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -21,6 +22,43 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationTrue_SendsEmailAndReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByEmailAsync(email)
|
||||
.ReturnsNull();
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.EnableEmailVerification = true;
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var mockedToken = "token";
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(mockedToken);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken, null);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_WhenFromMarketingIsPremium_SendsEmailWithMarketingParameterAndReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string email, string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
@@ -34,31 +72,35 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IMailService>()
|
||||
.SendRegistrationVerificationEmailAsync(email, Arg.Any<string>())
|
||||
.Returns(Task.CompletedTask);
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var mockedToken = "token";
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(mockedToken);
|
||||
|
||||
var fromMarketing = MarketingInitiativeConstants.Premium;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails);
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, fromMarketing);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken);
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken, fromMarketing);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_WhenIsExistingUserAndEnableEmailVerificationTrue_ReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string email, string name, bool receiveMarketingEmails)
|
||||
string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByEmailAsync(email)
|
||||
.Returns(new User());
|
||||
@@ -69,27 +111,33 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var mockedToken = "token";
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(mockedToken);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails);
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken);
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken, null);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationFalse_ReturnsToken(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string email, string name, bool receiveMarketingEmails)
|
||||
string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByEmailAsync(email)
|
||||
.ReturnsNull();
|
||||
@@ -100,13 +148,17 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var mockedToken = "token";
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(mockedToken);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails);
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(mockedToken, result);
|
||||
@@ -122,15 +174,17 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
.DisableUserRegistration = true;
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails));
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_WhenIsExistingUserAndEnableEmailVerificationFalse_ThrowsBadRequestException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string email, string name, bool receiveMarketingEmails)
|
||||
string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var email = $"test+{Guid.NewGuid()}@example.com";
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByEmailAsync(email)
|
||||
.Returns(new User());
|
||||
@@ -138,8 +192,15 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.EnableEmailVerification = false;
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails));
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -150,7 +211,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run(null, name, receiveMarketingEmails));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run(null, name, receiveMarketingEmails, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -160,6 +221,90 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
{
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run("", name, receiveMarketingEmails));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run("", name, receiveMarketingEmails, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_WhenBlockedDomain_ThrowsBadRequestException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var email = $"test+{Guid.NewGuid()}@blockedcompany.com";
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blockedcompany.com")
|
||||
.Returns(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_WhenAllowedDomain_Succeeds(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var email = $"test+{Guid.NewGuid()}@allowedcompany.com";
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByEmailAsync(email)
|
||||
.ReturnsNull();
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.EnableEmailVerification = false;
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowedcompany.com")
|
||||
.Returns(false);
|
||||
|
||||
var mockedToken = "token";
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(mockedToken);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(mockedToken, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_InvalidEmailFormat_ThrowsBadRequestException(
|
||||
SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var email = "invalid-email-format";
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));
|
||||
Assert.Equal("Invalid email address format.", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
37
test/Core.Test/Billing/Mocks/MockPlans.cs
Normal file
37
test/Core.Test/Billing/Mocks/MockPlans.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks;
|
||||
|
||||
public class MockPlans
|
||||
{
|
||||
public static List<Plan> Plans =>
|
||||
[
|
||||
new CustomPlan(),
|
||||
new Enterprise2019Plan(false),
|
||||
new Enterprise2019Plan(true),
|
||||
new Enterprise2020Plan(false),
|
||||
new Enterprise2020Plan(true),
|
||||
new Enterprise2023Plan(false),
|
||||
new Enterprise2023Plan(true),
|
||||
new EnterprisePlan(false),
|
||||
new EnterprisePlan(true),
|
||||
new Families2019Plan(),
|
||||
new Families2025Plan(),
|
||||
new FamiliesPlan(),
|
||||
new FreePlan(),
|
||||
new Teams2019Plan(false),
|
||||
new Teams2019Plan(true),
|
||||
new Teams2020Plan(false),
|
||||
new Teams2020Plan(true),
|
||||
new Teams2023Plan(false),
|
||||
new Teams2023Plan(true),
|
||||
new TeamsPlan(false),
|
||||
new TeamsPlan(true),
|
||||
new TeamsStarterPlan(),
|
||||
new TeamsStarterPlan2023()
|
||||
];
|
||||
|
||||
public static Plan Get(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType)!;
|
||||
}
|
||||
21
test/Core.Test/Billing/Mocks/Plans/CustomPlan.cs
Normal file
21
test/Core.Test/Billing/Mocks/Plans/CustomPlan.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record CustomPlan : Plan
|
||||
{
|
||||
public CustomPlan()
|
||||
{
|
||||
Type = PlanType.Custom;
|
||||
PasswordManager = new CustomPasswordManagerFeatures();
|
||||
}
|
||||
|
||||
private record CustomPasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public CustomPasswordManagerFeatures()
|
||||
{
|
||||
AllowSeatAutoscale = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
103
test/Core.Test/Billing/Mocks/Plans/Enterprise2019Plan.cs
Normal file
103
test/Core.Test/Billing/Mocks/Plans/Enterprise2019Plan.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record Enterprise2019Plan : Plan
|
||||
{
|
||||
public Enterprise2019Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.EnterpriseAnnually2019 : PlanType.EnterpriseMonthly2019;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = isAnnual ? "Enterprise (Annually) 2019" : "Enterprise (Monthly) 2019";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
LegacyYear = 2020;
|
||||
|
||||
SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);
|
||||
PasswordManager = new Enterprise2019PasswordManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Enterprise2019SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2019SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 200;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Enterprise2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2019PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "enterprise-org-seat-annually";
|
||||
SeatPrice = 36;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "enterprise-org-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 4M;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
test/Core.Test/Billing/Mocks/Plans/Enterprise2020Plan.cs
Normal file
103
test/Core.Test/Billing/Mocks/Plans/Enterprise2020Plan.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record Enterprise2020Plan : Plan
|
||||
{
|
||||
public Enterprise2020Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.EnterpriseAnnually2020 : PlanType.EnterpriseMonthly2020;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = isAnnual ? "Enterprise (Annually) 2020" : "Enterprise (Monthly) 2020";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
LegacyYear = 2023;
|
||||
|
||||
PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new Enterprise2020SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Enterprise2020SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2020SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 200;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Enterprise2020PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2020PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2020-enterprise-org-seat-annually";
|
||||
SeatPrice = 60;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2020-enterprise-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 6;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
test/Core.Test/Billing/Mocks/Plans/EnterprisePlan.cs
Normal file
106
test/Core.Test/Billing/Mocks/Plans/EnterprisePlan.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record EnterprisePlan : Plan
|
||||
{
|
||||
public EnterprisePlan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.EnterpriseAnnually : PlanType.EnterpriseMonthly;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = isAnnual ? "Enterprise (Annually)" : "Enterprise (Monthly)";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
|
||||
PasswordManager = new EnterprisePasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new EnterpriseSecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record EnterpriseSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public EnterpriseSecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 12;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record EnterprisePasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public EnterprisePasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-annually-2024";
|
||||
SeatPrice = 72;
|
||||
ProviderPortalSeatPrice = 72;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-enterprise-seat-monthly";
|
||||
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-monthly-2024";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 7;
|
||||
ProviderPortalSeatPrice = 6;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
test/Core.Test/Billing/Mocks/Plans/EnterprisePlan2023.cs
Normal file
104
test/Core.Test/Billing/Mocks/Plans/EnterprisePlan2023.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record Enterprise2023Plan : Plan
|
||||
{
|
||||
public Enterprise2023Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.EnterpriseAnnually2023 : PlanType.EnterpriseMonthly2023;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = isAnnual ? "Enterprise (Annually)" : "Enterprise (Monthly)";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
|
||||
LegacyYear = 2024;
|
||||
|
||||
PasswordManager = new Enterprise2023PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new Enterprise2023SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Enterprise2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2023SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 200;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Enterprise2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Enterprise2023PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||
SeatPrice = 72;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-enterprise-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
test/Core.Test/Billing/Mocks/Plans/Families2019Plan.cs
Normal file
50
test/Core.Test/Billing/Mocks/Plans/Families2019Plan.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record Families2019Plan : Plan
|
||||
{
|
||||
public Families2019Plan()
|
||||
{
|
||||
Type = PlanType.FamiliesAnnually2019;
|
||||
ProductTier = ProductTierType.Families;
|
||||
Name = "Families 2019";
|
||||
IsAnnual = true;
|
||||
NameLocalizationKey = "planNameFamilies";
|
||||
DescriptionLocalizationKey = "planDescFamilies";
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasSelfHost = true;
|
||||
HasTotp = true;
|
||||
|
||||
UpgradeSortOrder = 1;
|
||||
DisplaySortOrder = 1;
|
||||
LegacyYear = 2020;
|
||||
|
||||
PasswordManager = new Families2019PasswordManagerFeatures();
|
||||
}
|
||||
|
||||
private record Families2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Families2019PasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 5;
|
||||
BaseStorageGb = 1;
|
||||
MaxSeats = 5;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasPremiumAccessOption = true;
|
||||
|
||||
StripePlanId = "personal-org-annually";
|
||||
StripeStoragePlanId = "personal-storage-gb-annually";
|
||||
StripePremiumAccessPlanId = "personal-org-premium-access-annually";
|
||||
BasePrice = 12;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
PremiumAccessOptionPrice = 40;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
test/Core.Test/Billing/Mocks/Plans/Families2025Plan.cs
Normal file
47
test/Core.Test/Billing/Mocks/Plans/Families2025Plan.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record Families2025Plan : Plan
|
||||
{
|
||||
public Families2025Plan()
|
||||
{
|
||||
Type = PlanType.FamiliesAnnually2025;
|
||||
ProductTier = ProductTierType.Families;
|
||||
Name = "Families 2025";
|
||||
IsAnnual = true;
|
||||
NameLocalizationKey = "planNameFamilies";
|
||||
DescriptionLocalizationKey = "planDescFamilies";
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasSelfHost = true;
|
||||
HasTotp = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 1;
|
||||
DisplaySortOrder = 1;
|
||||
|
||||
PasswordManager = new Families2025PasswordManagerFeatures();
|
||||
}
|
||||
|
||||
private record Families2025PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Families2025PasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 6;
|
||||
BaseStorageGb = 1;
|
||||
MaxSeats = 6;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "2020-families-org-annually";
|
||||
StripeStoragePlanId = "personal-storage-gb-annually";
|
||||
BasePrice = 40;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
test/Core.Test/Billing/Mocks/Plans/FamiliesPlan.cs
Normal file
47
test/Core.Test/Billing/Mocks/Plans/FamiliesPlan.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record FamiliesPlan : Plan
|
||||
{
|
||||
public FamiliesPlan()
|
||||
{
|
||||
Type = PlanType.FamiliesAnnually;
|
||||
ProductTier = ProductTierType.Families;
|
||||
Name = "Families";
|
||||
IsAnnual = true;
|
||||
NameLocalizationKey = "planNameFamilies";
|
||||
DescriptionLocalizationKey = "planDescFamilies";
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasSelfHost = true;
|
||||
HasTotp = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 1;
|
||||
DisplaySortOrder = 1;
|
||||
|
||||
PasswordManager = new FamiliesPasswordManagerFeatures();
|
||||
}
|
||||
|
||||
private record FamiliesPasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public FamiliesPasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 6;
|
||||
BaseStorageGb = 1;
|
||||
MaxSeats = 6;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "2020-families-org-annually";
|
||||
StripeStoragePlanId = "personal-storage-gb-annually";
|
||||
BasePrice = 40;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
test/Core.Test/Billing/Mocks/Plans/FreePlan.cs
Normal file
48
test/Core.Test/Billing/Mocks/Plans/FreePlan.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record FreePlan : Plan
|
||||
{
|
||||
public FreePlan()
|
||||
{
|
||||
Type = PlanType.Free;
|
||||
ProductTier = ProductTierType.Free;
|
||||
Name = "Free";
|
||||
NameLocalizationKey = "planNameFree";
|
||||
DescriptionLocalizationKey = "planDescFree";
|
||||
|
||||
UpgradeSortOrder = -1; // Always the lowest plan, cannot be upgraded to
|
||||
DisplaySortOrder = -1;
|
||||
|
||||
PasswordManager = new FreePasswordManagerFeatures();
|
||||
SecretsManager = new FreeSecretsManagerFeatures();
|
||||
}
|
||||
|
||||
private record FreeSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public FreeSecretsManagerFeatures()
|
||||
{
|
||||
BaseSeats = 2;
|
||||
BaseServiceAccount = 3;
|
||||
MaxProjects = 3;
|
||||
MaxSeats = 2;
|
||||
MaxServiceAccounts = 3;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
|
||||
private record FreePasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public FreePasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 2;
|
||||
MaxCollections = 2;
|
||||
MaxSeats = 2;
|
||||
|
||||
AllowSeatAutoscale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
99
test/Core.Test/Billing/Mocks/Plans/Teams2019Plan.cs
Normal file
99
test/Core.Test/Billing/Mocks/Plans/Teams2019Plan.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record Teams2019Plan : Plan
|
||||
{
|
||||
public Teams2019Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.TeamsAnnually2019 : PlanType.TeamsMonthly2019;
|
||||
ProductTier = ProductTierType.Teams;
|
||||
Name = isAnnual ? "Teams (Annually) 2019" : "Teams (Monthly) 2019";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameTeams";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
LegacyYear = 2020;
|
||||
|
||||
SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);
|
||||
PasswordManager = new Teams2019PasswordManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Teams2019SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Teams2019SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Teams2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Teams2019PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 5;
|
||||
BaseStorageGb = 1;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripePlanId = "teams-org-annually";
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "teams-org-seat-annually";
|
||||
SeatPrice = 24;
|
||||
BasePrice = 60;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripePlanId = "teams-org-monthly";
|
||||
StripeSeatPlanId = "teams-org-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
BasePrice = 8;
|
||||
SeatPrice = 2.5M;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
test/Core.Test/Billing/Mocks/Plans/Teams2020Plan.cs
Normal file
96
test/Core.Test/Billing/Mocks/Plans/Teams2020Plan.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record Teams2020Plan : Plan
|
||||
{
|
||||
public Teams2020Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.TeamsAnnually2020 : PlanType.TeamsMonthly2020;
|
||||
ProductTier = ProductTierType.Teams;
|
||||
Name = isAnnual ? "Teams (Annually) 2020" : "Teams (Monthly) 2020";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameTeams";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
LegacyYear = 2023;
|
||||
|
||||
PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new Teams2020SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Teams2020SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Teams2020SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Teams2020PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Teams2020PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 0;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2020-teams-org-seat-annually";
|
||||
SeatPrice = 36;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2020-teams-org-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 4;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
test/Core.Test/Billing/Mocks/Plans/TeamsPlan.cs
Normal file
98
test/Core.Test/Billing/Mocks/Plans/TeamsPlan.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record TeamsPlan : Plan
|
||||
{
|
||||
public TeamsPlan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.TeamsAnnually : PlanType.TeamsMonthly;
|
||||
ProductTier = ProductTierType.Teams;
|
||||
Name = isAnnual ? "Teams (Annually)" : "Teams (Monthly)";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameTeams";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
HasScim = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
|
||||
PasswordManager = new TeamsPasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new TeamsSecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record TeamsSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public TeamsSecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 20;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 12;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record TeamsPasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public TeamsPasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 0;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-teams-org-seat-annually";
|
||||
SeatPrice = 48;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-teams-org-seat-monthly";
|
||||
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-teams-monthly-2024";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 5;
|
||||
ProviderPortalSeatPrice = 4;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
test/Core.Test/Billing/Mocks/Plans/TeamsPlan2023.cs
Normal file
97
test/Core.Test/Billing/Mocks/Plans/TeamsPlan2023.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record Teams2023Plan : Plan
|
||||
{
|
||||
public Teams2023Plan(bool isAnnual)
|
||||
{
|
||||
Type = isAnnual ? PlanType.TeamsAnnually2023 : PlanType.TeamsMonthly2023;
|
||||
ProductTier = ProductTierType.Teams;
|
||||
Name = isAnnual ? "Teams (Annually)" : "Teams (Monthly)";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameTeams";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
|
||||
LegacyYear = 2024;
|
||||
|
||||
PasswordManager = new Teams2023PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new Teams2023SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record Teams2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public Teams2023SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||
SeatPrice = 72;
|
||||
AdditionalPricePerServiceAccount = 6;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Teams2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public Teams2023PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 0;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-teams-org-seat-annually";
|
||||
SeatPrice = 48;
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-teams-org-seat-monthly";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 5;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan.cs
Normal file
74
test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record TeamsStarterPlan : Plan
|
||||
{
|
||||
public TeamsStarterPlan()
|
||||
{
|
||||
Type = PlanType.TeamsStarter;
|
||||
ProductTier = ProductTierType.TeamsStarter;
|
||||
Name = "Teams (Starter)";
|
||||
NameLocalizationKey = "planNameTeamsStarter";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 2;
|
||||
DisplaySortOrder = 2;
|
||||
|
||||
PasswordManager = new TeamsStarterPasswordManagerFeatures();
|
||||
SecretsManager = new TeamsStarterSecretsManagerFeatures();
|
||||
|
||||
LegacyYear = 2024;
|
||||
}
|
||||
|
||||
private record TeamsStarterSecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public TeamsStarterSecretsManagerFeatures()
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 20;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2024-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private record TeamsStarterPasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public TeamsStarterPasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 10;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 20;
|
||||
|
||||
MaxSeats = 10;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "teams-org-starter";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan2023.cs
Normal file
73
test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan2023.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Mocks.Plans;
|
||||
|
||||
public record TeamsStarterPlan2023 : Plan
|
||||
{
|
||||
public TeamsStarterPlan2023()
|
||||
{
|
||||
Type = PlanType.TeamsStarter2023;
|
||||
ProductTier = ProductTierType.TeamsStarter;
|
||||
Name = "Teams (Starter)";
|
||||
NameLocalizationKey = "planNameTeamsStarter";
|
||||
DescriptionLocalizationKey = "planDescTeams";
|
||||
CanBeUsedByBusiness = true;
|
||||
|
||||
TrialPeriodDays = 7;
|
||||
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
|
||||
UpgradeSortOrder = 2;
|
||||
DisplaySortOrder = 2;
|
||||
|
||||
PasswordManager = new TeamsStarter2023PasswordManagerFeatures();
|
||||
SecretsManager = new TeamsStarter2023SecretsManagerFeatures();
|
||||
LegacyYear = 2024;
|
||||
}
|
||||
|
||||
private record TeamsStarter2023SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public TeamsStarter2023SecretsManagerFeatures()
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 50;
|
||||
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalPricePerServiceAccount = 0.5M;
|
||||
}
|
||||
}
|
||||
|
||||
private record TeamsStarter2023PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public TeamsStarter2023PasswordManagerFeatures()
|
||||
{
|
||||
BaseSeats = 10;
|
||||
BaseStorageGb = 1;
|
||||
BasePrice = 20;
|
||||
|
||||
MaxSeats = 10;
|
||||
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "teams-org-starter";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Billing.Organizations.Commands;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
|
||||
@@ -8,7 +8,7 @@ using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -21,15 +21,6 @@ namespace Bit.Core.Test.Billing.Organizations.Queries;
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationMetadataQueryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NullOrganization_ReturnsNull(
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
var result = await sutProvider.Sut.Run(null);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_SelfHosted_ReturnsDefault(
|
||||
Organization organization,
|
||||
@@ -74,8 +65,7 @@ public class GetOrganizationMetadataQueryTests
|
||||
.Returns(new OrganizationSeatCounts { Users = 5, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.GetCustomer(organization)
|
||||
.ReturnsNull();
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
@@ -100,12 +90,12 @@ public class GetOrganizationMetadataQueryTests
|
||||
.Returns(new OrganizationSeatCounts { Users = 7, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.GetCustomer(organization)
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.Contains("discounts.coupon.applies_to")))
|
||||
.ReturnsNull();
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
@@ -124,23 +114,24 @@ public class GetOrganizationMetadataQueryTests
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
|
||||
var productId = "product_123";
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = [productId]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var customer = new Customer();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Discounts =
|
||||
[
|
||||
new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = [productId]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
@@ -162,17 +153,17 @@ public class GetOrganizationMetadataQueryTests
|
||||
.Returns(new OrganizationSeatCounts { Users = 15, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.GetCustomer(organization)
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.Contains("discounts.coupon.applies_to")))
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
@@ -189,13 +180,11 @@ public class GetOrganizationMetadataQueryTests
|
||||
organization.GatewaySubscriptionId = "sub_123";
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = null
|
||||
};
|
||||
var customer = new Customer();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Discounts = null,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
@@ -217,17 +206,17 @@ public class GetOrganizationMetadataQueryTests
|
||||
.Returns(new OrganizationSeatCounts { Users = 20, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.GetCustomer(organization)
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.Contains("discounts.coupon.applies_to")))
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
@@ -244,23 +233,24 @@ public class GetOrganizationMetadataQueryTests
|
||||
organization.GatewaySubscriptionId = "sub_123";
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = ["different_product_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var customer = new Customer();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Discounts =
|
||||
[
|
||||
new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = ["different_product_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
@@ -282,17 +272,17 @@ public class GetOrganizationMetadataQueryTests
|
||||
.Returns(new OrganizationSeatCounts { Users = 12, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.GetCustomer(organization)
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.Contains("discounts.coupon.applies_to")))
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
@@ -310,23 +300,24 @@ public class GetOrganizationMetadataQueryTests
|
||||
organization.PlanType = PlanType.FamiliesAnnually;
|
||||
|
||||
var productId = "product_123";
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = [productId]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var customer = new Customer();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Discounts =
|
||||
[
|
||||
new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = [productId]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
@@ -348,17 +339,17 @@ public class GetOrganizationMetadataQueryTests
|
||||
.Returns(new OrganizationSeatCounts { Users = 8, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.GetCustomer(organization)
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.Contains("discounts.coupon.applies_to")))
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal }
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }
|
||||
};
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
|
||||
|
||||
@@ -720,4 +720,63 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
await _stripeAdapter.DidNotReceive().SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_WithAdditionalStorage_SetsCorrectMaxStorageGb(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.Card;
|
||||
paymentMethod.Token = "card_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
const short additionalStorage = 2;
|
||||
|
||||
// Setup premium plan with 5GB provided storage
|
||||
var premiumPlan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }
|
||||
};
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
mockSubscription.Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, additionalStorage);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
Assert.Equal((short)3, user.MaxStorageGb); // 1 (provided) + 2 (additional) = 3
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
474
test/Core.Test/Billing/Pricing/PricingClientTests.cs
Normal file
474
test/Core.Test/Billing/Pricing/PricingClientTests.cs
Normal file
@@ -0,0 +1,474 @@
|
||||
using System.Net;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using RichardSzalay.MockHttp;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Pricing;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PricingClientTests
|
||||
{
|
||||
#region GetLookupKey Tests (via GetPlan)
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagEnabled_UsesFamilies2025LookupKey()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("families-2025", "Families 2025", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/families-2025")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagDisabled_UsesFamiliesLookupKey()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/families")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
// PreProcessFamiliesPreMigrationPlan should change "families" to "families-2025" when FF is disabled
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PreProcessFamiliesPreMigrationPlan Tests (via GetPlan)
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagDisabled_ReturnsFamiliesAnnually2025PlanType()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
// billing-pricing returns "families" lookup key because the flag is off
|
||||
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
// PreProcessFamiliesPreMigrationPlan should convert the families lookup key to families-2025
|
||||
// and the PlanAdapter should assign the correct FamiliesAnnually2025 plan type
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagEnabled_ReturnsFamiliesAnnually2025PlanType()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("families-2025", "Families", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
// PreProcessFamiliesPreMigrationPlan should ignore the lookup key because the flag is on
|
||||
// and the PlanAdapter should assign the correct FamiliesAnnually2025 plan type
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnuallyAndFeatureFlagEnabled_ReturnsFamiliesAnnuallyPlanType()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
// PreProcessFamiliesPreMigrationPlan should ignore the lookup key because the flag is on
|
||||
// and the PlanAdapter should assign the correct FamiliesAnnually plan type
|
||||
Assert.Equal(PlanType.FamiliesAnnually, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithOtherLookupKey_KeepsLookupKeyUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("enterprise-annually", "Enterprise", "enterprise", 144M, "price_id");
|
||||
|
||||
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/enterprise-annually")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(PlanType.EnterpriseAnnually, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ListPlans Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ListPlans_WithFeatureFlagDisabled_ReturnsListWithPreProcessing()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
// biling-pricing would return "families" because the flag is disabled
|
||||
var plansJson = $@"[
|
||||
{CreatePlanJson("families", "Families", "families", 40M, "price_id")},
|
||||
{CreatePlanJson("enterprise-annually", "Enterprise", "enterprise", 144M, "price_id")}
|
||||
]";
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization")
|
||||
.Respond("application/json", plansJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.ListPlans();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Count);
|
||||
// First plan should have been preprocessed from "families" to "families-2025"
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result[0].Type);
|
||||
// Second plan should remain unchanged
|
||||
Assert.Equal(PlanType.EnterpriseAnnually, result[1].Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListPlans_WithFeatureFlagEnabled_ReturnsListWithoutPreProcessing()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var plansJson = $@"[
|
||||
{CreatePlanJson("families", "Families", "families", 40M, "price_id")}
|
||||
]";
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization")
|
||||
.Respond("application/json", plansJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.ListPlans();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
// Plan should remain as FamiliesAnnually when FF is enabled
|
||||
Assert.Equal(PlanType.FamiliesAnnually, result[0].Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetPlan - Additional Coverage
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPlan_WhenSelfHosted_ReturnsNull(
|
||||
SutProvider<PricingClient> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var globalSettings = sutProvider.GetDependency<GlobalSettings>();
|
||||
globalSettings.SelfHosted = true;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPlan_WhenLookupKeyNotFound_ReturnsNull(
|
||||
SutProvider<PricingClient> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
|
||||
|
||||
// Act - Using PlanType that doesn't have a lookup key mapping
|
||||
var result = await sutProvider.Sut.GetPlan(unchecked((PlanType)999));
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WhenPricingServiceReturnsNotFound_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond(HttpStatusCode.NotFound);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WhenPricingServiceReturnsError_ThrowsBillingException()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond(HttpStatusCode.InternalServerError);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BillingException>(() =>
|
||||
pricingClient.GetPlan(PlanType.FamiliesAnnually2025));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ListPlans - Additional Coverage
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ListPlans_WhenSelfHosted_ReturnsEmptyList(
|
||||
SutProvider<PricingClient> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var globalSettings = sutProvider.GetDependency<GlobalSettings>();
|
||||
globalSettings.SelfHosted = true;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ListPlans();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListPlans_WhenPricingServiceReturnsError_ThrowsBillingException()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization")
|
||||
.Respond(HttpStatusCode.InternalServerError);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BillingException>(() =>
|
||||
pricingClient.ListPlans());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string CreatePlanJson(
|
||||
string lookupKey,
|
||||
string name,
|
||||
string tier,
|
||||
decimal seatsPrice,
|
||||
string seatsStripePriceId,
|
||||
int seatsQuantity = 1)
|
||||
{
|
||||
return $@"{{
|
||||
""lookupKey"": ""{lookupKey}"",
|
||||
""name"": ""{name}"",
|
||||
""tier"": ""{tier}"",
|
||||
""features"": [],
|
||||
""seats"": {{
|
||||
""type"": ""packaged"",
|
||||
""quantity"": {seatsQuantity},
|
||||
""price"": {seatsPrice},
|
||||
""stripePriceId"": ""{seatsStripePriceId}""
|
||||
}},
|
||||
""canUpgradeTo"": [],
|
||||
""additionalData"": {{
|
||||
""nameLocalizationKey"": ""{lookupKey}Name"",
|
||||
""descriptionLocalizationKey"": ""{lookupKey}Description""
|
||||
}}
|
||||
}}";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
@@ -10,7 +11,7 @@ using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -31,38 +32,39 @@ public class OrganizationBillingServiceTests
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||
sutProvider.GetDependency<IPricingClient>().ListPlans().Returns(StaticStore.Plans.ToList());
|
||||
sutProvider.GetDependency<IPricingClient>().ListPlans().Returns(MockPlans.Plans.ToList());
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var subscriberService = sutProvider.GetDependency<ISubscriberService>();
|
||||
var organizationSeatCount = new OrganizationSeatCounts { Users = 1, Sponsored = 0 };
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = ["product_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var customer = new Customer();
|
||||
|
||||
subscriberService
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.GetCustomer(organization)
|
||||
.Returns(customer);
|
||||
|
||||
subscriberService.GetSubscription(organization).Returns(new Subscription
|
||||
{
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
subscriberService.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.Contains("discounts.coupon.applies_to"))).Returns(new Subscription
|
||||
{
|
||||
Data =
|
||||
Discounts =
|
||||
[
|
||||
new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = ["product_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
@@ -72,8 +74,8 @@ public class OrganizationBillingServiceTests
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
@@ -96,10 +98,10 @@ public class OrganizationBillingServiceTests
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().ListPlans().Returns(StaticStore.Plans.ToList());
|
||||
sutProvider.GetDependency<IPricingClient>().ListPlans().Returns(MockPlans.Plans.ToList());
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
@@ -109,11 +111,12 @@ public class OrganizationBillingServiceTests
|
||||
|
||||
// Set up subscriber service to return null for customer
|
||||
subscriberService
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options => options.Expand.FirstOrDefault() == "discount.coupon.applies_to"))
|
||||
.GetCustomer(organization)
|
||||
.Returns((Customer)null);
|
||||
|
||||
// Set up subscriber service to return null for subscription
|
||||
subscriberService.GetSubscription(organization).Returns((Subscription)null);
|
||||
subscriberService.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.Contains("discounts.coupon.applies_to"))).Returns((Subscription)null);
|
||||
|
||||
var metadata = await sutProvider.Sut.GetMetadata(organizationId);
|
||||
|
||||
@@ -132,7 +135,7 @@ public class OrganizationBillingServiceTests
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
|
||||
var plan = MockPlans.Get(PlanType.TeamsAnnually);
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
organization.GatewayCustomerId = "cus_test123";
|
||||
organization.GatewaySubscriptionId = null;
|
||||
@@ -208,7 +211,7 @@ public class OrganizationBillingServiceTests
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
|
||||
var plan = MockPlans.Get(PlanType.TeamsAnnually);
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
organization.GatewayCustomerId = "cus_test123";
|
||||
organization.GatewaySubscriptionId = null;
|
||||
@@ -282,7 +285,7 @@ public class OrganizationBillingServiceTests
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
|
||||
var plan = MockPlans.Get(PlanType.TeamsAnnually);
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
organization.GatewayCustomerId = "cus_test123";
|
||||
organization.GatewaySubscriptionId = null;
|
||||
@@ -351,4 +354,97 @@ public class OrganizationBillingServiceTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_UpdatesStripeCustomer(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
organization.Name = "Short name";
|
||||
|
||||
CustomerUpdateOptions capturedOptions = null;
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.CustomerUpdateAsync(
|
||||
Arg.Is<string>(id => id == organization.GatewayCustomerId),
|
||||
Arg.Do<CustomerUpdateOptions>(options => capturedOptions = options))
|
||||
.Returns(new Customer());
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.Received(1)
|
||||
.CustomerUpdateAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
|
||||
Assert.NotNull(capturedOptions);
|
||||
Assert.Equal(organization.BillingEmail, capturedOptions.Email);
|
||||
Assert.Equal(organization.DisplayName(), capturedOptions.Description);
|
||||
Assert.NotNull(capturedOptions.InvoiceSettings);
|
||||
Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields);
|
||||
Assert.Single(capturedOptions.InvoiceSettings.CustomFields);
|
||||
|
||||
var customField = capturedOptions.InvoiceSettings.CustomFields.First();
|
||||
Assert.Equal(organization.SubscriberType(), customField.Name);
|
||||
Assert.Equal(organization.DisplayName(), customField.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenNameIsLong_TruncatesTo30Characters(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Name = "This is a very long organization name that exceeds thirty characters";
|
||||
|
||||
CustomerUpdateOptions capturedOptions = null;
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.CustomerUpdateAsync(
|
||||
Arg.Is<string>(id => id == organization.GatewayCustomerId),
|
||||
Arg.Do<CustomerUpdateOptions>(options => capturedOptions = options))
|
||||
.Returns(new Customer());
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.Received(1)
|
||||
.CustomerUpdateAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
|
||||
Assert.NotNull(capturedOptions);
|
||||
Assert.NotNull(capturedOptions.InvoiceSettings);
|
||||
Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields);
|
||||
|
||||
var customField = capturedOptions.InvoiceSettings.CustomFields.First();
|
||||
Assert.Equal(30, customField.Value.Length);
|
||||
|
||||
var expectedCustomFieldDisplayName = "This is a very long organizati";
|
||||
Assert.Equal(expectedCustomFieldDisplayName, customField.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsNull_ThrowsBillingException(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.GatewayCustomerId = null;
|
||||
organization.Name = "Test Organization";
|
||||
organization.BillingEmail = "billing@example.com";
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BillingException>(
|
||||
() => sutProvider.Sut.UpdateOrganizationNameAndEmail(organization));
|
||||
|
||||
Assert.Contains("Cannot update an organization in Stripe without a GatewayCustomerId.", exception.Response);
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
}
|
||||
|
||||
497
test/Core.Test/Models/Business/BillingCustomerDiscountTests.cs
Normal file
497
test/Core.Test/Models/Business/BillingCustomerDiscountTests.cs
Normal file
@@ -0,0 +1,497 @@
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Business;
|
||||
|
||||
public class BillingCustomerDiscountTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_PercentageDiscount_SetsIdActivePercentOffAndAppliesTo(string couponId)
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 25.5m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = new List<string> { "product1", "product2" }
|
||||
}
|
||||
},
|
||||
End = null // Active discount
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(couponId, result.Id);
|
||||
Assert.True(result.Active);
|
||||
Assert.Equal(25.5m, result.PercentOff);
|
||||
Assert.Null(result.AmountOff);
|
||||
Assert.NotNull(result.AppliesTo);
|
||||
Assert.Equal(2, result.AppliesTo.Count);
|
||||
Assert.Contains("product1", result.AppliesTo);
|
||||
Assert.Contains("product2", result.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_AmountDiscount_ConvertsFromCentsToDollars(string couponId)
|
||||
{
|
||||
// Arrange - Stripe sends 1400 cents for $14.00
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = null,
|
||||
AmountOff = 1400, // 1400 cents
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = new List<string>()
|
||||
}
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(couponId, result.Id);
|
||||
Assert.True(result.Active);
|
||||
Assert.Null(result.PercentOff);
|
||||
Assert.Equal(14.00m, result.AmountOff); // Converted to dollars
|
||||
Assert.NotNull(result.AppliesTo);
|
||||
Assert.Empty(result.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_InactiveDiscount_SetsActiveToFalse(string couponId)
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 15m
|
||||
},
|
||||
End = DateTime.UtcNow.AddDays(-1) // Expired discount
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(couponId, result.Id);
|
||||
Assert.False(result.Active);
|
||||
Assert.Equal(15m, result.PercentOff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullCoupon_SetsDiscountPropertiesToNull()
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = null,
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.Id);
|
||||
Assert.True(result.Active);
|
||||
Assert.Null(result.PercentOff);
|
||||
Assert.Null(result.AmountOff);
|
||||
Assert.Null(result.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_NullAmountOff_SetsAmountOffToNull(string couponId)
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 10m,
|
||||
AmountOff = null
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.AmountOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_ZeroAmountOff_ConvertsCorrectly(string couponId)
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
AmountOff = 0
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0m, result.AmountOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_LargeAmountOff_ConvertsCorrectly(string couponId)
|
||||
{
|
||||
// Arrange - $100.00 discount
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
AmountOff = 10000 // 10000 cents = $100.00
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(100.00m, result.AmountOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_SmallAmountOff_ConvertsCorrectly(string couponId)
|
||||
{
|
||||
// Arrange - $0.50 discount
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
AmountOff = 50 // 50 cents = $0.50
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0.50m, result.AmountOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_BothDiscountTypes_SetsPercentOffAndAmountOff(string couponId)
|
||||
{
|
||||
// Arrange - Coupon with both percentage and amount (edge case)
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 20m,
|
||||
AmountOff = 500 // $5.00
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(20m, result.PercentOff);
|
||||
Assert.Equal(5.00m, result.AmountOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithNullAppliesTo_SetsAppliesToNull(string couponId)
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 10m,
|
||||
AppliesTo = null
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithNullProductsList_SetsAppliesToNull(string couponId)
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 10m,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = null
|
||||
}
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithDecimalAmountOff_RoundsCorrectly(string couponId)
|
||||
{
|
||||
// Arrange - 1425 cents = $14.25
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
AmountOff = 1425
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(14.25m, result.AmountOff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultConstructor_InitializesAllPropertiesToNullOrFalse()
|
||||
{
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount();
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.Id);
|
||||
Assert.False(result.Active);
|
||||
Assert.Null(result.PercentOff);
|
||||
Assert.Null(result.AmountOff);
|
||||
Assert.Null(result.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithFutureEndDate_SetsActiveToFalse(string couponId)
|
||||
{
|
||||
// Arrange - Discount expires in the future
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 20m
|
||||
},
|
||||
End = DateTime.UtcNow.AddDays(30) // Expires in 30 days
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Active); // Should be inactive because End is not null
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithPastEndDate_SetsActiveToFalse(string couponId)
|
||||
{
|
||||
// Arrange - Discount already expired
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 20m
|
||||
},
|
||||
End = DateTime.UtcNow.AddDays(-30) // Expired 30 days ago
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Active); // Should be inactive because End is not null
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullCouponId_SetsIdToNull()
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = null,
|
||||
PercentOff = 20m
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.Id);
|
||||
Assert.True(result.Active);
|
||||
Assert.Equal(20m, result.PercentOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithNullPercentOff_SetsPercentOffToNull(string couponId)
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = null,
|
||||
AmountOff = 1000
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.PercentOff);
|
||||
Assert.Equal(10.00m, result.AmountOff);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCompleteStripeDiscount_MapsAllProperties()
|
||||
{
|
||||
// Arrange - Comprehensive test with all Stripe Discount properties set
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = "premium_discount_2024",
|
||||
PercentOff = 25m,
|
||||
AmountOff = 1500, // $15.00
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = new List<string> { "prod_premium", "prod_family", "prod_teams" }
|
||||
}
|
||||
},
|
||||
End = null // Active
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert - Verify all properties mapped correctly
|
||||
Assert.Equal("premium_discount_2024", result.Id);
|
||||
Assert.True(result.Active);
|
||||
Assert.Equal(25m, result.PercentOff);
|
||||
Assert.Equal(15.00m, result.AmountOff);
|
||||
Assert.NotNull(result.AppliesTo);
|
||||
Assert.Equal(3, result.AppliesTo.Count);
|
||||
Assert.Contains("prod_premium", result.AppliesTo);
|
||||
Assert.Contains("prod_family", result.AppliesTo);
|
||||
Assert.Contains("prod_teams", result.AppliesTo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMinimalStripeDiscount_HandlesNullsGracefully()
|
||||
{
|
||||
// Arrange - Minimal Stripe Discount with most properties null
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = null,
|
||||
PercentOff = null,
|
||||
AmountOff = null,
|
||||
AppliesTo = null
|
||||
},
|
||||
End = DateTime.UtcNow.AddDays(10) // Has end date
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert - Should handle all nulls gracefully
|
||||
Assert.Null(result.Id);
|
||||
Assert.False(result.Active);
|
||||
Assert.Null(result.PercentOff);
|
||||
Assert.Null(result.AmountOff);
|
||||
Assert.Null(result.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithEmptyProductsList_PreservesEmptyList(string couponId)
|
||||
{
|
||||
// Arrange
|
||||
var discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = couponId,
|
||||
PercentOff = 10m,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = new List<string>() // Empty but not null
|
||||
}
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.AppliesTo);
|
||||
Assert.Empty(result.AppliesTo);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -17,7 +17,7 @@ public class CompleteSubscriptionUpdateTests
|
||||
public void UpgradeItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions(
|
||||
Organization organization)
|
||||
{
|
||||
var teamsStarterPlan = StaticStore.GetPlan(PlanType.TeamsStarter);
|
||||
var teamsStarterPlan = MockPlans.Get(PlanType.TeamsStarter);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@@ -35,7 +35,7 @@ public class CompleteSubscriptionUpdateTests
|
||||
}
|
||||
};
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
var updatedSubscriptionData = new SubscriptionData
|
||||
{
|
||||
@@ -66,7 +66,7 @@ public class CompleteSubscriptionUpdateTests
|
||||
// 5 purchased, 1 base
|
||||
organization.MaxStorageGb = 6;
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@@ -102,7 +102,7 @@ public class CompleteSubscriptionUpdateTests
|
||||
}
|
||||
};
|
||||
|
||||
var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||
var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly);
|
||||
|
||||
var updatedSubscriptionData = new SubscriptionData
|
||||
{
|
||||
@@ -173,7 +173,7 @@ public class CompleteSubscriptionUpdateTests
|
||||
// 5 purchased, 1 base
|
||||
organization.MaxStorageGb = 6;
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@@ -209,7 +209,7 @@ public class CompleteSubscriptionUpdateTests
|
||||
}
|
||||
};
|
||||
|
||||
var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||
var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly);
|
||||
|
||||
var updatedSubscriptionData = new SubscriptionData
|
||||
{
|
||||
@@ -277,8 +277,8 @@ public class CompleteSubscriptionUpdateTests
|
||||
public void RevertItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions(
|
||||
Organization organization)
|
||||
{
|
||||
var teamsStarterPlan = StaticStore.GetPlan(PlanType.TeamsStarter);
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsStarterPlan = MockPlans.Get(PlanType.TeamsStarter);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@@ -325,8 +325,8 @@ public class CompleteSubscriptionUpdateTests
|
||||
// 5 purchased, 1 base
|
||||
organization.MaxStorageGb = 6;
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@@ -431,8 +431,8 @@ public class CompleteSubscriptionUpdateTests
|
||||
// 5 purchased, 1 base
|
||||
organization.MaxStorageGb = 6;
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -27,7 +27,7 @@ public class SeatSubscriptionUpdateTests
|
||||
|
||||
public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
organization.PlanType = planType;
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@@ -69,7 +69,7 @@ public class SeatSubscriptionUpdateTests
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
organization.PlanType = planType;
|
||||
var subscription = new Subscription
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
@@ -16,13 +16,13 @@ public class SecretsManagerSubscriptionUpdateTests
|
||||
private static TheoryData<Plan> ToPlanTheory(List<PlanType> types)
|
||||
{
|
||||
var theoryData = new TheoryData<Plan>();
|
||||
var plans = types.Select(StaticStore.GetPlan).ToArray();
|
||||
var plans = types.Select(MockPlans.Get).ToArray();
|
||||
theoryData.AddRange(plans);
|
||||
return theoryData;
|
||||
}
|
||||
|
||||
public static TheoryData<Plan> NonSmPlans =>
|
||||
ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019]);
|
||||
ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2025, PlanType.FamiliesAnnually2019]);
|
||||
|
||||
public static TheoryData<Plan> SmPlans => ToPlanTheory([
|
||||
PlanType.EnterpriseAnnually2019,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -27,7 +27,7 @@ public class ServiceAccountSubscriptionUpdateTests
|
||||
|
||||
public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
organization.PlanType = planType;
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@@ -69,7 +69,7 @@ public class ServiceAccountSubscriptionUpdateTests
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
organization.PlanType = planType;
|
||||
var quantity = 5;
|
||||
var subscription = new Subscription
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -27,7 +27,7 @@ public class SmSeatSubscriptionUpdateTests
|
||||
|
||||
public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
organization.PlanType = planType;
|
||||
var quantity = 3;
|
||||
var subscription = new Subscription
|
||||
@@ -70,7 +70,7 @@ public class SmSeatSubscriptionUpdateTests
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
organization.PlanType = planType;
|
||||
var quantity = 5;
|
||||
var subscription = new Subscription
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -26,7 +26,7 @@ public class StorageSubscriptionUpdateTests
|
||||
|
||||
public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
@@ -77,7 +77,7 @@ public class StorageSubscriptionUpdateTests
|
||||
[BitAutoData(PlanType.TeamsStarter)]
|
||||
public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = MockPlans.Get(planType);
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
|
||||
125
test/Core.Test/Models/Business/SubscriptionInfoTests.cs
Normal file
125
test/Core.Test/Models/Business/SubscriptionInfoTests.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using Bit.Core.Models.Business;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Business;
|
||||
|
||||
public class SubscriptionInfoTests
|
||||
{
|
||||
[Fact]
|
||||
public void BillingSubscriptionItem_NullPlan_HandlesGracefully()
|
||||
{
|
||||
// Arrange - SubscriptionItem with null Plan
|
||||
var subscriptionItem = new SubscriptionItem
|
||||
{
|
||||
Plan = null,
|
||||
Quantity = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem);
|
||||
|
||||
// Assert - Should handle null Plan gracefully
|
||||
Assert.Null(result.ProductId);
|
||||
Assert.Null(result.Name);
|
||||
Assert.Equal(0m, result.Amount); // Defaults to 0 when Plan is null
|
||||
Assert.Null(result.Interval);
|
||||
Assert.Equal(1, result.Quantity);
|
||||
Assert.False(result.SponsoredSubscriptionItem);
|
||||
Assert.False(result.AddonSubscriptionItem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BillingSubscriptionItem_NullAmount_SetsToZero()
|
||||
{
|
||||
// Arrange - SubscriptionItem with Plan but null Amount
|
||||
var subscriptionItem = new SubscriptionItem
|
||||
{
|
||||
Plan = new Plan
|
||||
{
|
||||
ProductId = "prod_test",
|
||||
Nickname = "Test Plan",
|
||||
Amount = null, // Null amount
|
||||
Interval = "month"
|
||||
},
|
||||
Quantity = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem);
|
||||
|
||||
// Assert - Should default to 0 when Amount is null
|
||||
Assert.Equal("prod_test", result.ProductId);
|
||||
Assert.Equal("Test Plan", result.Name);
|
||||
Assert.Equal(0m, result.Amount); // Business rule: defaults to 0 when null
|
||||
Assert.Equal("month", result.Interval);
|
||||
Assert.Equal(1, result.Quantity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BillingSubscriptionItem_ZeroAmount_PreservesZero()
|
||||
{
|
||||
// Arrange - SubscriptionItem with Plan and zero Amount
|
||||
var subscriptionItem = new SubscriptionItem
|
||||
{
|
||||
Plan = new Plan
|
||||
{
|
||||
ProductId = "prod_test",
|
||||
Nickname = "Test Plan",
|
||||
Amount = 0, // Zero amount (0 cents)
|
||||
Interval = "month"
|
||||
},
|
||||
Quantity = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem);
|
||||
|
||||
// Assert - Should preserve zero amount
|
||||
Assert.Equal("prod_test", result.ProductId);
|
||||
Assert.Equal("Test Plan", result.Name);
|
||||
Assert.Equal(0m, result.Amount); // Zero amount preserved
|
||||
Assert.Equal("month", result.Interval);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BillingUpcomingInvoice_ZeroAmountDue_ConvertsToZero()
|
||||
{
|
||||
// Arrange - Invoice with zero AmountDue
|
||||
// Note: Stripe's Invoice.AmountDue is non-nullable long, so we test with 0
|
||||
// The null-coalescing operator (?? 0) in the constructor handles the case where
|
||||
// ConvertFromStripeMinorUnits returns null, but since AmountDue is non-nullable,
|
||||
// this test verifies the conversion path works correctly for zero values
|
||||
var invoice = new Invoice
|
||||
{
|
||||
AmountDue = 0, // Zero amount due (0 cents)
|
||||
Created = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingUpcomingInvoice(invoice);
|
||||
|
||||
// Assert - Should convert zero correctly
|
||||
Assert.Equal(0m, result.Amount);
|
||||
Assert.NotNull(result.Date);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BillingUpcomingInvoice_ValidAmountDue_ConvertsCorrectly()
|
||||
{
|
||||
// Arrange - Invoice with valid AmountDue
|
||||
var invoice = new Invoice
|
||||
{
|
||||
AmountDue = 2500, // 2500 cents = $25.00
|
||||
Created = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionInfo.BillingUpcomingInvoice(invoice);
|
||||
|
||||
// Assert - Should convert correctly
|
||||
Assert.Equal(25.00m, result.Amount); // Converted from cents
|
||||
Assert.NotNull(result.Date);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
|
||||
namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
|
||||
|
||||
public abstract class FamiliesForEnterpriseTestsBase
|
||||
{
|
||||
public static IEnumerable<object[]> EnterprisePlanTypes =>
|
||||
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p });
|
||||
Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p });
|
||||
|
||||
public static IEnumerable<object[]> NonEnterprisePlanTypes =>
|
||||
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p });
|
||||
Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p });
|
||||
|
||||
public static IEnumerable<object[]> FamiliesPlanTypes =>
|
||||
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).ProductTier == ProductTierType.Families).Select(p => new object[] { p });
|
||||
Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Families).Select(p => new object[] { p });
|
||||
|
||||
public static IEnumerable<object[]> NonFamiliesPlanTypes =>
|
||||
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p });
|
||||
Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p });
|
||||
|
||||
public static IEnumerable<object[]> NonConfirmedOrganizationUsersStatuses =>
|
||||
Enum.GetValues<OrganizationUserStatusType>()
|
||||
|
||||
@@ -9,7 +9,7 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -42,7 +42,7 @@ public class AddSecretsManagerSubscriptionCommandTests
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);
|
||||
|
||||
await sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts);
|
||||
@@ -88,7 +88,7 @@ public class AddSecretsManagerSubscriptionCommandTests
|
||||
organization.GatewayCustomerId = null;
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));
|
||||
Assert.Contains("No payment method found.", exception.Message);
|
||||
@@ -106,7 +106,7 @@ public class AddSecretsManagerSubscriptionCommandTests
|
||||
organization.GatewaySubscriptionId = null;
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));
|
||||
Assert.Contains("No subscription found.", exception.Message);
|
||||
@@ -139,7 +139,7 @@ public class AddSecretsManagerSubscriptionCommandTests
|
||||
provider.Type = ProviderType.Msp;
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpAsync(organization, 10, 10));
|
||||
|
||||
@@ -11,7 +11,7 @@ using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -26,7 +26,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
private static TheoryData<Plan> ToPlanTheory(List<PlanType> types)
|
||||
{
|
||||
var theoryData = new TheoryData<Plan>();
|
||||
var plans = types.Select(StaticStore.GetPlan).ToArray();
|
||||
var plans = types.Select(MockPlans.Get).ToArray();
|
||||
theoryData.AddRange(plans);
|
||||
return theoryData;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, autoscaling).AdjustSeats(2);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||
@@ -180,7 +180,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
|
||||
organization.UseSecretsManager = false;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false);
|
||||
@@ -289,7 +289,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
organization.MaxAutoscaleSmSeats = maxSeatCount;
|
||||
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
@@ -334,7 +334,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
|
||||
var ownerDetailsList = new List<OrganizationUserUserDetails> { new() { Email = "owner@example.com" } };
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
@@ -372,7 +372,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.SmSeats = null;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
@@ -388,7 +388,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(-2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
@@ -404,7 +404,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
@@ -422,7 +422,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
organization.SmSeats = 9;
|
||||
organization.MaxAutoscaleSmSeats = 10;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
@@ -436,7 +436,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmSeats = organization.SmSeats + 10,
|
||||
@@ -455,7 +455,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmSeats = 0,
|
||||
@@ -475,7 +475,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.SmSeats = 8;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmSeats = 7,
|
||||
@@ -498,7 +498,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
var smServiceAccounts = 300;
|
||||
var existingServiceAccountCount = 299;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmServiceAccounts = smServiceAccounts,
|
||||
@@ -531,7 +531,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var smServiceAccounts = 300;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmServiceAccounts = smServiceAccounts,
|
||||
@@ -571,7 +571,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.SmServiceAccounts = null;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
@@ -585,7 +585,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(-2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
@@ -601,7 +601,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
@@ -619,7 +619,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
organization.SmServiceAccounts = 9;
|
||||
organization.MaxAutoscaleSmServiceAccounts = 10;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
@@ -639,7 +639,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
organization.SmServiceAccounts = smServiceAccount - 5;
|
||||
organization.MaxAutoscaleSmServiceAccounts = 2 * smServiceAccount;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmServiceAccounts = smServiceAccount,
|
||||
@@ -662,7 +662,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
|
||||
organization.SmServiceAccounts = newSmServiceAccounts - 10;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmServiceAccounts = newSmServiceAccounts,
|
||||
@@ -707,7 +707,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
organization.SmSeats = smSeats - 1;
|
||||
organization.MaxAutoscaleSmSeats = smSeats * 2;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmSeats = smSeats,
|
||||
@@ -728,7 +728,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
organization.SmSeats = 2;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
MaxAutoscaleSmSeats = 3
|
||||
@@ -748,7 +748,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
organization.SmSeats = 2;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
MaxAutoscaleSmSeats = 2
|
||||
@@ -769,7 +769,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
organization.PlanType = planType;
|
||||
organization.SmServiceAccounts = 3;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmServiceAccounts = 3 };
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
|
||||
@@ -8,7 +8,7 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -45,7 +45,7 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
upgrade.Plan = organization.PlanType;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
||||
@@ -61,7 +61,7 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
upgrade.AdditionalSmSeats = 10;
|
||||
upgrade.AdditionalServiceAccounts = 10;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
||||
Assert.Contains("already on this plan", exception.Message);
|
||||
@@ -73,11 +73,11 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||
upgrade.AdditionalSmSeats = 10;
|
||||
upgrade.AdditionalSeats = 10;
|
||||
upgrade.Plan = PlanType.TeamsAnnually;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
@@ -104,7 +104,7 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
|
||||
organization.PlanType = PlanType.FamiliesAnnually;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
organizationUpgrade.AdditionalSeats = 30;
|
||||
organizationUpgrade.UseSecretsManager = true;
|
||||
@@ -113,7 +113,7 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
organizationUpgrade.AdditionalStorageGb = 3;
|
||||
organizationUpgrade.Plan = planType;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organizationUpgrade.Plan).Returns(StaticStore.GetPlan(organizationUpgrade.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organizationUpgrade.Plan).Returns(MockPlans.Get(organizationUpgrade.Plan));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
@@ -123,7 +123,7 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade);
|
||||
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSubscription(
|
||||
organization,
|
||||
StaticStore.GetPlan(planType),
|
||||
MockPlans.Get(planType),
|
||||
organizationUpgrade.AdditionalSeats,
|
||||
organizationUpgrade.UseSecretsManager,
|
||||
organizationUpgrade.AdditionalSmSeats,
|
||||
@@ -141,12 +141,12 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade,
|
||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
upgrade.Plan = planType;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));
|
||||
|
||||
var plan = StaticStore.GetPlan(upgrade.Plan);
|
||||
var plan = MockPlans.Get(upgrade.Plan);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
@@ -184,10 +184,10 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
upgrade.AdditionalSeats = 15;
|
||||
upgrade.AdditionalSmSeats = 1;
|
||||
upgrade.AdditionalServiceAccounts = 0;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));
|
||||
|
||||
organization.SmSeats = 2;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
@@ -218,11 +218,11 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
upgrade.AdditionalSeats = 15;
|
||||
upgrade.AdditionalSmSeats = 1;
|
||||
upgrade.AdditionalServiceAccounts = 0;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));
|
||||
|
||||
organization.SmSeats = 1;
|
||||
organization.SmServiceAccounts = currentServiceAccounts;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.Platform.Mailer.TestMail;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Mailer;
|
||||
@@ -9,7 +12,10 @@ public class HandlebarMailRendererTests
|
||||
[Fact]
|
||||
public async Task RenderAsync_ReturnsExpectedHtmlAndTxt()
|
||||
{
|
||||
var renderer = new HandlebarMailRenderer();
|
||||
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
var renderer = new HandlebarMailRenderer(logger, globalSettings);
|
||||
|
||||
var view = new TestMailView { Name = "John Smith" };
|
||||
|
||||
var (html, txt) = await renderer.RenderAsync(view);
|
||||
@@ -17,4 +23,150 @@ public class HandlebarMailRendererTests
|
||||
Assert.Equal("Hello <b>John Smith</b>", html.Trim());
|
||||
Assert.Equal("Hello John Smith", txt.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenderAsync_LoadsFromDisk_WhenSelfHostedAndFileExists()
|
||||
{
|
||||
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var globalSettings = new GlobalSettings
|
||||
{
|
||||
SelfHosted = true,
|
||||
MailTemplateDirectory = tempDir
|
||||
};
|
||||
|
||||
// Create test template files on disk
|
||||
var htmlTemplatePath = Path.Combine(tempDir, "Bit.Core.Test.Platform.Mailer.TestMail.TestMailView.html.hbs");
|
||||
var txtTemplatePath = Path.Combine(tempDir, "Bit.Core.Test.Platform.Mailer.TestMail.TestMailView.text.hbs");
|
||||
await File.WriteAllTextAsync(htmlTemplatePath, "Custom HTML: <b>{{Name}}</b>");
|
||||
await File.WriteAllTextAsync(txtTemplatePath, "Custom TXT: {{Name}}");
|
||||
|
||||
var renderer = new HandlebarMailRenderer(logger, globalSettings);
|
||||
var view = new TestMailView { Name = "Jane Doe" };
|
||||
|
||||
var (html, txt) = await renderer.RenderAsync(view);
|
||||
|
||||
Assert.Equal("Custom HTML: <b>Jane Doe</b>", html.Trim());
|
||||
Assert.Equal("Custom TXT: Jane Doe", txt.Trim());
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("../../../etc/passwd")]
|
||||
[InlineData("../../../../malicious.txt")]
|
||||
[InlineData("../../malicious.txt")]
|
||||
[InlineData("../malicious.txt")]
|
||||
public async Task ReadSourceFromDiskAsync_PrevenetsPathTraversal_WhenMaliciousPathProvided(string maliciousPath)
|
||||
{
|
||||
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var globalSettings = new GlobalSettings
|
||||
{
|
||||
SelfHosted = true,
|
||||
MailTemplateDirectory = tempDir
|
||||
};
|
||||
|
||||
// Create a malicious file outside the template directory
|
||||
var maliciousFile = Path.Combine(Path.GetTempPath(), "malicious.txt");
|
||||
await File.WriteAllTextAsync(maliciousFile, "Malicious Content");
|
||||
|
||||
var renderer = new HandlebarMailRenderer(logger, globalSettings);
|
||||
|
||||
// Use reflection to call the private ReadSourceFromDiskAsync method
|
||||
var method = typeof(HandlebarMailRenderer).GetMethod("ReadSourceFromDiskAsync",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var task = (Task<string?>)method!.Invoke(renderer, new object[] { maliciousPath })!;
|
||||
var result = await task;
|
||||
|
||||
// Should return null and not load the malicious file
|
||||
Assert.Null(result);
|
||||
|
||||
// Verify that a warning was logged for the path traversal attempt
|
||||
logger.Received(1).Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
|
||||
// Cleanup malicious file
|
||||
if (File.Exists(maliciousFile))
|
||||
{
|
||||
File.Delete(maliciousFile);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSourceFromDiskAsync_AllowsValidFileWithDifferentCase_WhenCaseInsensitiveFileSystem()
|
||||
{
|
||||
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var globalSettings = new GlobalSettings
|
||||
{
|
||||
SelfHosted = true,
|
||||
MailTemplateDirectory = tempDir
|
||||
};
|
||||
|
||||
// Create a test template file
|
||||
var templateFileName = "TestTemplate.hbs";
|
||||
var templatePath = Path.Combine(tempDir, templateFileName);
|
||||
await File.WriteAllTextAsync(templatePath, "Test Content");
|
||||
|
||||
var renderer = new HandlebarMailRenderer(logger, globalSettings);
|
||||
|
||||
// Try to read with different case (should work on case-insensitive file systems like Windows/macOS)
|
||||
var method = typeof(HandlebarMailRenderer).GetMethod("ReadSourceFromDiskAsync",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var task = (Task<string?>)method!.Invoke(renderer, new object[] { templateFileName })!;
|
||||
var result = await task;
|
||||
|
||||
// Should successfully read the file
|
||||
Assert.Equal("Test Content", result);
|
||||
|
||||
// Verify no warning was logged
|
||||
logger.DidNotReceive().Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Platform.Mail.Delivery;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.Platform.Mailer.TestMail;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Mailer;
|
||||
|
||||
public class MailerTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendEmailAsync()
|
||||
{
|
||||
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
var deliveryService = Substitute.For<IMailDeliveryService>();
|
||||
var mailer = new Core.Platform.Mail.Mailer.Mailer(new HandlebarMailRenderer(), deliveryService);
|
||||
|
||||
var mailer = new Core.Platform.Mail.Mailer.Mailer(new HandlebarMailRenderer(logger, globalSettings), deliveryService);
|
||||
|
||||
var mail = new TestMail.TestMail()
|
||||
{
|
||||
|
||||
@@ -268,4 +268,115 @@ public class HandlebarsMailServiceTests
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendIndividualUserWelcomeEmailAsync_SendsCorrectEmail()
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = "test@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
await _sut.SendIndividualUserWelcomeEmailAsync(user);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>
|
||||
m.MetaData != null &&
|
||||
m.ToEmails.Contains("test@example.com") &&
|
||||
m.Subject == "Welcome to Bitwarden!" &&
|
||||
m.Category == "Welcome"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendOrganizationUserWelcomeEmailAsync_SendsCorrectEmailWithOrganizationName()
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = "user@company.com"
|
||||
};
|
||||
var organizationName = "Bitwarden Corp";
|
||||
|
||||
// Act
|
||||
await _sut.SendOrganizationUserWelcomeEmailAsync(user, organizationName);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>
|
||||
m.MetaData != null &&
|
||||
m.ToEmails.Contains("user@company.com") &&
|
||||
m.Subject == "Welcome to Bitwarden!" &&
|
||||
m.HtmlContent.Contains("Bitwarden Corp") &&
|
||||
m.Category == "Welcome"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync_SendsCorrectEmailWithFamilyTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = "family@example.com"
|
||||
};
|
||||
var familyOrganizationName = "Smith Family";
|
||||
|
||||
// Act
|
||||
await _sut.SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, familyOrganizationName);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>
|
||||
m.MetaData != null &&
|
||||
m.ToEmails.Contains("family@example.com") &&
|
||||
m.Subject == "Welcome to Bitwarden!" &&
|
||||
m.HtmlContent.Contains("Smith Family") &&
|
||||
m.Category == "Welcome"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Acme Corp", "Acme Corp")]
|
||||
[InlineData("Company & Associates", "Company & Associates")]
|
||||
[InlineData("Test \"Quoted\" Org", "Test "Quoted" Org")]
|
||||
public async Task SendOrganizationUserWelcomeEmailAsync_SanitizesOrganizationNameForEmail(string inputOrgName, string expectedSanitized)
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = "test@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
await _sut.SendOrganizationUserWelcomeEmailAsync(user, inputOrgName);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>
|
||||
m.HtmlContent.Contains(expectedSanitized) &&
|
||||
!m.HtmlContent.Contains("<script>") && // Ensure script tags are removed
|
||||
m.Category == "Welcome"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("test@example.com")]
|
||||
[InlineData("user+tag@domain.co.uk")]
|
||||
[InlineData("admin@organization.org")]
|
||||
public async Task SendIndividualUserWelcomeEmailAsync_HandlesVariousEmailFormats(string email)
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = email
|
||||
};
|
||||
|
||||
// Act
|
||||
await _sut.SendIndividualUserWelcomeEmailAsync(user);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>
|
||||
m.ToEmails.Contains(email)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -515,4 +516,399 @@ public class StripePaymentServiceTests
|
||||
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithCustomerDiscount_ReturnsDiscountFromCustomer(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = "cus_test123";
|
||||
subscriber.GatewaySubscriptionId = "sub_test123";
|
||||
|
||||
var customerDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,
|
||||
PercentOff = 20m,
|
||||
AmountOff = 1400
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically",
|
||||
Customer = new Customer
|
||||
{
|
||||
Discount = customerDiscount
|
||||
},
|
||||
Discounts = new List<Discount>(), // Empty list
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.SubscriptionGetAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff);
|
||||
Assert.Equal(14.00m, result.CustomerDiscount.AmountOff); // Converted from cents
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithoutCustomerDiscount_FallsBackToSubscriptionDiscounts(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = "cus_test123";
|
||||
subscriber.GatewaySubscriptionId = "sub_test123";
|
||||
|
||||
var subscriptionDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,
|
||||
PercentOff = 15m,
|
||||
AmountOff = null
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically",
|
||||
Customer = new Customer
|
||||
{
|
||||
Discount = null // No customer discount
|
||||
},
|
||||
Discounts = new List<Discount> { subscriptionDiscount },
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.SubscriptionGetAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert - Should use subscription discount as fallback
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.Equal(15m, result.CustomerDiscount.PercentOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithBothDiscounts_PrefersCustomerDiscount(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = "cus_test123";
|
||||
subscriber.GatewaySubscriptionId = "sub_test123";
|
||||
|
||||
var customerDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,
|
||||
PercentOff = 25m
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var subscriptionDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = "different-coupon-id",
|
||||
PercentOff = 10m
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically",
|
||||
Customer = new Customer
|
||||
{
|
||||
Discount = customerDiscount // Should prefer this
|
||||
},
|
||||
Discounts = new List<Discount> { subscriptionDiscount },
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.SubscriptionGetAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert - Should prefer customer discount over subscription discount
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.Equal(25m, result.CustomerDiscount.PercentOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithNoDiscounts_ReturnsNullDiscount(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = "cus_test123";
|
||||
subscriber.GatewaySubscriptionId = "sub_test123";
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically",
|
||||
Customer = new Customer
|
||||
{
|
||||
Discount = null
|
||||
},
|
||||
Discounts = new List<Discount>(), // Empty list, no discounts
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.SubscriptionGetAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithMultipleSubscriptionDiscounts_SelectsFirstDiscount(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange - Multiple subscription-level discounts, no customer discount
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = "cus_test123";
|
||||
subscriber.GatewaySubscriptionId = "sub_test123";
|
||||
|
||||
var firstDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = "coupon-10-percent",
|
||||
PercentOff = 10m
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var secondDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = "coupon-20-percent",
|
||||
PercentOff = 20m
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically",
|
||||
Customer = new Customer
|
||||
{
|
||||
Discount = null // No customer discount
|
||||
},
|
||||
// Multiple subscription discounts - FirstOrDefault() should select the first one
|
||||
Discounts = new List<Discount> { firstDiscount, secondDiscount },
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.SubscriptionGetAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert - Should select the first discount from the list (FirstOrDefault() behavior)
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal("coupon-10-percent", result.CustomerDiscount.Id);
|
||||
Assert.Equal(10m, result.CustomerDiscount.PercentOff);
|
||||
// Verify the second discount was not selected
|
||||
Assert.NotEqual("coupon-20-percent", result.CustomerDiscount.Id);
|
||||
Assert.NotEqual(20m, result.CustomerDiscount.PercentOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithNullCustomer_HandlesGracefully(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange - Subscription with null Customer (defensive null check scenario)
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = "cus_test123";
|
||||
subscriber.GatewaySubscriptionId = "sub_test123";
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically",
|
||||
Customer = null, // Customer not expanded or null
|
||||
Discounts = new List<Discount>(), // Empty discounts
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.SubscriptionGetAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert - Should handle null Customer gracefully without throwing NullReferenceException
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithNullDiscounts_HandlesGracefully(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange - Subscription with null Discounts (defensive null check scenario)
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = "cus_test123";
|
||||
subscriber.GatewaySubscriptionId = "sub_test123";
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically",
|
||||
Customer = new Customer
|
||||
{
|
||||
Discount = null // No customer discount
|
||||
},
|
||||
Discounts = null, // Discounts not expanded or null
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.SubscriptionGetAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert - Should handle null Discounts gracefully without throwing NullReferenceException
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_VerifiesCorrectExpandOptions(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = "cus_test123";
|
||||
subscriber.GatewaySubscriptionId = "sub_test123";
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically",
|
||||
Customer = new Customer { Discount = null },
|
||||
Discounts = new List<Discount>(), // Empty list
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.SubscriptionGetAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert - Verify expand options are correct
|
||||
await stripeAdapter.Received(1).SubscriptionGetAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionGetOptions>(o =>
|
||||
o.Expand.Contains("customer.discount.coupon.applies_to") &&
|
||||
o.Expand.Contains("discounts.coupon.applies_to") &&
|
||||
o.Expand.Contains("test_clock")));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithEmptyGatewaySubscriptionId_ReturnsEmptySubscriptionInfo(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
User subscriber)
|
||||
{
|
||||
// Arrange
|
||||
subscriber.GatewaySubscriptionId = null;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.Subscription);
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
Assert.Null(result.UpcomingInvoice);
|
||||
|
||||
// Verify no Stripe API calls were made
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceive()
|
||||
.SubscriptionGetAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,4 +238,55 @@ public class ImportCiphersAsyncCommandTests
|
||||
Assert.Equal("This organization can only have a maximum of " +
|
||||
$"{organization.MaxCollections} collections.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoOrganizationalVaultAsync_WithNullImportingOrgUser_SkipsCollectionUserCreation(
|
||||
Organization organization,
|
||||
Guid importingUserId,
|
||||
List<Collection> collections,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
organization.MaxCollections = null;
|
||||
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
collection.OrganizationId = organization.Id;
|
||||
}
|
||||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.OrganizationId = organization.Id;
|
||||
}
|
||||
|
||||
KeyValuePair<int, int>[] collectionRelationships = {
|
||||
new(0, 0),
|
||||
new(1, 1),
|
||||
new(2, 2)
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
// Simulate provider-created org with no members - importing user is NOT an org member
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(organization.Id, importingUserId)
|
||||
.Returns((OrganizationUser)null);
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new List<Collection>());
|
||||
|
||||
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
|
||||
|
||||
// Verify ciphers were created but no CollectionUser entries were created (because the organization user (importingUserId) is null)
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(
|
||||
ciphers,
|
||||
Arg.Is<IEnumerable<Collection>>(cols => cols.Count() == collections.Count),
|
||||
Arg.Is<IEnumerable<CollectionCipher>>(cc => cc.Count() == ciphers.Count),
|
||||
Arg.Is<IEnumerable<CollectionUser>>(cus => !cus.Any()));
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
}
|
||||
|
||||
51
test/Core.Test/Utilities/EmailValidationTests.cs
Normal file
51
test/Core.Test/Utilities/EmailValidationTests.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class EmailValidationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("user@Example.COM", "example.com")]
|
||||
[InlineData("user@EXAMPLE.COM", "example.com")]
|
||||
[InlineData("user@example.com", "example.com")]
|
||||
[InlineData("user@Example.Com", "example.com")]
|
||||
[InlineData("User@DOMAIN.CO.UK", "domain.co.uk")]
|
||||
public void GetDomain_WithMixedCaseEmail_ReturnsLowercaseDomain(string email, string expectedDomain)
|
||||
{
|
||||
// Act
|
||||
var result = EmailValidation.GetDomain(email);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedDomain, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("hello@world.com", "world.com")] // regular email address
|
||||
[InlineData("hello@world.planet.com", "world.planet.com")] // subdomain
|
||||
[InlineData("hello+1@world.com", "world.com")] // alias
|
||||
[InlineData("hello.there@world.com", "world.com")] // period in local-part
|
||||
[InlineData("hello@wörldé.com", "wörldé.com")] // unicode domain
|
||||
[InlineData("hello@world.cömé", "world.cömé")] // unicode top-level domain
|
||||
public void GetDomain_WithValidEmail_ReturnsLowercaseDomain(string email, string expectedDomain)
|
||||
{
|
||||
// Act
|
||||
var result = EmailValidation.GetDomain(email);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedDomain, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("invalid-email")]
|
||||
[InlineData("@example.com")]
|
||||
[InlineData("user@")]
|
||||
[InlineData("")]
|
||||
public void GetDomain_WithInvalidEmail_ThrowsBadRequestException(string email)
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<BadRequestException>(() => EmailValidation.GetDomain(email));
|
||||
Assert.Equal("Invalid email address format.", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class EventIntegrationsCacheConstantsTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void BuildCacheKeyForGroup_ReturnsExpectedKey(Guid groupId)
|
||||
{
|
||||
var expected = $"Group:{groupId:N}";
|
||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId);
|
||||
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void BuildCacheKeyForOrganization_ReturnsExpectedKey(Guid orgId)
|
||||
{
|
||||
var expected = $"Organization:{orgId:N}";
|
||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId);
|
||||
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void BuildCacheKeyForOrganizationUser_ReturnsExpectedKey(Guid orgId, Guid userId)
|
||||
{
|
||||
var expected = $"OrganizationUserUserDetails:{orgId:N}:{userId:N}";
|
||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId);
|
||||
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CacheName_ReturnsExpected()
|
||||
{
|
||||
Assert.Equal("EventIntegrations", EventIntegrationsCacheConstants.CacheName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using NSubstitute;
|
||||
using StackExchange.Redis;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class ExtendedCacheServiceCollectionExtensionsTests
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private const string _cacheName = "TestCache";
|
||||
|
||||
public ExtendedCacheServiceCollectionExtensionsTests()
|
||||
{
|
||||
_services = new ServiceCollection();
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>())
|
||||
.Build();
|
||||
|
||||
_globalSettings = new GlobalSettings();
|
||||
config.GetSection("GlobalSettings").Bind(_globalSettings);
|
||||
|
||||
_services.TryAddSingleton(config);
|
||||
_services.TryAddSingleton(_globalSettings);
|
||||
_services.TryAddSingleton<IGlobalSettings>(_globalSettings);
|
||||
_services.AddLogging();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_CustomSettings_OverridesDefaults()
|
||||
{
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
Duration = TimeSpan.FromMinutes(12),
|
||||
FailSafeMaxDuration = TimeSpan.FromHours(1.5),
|
||||
FailSafeThrottleDuration = TimeSpan.FromMinutes(1),
|
||||
EagerRefreshThreshold = 0.75f,
|
||||
FactorySoftTimeout = TimeSpan.FromMilliseconds(20),
|
||||
FactoryHardTimeout = TimeSpan.FromSeconds(3),
|
||||
DistributedCacheSoftTimeout = TimeSpan.FromSeconds(0.5),
|
||||
DistributedCacheHardTimeout = TimeSpan.FromSeconds(1.5),
|
||||
JitterMaxDuration = TimeSpan.FromSeconds(5),
|
||||
IsFailSafeEnabled = false,
|
||||
AllowBackgroundDistributedCacheOperations = false,
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
var opt = cache.DefaultEntryOptions;
|
||||
|
||||
Assert.Equal(TimeSpan.FromMinutes(12), opt.Duration);
|
||||
Assert.False(opt.IsFailSafeEnabled);
|
||||
Assert.Equal(TimeSpan.FromHours(1.5), opt.FailSafeMaxDuration);
|
||||
Assert.Equal(TimeSpan.FromMinutes(1), opt.FailSafeThrottleDuration);
|
||||
Assert.Equal(0.75f, opt.EagerRefreshThreshold);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(20), opt.FactorySoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(3000), opt.FactoryHardTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(0.5), opt.DistributedCacheSoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(1.5), opt.DistributedCacheHardTimeout);
|
||||
Assert.False(opt.AllowBackgroundDistributedCacheOperations);
|
||||
Assert.Equal(TimeSpan.FromSeconds(5), opt.JitterMaxDuration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_DefaultSettings_ConfiguresExpectedValues()
|
||||
{
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
var opt = cache.DefaultEntryOptions;
|
||||
|
||||
Assert.Equal(TimeSpan.FromMinutes(30), opt.Duration);
|
||||
Assert.True(opt.IsFailSafeEnabled);
|
||||
Assert.Equal(TimeSpan.FromHours(2), opt.FailSafeMaxDuration);
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), opt.FailSafeThrottleDuration);
|
||||
Assert.Equal(0.9f, opt.EagerRefreshThreshold);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(100), opt.FactorySoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(1500), opt.FactoryHardTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(1), opt.DistributedCacheSoftTimeout);
|
||||
Assert.Equal(TimeSpan.FromSeconds(2), opt.DistributedCacheHardTimeout);
|
||||
Assert.True(opt.AllowBackgroundDistributedCacheOperations);
|
||||
Assert.Equal(TimeSpan.FromSeconds(2), opt.JitterMaxDuration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_DisabledDistributedCache_DoesNotRegisterBackplaneOrRedis()
|
||||
{
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
EnableDistributedCache = false,
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.False(cache.HasDistributedCache);
|
||||
Assert.False(cache.HasBackplane);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_EmptyCacheName_DoesNothing()
|
||||
{
|
||||
_services.AddExtendedCache(string.Empty, _globalSettings);
|
||||
|
||||
var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList();
|
||||
Assert.Empty(regs);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetKeyedService<IFusionCache>(_cacheName);
|
||||
Assert.Null(cache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_MultipleCalls_OnlyAddsOneCacheService()
|
||||
{
|
||||
var settings = CreateGlobalSettings(new()
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }
|
||||
});
|
||||
|
||||
// Provide a multiplexer (shared)
|
||||
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
|
||||
var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList();
|
||||
Assert.Single(regs);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
Assert.NotNull(cache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_MultipleDifferentCaches_AddsAll()
|
||||
{
|
||||
_services.AddExtendedCache("Cache1", _globalSettings);
|
||||
_services.AddExtendedCache("Cache2", _globalSettings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
|
||||
var cache1 = provider.GetRequiredKeyedService<IFusionCache>("Cache1");
|
||||
var cache2 = provider.GetRequiredKeyedService<IFusionCache>("Cache2");
|
||||
|
||||
Assert.NotNull(cache1);
|
||||
Assert.NotNull(cache2);
|
||||
Assert.NotSame(cache1, cache2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_WithRedis_EnablesDistributedCacheAndBackplane()
|
||||
{
|
||||
var settings = CreateGlobalSettings(new()
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
||||
{ "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedRedisCache", "true" }
|
||||
});
|
||||
|
||||
// Provide a multiplexer (shared)
|
||||
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.True(cache.HasDistributedCache);
|
||||
Assert.True(cache.HasBackplane);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_InvalidRedisConnection_LogsAndThrows()
|
||||
{
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" }
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
Assert.Throws<RedisConnectionException>(() =>
|
||||
{
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
// Trigger lazy initialization
|
||||
cache.GetOrDefault<string>("test");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_WithExistingRedis_UsesExistingDistributedCacheAndBackplane()
|
||||
{
|
||||
var settings = CreateGlobalSettings(new()
|
||||
{
|
||||
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
|
||||
});
|
||||
|
||||
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||
_services.AddSingleton(Substitute.For<IDistributedCache>());
|
||||
|
||||
_services.AddExtendedCache(_cacheName, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.True(cache.HasDistributedCache);
|
||||
Assert.True(cache.HasBackplane);
|
||||
|
||||
var existingCache = provider.GetRequiredService<IDistributedCache>();
|
||||
Assert.NotNull(existingCache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_NoRedis_DisablesDistributedCacheAndBackplane()
|
||||
{
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.False(cache.HasDistributedCache);
|
||||
Assert.False(cache.HasBackplane);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_NoSharedRedisButNoConnectionString_DisablesDistributedCacheAndBackplane()
|
||||
{
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
// No Redis connection string
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
|
||||
|
||||
Assert.False(cache.HasDistributedCache);
|
||||
Assert.False(cache.HasBackplane);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_KeyedRedis_UsesSeparateMultiplexers()
|
||||
{
|
||||
var settingsA = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
EnableDistributedCache = true,
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
||||
};
|
||||
var settingsB = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
EnableDistributedCache = true,
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" }
|
||||
};
|
||||
|
||||
_services.AddKeyedSingleton("CacheA", Substitute.For<IConnectionMultiplexer>());
|
||||
_services.AddKeyedSingleton("CacheB", Substitute.For<IConnectionMultiplexer>());
|
||||
|
||||
_services.AddExtendedCache("CacheA", _globalSettings, settingsA);
|
||||
_services.AddExtendedCache("CacheB", _globalSettings, settingsB);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var muxA = provider.GetRequiredKeyedService<IConnectionMultiplexer>("CacheA");
|
||||
var muxB = provider.GetRequiredKeyedService<IConnectionMultiplexer>("CacheB");
|
||||
|
||||
Assert.NotNull(muxA);
|
||||
Assert.NotNull(muxB);
|
||||
Assert.NotSame(muxA, muxB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddExtendedCache_WithExistingKeyedDistributedCache_ReusesIt()
|
||||
{
|
||||
var existingCache = Substitute.For<IDistributedCache>();
|
||||
_services.AddKeyedSingleton<IDistributedCache>(_cacheName, existingCache);
|
||||
|
||||
var settings = new GlobalSettings.ExtendedCacheSettings
|
||||
{
|
||||
UseSharedRedisCache = false,
|
||||
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
|
||||
};
|
||||
|
||||
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
|
||||
|
||||
using var provider = _services.BuildServiceProvider();
|
||||
var resolved = provider.GetRequiredKeyedService<IDistributedCache>(_cacheName);
|
||||
|
||||
Assert.Same(existingCache, resolved);
|
||||
}
|
||||
|
||||
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(data)
|
||||
.Build();
|
||||
|
||||
var settings = new GlobalSettings();
|
||||
config.GetSection("GlobalSettings").Bind(settings);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Serilog;
|
||||
using Serilog.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
||||
@@ -23,18 +19,6 @@ public class LoggerFactoryExtensionsTests
|
||||
Assert.Empty(providers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSerilog_IsDevelopment_DevLoggingEnabled_AddsSerilog()
|
||||
{
|
||||
var providers = GetProviders(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:EnableDevLogging", "true" },
|
||||
}, "Development");
|
||||
|
||||
var provider = Assert.Single(providers);
|
||||
Assert.IsAssignableFrom<SerilogLoggerProvider>(provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSerilog_IsProduction_AddsSerilog()
|
||||
{
|
||||
@@ -52,7 +36,7 @@ public class LoggerFactoryExtensionsTests
|
||||
var providers = GetProviders(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:ProjectName", "Test" },
|
||||
{ "GlobalSetting:LogDirectoryByProject", "true" },
|
||||
{ "GlobalSettings:LogDirectoryByProject", "true" },
|
||||
{ "GlobalSettings:LogDirectory", tempDir.FullName },
|
||||
});
|
||||
|
||||
@@ -62,6 +46,8 @@ public class LoggerFactoryExtensionsTests
|
||||
var logger = provider.CreateLogger("Test");
|
||||
logger.LogWarning("This is a test");
|
||||
|
||||
provider.Dispose();
|
||||
|
||||
var logFile = Assert.Single(tempDir.EnumerateFiles("Test/*.txt"));
|
||||
|
||||
var logFileContents = await File.ReadAllTextAsync(logFile.FullName);
|
||||
@@ -104,62 +90,6 @@ public class LoggerFactoryExtensionsTests
|
||||
logFileContents
|
||||
);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Only for local development.")]
|
||||
public async Task AddSerilog_SyslogConfigured_Warns()
|
||||
{
|
||||
// Setup a fake syslog server
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
using var listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 25000);
|
||||
listener.Start();
|
||||
|
||||
var provider = GetServiceProvider(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SysLog:Destination", "tcp://127.0.0.1:25000" },
|
||||
{ "GlobalSettings:SiteName", "TestSite" },
|
||||
{ "GlobalSettings:ProjectName", "TestProject" },
|
||||
}, "Production");
|
||||
|
||||
var loggerFactory = provider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("Test");
|
||||
|
||||
logger.LogWarning("This is a test");
|
||||
|
||||
// Look in syslog for data
|
||||
using var socket = await listener.AcceptSocketAsync(cts.Token);
|
||||
|
||||
// This is rather lazy as opposed to implementing smarter syslog message
|
||||
// reading but thats not what this test about, so instead just give
|
||||
// the sink time to finish its work in the background
|
||||
|
||||
List<string> messages = [];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
var received = await socket.ReceiveAsync(buffer, SocketFlags.None, cts.Token);
|
||||
|
||||
if (received == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var response = Encoding.ASCII.GetString(buffer, 0, received);
|
||||
messages.Add(response);
|
||||
|
||||
if (messages.Count == 2)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Collection(
|
||||
messages,
|
||||
(firstMessage) => Assert.Contains("Syslog for logging has been deprecated", firstMessage),
|
||||
(secondMessage) => Assert.Contains("This is a test", secondMessage)
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<ILoggerProvider> GetProviders(Dictionary<string, string?> initialData, string environment = "Production")
|
||||
{
|
||||
var provider = GetServiceProvider(initialData, environment);
|
||||
@@ -172,23 +102,34 @@ public class LoggerFactoryExtensionsTests
|
||||
.AddInMemoryCollection(initialData)
|
||||
.Build();
|
||||
|
||||
var hostingEnvironment = Substitute.For<IWebHostEnvironment>();
|
||||
var hostingEnvironment = Substitute.For<IHostEnvironment>();
|
||||
|
||||
hostingEnvironment
|
||||
.EnvironmentName
|
||||
.Returns(environment);
|
||||
|
||||
var context = new WebHostBuilderContext
|
||||
var context = new HostBuilderContext(new Dictionary<object, object>())
|
||||
{
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
Configuration = config,
|
||||
};
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.AddSerilog(context);
|
||||
});
|
||||
|
||||
var hostBuilder = Substitute.For<IHostBuilder>();
|
||||
hostBuilder
|
||||
.When(h => h.ConfigureServices(Arg.Any<Action<HostBuilderContext, IServiceCollection>>()))
|
||||
.Do(call =>
|
||||
{
|
||||
var configureAction = call.Arg<Action<HostBuilderContext, IServiceCollection>>();
|
||||
configureAction(context, services);
|
||||
});
|
||||
|
||||
hostBuilder.AddSerilogFileLogging();
|
||||
|
||||
hostBuilder
|
||||
.ConfigureServices(Arg.Any<Action<HostBuilderContext, IServiceCollection>>())
|
||||
.Received(1);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
@@ -7,35 +6,13 @@ namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class StaticStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void StaticStore_Initialization_Success()
|
||||
{
|
||||
var plans = StaticStore.Plans.ToList();
|
||||
Assert.NotNull(plans);
|
||||
Assert.NotEmpty(plans);
|
||||
Assert.Equal(22, plans.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PlanType.EnterpriseAnnually)]
|
||||
[InlineData(PlanType.EnterpriseMonthly)]
|
||||
[InlineData(PlanType.TeamsMonthly)]
|
||||
[InlineData(PlanType.TeamsAnnually)]
|
||||
[InlineData(PlanType.TeamsStarter)]
|
||||
public void StaticStore_GetPlan_Success(PlanType planType)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
Assert.NotNull(plan);
|
||||
Assert.Equal(planType, plan.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticStore_GlobalEquivalentDomains_OnlyAsciiAllowed()
|
||||
{
|
||||
// Ref: https://daniel.haxx.se/blog/2025/05/16/detecting-malicious-unicode/
|
||||
// URLs can contain unicode characters that to a computer would point to completely seperate domains but to the
|
||||
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
|
||||
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
|
||||
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
|
||||
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
|
||||
// url update that could be missed in code review and then if they got a user to that URL Bitwarden could
|
||||
// consider it equivalent with a cipher in the users vault and offer autofill when we should not.
|
||||
// GitHub does now show a warning on non-ascii characters but it could still be missed.
|
||||
|
||||
@@ -225,130 +225,6 @@ public class CipherServiceTests
|
||||
Assert.NotNull(result.uploadUrl);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UploadFileForExistingAttachmentAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider,
|
||||
Cipher cipher)
|
||||
{
|
||||
var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1);
|
||||
var stream = new MemoryStream();
|
||||
var attachment = new CipherAttachment.MetaData
|
||||
{
|
||||
AttachmentId = "test-attachment-id",
|
||||
Size = 100,
|
||||
FileName = "test.txt",
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UploadFileForExistingAttachmentAsync(stream, cipher, attachment, lastKnownRevisionDate));
|
||||
Assert.Contains("out of date", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task UploadFileForExistingAttachmentAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher)
|
||||
{
|
||||
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||
var stream = new MemoryStream(new byte[100]);
|
||||
var attachmentId = "test-attachment-id";
|
||||
var attachment = new CipherAttachment.MetaData
|
||||
{
|
||||
AttachmentId = attachmentId,
|
||||
Size = 100,
|
||||
FileName = "test.txt",
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Set the attachment on the cipher so ValidateCipherAttachmentFile can find it
|
||||
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||
{
|
||||
[attachmentId] = attachment
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.UploadNewAttachmentAsync(stream, cipher, attachment)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.ValidateFileAsync(cipher, attachment, Arg.Any<long>())
|
||||
.Returns((true, 100L));
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await sutProvider.Sut.UploadFileForExistingAttachmentAsync(stream, cipher, attachment, lastKnownRevisionDate);
|
||||
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||
.UploadNewAttachmentAsync(stream, cipher, attachment);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAttachmentShareAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider,
|
||||
Cipher cipher, Guid organizationId)
|
||||
{
|
||||
var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1);
|
||||
var stream = new MemoryStream();
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
var attachmentId = "attachment-id";
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateAttachmentShareAsync(cipher, stream, fileName, key, 100, attachmentId, organizationId, lastKnownRevisionDate));
|
||||
Assert.Contains("out of date", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task CreateAttachmentShareAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid organizationId)
|
||||
{
|
||||
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||
var stream = new MemoryStream(new byte[100]);
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
var attachmentId = "attachment-id";
|
||||
|
||||
// Setup cipher with existing attachment (no TempMetadata)
|
||||
cipher.OrganizationId = null;
|
||||
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||
{
|
||||
[attachmentId] = new CipherAttachment.MetaData
|
||||
{
|
||||
AttachmentId = attachmentId,
|
||||
Size = 100,
|
||||
FileName = "existing.txt",
|
||||
Key = "existing-key"
|
||||
}
|
||||
});
|
||||
|
||||
// Mock organization
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
MaxStorageGb = 1
|
||||
};
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.UploadShareAttachmentAsync(stream, cipher.Id, organizationId, Arg.Any<CipherAttachment.MetaData>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await sutProvider.Sut.CreateAttachmentShareAsync(cipher, stream, fileName, key, 100, attachmentId, organizationId, lastKnownRevisionDate);
|
||||
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||
.UploadShareAttachmentAsync(stream, cipher.Id, organizationId, Arg.Any<CipherAttachment.MetaData>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveDetailsAsync_PersonalVault_WithOrganizationDataOwnershipPolicyEnabled_Throws(
|
||||
@@ -2286,6 +2162,63 @@ public class CipherServiceTests
|
||||
.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_CallsMarkAsCompleteByCipherIds(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.UserId = deletingUserId;
|
||||
cipherDetails.OrganizationId = null;
|
||||
cipherDetails.DeletedDate = null;
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipherDetails, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ISecurityTaskRepository>()
|
||||
.Received(1)
|
||||
.MarkAsCompleteByCipherIds(Arg.Is<IEnumerable<Guid>>(ids =>
|
||||
ids.Count() == 1 && ids.First() == cipherDetails.Id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteManyAsync_CallsMarkAsCompleteByCipherIds(
|
||||
Guid deletingUserId, List<CipherDetails> ciphers, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var cipherIds = ciphers.Select(c => c.Id).ToArray();
|
||||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.UserId = deletingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
cipher.Edit = true;
|
||||
cipher.DeletedDate = null;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteManyAsync(cipherIds, deletingUserId, null, false);
|
||||
|
||||
await sutProvider.GetDependency<ISecurityTaskRepository>()
|
||||
.Received(1)
|
||||
.MarkAsCompleteByCipherIds(Arg.Is<IEnumerable<Guid>>(ids =>
|
||||
ids.Count() == cipherIds.Length && ids.All(id => cipherIds.Contains(id))));
|
||||
}
|
||||
|
||||
private async Task AssertNoActionsAsync(SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);
|
||||
|
||||
Reference in New Issue
Block a user