1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 08:32:50 -05:00

[AC-1923] Add endpoint to create client organization (#3977)

* Add new endpoint for creating client organizations in consolidated billing

* Create empty org and then assign seats for code re-use

* Fixes made from debugging client side

* few more small fixes

* Vincent's feedback
This commit is contained in:
Alex Morask
2024-04-16 13:55:00 -04:00
committed by GitHub
parent 73e049f878
commit c4ba0dc2a5
36 changed files with 1462 additions and 441 deletions

View File

@ -1,5 +1,5 @@
using Bit.Api.Billing.Controllers;
using Bit.Api.Billing.Models;
using Bit.Api.Billing.Models.Responses;
using Bit.Core;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Queries;
@ -61,7 +61,7 @@ public class ProviderBillingControllerTests
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderBillingQueries>().GetSubscriptionData(providerId).ReturnsNull();
sutProvider.GetDependency<IProviderBillingQueries>().GetSubscriptionDTO(providerId).ReturnsNull();
var result = await sutProvider.Sut.GetSubscriptionAsync(providerId);
@ -79,7 +79,7 @@ public class ProviderBillingControllerTests
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
var configuredPlans = new List<ConfiguredProviderPlan>
var configuredProviderPlanDTOList = new List<ConfiguredProviderPlanDTO>
{
new (Guid.NewGuid(), providerId, PlanType.TeamsMonthly, 50, 10, 30),
new (Guid.NewGuid(), providerId, PlanType.EnterpriseMonthly, 100, 0, 90)
@ -92,25 +92,25 @@ public class ProviderBillingControllerTests
Customer = new Customer { Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } } }
};
var providerSubscriptionData = new ProviderSubscriptionData(
configuredPlans,
var providerSubscriptionDTO = new ProviderSubscriptionDTO(
configuredProviderPlanDTOList,
subscription);
sutProvider.GetDependency<IProviderBillingQueries>().GetSubscriptionData(providerId)
.Returns(providerSubscriptionData);
sutProvider.GetDependency<IProviderBillingQueries>().GetSubscriptionDTO(providerId)
.Returns(providerSubscriptionDTO);
var result = await sutProvider.Sut.GetSubscriptionAsync(providerId);
Assert.IsType<Ok<ProviderSubscriptionDTO>>(result);
Assert.IsType<Ok<ProviderSubscriptionResponse>>(result);
var providerSubscriptionDTO = ((Ok<ProviderSubscriptionDTO>)result).Value;
var providerSubscriptionResponse = ((Ok<ProviderSubscriptionResponse>)result).Value;
Assert.Equal(providerSubscriptionDTO.Status, subscription.Status);
Assert.Equal(providerSubscriptionDTO.CurrentPeriodEndDate, subscription.CurrentPeriodEnd);
Assert.Equal(providerSubscriptionDTO.DiscountPercentage, subscription.Customer!.Discount!.Coupon!.PercentOff);
Assert.Equal(providerSubscriptionResponse.Status, subscription.Status);
Assert.Equal(providerSubscriptionResponse.CurrentPeriodEndDate, subscription.CurrentPeriodEnd);
Assert.Equal(providerSubscriptionResponse.DiscountPercentage, subscription.Customer!.Discount!.Coupon!.PercentOff);
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var providerTeamsPlan = providerSubscriptionDTO.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name);
var providerTeamsPlan = providerSubscriptionResponse.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name);
Assert.NotNull(providerTeamsPlan);
Assert.Equal(50, providerTeamsPlan.SeatMinimum);
Assert.Equal(10, providerTeamsPlan.PurchasedSeats);
@ -119,7 +119,7 @@ public class ProviderBillingControllerTests
Assert.Equal("Monthly", providerTeamsPlan.Cadence);
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
var providerEnterprisePlan = providerSubscriptionDTO.Plans.FirstOrDefault(plan => plan.PlanName == enterprisePlan.Name);
var providerEnterprisePlan = providerSubscriptionResponse.Plans.FirstOrDefault(plan => plan.PlanName == enterprisePlan.Name);
Assert.NotNull(providerEnterprisePlan);
Assert.Equal(100, providerEnterprisePlan.SeatMinimum);
Assert.Equal(0, providerEnterprisePlan.PurchasedSeats);

View File

@ -0,0 +1,339 @@
using System.Security.Claims;
using Bit.Api.Billing.Controllers;
using Bit.Api.Billing.Models.Requests;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Commands;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http.HttpResults;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Api.Test.Billing.Controllers;
[ControllerCustomize(typeof(ProviderClientsController))]
[SutProviderCustomize]
public class ProviderClientsControllerTests
{
#region CreateAsync
[Theory, BitAutoData]
public async Task CreateAsync_FFDisabled_NotFound(
Guid providerId,
CreateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(false);
var result = await sutProvider.Sut.CreateAsync(providerId, requestBody);
Assert.IsType<NotFound>(result);
}
[Theory, BitAutoData]
public async Task CreateAsync_NoPrincipalUser_Unauthorized(
Guid providerId,
CreateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
var result = await sutProvider.Sut.CreateAsync(providerId, requestBody);
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory, BitAutoData]
public async Task CreateAsync_NotProviderAdmin_Unauthorized(
Guid providerId,
CreateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(new User());
sutProvider.GetDependency<ICurrentContext>().ManageProviderOrganizations(providerId)
.Returns(false);
var result = await sutProvider.Sut.CreateAsync(providerId, requestBody);
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory, BitAutoData]
public async Task CreateAsync_NoProvider_NotFound(
Guid providerId,
CreateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(new User());
sutProvider.GetDependency<ICurrentContext>().ManageProviderOrganizations(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.ReturnsNull();
var result = await sutProvider.Sut.CreateAsync(providerId, requestBody);
Assert.IsType<NotFound>(result);
}
[Theory, BitAutoData]
public async Task CreateAsync_MissingClientOrganization_ServerError(
Guid providerId,
CreateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
var user = new User();
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
sutProvider.GetDependency<ICurrentContext>().ManageProviderOrganizations(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.Returns(new Provider());
var clientOrganizationId = Guid.NewGuid();
sutProvider.GetDependency<IProviderService>().CreateOrganizationAsync(
providerId,
Arg.Any<OrganizationSignup>(),
requestBody.OwnerEmail,
user)
.Returns(new ProviderOrganization
{
OrganizationId = clientOrganizationId
});
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(clientOrganizationId).ReturnsNull();
var result = await sutProvider.Sut.CreateAsync(providerId, requestBody);
Assert.IsType<ProblemHttpResult>(result);
}
[Theory, BitAutoData]
public async Task CreateAsync_OK(
Guid providerId,
CreateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
var user = new User();
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<ICurrentContext>().ManageProviderOrganizations(providerId)
.Returns(true);
var provider = new Provider();
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.Returns(provider);
var clientOrganizationId = Guid.NewGuid();
sutProvider.GetDependency<IProviderService>().CreateOrganizationAsync(
providerId,
Arg.Is<OrganizationSignup>(signup =>
signup.Name == requestBody.Name &&
signup.Plan == requestBody.PlanType &&
signup.AdditionalSeats == requestBody.Seats &&
signup.OwnerKey == requestBody.Key &&
signup.PublicKey == requestBody.KeyPair.PublicKey &&
signup.PrivateKey == requestBody.KeyPair.EncryptedPrivateKey &&
signup.CollectionName == requestBody.CollectionName),
requestBody.OwnerEmail,
user)
.Returns(new ProviderOrganization
{
OrganizationId = clientOrganizationId
});
var clientOrganization = new Organization { Id = clientOrganizationId };
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(clientOrganizationId)
.Returns(clientOrganization);
var result = await sutProvider.Sut.CreateAsync(providerId, requestBody);
Assert.IsType<Ok>(result);
await sutProvider.GetDependency<ICreateCustomerCommand>().Received(1).CreateCustomer(
provider,
clientOrganization);
}
#endregion
#region UpdateAsync
[Theory, BitAutoData]
public async Task UpdateAsync_FFDisabled_NotFound(
Guid providerId,
Guid providerOrganizationId,
UpdateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(false);
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<NotFound>(result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_NotProviderAdmin_Unauthorized(
Guid providerId,
Guid providerOrganizationId,
UpdateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(false);
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_NoProvider_NotFound(
Guid providerId,
Guid providerOrganizationId,
UpdateClientOrganizationRequestBody requestBody,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.ReturnsNull();
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<NotFound>(result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_NoProviderOrganization_NotFound(
Guid providerId,
Guid providerOrganizationId,
UpdateClientOrganizationRequestBody requestBody,
Provider provider,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
.ReturnsNull();
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<NotFound>(result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_NoOrganization_ServerError(
Guid providerId,
Guid providerOrganizationId,
UpdateClientOrganizationRequestBody requestBody,
Provider provider,
ProviderOrganization providerOrganization,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
.Returns(providerOrganization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(providerOrganization.OrganizationId)
.ReturnsNull();
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<ProblemHttpResult>(result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_NoContent(
Guid providerId,
Guid providerOrganizationId,
UpdateClientOrganizationRequestBody requestBody,
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<ProviderClientsController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
.Returns(providerOrganization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(providerOrganization.OrganizationId)
.Returns(organization);
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
await sutProvider.GetDependency<IAssignSeatsToClientOrganizationCommand>().Received(1)
.AssignSeatsToClientOrganization(
provider,
organization,
requestBody.AssignedSeats);
Assert.IsType<Ok>(result);
}
#endregion
}

View File

@ -1,168 +0,0 @@
using Bit.Api.Billing.Controllers;
using Bit.Api.Billing.Models;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http.HttpResults;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
using ProviderOrganization = Bit.Core.AdminConsole.Entities.Provider.ProviderOrganization;
namespace Bit.Api.Test.Billing.Controllers;
[ControllerCustomize(typeof(ProviderOrganizationController))]
[SutProviderCustomize]
public class ProviderOrganizationControllerTests
{
[Theory, BitAutoData]
public async Task UpdateAsync_FFDisabled_NotFound(
Guid providerId,
Guid providerOrganizationId,
UpdateProviderOrganizationRequestBody requestBody,
SutProvider<ProviderOrganizationController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(false);
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<NotFound>(result);
}
[Theory, BitAutoData]
public async Task GetSubscriptionAsync_NotProviderAdmin_Unauthorized(
Guid providerId,
Guid providerOrganizationId,
UpdateProviderOrganizationRequestBody requestBody,
SutProvider<ProviderOrganizationController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(false);
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory, BitAutoData]
public async Task GetSubscriptionAsync_NoProvider_NotFound(
Guid providerId,
Guid providerOrganizationId,
UpdateProviderOrganizationRequestBody requestBody,
SutProvider<ProviderOrganizationController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.ReturnsNull();
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<NotFound>(result);
}
[Theory, BitAutoData]
public async Task GetSubscriptionAsync_NoProviderOrganization_NotFound(
Guid providerId,
Guid providerOrganizationId,
UpdateProviderOrganizationRequestBody requestBody,
Provider provider,
SutProvider<ProviderOrganizationController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
.ReturnsNull();
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<NotFound>(result);
}
[Theory, BitAutoData]
public async Task GetSubscriptionAsync_NoOrganization_ServerError(
Guid providerId,
Guid providerOrganizationId,
UpdateProviderOrganizationRequestBody requestBody,
Provider provider,
ProviderOrganization providerOrganization,
SutProvider<ProviderOrganizationController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
.Returns(providerOrganization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(providerOrganization.OrganizationId)
.ReturnsNull();
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
Assert.IsType<ProblemHttpResult>(result);
}
[Theory, BitAutoData]
public async Task GetSubscriptionAsync_NoContent(
Guid providerId,
Guid providerOrganizationId,
UpdateProviderOrganizationRequestBody requestBody,
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<ProviderOrganizationController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(providerId)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId)
.Returns(provider);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
.Returns(providerOrganization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(providerOrganization.OrganizationId)
.Returns(organization);
var result = await sutProvider.Sut.UpdateAsync(providerId, providerOrganizationId, requestBody);
await sutProvider.GetDependency<IAssignSeatsToClientOrganizationCommand>().Received(1)
.AssignSeatsToClientOrganization(
provider,
organization,
requestBody.AssignedSeats);
Assert.IsType<Ok>(result);
}
}

View File

@ -0,0 +1,63 @@
using Bit.Api.Utilities;
using Bit.Core.Enums;
using Xunit;
namespace Bit.Api.Test.Utilities;
public class EnumMatchesAttributeTests
{
[Fact]
public void IsValid_NullInput_False()
{
var enumMatchesAttribute =
new EnumMatchesAttribute<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly);
var result = enumMatchesAttribute.IsValid(null);
Assert.False(result);
}
[Fact]
public void IsValid_NullAccepted_False()
{
var enumMatchesAttribute =
new EnumMatchesAttribute<PlanType>();
var result = enumMatchesAttribute.IsValid(PlanType.TeamsMonthly);
Assert.False(result);
}
[Fact]
public void IsValid_EmptyAccepted_False()
{
var enumMatchesAttribute =
new EnumMatchesAttribute<PlanType>([]);
var result = enumMatchesAttribute.IsValid(PlanType.TeamsMonthly);
Assert.False(result);
}
[Fact]
public void IsValid_ParseFails_False()
{
var enumMatchesAttribute =
new EnumMatchesAttribute<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly);
var result = enumMatchesAttribute.IsValid(GatewayType.Stripe);
Assert.False(result);
}
[Fact]
public void IsValid_Matches_True()
{
var enumMatchesAttribute =
new EnumMatchesAttribute<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly);
var result = enumMatchesAttribute.IsValid(PlanType.TeamsMonthly);
Assert.True(result);
}
}

View File

@ -447,6 +447,47 @@ public class OrganizationServiceTests
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
}
[Theory, BitAutoData]
public async Task SignupClientAsync_Succeeds(
OrganizationSignup signup,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
signup.Plan = PlanType.TeamsMonthly;
var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup);
var plan = StaticStore.GetPlan(signup.Plan);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(Arg.Is<Organization>(org =>
org.Id == organization.Id &&
org.Name == signup.Name &&
org.Plan == plan.Name &&
org.PlanType == plan.Type &&
org.UsePolicies == plan.HasPolicies &&
org.PublicKey == signup.PublicKey &&
org.PrivateKey == signup.PrivateKey &&
org.UseSecretsManager == false));
await sutProvider.GetDependency<IOrganizationApiKeyRepository>().Received(1)
.CreateAsync(Arg.Is<OrganizationApiKey>(orgApiKey =>
orgApiKey.OrganizationId == organization.Id));
await sutProvider.GetDependency<IApplicationCacheService>().Received(1)
.UpsertOrganizationAbilityAsync(organization);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<ICollectionRepository>().Received(1)
.CreateAsync(Arg.Is<Collection>(c => c.Name == signup.CollectionName && c.OrganizationId == organization.Id), null, null);
await sutProvider.GetDependency<IReferenceEventService>().Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(
re =>
re.Type == ReferenceEventType.Signup &&
re.PlanType == plan.Type));
}
[Theory]
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize(FlexibleCollections = false), BitAutoData]

View File

@ -0,0 +1,129 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Commands.Implementations;
using Bit.Core.Billing.Queries;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Billing.Commands;
[SutProviderCustomize]
public class CreateCustomerCommandTests
{
private const string _customerId = "customer_id";
[Theory, BitAutoData]
public async Task CreateCustomer_ForClientOrg_ProviderNull_ThrowsArgumentNullException(
Organization organization,
SutProvider<CreateCustomerCommand> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.CreateCustomer(null, organization));
[Theory, BitAutoData]
public async Task CreateCustomer_ForClientOrg_OrganizationNull_ThrowsArgumentNullException(
Provider provider,
SutProvider<CreateCustomerCommand> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.CreateCustomer(provider, null));
[Theory, BitAutoData]
public async Task CreateCustomer_ForClientOrg_HasGatewayCustomerId_NoOp(
Provider provider,
Organization organization,
SutProvider<CreateCustomerCommand> sutProvider)
{
organization.GatewayCustomerId = _customerId;
await sutProvider.Sut.CreateCustomer(provider, organization);
await sutProvider.GetDependency<ISubscriberQueries>().DidNotReceiveWithAnyArgs()
.GetCustomerOrThrow(Arg.Any<ISubscriber>(), Arg.Any<CustomerGetOptions>());
}
[Theory, BitAutoData]
public async Task CreateCustomer_ForClientOrg_Succeeds(
Provider provider,
Organization organization,
SutProvider<CreateCustomerCommand> sutProvider)
{
organization.GatewayCustomerId = null;
organization.Name = "Name";
organization.BusinessName = "BusinessName";
var providerCustomer = new Customer
{
Address = new Address
{
Country = "USA",
PostalCode = "12345",
Line1 = "123 Main St.",
Line2 = "Unit 4",
City = "Fake Town",
State = "Fake State"
},
TaxIds = new StripeList<TaxId>
{
Data =
[
new TaxId { Type = "TYPE", Value = "VALUE" }
]
}
};
sutProvider.GetDependency<ISubscriberQueries>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
options => options.Expand.FirstOrDefault() == "tax_ids"))
.Returns(providerCustomer);
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
.Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings()) { CloudRegion = "US" });
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
options =>
options.Address.Country == providerCustomer.Address.Country &&
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
options.Address.Line1 == providerCustomer.Address.Line1 &&
options.Address.Line2 == providerCustomer.Address.Line2 &&
options.Address.City == providerCustomer.Address.City &&
options.Address.State == providerCustomer.Address.State &&
options.Name == organization.DisplayName() &&
options.Description == $"{provider.Name} Client Organization" &&
options.Email == provider.BillingEmail &&
options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" &&
options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" &&
options.Metadata["region"] == "US" &&
options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&
options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value))
.Returns(new Customer
{
Id = "customer_id"
});
await sutProvider.Sut.CreateCustomer(provider, organization);
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
options =>
options.Address.Country == providerCustomer.Address.Country &&
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
options.Address.Line1 == providerCustomer.Address.Line1 &&
options.Address.Line2 == providerCustomer.Address.Line2 &&
options.Address.City == providerCustomer.Address.City &&
options.Address.State == providerCustomer.Address.State &&
options.Name == organization.DisplayName() &&
options.Description == $"{provider.Name} Client Organization" &&
options.Email == provider.BillingEmail &&
options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" &&
options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" &&
options.Metadata["region"] == "US" &&
options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&
options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value));
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.GatewayCustomerId == "customer_id"));
}
}

View File

@ -29,7 +29,7 @@ public class ProviderBillingQueriesTests
providerRepository.GetByIdAsync(providerId).ReturnsNull();
var subscriptionData = await sutProvider.Sut.GetSubscriptionData(providerId);
var subscriptionData = await sutProvider.Sut.GetSubscriptionDTO(providerId);
Assert.Null(subscriptionData);
@ -50,7 +50,7 @@ public class ProviderBillingQueriesTests
subscriberQueries.GetSubscription(provider).ReturnsNull();
var subscriptionData = await sutProvider.Sut.GetSubscriptionData(providerId);
var subscriptionData = await sutProvider.Sut.GetSubscriptionDTO(providerId);
Assert.Null(subscriptionData);
@ -109,7 +109,7 @@ public class ProviderBillingQueriesTests
providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans);
var subscriptionData = await sutProvider.Sut.GetSubscriptionData(providerId);
var subscriptionData = await sutProvider.Sut.GetSubscriptionDTO(providerId);
Assert.NotNull(subscriptionData);
@ -140,7 +140,7 @@ public class ProviderBillingQueriesTests
return;
void Compare(ProviderPlan providerPlan, ConfiguredProviderPlan configuredProviderPlan)
void Compare(ProviderPlan providerPlan, ConfiguredProviderPlanDTO configuredProviderPlan)
{
Assert.NotNull(configuredProviderPlan);
Assert.Equal(providerPlan.Id, configuredProviderPlan.Id);

View File

@ -1,7 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Queries.Implementations;
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -17,6 +15,56 @@ namespace Bit.Core.Test.Billing.Queries;
[SutProviderCustomize]
public class SubscriberQueriesTests
{
#region GetCustomer
[Theory, BitAutoData]
public async Task GetCustomer_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberQueries> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetCustomer(null));
[Theory, BitAutoData]
public async Task GetCustomer_NoGatewayCustomerId_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
organization.GatewayCustomerId = null;
var customer = await sutProvider.Sut.GetCustomer(organization);
Assert.Null(customer);
}
[Theory, BitAutoData]
public async Task GetCustomer_NoCustomer_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ReturnsNull();
var customer = await sutProvider.Sut.GetCustomer(organization);
Assert.Null(customer);
}
[Theory, BitAutoData]
public async Task GetCustomer_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var customer = new Customer();
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.Returns(customer);
var gotCustomer = await sutProvider.Sut.GetCustomer(organization);
Assert.Equivalent(customer, gotCustomer);
}
#endregion
#region GetSubscription
[Theory, BitAutoData]
public async Task GetSubscription_NullSubscriber_ThrowsArgumentNullException(
@ -25,123 +73,91 @@ public class SubscriberQueriesTests
async () => await sutProvider.Sut.GetSubscription(null));
[Theory, BitAutoData]
public async Task GetSubscription_Organization_NoGatewaySubscriptionId_ReturnsNull(
public async Task GetSubscription_NoGatewaySubscriptionId_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
organization.GatewaySubscriptionId = null;
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(gotSubscription);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Organization_NoSubscription_ReturnsNull(
public async Task GetSubscription_NoSubscription_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(gotSubscription);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Organization_Succeeds(
public async Task GetSubscription_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
#region GetCustomerOrThrow
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberQueries> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetCustomerOrThrow(null));
[Theory, BitAutoData]
public async Task GetSubscription_User_NoGatewaySubscriptionId_ReturnsNull(
User user,
public async Task GetCustomerOrThrow_NoGatewaySubscriptionId_ThrowsGatewayException(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
user.GatewaySubscriptionId = null;
organization.GatewayCustomerId = null;
var gotSubscription = await sutProvider.Sut.GetSubscription(user);
Assert.Null(gotSubscription);
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscription_User_NoSubscription_ReturnsNull(
User user,
public async Task GetSubscriptionOrThrow_NoCustomer_ThrowsGatewayException(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ReturnsNull();
var gotSubscription = await sutProvider.Sut.GetSubscription(user);
Assert.Null(gotSubscription);
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscription_User_Succeeds(
User user,
public async Task GetCustomerOrThrow_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
var customer = new Customer();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.Returns(subscription);
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.Returns(customer);
var gotSubscription = await sutProvider.Sut.GetSubscription(user);
var gotCustomer = await sutProvider.Sut.GetCustomerOrThrow(organization);
Assert.Equivalent(subscription, gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Provider_NoGatewaySubscriptionId_ReturnsNull(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
provider.GatewaySubscriptionId = null;
var gotSubscription = await sutProvider.Sut.GetSubscription(provider);
Assert.Null(gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Provider_NoSubscription_ReturnsNull(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(provider.GatewaySubscriptionId)
.ReturnsNull();
var gotSubscription = await sutProvider.Sut.GetSubscription(provider);
Assert.Null(gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Provider_Succeeds(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(provider.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(provider);
Assert.Equivalent(subscription, gotSubscription);
Assert.Equivalent(customer, gotCustomer);
}
#endregion
@ -153,7 +169,7 @@ public class SubscriberQueriesTests
async () => await sutProvider.Sut.GetSubscriptionOrThrow(null));
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Organization_NoGatewaySubscriptionId_ThrowsGatewayException(
public async Task GetSubscriptionOrThrow_NoGatewaySubscriptionId_ThrowsGatewayException(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
@ -163,101 +179,31 @@ public class SubscriberQueriesTests
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Organization_NoSubscription_ThrowsGatewayException(
public async Task GetSubscriptionOrThrow_NoSubscription_ThrowsGatewayException(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Organization_Succeeds(
public async Task GetSubscriptionOrThrow_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(organization);
Assert.Equivalent(subscription, gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_User_NoGatewaySubscriptionId_ThrowsGatewayException(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
user.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(user));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_User_NoSubscription_ThrowsGatewayException(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(user));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_User_Succeeds(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(user);
Assert.Equivalent(subscription, gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Provider_NoGatewaySubscriptionId_ThrowsGatewayException(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
provider.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(provider));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Provider_NoSubscription_ThrowsGatewayException(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(provider.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(provider));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Provider_Succeeds(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(provider.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(provider);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
}