mirror of
https://github.com/bitwarden/server.git
synced 2024-11-22 12:15:36 +01:00
95139def0f
* Add RemovePaymentMethod to StripePaymentService * Add SendProviderUpdatePaymentMethod to HandlebarsMailService * Add RemoveOrganizationFromProviderCommand * Use RemoveOrganizationFromProviderCommand in ProviderOrganizationController * Remove RemoveOrganizationAsync from ProviderService * Add RemoveOrganizationFromProviderCommandTests * PR review feedback and refactoring * Remove RemovePaymentMethod from StripePaymentService * Review feedback * Add Organization RisksSubscriptionFailure endpoint * fix build error * Review feedback * [AC-1359] Bitwarden Portal Unlink Provider Buttons (#3588) * Added ability to unlink organization from provider from provider edit page * Refreshing provider edit page after removing an org * Added button to organization to remove the org from the provider * Updated based on product feedback * Removed organization name from alert message * Temporary logging * Remove coupon from Stripe org after disconnected from MSP * Updated test * Change payment terms on org disconnect from MSP * Set Stripe account email to new billing email * Remove logging --------- Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com>
368 lines
14 KiB
C#
368 lines
14 KiB
C#
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);
|
|
}
|
|
}
|