1
0
mirror of https://github.com/bitwarden/server synced 2026-01-02 00:23:40 +00:00

Merge branch 'main' into auth/pm-22975/client-version-validator

This commit is contained in:
Patrick Pimentel
2025-11-20 14:49:35 -05:00
163 changed files with 16066 additions and 885 deletions

View File

@@ -0,0 +1,225 @@
using System.Net;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private const string _mockEncryptedString = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private string _ownerEmail = null!;
public OrganizationUserControllerAutoConfirmTests(ApiApplicationFactory apiFactory)
{
_factory = apiFactory;
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
});
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
_ownerEmail = $"org-owner-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(_ownerEmail);
}
[Fact]
public async Task AutoConfirm_WhenUserCannotManageOtherUsers_ThenShouldReturnForbidden()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;
await _factory.GetService<IOrganizationRepository>()
.UpsertAsync(organization);
var testKey = $"test-key-{Guid.NewGuid()}";
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(userToConfirmEmail);
var (confirmingUserEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, organization.Id, OrganizationUserType.User);
await _loginHelper.LoginAsync(confirmingUserEmail);
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
_factory,
organization.Id,
userToConfirmEmail,
OrganizationUserType.User,
false,
new Permissions { ManageUsers = false },
OrganizationUserStatusType.Accepted);
var result = await _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
new OrganizationUserConfirmRequestModel
{
Key = testKey,
DefaultUserCollectionName = _mockEncryptedString
});
Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
}
[Fact]
public async Task AutoConfirm_WhenOwnerConfirmsValidUser_ThenShouldReturnNoContent()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;
await _factory.GetService<IOrganizationRepository>()
.UpsertAsync(organization);
var testKey = $"test-key-{Guid.NewGuid()}";
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.AutomaticUserConfirmation,
Enabled = true
});
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
});
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(userToConfirmEmail);
await _loginHelper.LoginAsync(_ownerEmail);
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
_factory,
organization.Id,
userToConfirmEmail,
OrganizationUserType.User,
false,
new Permissions(),
OrganizationUserStatusType.Accepted);
var result = await _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
new OrganizationUserConfirmRequestModel
{
Key = testKey,
DefaultUserCollectionName = _mockEncryptedString
});
Assert.Equal(HttpStatusCode.NoContent, result.StatusCode);
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
var confirmedUser = await orgUserRepository.GetByIdAsync(organizationUser.Id);
Assert.NotNull(confirmedUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);
Assert.Equal(testKey, confirmedUser.Key);
var collectionRepository = _factory.GetService<ICollectionRepository>();
var collections = await collectionRepository.GetManyByUserIdAsync(organizationUser.UserId!.Value);
Assert.NotEmpty(collections);
Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
}
[Fact]
public async Task AutoConfirm_WhenUserIsConfirmedMultipleTimes_ThenShouldSuccessAndOnlyConfirmOneUser()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;
await _factory.GetService<IOrganizationRepository>()
.UpsertAsync(organization);
var testKey = $"test-key-{Guid.NewGuid()}";
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(userToConfirmEmail);
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.AutomaticUserConfirmation,
Enabled = true
});
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
});
await _loginHelper.LoginAsync(_ownerEmail);
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
_factory,
organization.Id,
userToConfirmEmail,
OrganizationUserType.User,
false,
new Permissions(),
OrganizationUserStatusType.Accepted);
var tenRequests = Enumerable.Range(0, 10)
.Select(_ => _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
new OrganizationUserConfirmRequestModel
{
Key = testKey,
DefaultUserCollectionName = _mockEncryptedString
})).ToList();
var results = await Task.WhenAll(tenRequests);
Assert.Contains(results, r => r.StatusCode == HttpStatusCode.NoContent);
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
var confirmedUser = await orgUserRepository.GetByIdAsync(organizationUser.Id);
Assert.NotNull(confirmedUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);
Assert.Equal(testKey, confirmedUser.Key);
var collections = await _factory.GetService<ICollectionRepository>()
.GetManyByUserIdAsync(organizationUser.UserId!.Value);
Assert.NotEmpty(collections);
// validates user only received one default collection
Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
}

View File

@@ -218,7 +218,7 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
_ownerEmail = $"org-user-integration-test-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
}

View File

@@ -47,7 +47,7 @@ public class OrganizationUsersControllerPutResetPasswordTests : IClassFixture<Ap
_ownerEmail = $"reset-password-test-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
// Enable reset password and policies for the organization

View File

@@ -33,7 +33,7 @@ public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture<ApiApp
await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Authorize with the organization api key

View File

@@ -39,7 +39,7 @@ public class MembersControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Authorize with the organization api key

View File

@@ -39,7 +39,7 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Authorize with the organization api key

View File

@@ -9,10 +9,12 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2.Results;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
@@ -33,9 +35,11 @@ using Bit.Test.Common.AutoFixture.Attributes;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NSubstitute;
using OneOf.Types;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Controllers;
@@ -476,7 +480,7 @@ public class OrganizationUsersControllerTests
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
Assert.IsType<NotFound>(result);
Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
}
[Theory]
@@ -506,7 +510,7 @@ public class OrganizationUsersControllerTests
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
Assert.IsType<NotFound>(result);
Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
}
[Theory]
@@ -521,7 +525,7 @@ public class OrganizationUsersControllerTests
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
Assert.IsType<NotFound>(result);
Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
}
[Theory]
@@ -594,4 +598,190 @@ public class OrganizationUsersControllerTests
Assert.IsType<BadRequest<ModelStateDictionary>>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdNull_ReturnsUnauthorized(
Guid orgId,
Guid orgUserId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns((Guid?)null);
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdEmpty_ReturnsUnauthorized(
Guid orgId,
Guid orgUserId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(Guid.Empty);
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_Success_ReturnsOk(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(new None()));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<NoContent>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_NotFoundError_ReturnsNotFound(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(false);
var notFoundError = new OrganizationNotFound();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(notFoundError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var notFoundResult = Assert.IsType<NotFound<ErrorResponseModel>>(result);
Assert.Equal(notFoundError.Message, notFoundResult.Value.Message);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_BadRequestError_ReturnsBadRequest(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
var badRequestError = new UserIsNotAccepted();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(badRequestError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var badRequestResult = Assert.IsType<BadRequest<ErrorResponseModel>>(result);
Assert.Equal(badRequestError.Message, badRequestResult.Value.Message);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_InternalError_ReturnsProblem(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
var internalError = new FailedToWriteToEventLog();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(internalError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var problemResult = Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);
Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
}
}

View File

@@ -30,8 +30,8 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using NSubstitute;
using Xunit;
@@ -305,7 +305,7 @@ public class OrganizationsControllerTests : IDisposable
// Arrange
_currentContext.OrganizationOwner(organization.Id).Returns(true);
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
_pricingClient.GetPlan(Arg.Any<PlanType>()).Returns(plan);
_organizationService

View File

@@ -10,7 +10,7 @@ using Bit.Core.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
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;
@@ -24,11 +24,11 @@ namespace Bit.Api.Test.Billing.Controllers;
public class OrganizationSponsorshipsControllerTests
{
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[]> 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>()

View File

@@ -17,7 +17,7 @@ using Bit.Core.Context;
using Bit.Core.Models.Api;
using Bit.Core.Models.BitStripe;
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 Microsoft.AspNetCore.Http;
@@ -351,7 +351,7 @@ public class ProviderBillingControllerTests
foreach (var providerPlan in providerPlans)
{
var plan = StaticStore.GetPlan(providerPlan.PlanType);
var plan = MockPlans.Get(providerPlan.PlanType);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)
@@ -372,7 +372,7 @@ public class ProviderBillingControllerTests
Assert.Equal(subscription.Customer!.Discount!.Coupon!.PercentOff, response.DiscountPercentage);
Assert.Equal(subscription.CollectionMethod, response.CollectionMethod);
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var teamsPlan = MockPlans.Get(PlanType.TeamsMonthly);
var providerTeamsPlan = response.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name);
Assert.NotNull(providerTeamsPlan);
Assert.Equal(50, providerTeamsPlan.SeatMinimum);
@@ -381,7 +381,7 @@ public class ProviderBillingControllerTests
Assert.Equal(60 * teamsPlan.PasswordManager.ProviderPortalSeatPrice, providerTeamsPlan.Cost);
Assert.Equal("Monthly", providerTeamsPlan.Cadence);
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
var enterprisePlan = MockPlans.Get(PlanType.EnterpriseMonthly);
var providerEnterprisePlan = response.Plans.FirstOrDefault(plan => plan.PlanName == enterprisePlan.Name);
Assert.NotNull(providerEnterprisePlan);
Assert.Equal(100, providerEnterprisePlan.SeatMinimum);
@@ -498,7 +498,7 @@ public class ProviderBillingControllerTests
foreach (var providerPlan in providerPlans)
{
var plan = StaticStore.GetPlan(providerPlan.PlanType);
var plan = MockPlans.Get(providerPlan.PlanType);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)

View File

@@ -16,7 +16,7 @@ using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.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 Bit.Test.Common.Helpers;
@@ -121,7 +121,7 @@ public class ServiceAccountsControllerTests
{
ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization);
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.CreateAsync(organization.Id, data);

View File

@@ -18,9 +18,9 @@ using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
@@ -335,7 +335,7 @@ public class SyncControllerTests
if (matchedProviderUserOrgDetails != null)
{
var providerOrgProductType = StaticStore.GetPlan(matchedProviderUserOrgDetails.PlanType).ProductTier;
var providerOrgProductType = MockPlans.Get(matchedProviderUserOrgDetails.PlanType).ProductTier;
Assert.Equal(providerOrgProductType, profProviderOrg.ProductTierType);
}
}

View File

@@ -24,6 +24,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Billing\Billing.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Core.Test\Core.Test.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,7 +9,7 @@ using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Core.Test.Billing.Mocks;
using NSubstitute;
using Stripe;
using Xunit;
@@ -237,7 +237,7 @@ public class ProviderEventServiceTests
foreach (var providerPlan in providerPlans)
{
_pricingClient.GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType));
_pricingClient.GetPlanOrThrow(providerPlan.PlanType).Returns(MockPlans.Get(providerPlan.PlanType));
}
_providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans);
@@ -246,8 +246,8 @@ public class ProviderEventServiceTests
await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);
// Assert
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
var teamsPlan = MockPlans.Get(PlanType.TeamsMonthly);
var enterprisePlan = MockPlans.Get(PlanType.EnterpriseMonthly);
await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(
options =>

View File

@@ -8,11 +8,11 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks.Plans;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NSubstitute;

View File

@@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Pricing;
@@ -16,6 +15,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks.Plans;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
@@ -1141,7 +1141,7 @@ public class UpcomingInvoiceHandlerTests
}
[Fact]
public async Task HandleAsync_WhenMilestone3Disabled_DoesNotUpdateSubscription()
public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2019Plan_DoesNotUpdateSubscription()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
@@ -1789,4 +1789,170 @@ public class UpcomingInvoiceHandlerTests
email.ToEmails.Contains("org@example.com") &&
email.Subject == "Your Subscription Will Renew Soon"));
}
[Fact]
public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2025Plan_UpdatesSubscriptionOnlyNoAddons()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";
var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
}
};
var families2025Plan = new Families2025Plan();
var familiesPlan = new FamiliesPlan();
var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }
}
}
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};
var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
Address = new Address { Country = "US" }
};
var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2025
};
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
_stripeEventUtilityService
.GetIdsFromMetadata(subscription.Metadata)
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2025).Returns(families2025Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeFacade.Received(1).UpdateSubscription(
Arg.Is(subscriptionId),
Arg.Is<SubscriptionUpdateOptions>(o =>
o.Items.Count == 1 &&
o.Items[0].Id == passwordManagerItemId &&
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
o.Discounts == null &&
o.ProrationBehavior == ProrationBehavior.None));
await _organizationRepository.Received(1).ReplaceAsync(
Arg.Is<Organization>(org =>
org.Id == _organizationId &&
org.PlanType == PlanType.FamiliesAnnually &&
org.Plan == familiesPlan.Name &&
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
org.Seats == familiesPlan.PasswordManager.BaseSeats));
}
[Fact]
public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2025Plan_DoesNotUpdateSubscription()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";
var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
}
};
var families2025Plan = new Families2025Plan();
var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }
}
}
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};
var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
Address = new Address { Country = "US" }
};
var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2025
};
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
_stripeEventUtilityService
.GetIdsFromMetadata(subscription.Metadata)
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2025).Returns(families2025Plan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert - should not update subscription or organization when feature flag is disabled
await _stripeFacade.DidNotReceive().UpdateSubscription(
Arg.Any<string>(),
Arg.Any<SubscriptionUpdateOptions>());
await _organizationRepository.DidNotReceive().ReplaceAsync(
Arg.Is<Organization>(org => org.PlanType == PlanType.FamiliesAnnually));
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
@@ -28,7 +28,7 @@ public class CloudICloudOrganizationSignUpCommandTests
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
var plan = MockPlans.Get(signup.Plan);
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
@@ -37,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);
@@ -77,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;
@@ -112,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;
@@ -123,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);
@@ -164,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);
@@ -184,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));
@@ -204,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));
@@ -224,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));
@@ -244,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)

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -38,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);
@@ -62,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());
@@ -416,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
// -----------------------------------------------------------------------------------------------
@@ -425,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 =>
@@ -457,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 =>
@@ -495,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()));
@@ -524,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()));
@@ -561,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 =>
@@ -597,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 =>
@@ -640,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}";
@@ -662,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);
@@ -691,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}";
@@ -713,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();
@@ -762,6 +952,66 @@ public class RegisterUserCommandTests
}
// -----------------------------------------------------------------------------------------------
// 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]
@@ -799,6 +1049,194 @@ public class RegisterUserCommandTests
.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(

View File

@@ -21,9 +21,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationTrue_SendsEmailAndReturnsNull(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();
@@ -34,6 +36,10 @@ public class SendVerificationEmailForRegistrationCommandTests
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IMailService>()
.SendRegistrationVerificationEmailAsync(email, Arg.Any<string>())
.Returns(Task.CompletedTask);
@@ -56,9 +62,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[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,6 +77,10 @@ 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>())
@@ -87,9 +99,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[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,6 +114,10 @@ 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>())
@@ -128,9 +146,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[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,6 +158,13 @@ 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));
}
@@ -162,4 +189,88 @@ public class SendVerificationEmailForRegistrationCommandTests
.DisableUserRegistration = false;
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run("", name, receiveMarketingEmails));
}
[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));
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);
// 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));
Assert.Equal("Invalid email address format.", exception.Message);
}
}

View 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)!;
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@@ -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;

View File

@@ -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;
@@ -163,7 +163,7 @@ public class GetOrganizationMetadataQueryTests
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
.Returns(MockPlans.Get(organization.PlanType));
var result = await sutProvider.Sut.Run(organization);
@@ -216,7 +216,7 @@ public class GetOrganizationMetadataQueryTests
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
.Returns(MockPlans.Get(organization.PlanType));
var result = await sutProvider.Sut.Run(organization);
@@ -282,7 +282,7 @@ public class GetOrganizationMetadataQueryTests
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
.Returns(MockPlans.Get(organization.PlanType));
var result = await sutProvider.Sut.Run(organization);
@@ -349,7 +349,7 @@ public class GetOrganizationMetadataQueryTests
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
.Returns(MockPlans.Get(organization.PlanType));
var result = await sutProvider.Sut.Run(organization);

View File

@@ -3,7 +3,6 @@ using Bit.Core.Billing;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
@@ -34,7 +33,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -70,7 +68,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -109,7 +106,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -144,7 +140,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -179,7 +174,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -217,7 +211,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -258,7 +251,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -297,7 +289,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -339,33 +330,12 @@ public class PricingClientTests
Assert.Null(result);
}
[Theory, BitAutoData]
public async Task GetPlan_WhenPricingServiceDisabled_ReturnsStaticStorePlan(
SutProvider<PricingClient> sutProvider)
{
// Arrange
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.UsePricingService)
.Returns(false);
// Act
var result = await sutProvider.Sut.GetPlan(PlanType.FamiliesAnnually);
// Assert
Assert.NotNull(result);
Assert.Equal(PlanType.FamiliesAnnually, result.Type);
}
[Theory, BitAutoData]
public async Task GetPlan_WhenLookupKeyNotFound_ReturnsNull(
SutProvider<PricingClient> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.UsePricingService)
.Returns(true);
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));
@@ -384,7 +354,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -413,7 +382,6 @@ public class PricingClientTests
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
@@ -450,26 +418,6 @@ public class PricingClientTests
Assert.Empty(result);
}
[Theory, BitAutoData]
public async Task ListPlans_WhenPricingServiceDisabled_ReturnsStaticStorePlans(
SutProvider<PricingClient> sutProvider)
{
// Arrange
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.UsePricingService)
.Returns(false);
// Act
var result = await sutProvider.Sut.ListPlans();
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.Equal(StaticStore.Plans.Count(), result.Count);
}
[Fact]
public async Task ListPlans_WhenPricingServiceReturnsError_ThrowsBillingException()
{
@@ -479,7 +427,6 @@ public class PricingClientTests
.Respond(HttpStatusCode.InternalServerError);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };

View File

@@ -10,7 +10,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,10 +31,10 @@ 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 };
@@ -97,10 +97,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)
@@ -134,7 +134,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;
@@ -210,7 +210,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;
@@ -284,7 +284,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;

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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,7 +16,7 @@ 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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>()

View File

@@ -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));

View File

@@ -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));

View File

@@ -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>()

View File

@@ -1,11 +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;

View 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);
}
}

View File

@@ -14,6 +14,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
{
private readonly IServiceCollection _services;
private readonly GlobalSettings _globalSettings;
private const string _cacheName = "TestCache";
public ExtendedCacheServiceCollectionExtensionsTests()
{
@@ -33,129 +34,276 @@ public class ExtendedCacheServiceCollectionExtensionsTests
}
[Fact]
public void TryAddFusionCoreServices_CustomSettings_OverridesDefaults()
public void AddExtendedCache_CustomSettings_OverridesDefaults()
{
var settings = CreateGlobalSettings(new Dictionary<string, string?>
var settings = new GlobalSettings.ExtendedCacheSettings
{
{ "GlobalSettings:DistributedCache:Duration", "00:12:00" },
{ "GlobalSettings:DistributedCache:FailSafeMaxDuration", "01:30:00" },
{ "GlobalSettings:DistributedCache:FailSafeThrottleDuration", "00:01:00" },
{ "GlobalSettings:DistributedCache:EagerRefreshThreshold", "0.75" },
{ "GlobalSettings:DistributedCache:FactorySoftTimeout", "00:00:00.020" },
{ "GlobalSettings:DistributedCache:FactoryHardTimeout", "00:00:03" },
{ "GlobalSettings:DistributedCache:DistributedCacheSoftTimeout", "00:00:00.500" },
{ "GlobalSettings:DistributedCache:DistributedCacheHardTimeout", "00:00:01.500" },
{ "GlobalSettings:DistributedCache:JitterMaxDuration", "00:00:05" },
{ "GlobalSettings:DistributedCache:IsFailSafeEnabled", "false" },
{ "GlobalSettings:DistributedCache:AllowBackgroundDistributedCacheOperations", "false" },
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" }
});
_services.TryAddExtendedCacheServices(settings);
using var provider = _services.BuildServiceProvider();
var fusionCache = provider.GetRequiredService<IFusionCache>();
var options = fusionCache.DefaultEntryOptions;
// Provide a multiplexer (shared)
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
Assert.Equal(TimeSpan.FromMinutes(12), options.Duration);
Assert.False(options.IsFailSafeEnabled);
Assert.Equal(TimeSpan.FromHours(1.5), options.FailSafeMaxDuration);
Assert.Equal(TimeSpan.FromMinutes(1), options.FailSafeThrottleDuration);
Assert.Equal(0.75f, options.EagerRefreshThreshold);
Assert.Equal(TimeSpan.FromMilliseconds(20), options.FactorySoftTimeout);
Assert.Equal(TimeSpan.FromMilliseconds(3000), options.FactoryHardTimeout);
Assert.Equal(TimeSpan.FromSeconds(0.5), options.DistributedCacheSoftTimeout);
Assert.Equal(TimeSpan.FromSeconds(1.5), options.DistributedCacheHardTimeout);
Assert.False(options.AllowBackgroundDistributedCacheOperations);
Assert.Equal(TimeSpan.FromSeconds(5), options.JitterMaxDuration);
_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 TryAddFusionCoreServices_DefaultSettings_ConfiguresExpectedValues()
public void AddExtendedCache_MultipleDifferentCaches_AddsAll()
{
_services.TryAddExtendedCacheServices(_globalSettings);
_services.AddExtendedCache("Cache1", _globalSettings);
_services.AddExtendedCache("Cache2", _globalSettings);
using var provider = _services.BuildServiceProvider();
var fusionCache = provider.GetRequiredService<IFusionCache>();
var options = fusionCache.DefaultEntryOptions;
var cache1 = provider.GetRequiredKeyedService<IFusionCache>("Cache1");
var cache2 = provider.GetRequiredKeyedService<IFusionCache>("Cache2");
Assert.Equal(TimeSpan.FromMinutes(30), options.Duration);
Assert.True(options.IsFailSafeEnabled);
Assert.Equal(TimeSpan.FromHours(2), options.FailSafeMaxDuration);
Assert.Equal(TimeSpan.FromSeconds(30), options.FailSafeThrottleDuration);
Assert.Equal(0.9f, options.EagerRefreshThreshold);
Assert.Equal(TimeSpan.FromMilliseconds(100), options.FactorySoftTimeout);
Assert.Equal(TimeSpan.FromMilliseconds(1500), options.FactoryHardTimeout);
Assert.Equal(TimeSpan.FromSeconds(1), options.DistributedCacheSoftTimeout);
Assert.Equal(TimeSpan.FromSeconds(2), options.DistributedCacheHardTimeout);
Assert.True(options.AllowBackgroundDistributedCacheOperations);
Assert.Equal(TimeSpan.FromSeconds(2), options.JitterMaxDuration);
Assert.NotNull(cache1);
Assert.NotNull(cache2);
Assert.NotSame(cache1, cache2);
}
[Fact]
public void TryAddFusionCoreServices_MultipleCalls_OnlyConfiguresOnce()
public void AddExtendedCache_WithRedis_EnablesDistributedCacheAndBackplane()
{
var settings = CreateGlobalSettings(new Dictionary<string, string?>
var settings = CreateGlobalSettings(new()
{
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
{ "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedRedisCache", "true" }
});
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
_services.TryAddExtendedCacheServices(settings);
_services.TryAddExtendedCacheServices(settings);
_services.TryAddExtendedCacheServices(settings);
var registrations = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList();
Assert.Single(registrations);
// Provide a multiplexer (shared)
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
_services.AddExtendedCache(_cacheName, settings);
using var provider = _services.BuildServiceProvider();
var fusionCache = provider.GetRequiredService<IFusionCache>();
Assert.NotNull(fusionCache);
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
Assert.True(cache.HasDistributedCache);
Assert.True(cache.HasBackplane);
}
[Fact]
public void TryAddFusionCoreServices_WithRedis_EnablesDistributedCacheAndBackplane()
public void AddExtendedCache_InvalidRedisConnection_LogsAndThrows()
{
var settings = CreateGlobalSettings(new Dictionary<string, string?>
var settings = new GlobalSettings.ExtendedCacheSettings
{
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
});
UseSharedRedisCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" }
};
_services.AddExtendedCache(_cacheName, _globalSettings, settings);
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
_services.TryAddExtendedCacheServices(settings);
using var provider = _services.BuildServiceProvider();
var fusionCache = provider.GetRequiredService<IFusionCache>();
Assert.True(fusionCache.HasDistributedCache);
Assert.True(fusionCache.HasBackplane);
Assert.Throws<RedisConnectionException>(() =>
{
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName);
// Trigger lazy initialization
cache.GetOrDefault<string>("test");
});
}
[Fact]
public void TryAddFusionCoreServices_WithExistingRedis_EnablesDistributedCacheAndBackplane()
public void AddExtendedCache_WithExistingRedis_UsesExistingDistributedCacheAndBackplane()
{
var settings = CreateGlobalSettings(new Dictionary<string, string?>
var settings = CreateGlobalSettings(new()
{
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
});
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
_services.AddSingleton(Substitute.For<IDistributedCache>());
_services.TryAddExtendedCacheServices(settings);
using var provider = _services.BuildServiceProvider();
var fusionCache = provider.GetRequiredService<IFusionCache>();
Assert.True(fusionCache.HasDistributedCache);
Assert.True(fusionCache.HasBackplane);
var distributedCache = provider.GetRequiredService<IDistributedCache>();
Assert.NotNull(distributedCache);
_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 TryAddFusionCoreServices_WithoutRedis_DisablesDistributedCacheAndBackplane()
public void AddExtendedCache_NoRedis_DisablesDistributedCacheAndBackplane()
{
_services.TryAddExtendedCacheServices(_globalSettings);
using var provider = _services.BuildServiceProvider();
_services.AddExtendedCache(_cacheName, _globalSettings);
var fusionCache = provider.GetRequiredService<IFusionCache>();
Assert.False(fusionCache.HasDistributedCache);
Assert.False(fusionCache.HasBackplane);
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)

View File

@@ -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,28 +6,6 @@ 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(23, 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()
{

View File

@@ -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(

View File

@@ -242,7 +242,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
var orgInviteToken = "BwOrgUserInviteToken_CfDJ8HOzu6wr6nVLouuDxgOHsMwPcj9Guuip5k_XLD1bBGpwQS1f66c9kB6X4rvKGxNdywhgimzgvG9SgLwwJU70O8P879XyP94W6kSoT4N25a73kgW3nU3vl3fAtGSS52xdBjNU8o4sxmomRvhOZIQ0jwtVjdMC2IdybTbxwCZhvN0hKIFs265k6wFRSym1eu4NjjZ8pmnMneG0PlKnNZL93tDe8FMcqStJXoddIEgbA99VJp8z1LQmOMfEdoMEM7Zs8W5bZ34N4YEGu8XCrVau59kGtWQk7N4rPV5okzQbTpeoY_4FeywgLFGm-tDtTPEdSEBJkRjexANri7CGdg3dpnMifQc_bTmjZd32gOjw8N8v";
var orgUserId = new Guid("5e45fbdc-a080-4a77-93ff-b19c0161e81e");
var orgUser = new OrganizationUser { Id = orgUserId, Email = email };
var orgUser = new OrganizationUser { Id = orgUserId, Email = email, OrganizationId = Guid.NewGuid() };
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser)
{
@@ -259,6 +259,12 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
});
});
localFactory.SubstituteService<IOrganizationUserRepository>(orgUserRepository =>
{
orgUserRepository.GetByIdAsync(orgUserId)
.Returns(orgUser);
});
var registerFinishReqModel = new RegisterFinishRequestModel
{
Email = email,

View File

@@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Xunit;
@@ -7,7 +9,7 @@ namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories;
public class OrganizationDomainRepositoryTests
{
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetExpiredOrganizationDomainsAsync_ShouldReturn3DaysOldUnverifiedDomains(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
@@ -74,7 +76,7 @@ public class OrganizationDomainRepositoryTests
Assert.NotNull(expectedDomain2);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetExpiredOrganizationDomainsAsync_ShouldNotReturnDomainsUnder3DaysOld(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
@@ -120,7 +122,7 @@ public class OrganizationDomainRepositoryTests
Assert.Null(expectedDomain2);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetExpiredOrganizationDomainsAsync_ShouldNotReturnVerifiedDomains(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
@@ -189,7 +191,7 @@ public class OrganizationDomainRepositoryTests
Assert.Null(expectedDomain2);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetManyByNextRunDateAsync_ShouldReturnUnverifiedDomains(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
@@ -228,7 +230,7 @@ public class OrganizationDomainRepositoryTests
Assert.NotNull(expectedDomain);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetManyByNextRunDateAsync_ShouldNotReturnUnverifiedDomains_WhenNextRunDateIsOutside36hoursWindow(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
@@ -267,7 +269,7 @@ public class OrganizationDomainRepositoryTests
Assert.Null(expectedDomain);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetManyByNextRunDateAsync_ShouldNotReturnVerifiedDomains(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
@@ -307,7 +309,7 @@ public class OrganizationDomainRepositoryTests
Assert.Null(expectedDomain);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetVerifiedDomainsByOrganizationIdsAsync_ShouldVerifiedDomainsMatchesOrganizationIds(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
@@ -383,4 +385,437 @@ public class OrganizationDomainRepositoryTests
Assert.Null(otherOrganizationDomain);
Assert.Null(unverifiedDomain);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithVerifiedDomainAndBlockPolicy_ReturnsTrue(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.True(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUnverifiedDomain_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
// Do not verify the domain
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithDisabledPolicy_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = false
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithDisabledOrganization_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = false,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUsePoliciesFalse_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = false, // Organization doesn't have policies feature
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUseOrganizationDomainsFalse_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = false // Organization doesn't have organization domains feature
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithNoPolicyOfType_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
// No policy created
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithNonExistentDomain_ReturnsFalse(
IOrganizationDomainRepository organizationDomainRepository)
{
// Arrange
var domainName = $"nonexistent-{Guid.NewGuid()}.example.com";
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_ExcludeOrganization_WhenSameOrg_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act - Exclude the same organization that has the domain
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName, organization.Id);
// Assert - Should return false because we're excluding the only org with this domain
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_ExcludeOrganization_WhenDifferentOrg_ReturnsTrue(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization1 = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org 1 {id}",
BillingEmail = $"test1+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain1 = new OrganizationDomain
{
OrganizationId = organization1.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain1.SetNextRunDate(1);
organizationDomain1.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain1);
var policy1 = new Policy
{
OrganizationId = organization1.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy1);
// Create a second organization (the one we'll exclude)
var organization2 = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org 2 {id}",
BillingEmail = $"test2+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
// Act - Exclude organization2 (but organization1 still has the domain blocked)
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName, organization2.Id);
// Assert - Should return true because organization1 (not excluded) has the domain blocked
Assert.True(result);
}
}

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
@@ -1487,8 +1488,15 @@ public class OrganizationUserRepositoryTests
const string key = "test-key";
orgUser.Key = key;
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = orgUser.Id,
UserId = user.Id,
Key = key
};
// Act
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
// Assert
Assert.True(result);
@@ -1502,27 +1510,6 @@ public class OrganizationUserRepositoryTests
await userRepository.DeleteAsync(user);
}
[Theory, DatabaseData]
public async Task ConfirmOrganizationUserAsync_WhenUserIsInvited_ReturnsFalse(IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository)
{
// Arrange
var organization = await organizationRepository.CreateTestOrganizationAsync();
var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization);
// Act
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
// Assert
Assert.False(result);
var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
Assert.NotNull(unchangedUser);
Assert.Equal(OrganizationUserStatusType.Invited, unchangedUser.Status);
// Annul
await organizationRepository.DeleteAsync(organization);
}
[Theory, DatabaseData]
public async Task ConfirmOrganizationUserAsync_WhenUserIsAlreadyConfirmed_ReturnsFalse(IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
@@ -1533,8 +1520,17 @@ public class OrganizationUserRepositoryTests
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user);
orgUser.Status = OrganizationUserStatusType.Accepted; // To simulate a second call to ConfirmOrganizationUserAsync
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = orgUser.Id,
UserId = user.Id,
Key = "test-key"
};
// Act
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
// Assert
Assert.False(result);
@@ -1547,30 +1543,6 @@ public class OrganizationUserRepositoryTests
await userRepository.DeleteAsync(user);
}
[Theory, DatabaseData]
public async Task ConfirmOrganizationUserAsync_WhenUserIsRevoked_ReturnsFalse(IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IUserRepository userRepository)
{
// Arrange
var organization = await organizationRepository.CreateTestOrganizationAsync();
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateRevokedTestOrganizationUserAsync(organization, user);
// Act
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
// Assert
Assert.False(result);
var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
Assert.NotNull(unchangedUser);
Assert.Equal(OrganizationUserStatusType.Revoked, unchangedUser.Status);
// Annul
await organizationRepository.DeleteAsync(organization);
await userRepository.DeleteAsync(user);
}
[Theory, DatabaseData]
public async Task ConfirmOrganizationUserAsync_IsIdempotent_WhenCalledMultipleTimes(
IOrganizationUserRepository organizationUserRepository,
@@ -1582,9 +1554,16 @@ public class OrganizationUserRepositoryTests
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user);
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = orgUser.Id,
UserId = user.Id,
Key = "test-key"
};
// Act - First call should confirm
var firstResult = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
var secondResult = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
var firstResult = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
var secondResult = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
// Assert
Assert.True(firstResult);
@@ -1603,14 +1582,11 @@ public class OrganizationUserRepositoryTests
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var nonExistentUser = new OrganizationUser
var nonExistentUser = new AcceptedOrganizationUserToConfirm
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
OrganizationUserId = Guid.NewGuid(),
UserId = Guid.NewGuid(),
Email = "nonexistent@bitwarden.com",
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.Owner
Key = "test-key"
};
// Act