1
0
mirror of https://github.com/bitwarden/server synced 2025-12-16 00:03:54 +00:00

[PM-24964] Stripe-hosted bank account verification (#6263)

* Implement bank account hosted URL verification with webhook handling notification

* Fix tests

* Run dotnet format

* Remove unused VerifyBankAccount operation

* Stephon's feedback

* Removing unused test

* TEMP: Add logging for deployment check

* Run dotnet format

* fix test

* Revert "fix test"

This reverts commit b8743ab3b5.

* Revert "Run dotnet format"

This reverts commit 5c861b0b72.

* Revert "TEMP: Add logging for deployment check"

This reverts commit 0a88acd6a1.

* Resolve GetPaymentMethodQuery order of operations
This commit is contained in:
Alex Morask
2025-09-09 12:22:42 -05:00
committed by GitHub
parent ac718351a8
commit 3dd5accb56
42 changed files with 1136 additions and 814 deletions

View File

@@ -0,0 +1,242 @@
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Stripe;
using Xunit;
using Event = Stripe.Event;
namespace Bit.Billing.Test.Services;
public class SetupIntentSucceededHandlerTests
{
private static readonly Event _mockEvent = new() { Id = "evt_test", Type = "setup_intent.succeeded" };
private static readonly string[] _expand = ["payment_method"];
private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderRepository _providerRepository;
private readonly IPushNotificationAdapter _pushNotificationAdapter;
private readonly ISetupIntentCache _setupIntentCache;
private readonly IStripeAdapter _stripeAdapter;
private readonly IStripeEventService _stripeEventService;
private readonly SetupIntentSucceededHandler _handler;
public SetupIntentSucceededHandlerTests()
{
_organizationRepository = Substitute.For<IOrganizationRepository>();
_providerRepository = Substitute.For<IProviderRepository>();
_pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();
_setupIntentCache = Substitute.For<ISetupIntentCache>();
_stripeAdapter = Substitute.For<IStripeAdapter>();
_stripeEventService = Substitute.For<IStripeEventService>();
_handler = new SetupIntentSucceededHandler(
_organizationRepository,
_providerRepository,
_pushNotificationAdapter,
_setupIntentCache,
_stripeAdapter,
_stripeEventService);
}
[Fact]
public async Task HandleAsync_PaymentMethodNotUSBankAccount_Returns()
{
// Arrange
var setupIntent = CreateSetupIntent(hasUSBankAccount: false);
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _setupIntentCache.DidNotReceiveWithAnyArgs().GetSubscriberIdForSetupIntent(Arg.Any<string>());
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_NoSubscriberIdInCache_Returns()
{
// Arrange
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns((Guid?)null);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_ValidOrganization_AttachesPaymentMethodAndSendsNotification()
{
// Arrange
var organizationId = Guid.NewGuid();
var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = "cus_test" };
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns(organizationId);
_organizationRepository.GetByIdAsync(organizationId)
.Returns(organization);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.Received(1).PaymentMethodAttachAsync(
"pm_test",
Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == organization.GatewayCustomerId));
await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(organization);
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_ValidProvider_AttachesPaymentMethodAndSendsNotification()
{
// Arrange
var providerId = Guid.NewGuid();
var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = "cus_test" };
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns(providerId);
_organizationRepository.GetByIdAsync(providerId)
.Returns((Organization?)null);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.Received(1).PaymentMethodAttachAsync(
"pm_test",
Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == provider.GatewayCustomerId));
await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(provider);
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
}
[Fact]
public async Task HandleAsync_OrganizationWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()
{
// Arrange
var organizationId = Guid.NewGuid();
var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = null };
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns(organizationId);
_organizationRepository.GetByIdAsync(organizationId)
.Returns(organization);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_ProviderWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()
{
// Arrange
var providerId = Guid.NewGuid();
var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = null };
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns(providerId);
_organizationRepository.GetByIdAsync(providerId)
.Returns((Organization?)null);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
private static SetupIntent CreateSetupIntent(bool hasUSBankAccount = true)
{
var paymentMethod = new PaymentMethod
{
Id = "pm_test",
Type = "us_bank_account",
UsBankAccount = hasUSBankAccount ? new PaymentMethodUsBankAccount() : null
};
var setupIntent = new SetupIntent
{
Id = "seti_test",
PaymentMethod = paymentMethod
};
return setupIntent;
}
}

View File

@@ -1,8 +1,9 @@
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Billing.Test.Utilities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;
@@ -11,6 +12,9 @@ namespace Bit.Billing.Test.Services;
public class StripeEventServiceTests
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderRepository _providerRepository;
private readonly ISetupIntentCache _setupIntentCache;
private readonly IStripeFacade _stripeFacade;
private readonly StripeEventService _stripeEventService;
@@ -20,8 +24,11 @@ public class StripeEventServiceTests
var baseServiceUriSettings = new GlobalSettings.BaseServiceUriSettings(globalSettings) { CloudRegion = "US" };
globalSettings.BaseServiceUri = baseServiceUriSettings;
_organizationRepository = Substitute.For<IOrganizationRepository>();
_providerRepository = Substitute.For<IProviderRepository>();
_setupIntentCache = Substitute.For<ISetupIntentCache>();
_stripeFacade = Substitute.For<IStripeFacade>();
_stripeEventService = new StripeEventService(globalSettings, Substitute.For<ILogger<StripeEventService>>(), _stripeFacade);
_stripeEventService = new StripeEventService(globalSettings, _organizationRepository, _providerRepository, _setupIntentCache, _stripeFacade);
}
#region GetCharge
@@ -29,50 +36,44 @@ public class StripeEventServiceTests
public async Task GetCharge_EventNotChargeRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", new Invoice { Id = "in_test" });
// Act
var function = async () => await _stripeEventService.GetCharge(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetCharge(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
Arg.Any<string>(),
Arg.Any<ChargeGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<ChargeGetOptions>());
}
[Fact]
public async Task GetCharge_NotFresh_ReturnsEventCharge()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
var mockCharge = new Charge { Id = "ch_test", Amount = 1000 };
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", mockCharge);
// Act
var charge = await _stripeEventService.GetCharge(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as Charge, charge, true);
Assert.Equal(mockCharge.Id, charge.Id);
Assert.Equal(mockCharge.Amount, charge.Amount);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
Arg.Any<string>(),
Arg.Any<ChargeGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<ChargeGetOptions>());
}
[Fact]
public async Task GetCharge_Fresh_Expand_ReturnsAPICharge()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
var eventCharge = new Charge { Id = "ch_test", Amount = 1000 };
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", eventCharge);
var eventCharge = stripeEvent.Data.Object as Charge;
var apiCharge = Copy(eventCharge);
var apiCharge = new Charge { Id = "ch_test", Amount = 2000 };
var expand = new List<string> { "customer" };
@@ -90,9 +91,7 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetCharge(
apiCharge.Id,
Arg.Is<ChargeGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<ChargeGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -101,50 +100,44 @@ public class StripeEventServiceTests
public async Task GetCustomer_EventNotCustomerRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", new Invoice { Id = "in_test" });
// Act
var function = async () => await _stripeEventService.GetCustomer(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetCustomer(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
Arg.Any<string>(),
Arg.Any<CustomerGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<CustomerGetOptions>());
}
[Fact]
public async Task GetCustomer_NotFresh_ReturnsEventCustomer()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var mockCustomer = new Customer { Id = "cus_test", Email = "test@example.com" };
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", mockCustomer);
// Act
var customer = await _stripeEventService.GetCustomer(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as Customer, customer, true);
Assert.Equal(mockCustomer.Id, customer.Id);
Assert.Equal(mockCustomer.Email, customer.Email);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
Arg.Any<string>(),
Arg.Any<CustomerGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<CustomerGetOptions>());
}
[Fact]
public async Task GetCustomer_Fresh_Expand_ReturnsAPICustomer()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var eventCustomer = new Customer { Id = "cus_test", Email = "test@example.com" };
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", eventCustomer);
var eventCustomer = stripeEvent.Data.Object as Customer;
var apiCustomer = Copy(eventCustomer);
var apiCustomer = new Customer { Id = "cus_test", Email = "updated@example.com" };
var expand = new List<string> { "subscriptions" };
@@ -162,9 +155,7 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetCustomer(
apiCustomer.Id,
Arg.Is<CustomerGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<CustomerGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -173,50 +164,44 @@ public class StripeEventServiceTests
public async Task GetInvoice_EventNotInvoiceRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
// Act
var function = async () => await _stripeEventService.GetInvoice(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetInvoice(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
Arg.Any<string>(),
Arg.Any<InvoiceGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<InvoiceGetOptions>());
}
[Fact]
public async Task GetInvoice_NotFresh_ReturnsEventInvoice()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var mockInvoice = new Invoice { Id = "in_test", AmountDue = 1000 };
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", mockInvoice);
// Act
var invoice = await _stripeEventService.GetInvoice(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as Invoice, invoice, true);
Assert.Equal(mockInvoice.Id, invoice.Id);
Assert.Equal(mockInvoice.AmountDue, invoice.AmountDue);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
Arg.Any<string>(),
Arg.Any<InvoiceGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<InvoiceGetOptions>());
}
[Fact]
public async Task GetInvoice_Fresh_Expand_ReturnsAPIInvoice()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var eventInvoice = new Invoice { Id = "in_test", AmountDue = 1000 };
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", eventInvoice);
var eventInvoice = stripeEvent.Data.Object as Invoice;
var apiInvoice = Copy(eventInvoice);
var apiInvoice = new Invoice { Id = "in_test", AmountDue = 2000 };
var expand = new List<string> { "customer" };
@@ -234,9 +219,7 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetInvoice(
apiInvoice.Id,
Arg.Is<InvoiceGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<InvoiceGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -245,50 +228,44 @@ public class StripeEventServiceTests
public async Task GetPaymentMethod_EventNotPaymentMethodRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
// Act
var function = async () => await _stripeEventService.GetPaymentMethod(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetPaymentMethod(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
Arg.Any<string>(),
Arg.Any<PaymentMethodGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<PaymentMethodGetOptions>());
}
[Fact]
public async Task GetPaymentMethod_NotFresh_ReturnsEventPaymentMethod()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
var mockPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", mockPaymentMethod);
// Act
var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as PaymentMethod, paymentMethod, true);
Assert.Equal(mockPaymentMethod.Id, paymentMethod.Id);
Assert.Equal(mockPaymentMethod.Type, paymentMethod.Type);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
Arg.Any<string>(),
Arg.Any<PaymentMethodGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<PaymentMethodGetOptions>());
}
[Fact]
public async Task GetPaymentMethod_Fresh_Expand_ReturnsAPIPaymentMethod()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
var eventPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", eventPaymentMethod);
var eventPaymentMethod = stripeEvent.Data.Object as PaymentMethod;
var apiPaymentMethod = Copy(eventPaymentMethod);
var apiPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
var expand = new List<string> { "customer" };
@@ -306,9 +283,7 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetPaymentMethod(
apiPaymentMethod.Id,
Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -317,50 +292,44 @@ public class StripeEventServiceTests
public async Task GetSubscription_EventNotSubscriptionRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
// Act
var function = async () => await _stripeEventService.GetSubscription(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetSubscription(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
Arg.Any<string>(),
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task GetSubscription_NotFresh_ReturnsEventSubscription()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test", Status = "active" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
// Act
var subscription = await _stripeEventService.GetSubscription(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as Subscription, subscription, true);
Assert.Equal(mockSubscription.Id, subscription.Id);
Assert.Equal(mockSubscription.Status, subscription.Status);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
Arg.Any<string>(),
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task GetSubscription_Fresh_Expand_ReturnsAPISubscription()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var eventSubscription = new Subscription { Id = "sub_test", Status = "active" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", eventSubscription);
var eventSubscription = stripeEvent.Data.Object as Subscription;
var apiSubscription = Copy(eventSubscription);
var apiSubscription = new Subscription { Id = "sub_test", Status = "canceled" };
var expand = new List<string> { "customer" };
@@ -378,9 +347,71 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetSubscription(
apiSubscription.Id,
Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand));
}
#endregion
#region GetSetupIntent
[Fact]
public async Task GetSetupIntent_EventNotSetupIntentRelated_ThrowsException()
{
// Arrange
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetSetupIntent(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(SetupIntent)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(
Arg.Any<string>(),
Arg.Any<SetupIntentGetOptions>());
}
[Fact]
public async Task GetSetupIntent_NotFresh_ReturnsEventSetupIntent()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test", Status = "succeeded" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
// Act
var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent);
// Assert
Assert.Equal(mockSetupIntent.Id, setupIntent.Id);
Assert.Equal(mockSetupIntent.Status, setupIntent.Status);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(
Arg.Any<string>(),
Arg.Any<SetupIntentGetOptions>());
}
[Fact]
public async Task GetSetupIntent_Fresh_Expand_ReturnsAPISetupIntent()
{
// Arrange
var eventSetupIntent = new SetupIntent { Id = "seti_test", Status = "succeeded" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", eventSetupIntent);
var apiSetupIntent = new SetupIntent { Id = "seti_test", Status = "requires_action" };
var expand = new List<string> { "customer" };
_stripeFacade.GetSetupIntent(
apiSetupIntent.Id,
Arg.Is<SetupIntentGetOptions>(options => options.Expand == expand))
.Returns(apiSetupIntent);
// Act
var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent, true, expand);
// Assert
Assert.Equal(apiSetupIntent, setupIntent);
Assert.NotSame(eventSetupIntent, setupIntent);
await _stripeFacade.Received().GetSetupIntent(
apiSetupIntent.Id,
Arg.Is<SetupIntentGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -389,18 +420,16 @@ public class StripeEventServiceTests
public async Task ValidateCloudRegion_SubscriptionUpdated_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
var subscription = Copy(stripeEvent.Data.Object as Subscription);
var customer = await GetCustomerAsync();
subscription.Customer = customer;
var customer = CreateMockCustomer();
mockSubscription.Customer = customer;
_stripeFacade.GetSubscription(
subscription.Id,
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
.Returns(mockSubscription);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -409,28 +438,24 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetSubscription(
subscription.Id,
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_ChargeSucceeded_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
var mockCharge = new Charge { Id = "ch_test" };
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", mockCharge);
var charge = Copy(stripeEvent.Data.Object as Charge);
var customer = await GetCustomerAsync();
charge.Customer = customer;
var customer = CreateMockCustomer();
mockCharge.Customer = customer;
_stripeFacade.GetCharge(
charge.Id,
mockCharge.Id,
Arg.Any<ChargeGetOptions>())
.Returns(charge);
.Returns(mockCharge);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -439,24 +464,21 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetCharge(
charge.Id,
Arg.Any<ChargeGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockCharge.Id,
Arg.Any<ChargeGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_UpcomingInvoice_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceUpcoming);
var mockInvoice = new Invoice { Id = "in_test", CustomerId = "cus_test" };
var stripeEvent = CreateMockEvent("evt_test", "invoice.upcoming", mockInvoice);
var invoice = Copy(stripeEvent.Data.Object as Invoice);
var customer = await GetCustomerAsync();
var customer = CreateMockCustomer();
_stripeFacade.GetCustomer(
invoice.CustomerId,
mockInvoice.CustomerId,
Arg.Any<CustomerGetOptions>())
.Returns(customer);
@@ -467,28 +489,24 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetCustomer(
invoice.CustomerId,
Arg.Any<CustomerGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockInvoice.CustomerId,
Arg.Any<CustomerGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_InvoiceCreated_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var mockInvoice = new Invoice { Id = "in_test" };
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", mockInvoice);
var invoice = Copy(stripeEvent.Data.Object as Invoice);
var customer = await GetCustomerAsync();
invoice.Customer = customer;
var customer = CreateMockCustomer();
mockInvoice.Customer = customer;
_stripeFacade.GetInvoice(
invoice.Id,
mockInvoice.Id,
Arg.Any<InvoiceGetOptions>())
.Returns(invoice);
.Returns(mockInvoice);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -497,28 +515,24 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetInvoice(
invoice.Id,
Arg.Any<InvoiceGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockInvoice.Id,
Arg.Any<InvoiceGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_PaymentMethodAttached_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
var mockPaymentMethod = new PaymentMethod { Id = "pm_test" };
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", mockPaymentMethod);
var paymentMethod = Copy(stripeEvent.Data.Object as PaymentMethod);
var customer = await GetCustomerAsync();
paymentMethod.Customer = customer;
var customer = CreateMockCustomer();
mockPaymentMethod.Customer = customer;
_stripeFacade.GetPaymentMethod(
paymentMethod.Id,
mockPaymentMethod.Id,
Arg.Any<PaymentMethodGetOptions>())
.Returns(paymentMethod);
.Returns(mockPaymentMethod);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -527,24 +541,21 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetPaymentMethod(
paymentMethod.Id,
Arg.Any<PaymentMethodGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockPaymentMethod.Id,
Arg.Any<PaymentMethodGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_CustomerUpdated_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var customer = Copy(stripeEvent.Data.Object as Customer);
var mockCustomer = CreateMockCustomer();
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", mockCustomer);
_stripeFacade.GetCustomer(
customer.Id,
mockCustomer.Id,
Arg.Any<CustomerGetOptions>())
.Returns(customer);
.Returns(mockCustomer);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -553,29 +564,24 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetCustomer(
customer.Id,
Arg.Any<CustomerGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockCustomer.Id,
Arg.Any<CustomerGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_MetadataNull_ReturnsFalse()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
var subscription = Copy(stripeEvent.Data.Object as Subscription);
var customer = await GetCustomerAsync();
customer.Metadata = null;
subscription.Customer = customer;
var customer = new Customer { Id = "cus_test", Metadata = null };
mockSubscription.Customer = customer;
_stripeFacade.GetSubscription(
subscription.Id,
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
.Returns(mockSubscription);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -584,29 +590,24 @@ public class StripeEventServiceTests
Assert.False(cloudRegionValid);
await _stripeFacade.Received(1).GetSubscription(
subscription.Id,
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_MetadataNoRegion_DefaultUS_ReturnsTrue()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
var subscription = Copy(stripeEvent.Data.Object as Subscription);
var customer = await GetCustomerAsync();
customer.Metadata = new Dictionary<string, string>();
subscription.Customer = customer;
var customer = new Customer { Id = "cus_test", Metadata = new Dictionary<string, string>() };
mockSubscription.Customer = customer;
_stripeFacade.GetSubscription(
subscription.Id,
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
.Returns(mockSubscription);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -615,32 +616,28 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetSubscription(
subscription.Id,
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_MetadataMiscasedRegion_ReturnsTrue()
public async Task ValidateCloudRegion_MetadataIncorrectlyCasedRegion_ReturnsTrue()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
var subscription = Copy(stripeEvent.Data.Object as Subscription);
var customer = await GetCustomerAsync();
customer.Metadata = new Dictionary<string, string>
var customer = new Customer
{
{ "Region", "US" }
Id = "cus_test",
Metadata = new Dictionary<string, string> { { "Region", "US" } }
};
subscription.Customer = customer;
mockSubscription.Customer = customer;
_stripeFacade.GetSubscription(
subscription.Id,
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
.Returns(mockSubscription);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -649,31 +646,209 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetSubscription(
subscription.Id,
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationCustomer_Success()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
var organizationId = Guid.NewGuid();
var organizationCustomerId = "cus_org_test";
var mockOrganization = new Core.AdminConsole.Entities.Organization
{
Id = organizationId,
GatewayCustomerId = organizationCustomerId
};
var customer = CreateMockCustomer();
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns(organizationId);
_organizationRepository.GetByIdAsync(organizationId)
.Returns(mockOrganization);
_stripeFacade.GetCustomer(organizationCustomerId)
.Returns(customer);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.True(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.Received(1).GetByIdAsync(organizationId);
await _stripeFacade.Received(1).GetCustomer(organizationCustomerId);
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderCustomer_Success()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
var providerId = Guid.NewGuid();
var providerCustomerId = "cus_provider_test";
var mockProvider = new Core.AdminConsole.Entities.Provider.Provider
{
Id = providerId,
GatewayCustomerId = providerCustomerId
};
var customer = CreateMockCustomer();
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns(providerId);
_organizationRepository.GetByIdAsync(providerId)
.Returns((Core.AdminConsole.Entities.Organization?)null);
_providerRepository.GetByIdAsync(providerId)
.Returns(mockProvider);
_stripeFacade.GetCustomer(providerCustomerId)
.Returns(customer);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.True(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.Received(1).GetByIdAsync(providerId);
await _providerRepository.Received(1).GetByIdAsync(providerId);
await _stripeFacade.Received(1).GetCustomer(providerCustomerId);
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_NoSubscriberIdInCache_ReturnsFalse()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns((Guid?)null);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.False(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await _providerRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any<string>());
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationWithoutGatewayCustomerId_ChecksProvider()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
var subscriberId = Guid.NewGuid();
var providerCustomerId = "cus_provider_test";
var mockOrganizationWithoutCustomerId = new Core.AdminConsole.Entities.Organization
{
Id = subscriberId,
GatewayCustomerId = null
};
var mockProvider = new Core.AdminConsole.Entities.Provider.Provider
{
Id = subscriberId,
GatewayCustomerId = providerCustomerId
};
var customer = CreateMockCustomer();
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns(subscriberId);
_organizationRepository.GetByIdAsync(subscriberId)
.Returns(mockOrganizationWithoutCustomerId);
_providerRepository.GetByIdAsync(subscriberId)
.Returns(mockProvider);
_stripeFacade.GetCustomer(providerCustomerId)
.Returns(customer);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.True(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.Received(1).GetByIdAsync(subscriberId);
await _providerRepository.Received(1).GetByIdAsync(subscriberId);
await _stripeFacade.Received(1).GetCustomer(providerCustomerId);
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderWithoutGatewayCustomerId_ReturnsFalse()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
var subscriberId = Guid.NewGuid();
var mockProviderWithoutCustomerId = new Core.AdminConsole.Entities.Provider.Provider
{
Id = subscriberId,
GatewayCustomerId = null
};
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns(subscriberId);
_organizationRepository.GetByIdAsync(subscriberId)
.Returns((Core.AdminConsole.Entities.Organization?)null);
_providerRepository.GetByIdAsync(subscriberId)
.Returns(mockProviderWithoutCustomerId);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.False(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.Received(1).GetByIdAsync(subscriberId);
await _providerRepository.Received(1).GetByIdAsync(subscriberId);
await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any<string>());
}
#endregion
private static T Copy<T>(T input)
private static Event CreateMockEvent<T>(string id, string type, T dataObject) where T : IStripeEntity
{
var copy = (T)Activator.CreateInstance(typeof(T));
var properties = input.GetType().GetProperties();
foreach (var property in properties)
return new Event
{
var value = property.GetValue(input);
copy!
.GetType()
.GetProperty(property.Name)!
.SetValue(copy, value);
}
return copy;
Id = id,
Type = type,
Data = new EventData
{
Object = (IHasObject)dataObject
}
};
}
private static async Task<Customer> GetCustomerAsync()
=> (await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated)).Data.Object as Customer;
private static Customer CreateMockCustomer()
{
return new Customer
{
Id = "cus_test",
Metadata = new Dictionary<string, string> { { "region", "US" } }
};
}
}

View File

@@ -11,7 +11,6 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
@@ -33,7 +32,6 @@ public class SubscriptionUpdatedHandlerTests
private readonly IStripeFacade _stripeFacade;
private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;
private readonly IUserService _userService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationEnableCommand _organizationEnableCommand;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
@@ -42,6 +40,7 @@ public class SubscriptionUpdatedHandlerTests
private readonly IProviderRepository _providerRepository;
private readonly IProviderService _providerService;
private readonly IScheduler _scheduler;
private readonly IPushNotificationAdapter _pushNotificationAdapter;
private readonly SubscriptionUpdatedHandler _sut;
public SubscriptionUpdatedHandlerTests()
@@ -53,7 +52,6 @@ public class SubscriptionUpdatedHandlerTests
_organizationSponsorshipRenewCommand = Substitute.For<IOrganizationSponsorshipRenewCommand>();
_userService = Substitute.For<IUserService>();
_providerService = Substitute.For<IProviderService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_organizationRepository = Substitute.For<IOrganizationRepository>();
var schedulerFactory = Substitute.For<ISchedulerFactory>();
_organizationEnableCommand = Substitute.For<IOrganizationEnableCommand>();
@@ -64,6 +62,7 @@ public class SubscriptionUpdatedHandlerTests
_providerService = Substitute.For<IProviderService>();
var logger = Substitute.For<ILogger<SubscriptionUpdatedHandler>>();
_scheduler = Substitute.For<IScheduler>();
_pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();
schedulerFactory.GetScheduler().Returns(_scheduler);
@@ -74,7 +73,6 @@ public class SubscriptionUpdatedHandlerTests
_stripeFacade,
_organizationSponsorshipRenewCommand,
_userService,
_pushNotificationService,
_organizationRepository,
schedulerFactory,
_organizationEnableCommand,
@@ -83,7 +81,8 @@ public class SubscriptionUpdatedHandlerTests
_featureService,
_providerRepository,
_providerService,
logger);
logger,
_pushNotificationAdapter);
}
[Fact]
@@ -540,8 +539,8 @@ public class SubscriptionUpdatedHandlerTests
.EnableAsync(organizationId);
await _organizationService.Received(1)
.UpdateExpirationDateAsync(organizationId, currentPeriodEnd);
await _pushNotificationService.Received(1)
.PushSyncOrganizationStatusAsync(organization);
await _pushNotificationAdapter.Received(1)
.NotifyEnabledChangedAsync(organization);
}
[Fact]

View File

@@ -71,7 +71,7 @@ public class GetOrganizationWarningsQueryTests
});
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns((string?)null);
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(organization.Id).Returns((string?)null);
var response = await sutProvider.Sut.Run(organization);
@@ -109,7 +109,7 @@ public class GetOrganizationWarningsQueryTests
});
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns(setupIntentId);
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(organization.Id).Returns(setupIntentId);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(
options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent
{

View File

@@ -74,7 +74,10 @@ public class UpdatePaymentMethodCommandTests
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
{
HostedVerificationUrl = "https://example.com"
}
},
Status = "requires_action"
};
@@ -95,7 +98,7 @@ public class UpdatePaymentMethodCommandTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
}
@@ -133,7 +136,10 @@ public class UpdatePaymentMethodCommandTests
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
{
HostedVerificationUrl = "https://example.com"
}
},
Status = "requires_action"
};
@@ -154,7 +160,7 @@ public class UpdatePaymentMethodCommandTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
await _subscriberService.Received(1).CreateStripeCustomer(organization);
@@ -199,7 +205,10 @@ public class UpdatePaymentMethodCommandTests
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
{
HostedVerificationUrl = "https://example.com"
}
},
Status = "requires_action"
};
@@ -220,7 +229,7 @@ public class UpdatePaymentMethodCommandTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>

View File

@@ -1,81 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Extensions;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Payment.Commands;
public class VerifyBankAccountCommandTests
{
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
private readonly VerifyBankAccountCommand _command;
public VerifyBankAccountCommandTests()
{
_command = new VerifyBankAccountCommand(
Substitute.For<ILogger<VerifyBankAccountCommand>>(),
_setupIntentCache,
_stripeAdapter);
}
[Fact]
public async Task Run_MakesCorrectInvocations_ReturnsMaskedBankAccount()
{
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewayCustomerId = "cus_123"
};
const string setupIntentId = "seti_123";
_setupIntentCache.Get(organization.Id).Returns(setupIntentId);
var setupIntent = new SetupIntent
{
Id = setupIntentId,
PaymentMethodId = "pm_123",
PaymentMethod =
new PaymentMethod
{
Id = "pm_123",
Type = "us_bank_account",
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
},
Status = "requires_action"
};
_stripeAdapter.SetupIntentGet(setupIntentId,
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method"))).Returns(setupIntent);
_stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
Arg.Is<PaymentMethodAttachOptions>(options => options.Customer == organization.GatewayCustomerId))
.Returns(setupIntent.PaymentMethod);
var result = await _command.Run(organization, "DESCRIPTOR_CODE");
Assert.True(result.IsT0);
var maskedPaymentMethod = result.AsT0;
Assert.True(maskedPaymentMethod.IsT0);
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.True(maskedBankAccount.Verified);
await _stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id,
Arg.Is<SetupIntentVerifyMicrodepositsOptions>(options => options.DescriptorCode == "DESCRIPTOR_CODE"));
await _stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId));
}
}

View File

@@ -13,7 +13,7 @@ public class MaskedPaymentMethodTests
{
BankName = "Chase",
Last4 = "9999",
Verified = true
HostedVerificationUrl = "https://example.com"
};
var json = JsonSerializer.Serialize(input);
@@ -32,7 +32,7 @@ public class MaskedPaymentMethodTests
{
BankName = "Chase",
Last4 = "9999",
Verified = true
HostedVerificationUrl = "https://example.com"
};
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

View File

@@ -108,7 +108,7 @@ public class GetPaymentMethodQueryTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.True(maskedBankAccount.Verified);
Assert.Null(maskedBankAccount.HostedVerificationUrl);
}
[Fact]
@@ -142,7 +142,7 @@ public class GetPaymentMethodQueryTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.True(maskedBankAccount.Verified);
Assert.Null(maskedBankAccount.HostedVerificationUrl);
}
[Fact]
@@ -163,7 +163,7 @@ public class GetPaymentMethodQueryTests
Arg.Is<CustomerGetOptions>(options =>
options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer);
_setupIntentCache.Get(organization.Id).Returns("seti_123");
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
_stripeAdapter
.SetupIntentGet("seti_123",
@@ -177,7 +177,10 @@ public class GetPaymentMethodQueryTests
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
{
HostedVerificationUrl = "https://example.com"
}
},
Status = "requires_action"
});
@@ -189,7 +192,7 @@ public class GetPaymentMethodQueryTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
}
[Fact]

View File

@@ -670,7 +670,7 @@ public class SubscriberServiceTests
}
};
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id,
Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
@@ -1876,7 +1876,7 @@ public class SubscriberServiceTests
PaymentMethodId = "payment_method_id"
};
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();

View File

@@ -651,31 +651,6 @@ public class AzureQueuePushEngineTests
);
}
[Fact]
public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse()
{
var organization = new Organization
{
Id = Guid.NewGuid(),
Enabled = true,
};
var expectedPayload = new JsonObject
{
["Type"] = 18,
["Payload"] = new JsonObject
{
["OrganizationId"] = organization.Id,
["Enabled"] = organization.Enabled,
},
};
await VerifyNotificationAsync(
async sut => await sut.PushSyncOrganizationStatusAsync(organization),
expectedPayload
);
}
[Fact]
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
{

View File

@@ -413,21 +413,6 @@ public abstract class PushTestBase
);
}
[Fact]
public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse()
{
var organization = new Organization
{
Id = Guid.NewGuid(),
Enabled = true,
};
await VerifyNotificationAsync(
async sut => await sut.PushSyncOrganizationStatusAsync(organization),
GetPushSyncOrganizationStatusResponsePayload(organization)
);
}
[Fact]
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
{