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

[AC-2576] Replace Billing commands and queries with services (#4070)

* Replace SubscriberQueries with SubscriberService

* Replace OrganizationBillingQueries with OrganizationBillingService

* Replace ProviderBillingQueries with ProviderBillingService, move to Commercial

* Replace AssignSeatsToClientOrganizationCommand with ProviderBillingService, move to commercial

* Replace ScaleSeatsCommand with ProviderBillingService and move to Commercial

* Replace CancelSubscriptionCommand with SubscriberService

* Replace CreateCustomerCommand with ProviderBillingService and move to Commercial

* Replace StartSubscriptionCommand with ProviderBillingService and moved to Commercial

* Replaced RemovePaymentMethodCommand with SubscriberService

* Formatting

* Used dotnet format this time

* Changing ProviderBillingService to scoped

* Found circular dependency'

* One more time with feeling

* Formatting

* Fix error in remove org from provider

* Missed test fix in conflit

* [AC-1937] Server: Implement endpoint to retrieve provider payment information (#4107)

* Move the gettax and paymentmethod from stripepayment class

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Add the method to retrieve the tax and payment details

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Add unit tests for the paymentInformation method

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Add the endpoint to retrieve paymentinformation

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Add unit tests to the SubscriberService

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Remove the getTaxInfoAsync update reference

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

---------

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

---------

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
This commit is contained in:
Alex Morask
2024-05-23 10:17:00 -04:00
committed by GitHub
parent a9ab894893
commit 06910175e2
56 changed files with 3452 additions and 3426 deletions

View File

@ -1,339 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing;
using Bit.Core.Billing.Commands.Implementations;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Queries;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
namespace Bit.Core.Test.Billing.Commands;
[SutProviderCustomize]
public class AssignSeatsToClientOrganizationCommandTests
{
[Theory, BitAutoData]
public Task AssignSeatsToClientOrganization_NullProvider_ArgumentNullException(
Organization organization,
int seats,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
=> Assert.ThrowsAsync<ArgumentNullException>(() =>
sutProvider.Sut.AssignSeatsToClientOrganization(null, organization, seats));
[Theory, BitAutoData]
public Task AssignSeatsToClientOrganization_NullOrganization_ArgumentNullException(
Provider provider,
int seats,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
=> Assert.ThrowsAsync<ArgumentNullException>(() =>
sutProvider.Sut.AssignSeatsToClientOrganization(provider, null, seats));
[Theory, BitAutoData]
public Task AssignSeatsToClientOrganization_NegativeSeats_BillingException(
Provider provider,
Organization organization,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
=> Assert.ThrowsAsync<BillingException>(() =>
sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, -5));
[Theory, BitAutoData]
public async Task AssignSeatsToClientOrganization_CurrentSeatsMatchesNewSeats_NoOp(
Provider provider,
Organization organization,
int seats,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
{
organization.PlanType = PlanType.TeamsMonthly;
organization.Seats = seats;
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
await sutProvider.GetDependency<IProviderPlanRepository>().DidNotReceive().GetByProviderId(provider.Id);
}
[Theory, BitAutoData]
public async Task AssignSeatsToClientOrganization_OrganizationPlanTypeDoesNotSupportConsolidatedBilling_ContactSupport(
Provider provider,
Organization organization,
int seats,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
{
organization.PlanType = PlanType.FamiliesAnnually;
await ThrowsContactSupportAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats));
}
[Theory, BitAutoData]
public async Task AssignSeatsToClientOrganization_ProviderPlanIsNotConfigured_ContactSupport(
Provider provider,
Organization organization,
int seats,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
{
organization.PlanType = PlanType.TeamsMonthly;
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(new List<ProviderPlan>
{
new ()
{
Id = Guid.NewGuid(),
PlanType = PlanType.TeamsMonthly,
ProviderId = provider.Id
}
});
await ThrowsContactSupportAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats));
}
[Theory, BitAutoData]
public async Task AssignSeatsToClientOrganization_BelowToBelow_Succeeds(
Provider provider,
Organization organization,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
{
organization.Seats = 10;
organization.PlanType = PlanType.TeamsMonthly;
// Scale up 10 seats
const int seats = 20;
var providerPlans = new List<ProviderPlan>
{
new()
{
Id = Guid.NewGuid(),
PlanType = PlanType.TeamsMonthly,
ProviderId = provider.Id,
PurchasedSeats = 0,
// 100 minimum
SeatMinimum = 100,
AllocatedSeats = 50
},
new()
{
Id = Guid.NewGuid(),
PlanType = PlanType.EnterpriseMonthly,
ProviderId = provider.Id,
PurchasedSeats = 0,
SeatMinimum = 500,
AllocatedSeats = 0
}
};
var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
// 50 seats currently assigned with a seat minimum of 100
sutProvider.GetDependency<IProviderBillingQueries>().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(50);
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
// 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum
await sutProvider.GetDependency<IPaymentService>().DidNotReceiveWithAnyArgs().AdjustSeats(
Arg.Any<Provider>(),
Arg.Any<Plan>(),
Arg.Any<int>(),
Arg.Any<int>());
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.Id == organization.Id && org.Seats == seats));
await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
pPlan => pPlan.AllocatedSeats == 60));
}
[Theory, BitAutoData]
public async Task AssignSeatsToClientOrganization_BelowToAbove_Succeeds(
Provider provider,
Organization organization,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
{
organization.Seats = 10;
organization.PlanType = PlanType.TeamsMonthly;
// Scale up 10 seats
const int seats = 20;
var providerPlans = new List<ProviderPlan>
{
new()
{
Id = Guid.NewGuid(),
PlanType = PlanType.TeamsMonthly,
ProviderId = provider.Id,
PurchasedSeats = 0,
// 100 minimum
SeatMinimum = 100,
AllocatedSeats = 95
},
new()
{
Id = Guid.NewGuid(),
PlanType = PlanType.EnterpriseMonthly,
ProviderId = provider.Id,
PurchasedSeats = 0,
SeatMinimum = 500,
AllocatedSeats = 0
}
};
var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
// 95 seats currently assigned with a seat minimum of 100
sutProvider.GetDependency<IProviderBillingQueries>().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(95);
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
// 95 current + 10 seat scale = 105 seats, 5 above the minimum
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSeats(
provider,
StaticStore.GetPlan(providerPlan.PlanType),
providerPlan.SeatMinimum!.Value,
105);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.Id == organization.Id && org.Seats == seats));
// 105 total seats - 100 minimum = 5 purchased seats
await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 5 && pPlan.AllocatedSeats == 105));
}
[Theory, BitAutoData]
public async Task AssignSeatsToClientOrganization_AboveToAbove_Succeeds(
Provider provider,
Organization organization,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
{
organization.Seats = 10;
organization.PlanType = PlanType.TeamsMonthly;
// Scale up 10 seats
const int seats = 20;
var providerPlans = new List<ProviderPlan>
{
new()
{
Id = Guid.NewGuid(),
PlanType = PlanType.TeamsMonthly,
ProviderId = provider.Id,
// 10 additional purchased seats
PurchasedSeats = 10,
// 100 seat minimum
SeatMinimum = 100,
AllocatedSeats = 110
},
new()
{
Id = Guid.NewGuid(),
PlanType = PlanType.EnterpriseMonthly,
ProviderId = provider.Id,
PurchasedSeats = 0,
SeatMinimum = 500,
AllocatedSeats = 0
}
};
var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
// 110 seats currently assigned with a seat minimum of 100
sutProvider.GetDependency<IProviderBillingQueries>().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(110);
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
// 110 current + 10 seat scale up = 120 seats
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSeats(
provider,
StaticStore.GetPlan(providerPlan.PlanType),
110,
120);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.Id == organization.Id && org.Seats == seats));
// 120 total seats - 100 seat minimum = 20 purchased seats
await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 20 && pPlan.AllocatedSeats == 120));
}
[Theory, BitAutoData]
public async Task AssignSeatsToClientOrganization_AboveToBelow_Succeeds(
Provider provider,
Organization organization,
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
{
organization.Seats = 50;
organization.PlanType = PlanType.TeamsMonthly;
// Scale down 30 seats
const int seats = 20;
var providerPlans = new List<ProviderPlan>
{
new()
{
Id = Guid.NewGuid(),
PlanType = PlanType.TeamsMonthly,
ProviderId = provider.Id,
// 10 additional purchased seats
PurchasedSeats = 10,
// 100 seat minimum
SeatMinimum = 100,
AllocatedSeats = 110
},
new()
{
Id = Guid.NewGuid(),
PlanType = PlanType.EnterpriseMonthly,
ProviderId = provider.Id,
PurchasedSeats = 0,
SeatMinimum = 500,
AllocatedSeats = 0
}
};
var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
// 110 seats currently assigned with a seat minimum of 100
sutProvider.GetDependency<IProviderBillingQueries>().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(110);
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
// 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum.
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSeats(
provider,
StaticStore.GetPlan(providerPlan.PlanType),
110,
providerPlan.SeatMinimum!.Value);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.Id == organization.Id && org.Seats == seats));
// Being below the seat minimum means no purchased seats.
await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 0 && pPlan.AllocatedSeats == 80));
}
}

View File

@ -1,163 +0,0 @@
using System.Linq.Expressions;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Commands.Implementations;
using Bit.Core.Billing.Models;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
namespace Bit.Core.Test.Billing.Commands;
[SutProviderCustomize]
public class CancelSubscriptionCommandTests
{
private const string _subscriptionId = "subscription_id";
private const string _cancellingUserIdKey = "cancellingUserId";
[Theory, BitAutoData]
public async Task CancelSubscription_SubscriptionInactive_ThrowsGatewayException(
SutProvider<CancelSubscriptionCommand> sutProvider)
{
var subscription = new Subscription
{
Status = "canceled"
};
await ThrowsContactSupportAsync(() =>
sutProvider.Sut.CancelSubscription(subscription, new OffboardingSurveyResponse(), false));
await DidNotUpdateSubscription(sutProvider);
await DidNotCancelSubscription(sutProvider);
}
[Theory, BitAutoData]
public async Task CancelSubscription_CancelImmediately_BelongsToOrganization_UpdatesSubscription_CancelSubscriptionImmediately(
SutProvider<CancelSubscriptionCommand> sutProvider)
{
var userId = Guid.NewGuid();
var subscription = new Subscription
{
Id = _subscriptionId,
Status = "active",
Metadata = new Dictionary<string, string>
{
{ "organizationId", "organization_id" }
}
};
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(subscription, offboardingSurveyResponse, true);
await UpdatedSubscriptionWith(sutProvider, options => options.Metadata[_cancellingUserIdKey] == userId.ToString());
await CancelledSubscriptionWith(sutProvider, options =>
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason);
}
[Theory, BitAutoData]
public async Task CancelSubscription_CancelImmediately_BelongsToUser_CancelSubscriptionImmediately(
SutProvider<CancelSubscriptionCommand> sutProvider)
{
var userId = Guid.NewGuid();
var subscription = new Subscription
{
Id = _subscriptionId,
Status = "active",
Metadata = new Dictionary<string, string>
{
{ "userId", "user_id" }
}
};
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(subscription, offboardingSurveyResponse, true);
await DidNotUpdateSubscription(sutProvider);
await CancelledSubscriptionWith(sutProvider, options =>
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason);
}
[Theory, BitAutoData]
public async Task CancelSubscription_DoNotCancelImmediately_UpdateSubscriptionToCancelAtEndOfPeriod(
Organization organization,
SutProvider<CancelSubscriptionCommand> sutProvider)
{
var userId = Guid.NewGuid();
organization.ExpirationDate = DateTime.UtcNow.AddDays(5);
var subscription = new Subscription
{
Id = _subscriptionId,
Status = "active"
};
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(subscription, offboardingSurveyResponse, false);
await UpdatedSubscriptionWith(sutProvider, options =>
options.CancelAtPeriodEnd == true &&
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason &&
options.Metadata[_cancellingUserIdKey] == userId.ToString());
await DidNotCancelSubscription(sutProvider);
}
private static Task<Subscription> DidNotCancelSubscription(SutProvider<CancelSubscriptionCommand> sutProvider)
=> sutProvider
.GetDependency<IStripeAdapter>()
.DidNotReceiveWithAnyArgs()
.SubscriptionCancelAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
private static Task<Subscription> DidNotUpdateSubscription(SutProvider<CancelSubscriptionCommand> sutProvider)
=> sutProvider
.GetDependency<IStripeAdapter>()
.DidNotReceiveWithAnyArgs()
.SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
private static Task<Subscription> CancelledSubscriptionWith(
SutProvider<CancelSubscriptionCommand> sutProvider,
Expression<Predicate<SubscriptionCancelOptions>> predicate)
=> sutProvider
.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCancelAsync(_subscriptionId, Arg.Is(predicate));
private static Task<Subscription> UpdatedSubscriptionWith(
SutProvider<CancelSubscriptionCommand> sutProvider,
Expression<Predicate<SubscriptionUpdateOptions>> predicate)
=> sutProvider
.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionUpdateAsync(_subscriptionId, Arg.Is(predicate));
}

View File

@ -1,129 +0,0 @@
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

@ -1,358 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Commands.Implementations;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
using BT = Braintree;
using S = Stripe;
namespace Bit.Core.Test.Billing.Commands;
[SutProviderCustomize]
public class RemovePaymentMethodCommandTests
{
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NullOrganization_ArgumentNullException(
SutProvider<RemovePaymentMethodCommand> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.RemovePaymentMethod(null));
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NonStripeGateway_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.BitPay;
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NoGatewayCustomerId_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
organization.GatewayCustomerId = null;
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NoStripeCustomer_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.ReturnsNull();
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_NoCustomer_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string braintreeCustomerId = "1";
var stripeCustomer = new S.Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
var (braintreeGateway, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency<BT.IBraintreeGateway>());
customerGateway.FindAsync(braintreeCustomerId).ReturnsNull();
braintreeGateway.Customer.Returns(customerGateway);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<string>(), Arg.Any<BT.CustomerRequest>());
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_NoPaymentMethod_NoOp(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string braintreeCustomerId = "1";
var stripeCustomer = new S.Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency<BT.IBraintreeGateway>());
var braintreeCustomer = Substitute.For<BT.Customer>();
braintreeCustomer.PaymentMethods.Returns(Array.Empty<BT.PaymentMethod>());
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
await sutProvider.Sut.RemovePaymentMethod(organization);
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any<string>(), Arg.Any<BT.CustomerRequest>());
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_CustomerUpdateFails_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string braintreeCustomerId = "1";
const string braintreePaymentMethodToken = "TOKEN";
var stripeCustomer = new S.Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency<BT.IBraintreeGateway>());
var braintreeCustomer = Substitute.For<BT.Customer>();
var paymentMethod = Substitute.For<BT.PaymentMethod>();
paymentMethod.Token.Returns(braintreePaymentMethodToken);
paymentMethod.IsDefault.Returns(true);
braintreeCustomer.PaymentMethods.Returns(new[]
{
paymentMethod
});
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var updateBraintreeCustomerResult = Substitute.For<BT.Result<BT.Customer>>();
updateBraintreeCustomerResult.IsSuccess().Returns(false);
customerGateway.UpdateAsync(
braintreeCustomerId,
Arg.Is<BT.CustomerRequest>(request => request.DefaultPaymentMethodToken == null))
.Returns(updateBraintreeCustomerResult);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<BT.CustomerRequest>(request =>
request.DefaultPaymentMethodToken == null));
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(paymentMethod.Token);
await customerGateway.DidNotReceive().UpdateAsync(braintreeCustomerId, Arg.Is<BT.CustomerRequest>(request =>
request.DefaultPaymentMethodToken == paymentMethod.Token));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_PaymentMethodDeleteFails_RollBack_ContactSupport(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string braintreeCustomerId = "1";
const string braintreePaymentMethodToken = "TOKEN";
var stripeCustomer = new S.Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency<BT.IBraintreeGateway>());
var braintreeCustomer = Substitute.For<BT.Customer>();
var paymentMethod = Substitute.For<BT.PaymentMethod>();
paymentMethod.Token.Returns(braintreePaymentMethodToken);
paymentMethod.IsDefault.Returns(true);
braintreeCustomer.PaymentMethods.Returns(new[]
{
paymentMethod
});
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var updateBraintreeCustomerResult = Substitute.For<BT.Result<BT.Customer>>();
updateBraintreeCustomerResult.IsSuccess().Returns(true);
customerGateway.UpdateAsync(braintreeCustomerId, Arg.Any<BT.CustomerRequest>())
.Returns(updateBraintreeCustomerResult);
var deleteBraintreePaymentMethodResult = Substitute.For<BT.Result<BT.PaymentMethod>>();
deleteBraintreePaymentMethodResult.IsSuccess().Returns(false);
paymentMethodGateway.DeleteAsync(paymentMethod.Token).Returns(deleteBraintreePaymentMethodResult);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<BT.CustomerRequest>(request =>
request.DefaultPaymentMethodToken == null));
await paymentMethodGateway.Received(1).DeleteAsync(paymentMethod.Token);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<BT.CustomerRequest>(request =>
request.DefaultPaymentMethodToken == paymentMethod.Token));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Stripe_Legacy_RemovesSources(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string bankAccountId = "bank_account_id";
const string cardId = "card_id";
var sources = new List<S.IPaymentSource>
{
new S.BankAccount { Id = bankAccountId }, new S.Card { Id = cardId }
};
var stripeCustomer = new S.Customer { Sources = new S.StripeList<S.IPaymentSource> { Data = sources } };
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.PaymentMethodListAutoPagingAsync(Arg.Any<S.PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<S.PaymentMethod>()));
await sutProvider.Sut.RemovePaymentMethod(organization);
await stripeAdapter.Received(1).BankAccountDeleteAsync(stripeCustomer.Id, bankAccountId);
await stripeAdapter.Received(1).CardDeleteAsync(stripeCustomer.Id, cardId);
await stripeAdapter.DidNotReceiveWithAnyArgs()
.PaymentMethodDetachAsync(Arg.Any<string>(), Arg.Any<S.PaymentMethodDetachOptions>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Stripe_DetachesPaymentMethods(
Organization organization,
SutProvider<RemovePaymentMethodCommand> sutProvider)
{
organization.Gateway = GatewayType.Stripe;
const string bankAccountId = "bank_account_id";
const string cardId = "card_id";
var sources = new List<S.IPaymentSource>();
var stripeCustomer = new S.Customer { Sources = new S.StripeList<S.IPaymentSource> { Data = sources } };
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<S.CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.PaymentMethodListAutoPagingAsync(Arg.Any<S.PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<S.PaymentMethod>
{
new ()
{
Id = bankAccountId
},
new ()
{
Id = cardId
}
}));
await sutProvider.Sut.RemovePaymentMethod(organization);
await stripeAdapter.DidNotReceiveWithAnyArgs().BankAccountDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.DidNotReceiveWithAnyArgs().CardDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.Received(1)
.PaymentMethodDetachAsync(bankAccountId, Arg.Any<S.PaymentMethodDetachOptions>());
await stripeAdapter.Received(1)
.PaymentMethodDetachAsync(cardId, Arg.Any<S.PaymentMethodDetachOptions>());
}
private static async IAsyncEnumerable<S.PaymentMethod> GetPaymentMethodsAsync(
IEnumerable<S.PaymentMethod> paymentMethods)
{
foreach (var paymentMethod in paymentMethods)
{
yield return paymentMethod;
}
await Task.CompletedTask;
}
private static (BT.IBraintreeGateway, BT.ICustomerGateway, BT.IPaymentMethodGateway) Setup(
BT.IBraintreeGateway braintreeGateway)
{
var customerGateway = Substitute.For<BT.ICustomerGateway>();
var paymentMethodGateway = Substitute.For<BT.IPaymentMethodGateway>();
braintreeGateway.Customer.Returns(customerGateway);
braintreeGateway.PaymentMethod.Returns(paymentMethodGateway);
return (braintreeGateway, customerGateway, paymentMethodGateway);
}
}

View File

@ -1,420 +0,0 @@
using System.Net;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands.Implementations;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
namespace Bit.Core.Test.Billing.Commands;
[SutProviderCustomize]
public class StartSubscriptionCommandTests
{
private const string _customerId = "customer_id";
private const string _subscriptionId = "subscription_id";
// These tests are only trying to assert on the thrown exceptions and thus use the least amount of data setup possible.
#region Error Cases
[Theory, BitAutoData]
public async Task StartSubscription_NullProvider_ThrowsArgumentNullException(
SutProvider<StartSubscriptionCommand> sutProvider,
TaxInfo taxInfo) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.StartSubscription(null, taxInfo));
[Theory, BitAutoData]
public async Task StartSubscription_NullTaxInfo_ThrowsArgumentNullException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.StartSubscription(provider, null));
[Theory, BitAutoData]
public async Task StartSubscription_AlreadyHasGatewaySubscriptionId_ThrowsBillingException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = _subscriptionId;
await ThrowsContactSupportAsync(() => sutProvider.Sut.StartSubscription(provider, taxInfo));
await DidNotRetrieveCustomerAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task StartSubscription_MissingCountry_ThrowsBillingException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = null;
taxInfo.BillingAddressCountry = null;
await ThrowsContactSupportAsync(() => sutProvider.Sut.StartSubscription(provider, taxInfo));
await DidNotRetrieveCustomerAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task StartSubscription_MissingPostalCode_ThrowsBillingException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = null;
taxInfo.BillingAddressPostalCode = null;
await ThrowsContactSupportAsync(() => sutProvider.Sut.StartSubscription(provider, taxInfo));
await DidNotRetrieveCustomerAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task StartSubscription_MissingStripeCustomer_ThrowsBillingException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = null;
SetCustomerRetrieval(sutProvider, null);
await ThrowsContactSupportAsync(() => sutProvider.Sut.StartSubscription(provider, taxInfo));
await DidNotRetrieveProviderPlansAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task StartSubscription_NoProviderPlans_ThrowsBillingException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = null;
SetCustomerRetrieval(sutProvider, new Customer
{
Id = _customerId,
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
});
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(new List<ProviderPlan>());
await ThrowsContactSupportAsync(() => sutProvider.Sut.StartSubscription(provider, taxInfo));
await DidNotCreateSubscriptionAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task StartSubscription_NoProviderTeamsPlan_ThrowsBillingException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = null;
SetCustomerRetrieval(sutProvider, new Customer
{
Id = _customerId,
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
});
var providerPlans = new List<ProviderPlan>
{
new ()
{
PlanType = PlanType.EnterpriseMonthly
}
};
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
await ThrowsContactSupportAsync(() => sutProvider.Sut.StartSubscription(provider, taxInfo));
await DidNotCreateSubscriptionAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task StartSubscription_NoProviderEnterprisePlan_ThrowsBillingException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = null;
SetCustomerRetrieval(sutProvider, new Customer
{
Id = _customerId,
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
});
var providerPlans = new List<ProviderPlan>
{
new ()
{
PlanType = PlanType.TeamsMonthly
}
};
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
await ThrowsContactSupportAsync(() => sutProvider.Sut.StartSubscription(provider, taxInfo));
await DidNotCreateSubscriptionAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task StartSubscription_SubscriptionIncomplete_ThrowsBillingException(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = null;
SetCustomerRetrieval(sutProvider, new Customer
{
Id = _customerId,
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
});
var providerPlans = new List<ProviderPlan>
{
new ()
{
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 100
},
new ()
{
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100
}
};
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
{
Id = _subscriptionId,
Status = StripeConstants.SubscriptionStatus.Incomplete
});
await ThrowsContactSupportAsync(() => sutProvider.Sut.StartSubscription(provider, taxInfo));
await sutProvider.GetDependency<IProviderRepository>().Received(1).ReplaceAsync(provider);
}
#endregion
#region Success Cases
[Theory, BitAutoData]
public async Task StartSubscription_ExistingCustomer_Succeeds(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = _customerId;
provider.GatewaySubscriptionId = null;
SetCustomerRetrieval(sutProvider, new Customer
{
Id = _customerId,
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
});
var providerPlans = new List<ProviderPlan>
{
new ()
{
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 100
},
new ()
{
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100
}
};
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub =>
sub.AutomaticTax.Enabled == true &&
sub.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
sub.Customer == _customerId &&
sub.DaysUntilDue == 30 &&
sub.Items.Count == 2 &&
sub.Items.ElementAt(0).Price == teamsPlan.PasswordManager.StripeSeatPlanId &&
sub.Items.ElementAt(0).Quantity == 100 &&
sub.Items.ElementAt(1).Price == enterprisePlan.PasswordManager.StripeSeatPlanId &&
sub.Items.ElementAt(1).Quantity == 100 &&
sub.Metadata["providerId"] == provider.Id.ToString() &&
sub.OffSession == true &&
sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations)).Returns(new Subscription
{
Id = _subscriptionId,
Status = StripeConstants.SubscriptionStatus.Active
});
await sutProvider.Sut.StartSubscription(provider, taxInfo);
await sutProvider.GetDependency<IProviderRepository>().Received(1).ReplaceAsync(provider);
}
[Theory, BitAutoData]
public async Task StartSubscription_NewCustomer_Succeeds(
SutProvider<StartSubscriptionCommand> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.GatewayCustomerId = null;
provider.GatewaySubscriptionId = null;
provider.Name = "MSP";
taxInfo.BillingAddressCountry = "AD";
sutProvider.GetDependency<IStripeAdapter>().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.Coupon == "msp-discount-35" &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Email == provider.BillingEmail &&
o.Expand.FirstOrDefault() == "tax" &&
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(new Customer
{
Id = _customerId,
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
});
var providerPlans = new List<ProviderPlan>
{
new ()
{
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 100
},
new ()
{
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100
}
};
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub =>
sub.AutomaticTax.Enabled == true &&
sub.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
sub.Customer == _customerId &&
sub.DaysUntilDue == 30 &&
sub.Items.Count == 2 &&
sub.Items.ElementAt(0).Price == teamsPlan.PasswordManager.StripeSeatPlanId &&
sub.Items.ElementAt(0).Quantity == 100 &&
sub.Items.ElementAt(1).Price == enterprisePlan.PasswordManager.StripeSeatPlanId &&
sub.Items.ElementAt(1).Quantity == 100 &&
sub.Metadata["providerId"] == provider.Id.ToString() &&
sub.OffSession == true &&
sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations)).Returns(new Subscription
{
Id = _subscriptionId,
Status = StripeConstants.SubscriptionStatus.Active
});
await sutProvider.Sut.StartSubscription(provider, taxInfo);
await sutProvider.GetDependency<IProviderRepository>().Received(2).ReplaceAsync(provider);
}
#endregion
private static async Task DidNotCreateSubscriptionAsync(SutProvider<StartSubscriptionCommand> sutProvider) =>
await sutProvider.GetDependency<IStripeAdapter>()
.DidNotReceiveWithAnyArgs()
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
private static async Task DidNotRetrieveCustomerAsync(SutProvider<StartSubscriptionCommand> sutProvider) =>
await sutProvider.GetDependency<IStripeAdapter>()
.DidNotReceiveWithAnyArgs()
.CustomerGetAsync(Arg.Any<string>(), Arg.Any<CustomerGetOptions>());
private static async Task DidNotRetrieveProviderPlansAsync(SutProvider<StartSubscriptionCommand> sutProvider) =>
await sutProvider.GetDependency<IProviderPlanRepository>()
.DidNotReceiveWithAnyArgs()
.GetByProviderId(Arg.Any<Guid>());
private static void SetCustomerRetrieval(SutProvider<StartSubscriptionCommand> sutProvider,
Customer customer) => sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(_customerId, Arg.Is<CustomerGetOptions>(o => o.Expand.FirstOrDefault() == "tax"))
.Returns(customer);
}

View File

@ -1,154 +0,0 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Queries;
using Bit.Core.Billing.Queries.Implementations;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Queries;
[SutProviderCustomize]
public class ProviderBillingQueriesTests
{
#region GetSubscriptionData
[Theory, BitAutoData]
public async Task GetSubscriptionData_NullProvider_ReturnsNull(
SutProvider<ProviderBillingQueries> sutProvider,
Guid providerId)
{
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(providerId).ReturnsNull();
var subscriptionData = await sutProvider.Sut.GetSubscriptionDTO(providerId);
Assert.Null(subscriptionData);
await providerRepository.Received(1).GetByIdAsync(providerId);
}
[Theory, BitAutoData]
public async Task GetSubscriptionData_NullSubscription_ReturnsNull(
SutProvider<ProviderBillingQueries> sutProvider,
Guid providerId,
Provider provider)
{
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(providerId).Returns(provider);
var subscriberQueries = sutProvider.GetDependency<ISubscriberQueries>();
subscriberQueries.GetSubscription(provider).ReturnsNull();
var subscriptionData = await sutProvider.Sut.GetSubscriptionDTO(providerId);
Assert.Null(subscriptionData);
await providerRepository.Received(1).GetByIdAsync(providerId);
await subscriberQueries.Received(1).GetSubscription(
provider,
Arg.Is<SubscriptionGetOptions>(
options => options.Expand.Count == 1 && options.Expand.First() == "customer"));
}
[Theory, BitAutoData]
public async Task GetSubscriptionData_Success(
SutProvider<ProviderBillingQueries> sutProvider,
Guid providerId,
Provider provider)
{
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(providerId).Returns(provider);
var subscriberQueries = sutProvider.GetDependency<ISubscriberQueries>();
var subscription = new Subscription();
subscriberQueries.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(
options => options.Expand.Count == 1 && options.Expand.First() == "customer")).Returns(subscription);
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
var enterprisePlan = new ProviderPlan
{
Id = Guid.NewGuid(),
ProviderId = providerId,
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
};
var teamsPlan = new ProviderPlan
{
Id = Guid.NewGuid(),
ProviderId = providerId,
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 50,
PurchasedSeats = 10,
AllocatedSeats = 60
};
var providerPlans = new List<ProviderPlan>
{
enterprisePlan,
teamsPlan,
};
providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans);
var subscriptionData = await sutProvider.Sut.GetSubscriptionDTO(providerId);
Assert.NotNull(subscriptionData);
Assert.Equivalent(subscriptionData.Subscription, subscription);
Assert.Equal(2, subscriptionData.ProviderPlans.Count);
var configuredEnterprisePlan =
subscriptionData.ProviderPlans.FirstOrDefault(configuredPlan =>
configuredPlan.PlanType == PlanType.EnterpriseMonthly);
var configuredTeamsPlan =
subscriptionData.ProviderPlans.FirstOrDefault(configuredPlan =>
configuredPlan.PlanType == PlanType.TeamsMonthly);
Compare(enterprisePlan, configuredEnterprisePlan);
Compare(teamsPlan, configuredTeamsPlan);
await providerRepository.Received(1).GetByIdAsync(providerId);
await subscriberQueries.Received(1).GetSubscription(
provider,
Arg.Is<SubscriptionGetOptions>(
options => options.Expand.Count == 1 && options.Expand.First() == "customer"));
await providerPlanRepository.Received(1).GetByProviderId(providerId);
return;
void Compare(ProviderPlan providerPlan, ConfiguredProviderPlanDTO configuredProviderPlan)
{
Assert.NotNull(configuredProviderPlan);
Assert.Equal(providerPlan.Id, configuredProviderPlan.Id);
Assert.Equal(providerPlan.ProviderId, configuredProviderPlan.ProviderId);
Assert.Equal(providerPlan.SeatMinimum!.Value, configuredProviderPlan.SeatMinimum);
Assert.Equal(providerPlan.PurchasedSeats!.Value, configuredProviderPlan.PurchasedSeats);
Assert.Equal(providerPlan.AllocatedSeats!.Value, configuredProviderPlan.AssignedSeats);
}
}
#endregion
}

View File

@ -1,272 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Queries.Implementations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
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_StripeException_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ThrowsAsync<StripeException>();
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(
SutProvider<SubscriberQueries> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscription(null));
[Theory, BitAutoData]
public async Task GetSubscription_NoGatewaySubscriptionId_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
organization.GatewaySubscriptionId = null;
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_NoSubscription_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_StripeException_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ThrowsAsync<StripeException>();
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
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 GetCustomerOrThrow_NoGatewayCustomerId_ContactSupport(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
organization.GatewayCustomerId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_NoCustomer_ContactSupport(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_StripeException_ContactSupport(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var stripeException = new StripeException();
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ThrowsAsync(stripeException);
await ThrowsContactSupportAsync(
async () => await sutProvider.Sut.GetCustomerOrThrow(organization),
"An error occurred while trying to retrieve a Stripe Customer",
stripeException);
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var customer = new Customer();
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.Returns(customer);
var gotCustomer = await sutProvider.Sut.GetCustomerOrThrow(organization);
Assert.Equivalent(customer, gotCustomer);
}
#endregion
#region GetSubscriptionOrThrow
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberQueries> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscriptionOrThrow(null));
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NoGatewaySubscriptionId_ContactSupport(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
organization.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NoSubscription_ContactSupport(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_StripeException_ContactSupport(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var stripeException = new StripeException();
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ThrowsAsync(stripeException);
await ThrowsContactSupportAsync(
async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization),
"An error occurred while trying to retrieve a Stripe Subscription",
stripeException);
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(organization);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
}

View File

@ -1,7 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Queries;
using Bit.Core.Billing.Queries.Implementations;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -9,16 +9,16 @@ using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Queries;
namespace Bit.Core.Test.Billing.Services;
[SutProviderCustomize]
public class OrganizationBillingQueriesTests
public class OrganizationBillingServiceTests
{
#region GetMetadata
[Theory, BitAutoData]
public async Task GetMetadata_OrganizationNull_ReturnsNull(
Guid organizationId,
SutProvider<OrganizationBillingQueries> sutProvider)
SutProvider<OrganizationBillingService> sutProvider)
{
var metadata = await sutProvider.Sut.GetMetadata(organizationId);
@ -29,7 +29,7 @@ public class OrganizationBillingQueriesTests
public async Task GetMetadata_CustomerNull_ReturnsNull(
Guid organizationId,
Organization organization,
SutProvider<OrganizationBillingQueries> sutProvider)
SutProvider<OrganizationBillingService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
@ -42,11 +42,11 @@ public class OrganizationBillingQueriesTests
public async Task GetMetadata_SubscriptionNull_ReturnsNull(
Guid organizationId,
Organization organization,
SutProvider<OrganizationBillingQueries> sutProvider)
SutProvider<OrganizationBillingService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
sutProvider.GetDependency<ISubscriberQueries>().GetCustomer(organization).Returns(new Customer());
sutProvider.GetDependency<ISubscriberService>().GetCustomer(organization).Returns(new Customer());
var metadata = await sutProvider.Sut.GetMetadata(organizationId);
@ -57,13 +57,13 @@ public class OrganizationBillingQueriesTests
public async Task GetMetadata_Succeeds(
Guid organizationId,
Organization organization,
SutProvider<OrganizationBillingQueries> sutProvider)
SutProvider<OrganizationBillingService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
var subscriberQueries = sutProvider.GetDependency<ISubscriberQueries>();
var subscriberService = sutProvider.GetDependency<ISubscriberService>();
subscriberQueries
subscriberService
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options => options.Expand.FirstOrDefault() == "discount.coupon.applies_to"))
.Returns(new Customer
{
@ -80,7 +80,7 @@ public class OrganizationBillingQueriesTests
}
});
subscriberQueries.GetSubscription(organization).Returns(new Subscription
subscriberService.GetSubscription(organization).Returns(new Subscription
{
Items = new StripeList<SubscriptionItem>
{

View File

@ -0,0 +1,881 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Braintree;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
using Customer = Stripe.Customer;
using PaymentMethod = Stripe.PaymentMethod;
using Subscription = Stripe.Subscription;
namespace Bit.Core.Test.Billing.Services;
[SutProviderCustomize]
public class SubscriberServiceTests
{
#region CancelSubscription
[Theory, BitAutoData]
public async Task CancelSubscription_SubscriptionInactive_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var subscription = new Subscription
{
Status = "canceled"
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
await ThrowsContactSupportAsync(() =>
sutProvider.Sut.CancelSubscription(organization, new OffboardingSurveyResponse(), false));
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.SubscriptionCancelAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
}
[Theory, BitAutoData]
public async Task CancelSubscription_CancelImmediately_BelongsToOrganization_UpdatesSubscription_CancelSubscriptionImmediately(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var userId = Guid.NewGuid();
const string subscriptionId = "subscription_id";
var subscription = new Subscription
{
Id = subscriptionId,
Status = "active",
Metadata = new Dictionary<string, string>
{
{ "organizationId", "organization_id" }
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, true);
await stripeAdapter
.Received(1)
.SubscriptionUpdateAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(
options => options.Metadata["cancellingUserId"] == userId.ToString()));
await stripeAdapter
.Received(1)
.SubscriptionCancelAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason));
}
[Theory, BitAutoData]
public async Task CancelSubscription_CancelImmediately_BelongsToUser_CancelSubscriptionImmediately(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var userId = Guid.NewGuid();
const string subscriptionId = "subscription_id";
var subscription = new Subscription
{
Id = subscriptionId,
Status = "active",
Metadata = new Dictionary<string, string>
{
{ "userId", "user_id" }
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, true);
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await stripeAdapter
.Received(1)
.SubscriptionCancelAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason));
}
[Theory, BitAutoData]
public async Task CancelSubscription_DoNotCancelImmediately_UpdateSubscriptionToCancelAtEndOfPeriod(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var userId = Guid.NewGuid();
const string subscriptionId = "subscription_id";
organization.ExpirationDate = DateTime.UtcNow.AddDays(5);
var subscription = new Subscription
{
Id = subscriptionId,
Status = "active"
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, false);
await stripeAdapter
.Received(1)
.SubscriptionUpdateAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAtPeriodEnd == true &&
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason &&
options.Metadata["cancellingUserId"] == userId.ToString()));
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.SubscriptionCancelAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>()); ;
}
#endregion
#region GetCustomer
[Theory, BitAutoData]
public async Task GetCustomer_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetCustomer(null));
[Theory, BitAutoData]
public async Task GetCustomer_NoGatewayCustomerId_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> 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<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ReturnsNull();
var customer = await sutProvider.Sut.GetCustomer(organization);
Assert.Null(customer);
}
[Theory, BitAutoData]
public async Task GetCustomer_StripeException_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ThrowsAsync<StripeException>();
var customer = await sutProvider.Sut.GetCustomer(organization);
Assert.Null(customer);
}
[Theory, BitAutoData]
public async Task GetCustomer_Succeeds(
Organization organization,
SutProvider<SubscriberService> 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 GetCustomerOrThrow
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetCustomerOrThrow(null));
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_NoGatewayCustomerId_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewayCustomerId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_NoCustomer_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_StripeException_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeException = new StripeException();
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.ThrowsAsync(stripeException);
await ThrowsContactSupportAsync(
async () => await sutProvider.Sut.GetCustomerOrThrow(organization),
"An error occurred while trying to retrieve a Stripe Customer",
stripeException);
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_Succeeds(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer();
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.Returns(customer);
var gotCustomer = await sutProvider.Sut.GetCustomerOrThrow(organization);
Assert.Equivalent(customer, gotCustomer);
}
#endregion
#region GetSubscription
[Theory, BitAutoData]
public async Task GetSubscription_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscription(null));
[Theory, BitAutoData]
public async Task GetSubscription_NoGatewaySubscriptionId_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewaySubscriptionId = null;
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_NoSubscription_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_StripeException_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ThrowsAsync<StripeException>();
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Succeeds(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
#region GetSubscriptionOrThrow
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscriptionOrThrow(null));
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NoGatewaySubscriptionId_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NoSubscription_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_StripeException_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeException = new StripeException();
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ThrowsAsync(stripeException);
await ThrowsContactSupportAsync(
async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization),
"An error occurred while trying to retrieve a Stripe Subscription",
stripeException);
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Succeeds(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(organization);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
#region RemovePaymentMethod
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NullSubscriber_ArgumentNullException(
SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.RemovePaymentMethod(null));
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_NoCustomer_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "1";
var stripeCustomer = new Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (braintreeGateway, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
customerGateway.FindAsync(braintreeCustomerId).ReturnsNull();
braintreeGateway.Customer.Returns(customerGateway);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_NoPaymentMethod_NoOp(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "1";
var stripeCustomer = new Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var braintreeCustomer = Substitute.For<Braintree.Customer>();
braintreeCustomer.PaymentMethods.Returns([]);
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
await sutProvider.Sut.RemovePaymentMethod(organization);
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_CustomerUpdateFails_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "1";
const string braintreePaymentMethodToken = "TOKEN";
var stripeCustomer = new Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var braintreeCustomer = Substitute.For<Braintree.Customer>();
var paymentMethod = Substitute.For<Braintree.PaymentMethod>();
paymentMethod.Token.Returns(braintreePaymentMethodToken);
paymentMethod.IsDefault.Returns(true);
braintreeCustomer.PaymentMethods.Returns([
paymentMethod
]);
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var updateBraintreeCustomerResult = Substitute.For<Result<Braintree.Customer>>();
updateBraintreeCustomerResult.IsSuccess().Returns(false);
customerGateway.UpdateAsync(
braintreeCustomerId,
Arg.Is<CustomerRequest>(request => request.DefaultPaymentMethodToken == null))
.Returns(updateBraintreeCustomerResult);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>
request.DefaultPaymentMethodToken == null));
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(paymentMethod.Token);
await customerGateway.DidNotReceive().UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>
request.DefaultPaymentMethodToken == paymentMethod.Token));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_PaymentMethodDeleteFails_RollBack_ContactSupport(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "1";
const string braintreePaymentMethodToken = "TOKEN";
var stripeCustomer = new Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var braintreeCustomer = Substitute.For<Braintree.Customer>();
var paymentMethod = Substitute.For<Braintree.PaymentMethod>();
paymentMethod.Token.Returns(braintreePaymentMethodToken);
paymentMethod.IsDefault.Returns(true);
braintreeCustomer.PaymentMethods.Returns([
paymentMethod
]);
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var updateBraintreeCustomerResult = Substitute.For<Result<Braintree.Customer>>();
updateBraintreeCustomerResult.IsSuccess().Returns(true);
customerGateway.UpdateAsync(braintreeCustomerId, Arg.Any<CustomerRequest>())
.Returns(updateBraintreeCustomerResult);
var deleteBraintreePaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
deleteBraintreePaymentMethodResult.IsSuccess().Returns(false);
paymentMethodGateway.DeleteAsync(paymentMethod.Token).Returns(deleteBraintreePaymentMethodResult);
await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>
request.DefaultPaymentMethodToken == null));
await paymentMethodGateway.Received(1).DeleteAsync(paymentMethod.Token);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>
request.DefaultPaymentMethodToken == paymentMethod.Token));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Stripe_Legacy_RemovesSources(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string bankAccountId = "bank_account_id";
const string cardId = "card_id";
var sources = new List<IPaymentSource>
{
new BankAccount { Id = bankAccountId }, new Card { Id = cardId }
};
var stripeCustomer = new Customer { Sources = new StripeList<IPaymentSource> { Data = sources } };
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.PaymentMethodListAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<Stripe.PaymentMethod>()));
await sutProvider.Sut.RemovePaymentMethod(organization);
await stripeAdapter.Received(1).BankAccountDeleteAsync(stripeCustomer.Id, bankAccountId);
await stripeAdapter.Received(1).CardDeleteAsync(stripeCustomer.Id, cardId);
await stripeAdapter.DidNotReceiveWithAnyArgs()
.PaymentMethodDetachAsync(Arg.Any<string>(), Arg.Any<PaymentMethodDetachOptions>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Stripe_DetachesPaymentMethods(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string bankAccountId = "bank_account_id";
const string cardId = "card_id";
var sources = new List<IPaymentSource>();
var stripeCustomer = new Customer { Sources = new StripeList<IPaymentSource> { Data = sources } };
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.PaymentMethodListAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<Stripe.PaymentMethod>
{
new ()
{
Id = bankAccountId
},
new ()
{
Id = cardId
}
}));
await sutProvider.Sut.RemovePaymentMethod(organization);
await stripeAdapter.DidNotReceiveWithAnyArgs().BankAccountDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.DidNotReceiveWithAnyArgs().CardDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.Received(1)
.PaymentMethodDetachAsync(bankAccountId);
await stripeAdapter.Received(1)
.PaymentMethodDetachAsync(cardId);
}
private static async IAsyncEnumerable<Stripe.PaymentMethod> GetPaymentMethodsAsync(
IEnumerable<Stripe.PaymentMethod> paymentMethods)
{
foreach (var paymentMethod in paymentMethods)
{
yield return paymentMethod;
}
await Task.CompletedTask;
}
private static (IBraintreeGateway, ICustomerGateway, IPaymentMethodGateway) SetupBraintree(
IBraintreeGateway braintreeGateway)
{
var customerGateway = Substitute.For<ICustomerGateway>();
var paymentMethodGateway = Substitute.For<IPaymentMethodGateway>();
braintreeGateway.Customer.Returns(customerGateway);
braintreeGateway.PaymentMethod.Returns(paymentMethodGateway);
return (braintreeGateway, customerGateway, paymentMethodGateway);
}
#endregion
#region GetTaxInformationAsync
[Theory, BitAutoData]
public async Task GetTaxInformationAsync_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetTaxInformationAsync(null));
[Theory, BitAutoData]
public async Task GetTaxInformationAsync_NoGatewayCustomerId_ReturnsNull(
Provider subscriber,
SutProvider<SubscriberService> sutProvider)
{
subscriber.GatewayCustomerId = null;
var taxInfo = await sutProvider.Sut.GetTaxInformationAsync(subscriber);
Assert.Null(taxInfo);
}
[Theory, BitAutoData]
public async Task GetTaxInformationAsync_NoCustomer_ReturnsNull(
Provider subscriber,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(subscriber.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns((Customer)null);
await Assert.ThrowsAsync<BillingException>(
() => sutProvider.Sut.GetTaxInformationAsync(subscriber));
}
[Theory, BitAutoData]
public async Task GetTaxInformationAsync_StripeException_ReturnsNull(
Provider subscriber,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(subscriber.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.ThrowsAsync(new StripeException());
await Assert.ThrowsAsync<BillingException>(
() => sutProvider.Sut.GetTaxInformationAsync(subscriber));
}
[Theory, BitAutoData]
public async Task GetTaxInformationAsync_Succeeds(
Provider subscriber,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
Address = new Stripe.Address
{
Line1 = "123 Main St",
Line2 = "Apt 4B",
City = "Metropolis",
State = "NY",
PostalCode = "12345",
Country = "US"
}
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(subscriber.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(customer);
var taxInfo = await sutProvider.Sut.GetTaxInformationAsync(subscriber);
Assert.NotNull(taxInfo);
Assert.Equal("123 Main St", taxInfo.BillingAddressLine1);
Assert.Equal("Apt 4B", taxInfo.BillingAddressLine2);
Assert.Equal("Metropolis", taxInfo.BillingAddressCity);
Assert.Equal("NY", taxInfo.BillingAddressState);
Assert.Equal("12345", taxInfo.BillingAddressPostalCode);
Assert.Equal("US", taxInfo.BillingAddressCountry);
}
#endregion
#region GetPaymentMethodAsync
[Theory, BitAutoData]
public async Task GetPaymentMethodAsync_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
{
await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetPaymentMethodAsync(null));
}
[Theory, BitAutoData]
public async Task GetPaymentMethodAsync_NoCustomer_ReturnsNull(
Provider subscriber,
SutProvider<SubscriberService> sutProvider)
{
subscriber.GatewayCustomerId = null;
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(subscriber.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns((Customer)null);
await Assert.ThrowsAsync<BillingException>(() => sutProvider.Sut.GetPaymentMethodAsync(subscriber));
}
[Theory, BitAutoData]
public async Task GetPaymentMethodAsync_StripeCardPaymentMethod_ReturnsBillingSource(
Provider subscriber,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer();
var paymentMethod = CreateSamplePaymentMethod();
subscriber.GatewayCustomerId = "test_customer_id";
customer.InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = paymentMethod
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(subscriber.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(customer);
var billingSource = await sutProvider.Sut.GetPaymentMethodAsync(subscriber);
Assert.NotNull(billingSource);
Assert.Equal(paymentMethod.Card.Brand, billingSource.CardBrand);
}
private static PaymentMethod CreateSamplePaymentMethod()
{
var paymentMethod = new PaymentMethod
{
Id = "pm_test123",
Type = "card",
Card = new PaymentMethodCard
{
Brand = "visa",
Last4 = "4242",
ExpMonth = 12,
ExpYear = 2024
}
};
return paymentMethod;
}
#endregion
}