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); } }