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:
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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>
|
||||
{
|
881
test/Core.Test/Billing/Services/SubscriberServiceTests.cs
Normal file
881
test/Core.Test/Billing/Services/SubscriberServiceTests.cs
Normal 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
|
||||
}
|
Reference in New Issue
Block a user