You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
367 lines
14 KiB
367 lines
14 KiB
using Bit.Core.AdminConsole.Entities; |
|
using Bit.Core.Billing.Commands.Implementations; |
|
using Bit.Core.Enums; |
|
using Bit.Core.Exceptions; |
|
using Bit.Core.Services; |
|
using Bit.Test.Common.AutoFixture; |
|
using Bit.Test.Common.AutoFixture.Attributes; |
|
using NSubstitute; |
|
using NSubstitute.ReturnsExtensions; |
|
using Xunit; |
|
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); |
|
} |
|
|
|
private static async Task ThrowsContactSupportAsync(Func<Task> function) |
|
{ |
|
const string message = "Could not remove your payment method. Please contact support for assistance."; |
|
|
|
var exception = await Assert.ThrowsAsync<GatewayException>(function); |
|
|
|
Assert.Equal(message, exception.Message); |
|
} |
|
}
|
|
|