1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

Merge branch 'main' into ac/pm-15161/create-resellerclientorganizationsignup-command

This commit is contained in:
Rui Tomé
2025-05-06 10:30:13 +01:00
committed by GitHub
89 changed files with 2972 additions and 266 deletions

View File

@ -1,5 +1,6 @@
using Bit.Commercial.Core.AdminConsole.Services;
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
@ -8,6 +9,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
@ -39,7 +41,7 @@ public class ProviderServiceTests
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default));
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null));
Assert.Contains("Invalid owner.", exception.Message);
}
@ -51,12 +53,85 @@ public class ProviderServiceTests
userService.GetUserByIdAsync(user.Id).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default));
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null));
Assert.Contains("Invalid token.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo,
public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException(
User user,
Provider provider,
string key,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
taxInfo.BillingAddressCountry = null;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException(
User user,
Provider provider,
string key,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
Assert.Equal("A payment method is required to set up your provider.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
@ -76,7 +151,7 @@ public class ProviderServiceTests
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
var customer = new Customer { Id = "customer_id" };
providerBillingService.SetupCustomer(provider, taxInfo).Returns(customer);
providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer);
var subscription = new Subscription { Id = "subscription_id" };
providerBillingService.SetupSubscription(provider).Returns(subscription);
@ -85,7 +160,7 @@ public class ProviderServiceTests
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo);
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource);
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
p =>

View File

@ -2,14 +2,17 @@
using System.Net;
using Bit.Commercial.Core.Billing;
using Bit.Commercial.Core.Billing.Models;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
@ -24,11 +27,17 @@ using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Braintree;
using CsvHelper;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Stripe;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
using Address = Stripe.Address;
using Customer = Stripe.Customer;
using PaymentMethod = Stripe.PaymentMethod;
using Subscription = Stripe.Subscription;
namespace Bit.Commercial.Core.Test.Billing;
@ -833,7 +842,7 @@ public class ProviderBillingServiceTests
}
[Theory, BitAutoData]
public async Task SetupCustomer_Success(
public async Task SetupCustomer_NoPaymentMethod_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
@ -877,6 +886,301 @@ public class ProviderBillingServiceTests
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
await ThrowsBillingExceptionAsync(() =>
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithBankAccount_Error_Reverts(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
new SetupIntent { Id = "setup_intent_id" }
]);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Email == provider.BillingEmail &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.Metadata["region"] == "" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
.Throws<StripeException>();
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns("setup_intent_id");
await Assert.ThrowsAsync<StripeException>(() =>
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
options.CancellationReason == "abandoned"));
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Remove(provider.Id);
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithPayPal_Error_Reverts(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
.Returns("braintree_customer_id");
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Email == provider.BillingEmail &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.Metadata["region"] == "" &&
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
.Throws<StripeException>();
await Assert.ThrowsAsync<StripeException>(() =>
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
await sutProvider.GetDependency<IBraintreeGateway>().Customer.Received(1).DeleteAsync("braintree_customer_id");
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithBankAccount_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var expected = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
new SetupIntent { Id = "setup_intent_id" }
]);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Email == provider.BillingEmail &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.Metadata["region"] == "" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
.Returns(expected);
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
Assert.Equivalent(expected, actual);
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithPayPal_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var expected = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
.Returns("braintree_customer_id");
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Email == provider.BillingEmail &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.Metadata["region"] == "" &&
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
.Returns(expected);
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithCard_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var expected = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Email == provider.BillingEmail &&
o.PaymentMethod == tokenizedPaymentSource.Token &&
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.Metadata["region"] == "" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
.Returns(expected);
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
SutProvider<ProviderBillingService> sutProvider,
@ -1044,7 +1348,7 @@ public class ProviderBillingServiceTests
}
[Theory, BitAutoData]
public async Task SetupSubscription_Succeeds(
public async Task SetupSubscription_SendInvoice_Succeeds(
SutProvider<ProviderBillingService> sutProvider,
Provider provider)
{
@ -1127,6 +1431,303 @@ public class ProviderBillingServiceTests
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupSubscription_ChargeAutomatically_HasCard_Succeeds(
SutProvider<ProviderBillingService> sutProvider,
Provider provider)
{
provider.Type = ProviderType.Msp;
provider.GatewaySubscriptionId = null;
var customer = new Customer
{
Id = "customer_id",
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethodId = "pm_123"
},
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(
provider,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
var providerPlans = new List<ProviderPlan>
{
new()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
},
new()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
}
};
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IAutomaticTaxStrategy>()
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id")
, Arg.Is<Customer>(p => p == customer)))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub =>
sub.AutomaticTax.Enabled == true &&
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
sub.Customer == "customer_id" &&
sub.DaysUntilDue == null &&
sub.Items.Count == 2 &&
sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&
sub.Items.ElementAt(0).Quantity == 100 &&
sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&
sub.Items.ElementAt(1).Quantity == 100 &&
sub.Metadata["providerId"] == provider.Id.ToString() &&
sub.OffSession == true &&
sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
sub.TrialPeriodDays == 14)).Returns(expected);
var actual = await sutProvider.Sut.SetupSubscription(provider);
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupSubscription_ChargeAutomatically_HasBankAccount_Succeeds(
SutProvider<ProviderBillingService> sutProvider,
Provider provider)
{
provider.Type = ProviderType.Msp;
provider.GatewaySubscriptionId = null;
var customer = new Customer
{
Id = "customer_id",
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>(),
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(
provider,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
var providerPlans = new List<ProviderPlan>
{
new()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
},
new()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
}
};
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IAutomaticTaxStrategy>()
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id")
, Arg.Is<Customer>(p => p == customer)))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
const string setupIntentId = "seti_123";
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntentId);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
{
Id = setupIntentId,
Status = "requires_action",
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
},
PaymentMethod = new PaymentMethod
{
UsBankAccount = new PaymentMethodUsBankAccount()
}
});
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub =>
sub.AutomaticTax.Enabled == true &&
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
sub.Customer == "customer_id" &&
sub.DaysUntilDue == null &&
sub.Items.Count == 2 &&
sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&
sub.Items.ElementAt(0).Quantity == 100 &&
sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&
sub.Items.ElementAt(1).Quantity == 100 &&
sub.Metadata["providerId"] == provider.Id.ToString() &&
sub.OffSession == true &&
sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
sub.TrialPeriodDays == 14)).Returns(expected);
var actual = await sutProvider.Sut.SetupSubscription(provider);
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupSubscription_ChargeAutomatically_HasPayPal_Succeeds(
SutProvider<ProviderBillingService> sutProvider,
Provider provider)
{
provider.Type = ProviderType.Msp;
provider.GatewaySubscriptionId = null;
var customer = new Customer
{
Id = "customer_id",
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>
{
["btCustomerId"] = "braintree_customer_id"
},
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(
provider,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
var providerPlans = new List<ProviderPlan>
{
new()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
},
new()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
}
};
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IAutomaticTaxStrategy>()
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id")
, Arg.Is<Customer>(p => p == customer)))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub =>
sub.AutomaticTax.Enabled == true &&
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
sub.Customer == "customer_id" &&
sub.DaysUntilDue == null &&
sub.Items.Count == 2 &&
sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&
sub.Items.ElementAt(0).Quantity == 100 &&
sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&
sub.Items.ElementAt(1).Quantity == 100 &&
sub.Metadata["providerId"] == provider.Id.ToString() &&
sub.OffSession == true &&
sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
sub.TrialPeriodDays == 14)).Returns(expected);
var actual = await sutProvider.Sut.SetupSubscription(provider);
Assert.Equivalent(expected, actual);
}
#endregion
#region UpdateSeatMinimums

View File

@ -1,9 +1,16 @@
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Licenses;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -32,7 +39,7 @@ public class MaxProjectsQueryTests
[BitAutoData(PlanType.FamiliesAnnually2019)]
[BitAutoData(PlanType.Custom)]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
public async Task GetByOrgIdAsync_Cloud_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
@ -40,6 +47,34 @@ public class MaxProjectsQueryTests
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
var plan = StaticStore.GetPlan(planType);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
await sutProvider.GetDependency<IProjectRepository>()
.DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory]
[BitAutoData]
public async Task GetByOrgIdAsync_SelfHosted_NoMaxProjectsClaim_ThrowsBadRequest(
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var license = new OrganizationLicense();
var claimsPrincipal = new ClaimsPrincipal();
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
@ -62,12 +97,58 @@ public class MaxProjectsQueryTests
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
public async Task GetByOrgIdAsync_Cloud_SmNoneFreePlans_ReturnsNull(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
var plan = StaticStore.GetPlan(planType);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
Assert.Null(limit);
Assert.Null(overLimit);
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task GetByOrgIdAsync_SelfHosted_SmNoneFreePlans_ReturnsNull(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var license = new OrganizationLicense();
var plan = StaticStore.GetPlan(planType);
var claims = new List<Claim>
{
new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()),
new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString())
};
var identity = new ClaimsIdentity(claims, "TestAuthenticationType");
var claimsPrincipal = new ClaimsPrincipal(identity);
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
Assert.Null(limit);
@ -102,7 +183,7 @@ public class MaxProjectsQueryTests
[BitAutoData(PlanType.Free, 3, 4, true)]
[BitAutoData(PlanType.Free, 4, 4, true)]
[BitAutoData(PlanType.Free, 40, 4, true)]
public async Task GetByOrgIdAsync_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
public async Task GetByOrgIdAsync_Cloud_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
@ -110,6 +191,67 @@ public class MaxProjectsQueryTests
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
.Returns(projects);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
var plan = StaticStore.GetPlan(planType);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
Assert.NotNull(max);
Assert.NotNull(overMax);
Assert.Equal(3, max.Value);
Assert.Equal(expectedOverMax, overMax);
await sutProvider.GetDependency<IProjectRepository>().Received(1)
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory]
[BitAutoData(PlanType.Free, 0, 1, false)]
[BitAutoData(PlanType.Free, 1, 1, false)]
[BitAutoData(PlanType.Free, 2, 1, false)]
[BitAutoData(PlanType.Free, 3, 1, true)]
[BitAutoData(PlanType.Free, 4, 1, true)]
[BitAutoData(PlanType.Free, 40, 1, true)]
[BitAutoData(PlanType.Free, 0, 2, false)]
[BitAutoData(PlanType.Free, 1, 2, false)]
[BitAutoData(PlanType.Free, 2, 2, true)]
[BitAutoData(PlanType.Free, 3, 2, true)]
[BitAutoData(PlanType.Free, 4, 2, true)]
[BitAutoData(PlanType.Free, 40, 2, true)]
[BitAutoData(PlanType.Free, 0, 3, false)]
[BitAutoData(PlanType.Free, 1, 3, true)]
[BitAutoData(PlanType.Free, 2, 3, true)]
[BitAutoData(PlanType.Free, 3, 3, true)]
[BitAutoData(PlanType.Free, 4, 3, true)]
[BitAutoData(PlanType.Free, 40, 3, true)]
[BitAutoData(PlanType.Free, 0, 4, true)]
[BitAutoData(PlanType.Free, 1, 4, true)]
[BitAutoData(PlanType.Free, 2, 4, true)]
[BitAutoData(PlanType.Free, 3, 4, true)]
[BitAutoData(PlanType.Free, 4, 4, true)]
[BitAutoData(PlanType.Free, 40, 4, true)]
public async Task GetByOrgIdAsync_SelfHosted_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
.Returns(projects);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var license = new OrganizationLicense();
var plan = StaticStore.GetPlan(planType);
var claims = new List<Claim>
{
new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()),
new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString())
};
var identity = new ClaimsIdentity(claims, "TestAuthenticationType");
var claimsPrincipal = new ClaimsPrincipal(identity);
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
Assert.NotNull(max);