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 sutProvider) => await Assert.ThrowsAsync(() => sutProvider.Sut.RemovePaymentMethod(null)); [Theory, BitAutoData] public async Task RemovePaymentMethod_NonStripeGateway_ContactSupport( Organization organization, SutProvider sutProvider) { organization.Gateway = GatewayType.BitPay; await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization)); } [Theory, BitAutoData] public async Task RemovePaymentMethod_NoGatewayCustomerId_ContactSupport( Organization organization, SutProvider 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 sutProvider) { organization.Gateway = GatewayType.Stripe; sutProvider.GetDependency() .CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) .ReturnsNull(); await ThrowsContactSupportAsync(() => sutProvider.Sut.RemovePaymentMethod(organization)); } [Theory, BitAutoData] public async Task RemovePaymentMethod_Braintree_NoCustomer_ContactSupport( Organization organization, SutProvider sutProvider) { organization.Gateway = GatewayType.Stripe; const string braintreeCustomerId = "1"; var stripeCustomer = new S.Customer { Metadata = new Dictionary { { "btCustomerId", braintreeCustomerId } } }; sutProvider.GetDependency() .CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) .Returns(stripeCustomer); var (braintreeGateway, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency()); 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(), Arg.Any()); await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any()); } [Theory, BitAutoData] public async Task RemovePaymentMethod_Braintree_NoPaymentMethod_NoOp( Organization organization, SutProvider sutProvider) { organization.Gateway = GatewayType.Stripe; const string braintreeCustomerId = "1"; var stripeCustomer = new S.Customer { Metadata = new Dictionary { { "btCustomerId", braintreeCustomerId } } }; sutProvider.GetDependency() .CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) .Returns(stripeCustomer); var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency()); var braintreeCustomer = Substitute.For(); braintreeCustomer.PaymentMethods.Returns(Array.Empty()); customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer); await sutProvider.Sut.RemovePaymentMethod(organization); await customerGateway.Received(1).FindAsync(braintreeCustomerId); await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any(), Arg.Any()); await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any()); } [Theory, BitAutoData] public async Task RemovePaymentMethod_Braintree_CustomerUpdateFails_ContactSupport( Organization organization, SutProvider sutProvider) { organization.Gateway = GatewayType.Stripe; const string braintreeCustomerId = "1"; const string braintreePaymentMethodToken = "TOKEN"; var stripeCustomer = new S.Customer { Metadata = new Dictionary { { "btCustomerId", braintreeCustomerId } } }; sutProvider.GetDependency() .CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) .Returns(stripeCustomer); var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency()); var braintreeCustomer = Substitute.For(); var paymentMethod = Substitute.For(); paymentMethod.Token.Returns(braintreePaymentMethodToken); paymentMethod.IsDefault.Returns(true); braintreeCustomer.PaymentMethods.Returns(new[] { paymentMethod }); customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer); var updateBraintreeCustomerResult = Substitute.For>(); updateBraintreeCustomerResult.IsSuccess().Returns(false); customerGateway.UpdateAsync( braintreeCustomerId, Arg.Is(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(request => request.DefaultPaymentMethodToken == null)); await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(paymentMethod.Token); await customerGateway.DidNotReceive().UpdateAsync(braintreeCustomerId, Arg.Is(request => request.DefaultPaymentMethodToken == paymentMethod.Token)); } [Theory, BitAutoData] public async Task RemovePaymentMethod_Braintree_PaymentMethodDeleteFails_RollBack_ContactSupport( Organization organization, SutProvider sutProvider) { organization.Gateway = GatewayType.Stripe; const string braintreeCustomerId = "1"; const string braintreePaymentMethodToken = "TOKEN"; var stripeCustomer = new S.Customer { Metadata = new Dictionary { { "btCustomerId", braintreeCustomerId } } }; sutProvider.GetDependency() .CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) .Returns(stripeCustomer); var (_, customerGateway, paymentMethodGateway) = Setup(sutProvider.GetDependency()); var braintreeCustomer = Substitute.For(); var paymentMethod = Substitute.For(); paymentMethod.Token.Returns(braintreePaymentMethodToken); paymentMethod.IsDefault.Returns(true); braintreeCustomer.PaymentMethods.Returns(new[] { paymentMethod }); customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer); var updateBraintreeCustomerResult = Substitute.For>(); updateBraintreeCustomerResult.IsSuccess().Returns(true); customerGateway.UpdateAsync(braintreeCustomerId, Arg.Any()) .Returns(updateBraintreeCustomerResult); var deleteBraintreePaymentMethodResult = Substitute.For>(); 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(request => request.DefaultPaymentMethodToken == null)); await paymentMethodGateway.Received(1).DeleteAsync(paymentMethod.Token); await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is(request => request.DefaultPaymentMethodToken == paymentMethod.Token)); } [Theory, BitAutoData] public async Task RemovePaymentMethod_Stripe_Legacy_RemovesSources( Organization organization, SutProvider sutProvider) { organization.Gateway = GatewayType.Stripe; const string bankAccountId = "bank_account_id"; const string cardId = "card_id"; var sources = new List { new S.BankAccount { Id = bankAccountId }, new S.Card { Id = cardId } }; var stripeCustomer = new S.Customer { Sources = new S.StripeList { Data = sources } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) .Returns(stripeCustomer); stripeAdapter .PaymentMethodListAutoPagingAsync(Arg.Any()) .Returns(GetPaymentMethodsAsync(new List())); 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(), Arg.Any()); } [Theory, BitAutoData] public async Task RemovePaymentMethod_Stripe_DetachesPaymentMethods( Organization organization, SutProvider sutProvider) { organization.Gateway = GatewayType.Stripe; const string bankAccountId = "bank_account_id"; const string cardId = "card_id"; var sources = new List(); var stripeCustomer = new S.Customer { Sources = new S.StripeList { Data = sources } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) .Returns(stripeCustomer); stripeAdapter .PaymentMethodListAutoPagingAsync(Arg.Any()) .Returns(GetPaymentMethodsAsync(new List { new () { Id = bankAccountId }, new () { Id = cardId } })); await sutProvider.Sut.RemovePaymentMethod(organization); await stripeAdapter.DidNotReceiveWithAnyArgs().BankAccountDeleteAsync(Arg.Any(), Arg.Any()); await stripeAdapter.DidNotReceiveWithAnyArgs().CardDeleteAsync(Arg.Any(), Arg.Any()); await stripeAdapter.Received(1) .PaymentMethodDetachAsync(bankAccountId, Arg.Any()); await stripeAdapter.Received(1) .PaymentMethodDetachAsync(cardId, Arg.Any()); } private static async IAsyncEnumerable GetPaymentMethodsAsync( IEnumerable 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(); var paymentMethodGateway = Substitute.For(); braintreeGateway.Customer.Returns(customerGateway); braintreeGateway.PaymentMethod.Returns(paymentMethodGateway); return (braintreeGateway, customerGateway, paymentMethodGateway); } }