1
0
mirror of https://github.com/bitwarden/server synced 2026-02-05 11:13:20 +00:00
Files
server/test/Core.Test/Billing/Services/SubscriberServiceTests.cs
2026-01-21 21:50:38 +01:00

2352 lines
92 KiB
C#

using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Enums;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Braintree;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
using Address = Stripe.Address;
using Customer = Stripe.Customer;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
using PaymentMethod = Stripe.PaymentMethod;
using Subscription = Stripe.Subscription;
namespace Bit.Core.Test.Billing.Services;
[SutProviderCustomize]
public class SubscriberServiceTests
{
#region CancelSubscription
[Theory, BitAutoData]
public async Task CancelSubscription_SubscriptionInactive_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var subscription = new Subscription
{
Status = "canceled"
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
await ThrowsBillingExceptionAsync(() =>
sutProvider.Sut.CancelSubscription(organization, new OffboardingSurveyResponse(), false));
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.CancelSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
}
[Theory, BitAutoData]
public async Task CancelSubscription_CancelImmediately_BelongsToOrganization_UpdatesSubscription_CancelSubscriptionImmediately(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var userId = Guid.NewGuid();
const string subscriptionId = "subscription_id";
var subscription = new Subscription
{
Id = subscriptionId,
Status = "active",
Metadata = new Dictionary<string, string>
{
{ "organizationId", "organization_id" }
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, true);
await stripeAdapter
.Received(1)
.UpdateSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(
options => options.Metadata["cancellingUserId"] == userId.ToString()));
await stripeAdapter
.Received(1)
.CancelSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason));
}
[Theory, BitAutoData]
public async Task CancelSubscription_CancelImmediately_BelongsToUser_CancelSubscriptionImmediately(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var userId = Guid.NewGuid();
const string subscriptionId = "subscription_id";
var subscription = new Subscription
{
Id = subscriptionId,
Status = "active",
Metadata = new Dictionary<string, string>
{
{ "userId", "user_id" }
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, true);
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await stripeAdapter
.Received(1)
.CancelSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason));
}
[Theory, BitAutoData]
public async Task CancelSubscription_DoNotCancelImmediately_UpdateSubscriptionToCancelAtEndOfPeriod(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var userId = Guid.NewGuid();
const string subscriptionId = "subscription_id";
organization.ExpirationDate = DateTime.UtcNow.AddDays(5);
var subscription = new Subscription
{
Id = subscriptionId,
Status = "active",
Metadata = new Dictionary<string, string>()
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = userId,
Reason = "missing_features",
Feedback = "Lorem ipsum"
};
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, false);
await stripeAdapter
.Received(1)
.UpdateSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAtPeriodEnd == true &&
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason &&
options.Metadata["cancellingUserId"] == userId.ToString()));
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.CancelSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
}
[Theory, BitAutoData]
public async Task CancelSubscription_WithPremiumUpgradeMetadata_DuringTrial_RevertsToOriginalPremiumPlan(
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewaySubscriptionId = "sub_test"
};
var userId = Guid.NewGuid();
var user = new Bit.Core.Entities.User
{
Id = userId,
Premium = false,
GatewaySubscriptionId = null
};
var previousPeriodEndDate = DateTime.UtcNow.AddMonths(1);
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Trialing,
CustomerId = "cus_test",
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new() { Id = "si_org_seat", Price = new Price { Id = "org-seat-price" } }
}
},
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = "premium-annually-2020",
[StripeConstants.MetadataKeys.UpgradedOrganizationId] = organization.Id.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = userId.ToString(),
[StripeConstants.MetadataKeys.PreviousPeriodEndDate] = previousPeriodEndDate.ToString("O"),
[StripeConstants.MetadataKeys.PreviousAdditionalStorage] = "5",
[StripeConstants.MetadataKeys.PreviousStoragePriceId] = "storage-annually-2020",
[StripeConstants.MetadataKeys.OrganizationId] = organization.Id.ToString()
}
};
var premiumPlan = new Bit.Core.Billing.Pricing.Premium.Plan
{
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "premium-annually",
Price = 10m,
Provided = 1
},
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "storage-annually",
Price = 4m,
Provided = 1
},
Available = true
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var userRepository = sutProvider.GetDependency<Bit.Core.Repositories.IUserRepository>();
var organizationRepository = sutProvider.GetDependency<Bit.Core.Repositories.IOrganizationRepository>();
var pricingClient = sutProvider.GetDependency<Bit.Core.Billing.Pricing.IPricingClient>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
userRepository.GetByIdAsync(userId).Returns(user);
pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
// Act
await sutProvider.Sut.CancelSubscription(
organization,
new OffboardingSurveyResponse { UserId = Guid.NewGuid() },
false);
// Assert
await stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_test",
Arg.Is<SubscriptionUpdateOptions>((SubscriptionUpdateOptions opts) =>
opts.Items != null &&
opts.Items.Count == 3 && // 1 delete + 1 premium seat + 1 storage
opts.Items.Count(i => i.Deleted == true) == 1 &&
opts.Items.Any(i => i.Price == "premium-annually-2020" && i.Quantity == 1) &&
opts.Items.Any(i => i.Price == "storage-annually-2020" && i.Quantity == 5) &&
opts.Metadata[StripeConstants.MetadataKeys.UserId] == userId.ToString() &&
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousPremiumPriceId) &&
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId)));
await userRepository.Received(1).ReplaceAsync(Arg.Is<Bit.Core.Entities.User>(u =>
u.Id == userId &&
u.Premium == true &&
u.GatewaySubscriptionId == "sub_test"));
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(o =>
o.Id == organization.Id &&
o.GatewaySubscriptionId == null));
}
[Theory, BitAutoData]
public async Task CancelSubscription_WithPremiumUpgradeMetadata_AfterTrial_DoesNotRevert(
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewaySubscriptionId = "sub_test"
};
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Active, // Already active, not trialing
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = "premium-annually-2020",
[StripeConstants.MetadataKeys.UpgradedOrganizationId] = organization.Id.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = Guid.NewGuid().ToString(),
[StripeConstants.MetadataKeys.OrganizationId] = organization.Id.ToString()
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = Guid.NewGuid(),
Reason = "other",
Feedback = "test"
};
// Act
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, false);
// Assert - should fall through to standard cancellation, not reversion
await stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_test",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.CancelAtPeriodEnd == true)); // Standard cancellation
}
[Theory, BitAutoData]
public async Task CancelSubscription_WithoutPremiumUpgradeMetadata_UsesStandardCancellation(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Active,
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.OrganizationId] = organization.Id.ToString()
// No premium upgrade metadata
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = Guid.NewGuid(),
Reason = "other",
Feedback = "test"
};
// Act
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, false);
// Assert - should use standard cancellation
await stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_test",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.CancelAtPeriodEnd == true));
}
[Theory, BitAutoData]
public async Task CancelSubscription_WithPremiumUpgradeMetadata_UserNotFound_ThrowsBillingException(
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewaySubscriptionId = "sub_test"
};
var userId = Guid.NewGuid();
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Trialing,
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = "premium-annually",
[StripeConstants.MetadataKeys.UpgradedOrganizationId] = organization.Id.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = userId.ToString()
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var userRepository = sutProvider.GetDependency<Bit.Core.Repositories.IUserRepository>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
userRepository.GetByIdAsync(userId).ReturnsNull();
// Act & Assert
var exception = await Assert.ThrowsAsync<BillingException>(() =>
sutProvider.Sut.CancelSubscription(
organization,
new OffboardingSurveyResponse { UserId = Guid.NewGuid() },
false));
Assert.Equal("Failed to revert subscription upgrade", exception.Message);
Assert.NotNull(exception.InnerException);
Assert.IsType<BillingException>(exception.InnerException);
Assert.Equal("Cannot revert subscription - original Premium user not found", exception.InnerException.Message);
}
[Theory, BitAutoData]
public async Task CancelSubscription_WithPremiumUpgradeMetadata_WrongOrganizationId_DoesNotRevert(
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewaySubscriptionId = "sub_test"
};
var differentOrgId = Guid.NewGuid();
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Active,
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = "premium-annually",
[StripeConstants.MetadataKeys.UpgradedOrganizationId] = differentOrgId.ToString(), // Wrong org
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = Guid.NewGuid().ToString()
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
{
UserId = Guid.NewGuid(),
Reason = "other",
Feedback = "test"
};
// Act
await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, false);
// Assert - should fall through to standard cancellation
await stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_test",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.CancelAtPeriodEnd == true));
}
[Theory, BitAutoData]
public async Task CancelSubscription_DuringReversion_UsesHistoricalSeatPriceFromMetadata(
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewaySubscriptionId = "sub_test"
};
var userId = Guid.NewGuid();
var user = new Bit.Core.Entities.User { Id = userId, Premium = false };
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Trialing,
CustomerId = "cus_test",
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new() { Id = "si_org", Price = new Price { Id = "org-price" } }
}
},
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = "premium-annually-2020", // Historical
[StripeConstants.MetadataKeys.UpgradedOrganizationId] = organization.Id.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = userId.ToString(),
[StripeConstants.MetadataKeys.PreviousAdditionalStorage] = "0"
}
};
var currentPremiumPlan = new Bit.Core.Billing.Pricing.Premium.Plan
{
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "premium-annually", // Current (different from metadata)
Price = 10m,
Provided = 1
},
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "storage-annually",
Price = 4m,
Provided = 1
},
Available = true
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var userRepository = sutProvider.GetDependency<Bit.Core.Repositories.IUserRepository>();
var pricingClient = sutProvider.GetDependency<Bit.Core.Billing.Pricing.IPricingClient>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
userRepository.GetByIdAsync(userId).Returns(user);
pricingClient.GetAvailablePremiumPlan().Returns(currentPremiumPlan);
// Act
await sutProvider.Sut.CancelSubscription(
organization,
new OffboardingSurveyResponse { UserId = Guid.NewGuid() },
false);
// Assert - should use historical price, not current
await stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_test",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Any(i => i.Price == "premium-annually-2020"))); // Historical, not "premium-annually"
}
[Theory, BitAutoData]
public async Task CancelSubscription_DuringReversion_UsesHistoricalStoragePriceFromMetadata(
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewaySubscriptionId = "sub_test"
};
var userId = Guid.NewGuid();
var user = new Bit.Core.Entities.User { Id = userId, Premium = false };
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Trialing,
CustomerId = "cus_test",
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new() { Id = "si_org", Price = new Price { Id = "org-price" } }
}
},
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = "premium-annually",
[StripeConstants.MetadataKeys.UpgradedOrganizationId] = organization.Id.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = userId.ToString(),
[StripeConstants.MetadataKeys.PreviousAdditionalStorage] = "3",
[StripeConstants.MetadataKeys.PreviousStoragePriceId] = "storage-annually-2020" // Historical
}
};
var currentPremiumPlan = new Bit.Core.Billing.Pricing.Premium.Plan
{
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "premium-annually",
Price = 10m,
Provided = 1
},
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "storage-annually", // Current (different from metadata)
Price = 4m,
Provided = 1
},
Available = true
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var userRepository = sutProvider.GetDependency<Bit.Core.Repositories.IUserRepository>();
var pricingClient = sutProvider.GetDependency<Bit.Core.Billing.Pricing.IPricingClient>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
userRepository.GetByIdAsync(userId).Returns(user);
pricingClient.GetAvailablePremiumPlan().Returns(currentPremiumPlan);
// Act
await sutProvider.Sut.CancelSubscription(
organization,
new OffboardingSurveyResponse { UserId = Guid.NewGuid() },
false);
// Assert - should use historical storage price, not current
await stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_test",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Any(i => i.Price == "storage-annually-2020" && i.Quantity == 3))); // Historical, not "storage-annually"
}
[Theory, BitAutoData]
public async Task CancelSubscription_DuringReversion_WithoutStoragePriceId_FallsBackToCurrentPrice(
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewaySubscriptionId = "sub_test"
};
var userId = Guid.NewGuid();
var user = new Bit.Core.Entities.User { Id = userId, Premium = false };
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Trialing,
CustomerId = "cus_test",
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new() { Id = "si_org", Price = new Price { Id = "org-price" } }
}
},
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = "premium-annually",
[StripeConstants.MetadataKeys.UpgradedOrganizationId] = organization.Id.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = userId.ToString(),
[StripeConstants.MetadataKeys.PreviousAdditionalStorage] = "2"
// No PreviousStoragePriceId - should fallback to current
}
};
var currentPremiumPlan = new Bit.Core.Billing.Pricing.Premium.Plan
{
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "premium-annually",
Price = 10m,
Provided = 1
},
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "storage-annually-current", // Current price
Price = 4m,
Provided = 1
},
Available = true
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var userRepository = sutProvider.GetDependency<Bit.Core.Repositories.IUserRepository>();
var pricingClient = sutProvider.GetDependency<Bit.Core.Billing.Pricing.IPricingClient>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
userRepository.GetByIdAsync(userId).Returns(user);
pricingClient.GetAvailablePremiumPlan().Returns(currentPremiumPlan);
// Act
await sutProvider.Sut.CancelSubscription(
organization,
new OffboardingSurveyResponse { UserId = Guid.NewGuid() },
false);
// Assert - should fallback to current storage price
await stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_test",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Any(i => i.Price == "storage-annually-current" && i.Quantity == 2))); // Current price used
}
[Theory, BitAutoData]
public async Task CancelSubscription_DuringReversion_RemovesAllPremiumUpgradeMetadata(
SutProvider<SubscriberService> sutProvider)
{
// Arrange
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewaySubscriptionId = "sub_test"
};
var userId = Guid.NewGuid();
var user = new Bit.Core.Entities.User { Id = userId, Premium = false };
var subscription = new Subscription
{
Id = "sub_test",
Status = StripeConstants.SubscriptionStatus.Trialing,
CustomerId = "cus_test",
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new() { Id = "si_org", Price = new Price { Id = "org-price" } }
}
},
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = "premium-annually",
[StripeConstants.MetadataKeys.UpgradedOrganizationId] = organization.Id.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = userId.ToString(),
[StripeConstants.MetadataKeys.PreviousPeriodEndDate] = DateTime.UtcNow.AddMonths(1).ToString("O"),
[StripeConstants.MetadataKeys.PreviousAdditionalStorage] = "5",
[StripeConstants.MetadataKeys.PreviousStoragePriceId] = "storage-annually",
[StripeConstants.MetadataKeys.OrganizationId] = organization.Id.ToString(),
["other_metadata"] = "should_remain"
}
};
var premiumPlan = new Bit.Core.Billing.Pricing.Premium.Plan
{
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "premium-annually",
Price = 10m,
Provided = 1
},
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
{
StripePriceId = "storage-annually",
Price = 4m,
Provided = 1
},
Available = true
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var userRepository = sutProvider.GetDependency<Bit.Core.Repositories.IUserRepository>();
var pricingClient = sutProvider.GetDependency<Bit.Core.Billing.Pricing.IPricingClient>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(subscription);
userRepository.GetByIdAsync(userId).Returns(user);
pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
// Act
await sutProvider.Sut.CancelSubscription(
organization,
new OffboardingSurveyResponse { UserId = Guid.NewGuid() },
false);
// Assert - all premium upgrade metadata should be removed
await stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_test",
Arg.Is<SubscriptionUpdateOptions>(opts =>
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousPremiumPriceId) &&
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousPeriodEndDate) &&
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.UpgradedOrganizationId) &&
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousPremiumUserId) &&
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousAdditionalStorage) &&
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousStoragePriceId) &&
!opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.UserId) &&
opts.Metadata[StripeConstants.MetadataKeys.UserId] == userId.ToString() &&
opts.Metadata.ContainsKey("other_metadata"))); // Other metadata preserved
}
#endregion
#region GetCustomer
[Theory, BitAutoData]
public async Task GetCustomer_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetCustomer(null));
[Theory, BitAutoData]
public async Task GetCustomer_NoGatewayCustomerId_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewayCustomerId = null;
var customer = await sutProvider.Sut.GetCustomer(organization);
Assert.Null(customer);
}
[Theory, BitAutoData]
public async Task GetCustomer_NoCustomer_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId)
.ReturnsNull();
var customer = await sutProvider.Sut.GetCustomer(organization);
Assert.Null(customer);
}
[Theory, BitAutoData]
public async Task GetCustomer_StripeException_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId)
.ThrowsAsync<StripeException>();
var customer = await sutProvider.Sut.GetCustomer(organization);
Assert.Null(customer);
}
[Theory, BitAutoData]
public async Task GetCustomer_Succeeds(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer();
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId)
.Returns(customer);
var gotCustomer = await sutProvider.Sut.GetCustomer(organization);
Assert.Equivalent(customer, gotCustomer);
}
#endregion
#region GetCustomerOrThrow
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetCustomerOrThrow(null));
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_NoGatewayCustomerId_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewayCustomerId = null;
await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_NoCustomer_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId)
.ReturnsNull();
await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_StripeException_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeException = new StripeException();
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId)
.ThrowsAsync(stripeException);
await ThrowsBillingExceptionAsync(
async () => await sutProvider.Sut.GetCustomerOrThrow(organization),
message: "An error occurred while trying to retrieve a Stripe customer",
innerException: stripeException);
}
[Theory, BitAutoData]
public async Task GetCustomerOrThrow_Succeeds(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer();
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId)
.Returns(customer);
var gotCustomer = await sutProvider.Sut.GetCustomerOrThrow(organization);
Assert.Equivalent(customer, gotCustomer);
}
#endregion
#region GetPaymentSource
[Theory, BitAutoData]
public async Task GetPaymentSource_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
[Theory, BitAutoData]
public async Task GetPaymentSource_Braintree_NoDefaultPaymentMethod_ReturnsNull(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "braintree_customer_id";
var customer = new Customer
{
Id = provider.GatewayCustomerId,
Metadata = new Dictionary<string, string>
{
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
}
};
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
.Returns(customer);
var (_, customerGateway, _) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var braintreeCustomer = Substitute.For<Braintree.Customer>();
braintreeCustomer.Id.Returns(braintreeCustomerId);
braintreeCustomer.PaymentMethods.Returns([]);
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
Assert.Null(paymentMethod);
}
[Theory, BitAutoData]
public async Task GetPaymentSource_Braintree_PayPalAccount_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "braintree_customer_id";
var customer = new Customer
{
Id = provider.GatewayCustomerId,
Metadata = new Dictionary<string, string>
{
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
}
};
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
.Returns(customer);
var (_, customerGateway, _) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var braintreeCustomer = Substitute.For<Braintree.Customer>();
braintreeCustomer.Id.Returns(braintreeCustomerId);
var payPalAccount = Substitute.For<PayPalAccount>();
payPalAccount.IsDefault.Returns(true);
payPalAccount.Email.Returns("a@example.com");
braintreeCustomer.PaymentMethods.Returns([payPalAccount]);
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
Assert.Equal(PaymentMethodType.PayPal, paymentMethod.Type);
Assert.Equal("a@example.com", paymentMethod.Description);
Assert.False(paymentMethod.NeedsVerification);
}
// TODO: Determine if we need to test Braintree.CreditCard
// TODO: Determine if we need to test Braintree.UsBankAccount
[Theory, BitAutoData]
public async Task GetPaymentSource_Stripe_BankAccountPaymentMethod_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = new PaymentMethod
{
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
UsBankAccount = new PaymentMethodUsBankAccount
{
BankName = "Chase",
Last4 = "9999"
}
}
}
};
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
.Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);
Assert.Equal("Chase, *9999", paymentMethod.Description);
Assert.False(paymentMethod.NeedsVerification);
}
[Theory, BitAutoData]
public async Task GetPaymentSource_Stripe_CardPaymentMethod_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = new PaymentMethod
{
Type = StripeConstants.PaymentMethodTypes.Card,
Card = new PaymentMethodCard
{
Brand = "Visa",
Last4 = "9999",
ExpMonth = 9,
ExpYear = 2028
}
}
}
};
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
.Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);
Assert.Equal("VISA, *9999, 09/2028", paymentMethod.Description);
Assert.False(paymentMethod.NeedsVerification);
}
[Theory, BitAutoData]
public async Task GetPaymentSource_Stripe_SetupIntentForBankAccount_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer { Id = provider.GatewayCustomerId };
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
.Returns(customer);
var setupIntent = new SetupIntent
{
Id = "setup_intent_id",
Status = "requires_action",
NextAction =
new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
},
PaymentMethod = new PaymentMethod
{
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
}
};
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<IStripeAdapter>().GetSetupIntentAsync(setupIntent.Id,
Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);
Assert.Equal("Chase, *9999", paymentMethod.Description);
Assert.True(paymentMethod.NeedsVerification);
}
[Theory, BitAutoData]
public async Task GetPaymentSource_Stripe_LegacyBankAccount_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
DefaultSource = new BankAccount { Status = "verified", BankName = "Chase", Last4 = "9999" }
};
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
.Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);
Assert.Equal("Chase, *9999 - Verified", paymentMethod.Description);
Assert.False(paymentMethod.NeedsVerification);
}
[Theory, BitAutoData]
public async Task GetPaymentSource_Stripe_LegacyCard_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
DefaultSource = new Card { Brand = "Visa", Last4 = "9999", ExpMonth = 9, ExpYear = 2028 }
};
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
.Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);
Assert.Equal("VISA, *9999, 09/2028", paymentMethod.Description);
Assert.False(paymentMethod.NeedsVerification);
}
[Theory, BitAutoData]
public async Task GetPaymentSource_Stripe_LegacySourceCard_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
DefaultSource = new Source
{
Card = new SourceCard
{
Brand = "Visa",
Last4 = "9999",
ExpMonth = 9,
ExpYear = 2028
}
}
};
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
.Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);
Assert.Equal("VISA, *9999, 09/2028", paymentMethod.Description);
Assert.False(paymentMethod.NeedsVerification);
}
#endregion
#region GetSubscription
[Theory, BitAutoData]
public async Task GetSubscription_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscription(null));
[Theory, BitAutoData]
public async Task GetSubscription_NoGatewaySubscriptionId_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewaySubscriptionId = null;
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_NoSubscription_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_StripeException_ReturnsNull(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.ThrowsAsync<StripeException>();
var subscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(subscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Succeeds(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>()
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
#region GetSubscriptionOrThrow
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscriptionOrThrow(null));
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NoGatewaySubscriptionId_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewaySubscriptionId = null;
await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NoSubscription_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_StripeException_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeException = new StripeException();
sutProvider.GetDependency<IStripeAdapter>()
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.ThrowsAsync(stripeException);
await ThrowsBillingExceptionAsync(
async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization),
message: "An error occurred while trying to retrieve a Stripe subscription",
innerException: stripeException);
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Succeeds(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>()
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(organization);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
#region RemovePaymentMethod
[Theory, BitAutoData]
public async Task RemovePaymentMethod_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.RemovePaymentSource(null));
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_NoCustomer_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "1";
var stripeCustomer = new Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (braintreeGateway, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
customerGateway.FindAsync(braintreeCustomerId).ReturnsNull();
braintreeGateway.Customer.Returns(customerGateway);
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_NoPaymentMethod_NoOp(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "1";
var stripeCustomer = new Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var braintreeCustomer = Substitute.For<Braintree.Customer>();
braintreeCustomer.PaymentMethods.Returns([]);
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
await sutProvider.Sut.RemovePaymentSource(organization);
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_CustomerUpdateFails_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "1";
const string braintreePaymentMethodToken = "TOKEN";
var stripeCustomer = new Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var braintreeCustomer = Substitute.For<Braintree.Customer>();
var paymentMethod = Substitute.For<Braintree.PaymentMethod>();
paymentMethod.Token.Returns(braintreePaymentMethodToken);
paymentMethod.IsDefault.Returns(true);
braintreeCustomer.PaymentMethods.Returns([
paymentMethod
]);
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var updateBraintreeCustomerResult = Substitute.For<Result<Braintree.Customer>>();
updateBraintreeCustomerResult.IsSuccess().Returns(false);
customerGateway.UpdateAsync(
braintreeCustomerId,
Arg.Is<CustomerRequest>(request => request.DefaultPaymentMethodToken == null))
.Returns(updateBraintreeCustomerResult);
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>
request.DefaultPaymentMethodToken == null));
await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(paymentMethod.Token);
await customerGateway.DidNotReceive().UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>
request.DefaultPaymentMethodToken == paymentMethod.Token));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Braintree_PaymentMethodDeleteFails_RollBack_ThrowsBillingException(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "1";
const string braintreePaymentMethodToken = "TOKEN";
var stripeCustomer = new Customer
{
Metadata = new Dictionary<string, string>
{
{ "btCustomerId", braintreeCustomerId }
}
};
sutProvider.GetDependency<IStripeAdapter>()
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var braintreeCustomer = Substitute.For<Braintree.Customer>();
var paymentMethod = Substitute.For<Braintree.PaymentMethod>();
paymentMethod.Token.Returns(braintreePaymentMethodToken);
paymentMethod.IsDefault.Returns(true);
braintreeCustomer.PaymentMethods.Returns([
paymentMethod
]);
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
var updateBraintreeCustomerResult = Substitute.For<Result<Braintree.Customer>>();
updateBraintreeCustomerResult.IsSuccess().Returns(true);
customerGateway.UpdateAsync(braintreeCustomerId, Arg.Any<CustomerRequest>())
.Returns(updateBraintreeCustomerResult);
var deleteBraintreePaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
deleteBraintreePaymentMethodResult.IsSuccess().Returns(false);
paymentMethodGateway.DeleteAsync(paymentMethod.Token).Returns(deleteBraintreePaymentMethodResult);
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>
request.DefaultPaymentMethodToken == null));
await paymentMethodGateway.Received(1).DeleteAsync(paymentMethod.Token);
await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>
request.DefaultPaymentMethodToken == paymentMethod.Token));
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Stripe_Legacy_RemovesSources(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string bankAccountId = "bank_account_id";
const string cardId = "card_id";
var sources = new List<IPaymentSource>
{
new BankAccount { Id = bankAccountId }, new Card { Id = cardId }
};
var stripeCustomer = new Customer { Sources = new StripeList<IPaymentSource> { Data = sources } };
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.ListPaymentMethodsAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<PaymentMethod>()));
await sutProvider.Sut.RemovePaymentSource(organization);
await stripeAdapter.Received(1).DeleteBankAccountAsync(stripeCustomer.Id, bankAccountId);
await stripeAdapter.Received(1).DeleteCardAsync(stripeCustomer.Id, cardId);
await stripeAdapter.DidNotReceiveWithAnyArgs()
.DetachPaymentMethodAsync(Arg.Any<string>(), Arg.Any<PaymentMethodDetachOptions>());
}
[Theory, BitAutoData]
public async Task RemovePaymentMethod_Stripe_DetachesPaymentMethods(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
const string bankAccountId = "bank_account_id";
const string cardId = "card_id";
var sources = new List<IPaymentSource>();
var stripeCustomer = new Customer { Sources = new StripeList<IPaymentSource> { Data = sources } };
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.ListPaymentMethodsAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<PaymentMethod>
{
new ()
{
Id = bankAccountId
},
new ()
{
Id = cardId
}
}));
await sutProvider.Sut.RemovePaymentSource(organization);
await stripeAdapter.DidNotReceiveWithAnyArgs().DeleteBankAccountAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.DidNotReceiveWithAnyArgs().DeleteCardAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.Received(1)
.DetachPaymentMethodAsync(bankAccountId);
await stripeAdapter.Received(1)
.DetachPaymentMethodAsync(cardId);
}
private static async IAsyncEnumerable<PaymentMethod> GetPaymentMethodsAsync(
IEnumerable<PaymentMethod> paymentMethods)
{
foreach (var paymentMethod in paymentMethods)
{
yield return paymentMethod;
}
await Task.CompletedTask;
}
private static (IBraintreeGateway, ICustomerGateway, IPaymentMethodGateway) SetupBraintree(
IBraintreeGateway braintreeGateway)
{
var customerGateway = Substitute.For<ICustomerGateway>();
var paymentMethodGateway = Substitute.For<IPaymentMethodGateway>();
braintreeGateway.Customer.Returns(customerGateway);
braintreeGateway.PaymentMethod.Returns(paymentMethodGateway);
return (braintreeGateway, customerGateway, paymentMethodGateway);
}
#endregion
#region UpdatePaymentMethod
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdatePaymentSource(null, null));
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_NullTokenizedPaymentMethod_ThrowsArgumentNullException(
Provider provider,
SutProvider<SubscriberService> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdatePaymentSource(provider, null));
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_NoToken_ThrowsBillingException(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer());
await ThrowsBillingExceptionAsync(() =>
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.Card, null)));
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_UnsupportedPaymentMethod_ThrowsBillingException(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer());
await ThrowsBillingExceptionAsync(() =>
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.BitPay, "TOKEN")));
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_BankAccount_IncorrectNumberOfSetupIntentsForToken_ThrowsBillingException(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer());
stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
.Returns([new SetupIntent(), new SetupIntent()]);
await ThrowsBillingExceptionAsync(() =>
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.BankAccount, "TOKEN")));
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_BankAccount_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
Metadata = new Dictionary<string, string>
{
[Core.Billing.Utilities.BraintreeCustomerIdKey] = "braintree_customer_id"
}
});
var matchingSetupIntent = new SetupIntent { Id = "setup_intent_1" };
stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
.Returns([matchingSetupIntent]);
stripeAdapter.ListCustomerPaymentMethodsAsync(provider.GatewayCustomerId).Returns([
new PaymentMethod { Id = "payment_method_1" }
]);
await sutProvider.Sut.UpdatePaymentSource(provider,
new TokenizedPaymentSource(PaymentMethodType.BankAccount, "TOKEN"));
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_1");
await stripeAdapter.DidNotReceive().CancelSetupIntentAsync(Arg.Any<string>(),
Arg.Any<SetupIntentCancelOptions>());
await stripeAdapter.Received(1).DetachPaymentMethodAsync("payment_method_1");
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == null));
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_Card_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))
)
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
Metadata = new Dictionary<string, string>
{
[Core.Billing.Utilities.BraintreeCustomerIdKey] = "braintree_customer_id"
}
});
stripeAdapter.ListCustomerPaymentMethodsAsync(provider.GatewayCustomerId).Returns([
new PaymentMethod { Id = "payment_method_1" }
]);
await sutProvider.Sut.UpdatePaymentSource(provider,
new TokenizedPaymentSource(PaymentMethodType.Card, "TOKEN"));
await stripeAdapter.DidNotReceive().CancelSetupIntentAsync(Arg.Any<string>(),
Arg.Any<SetupIntentCancelOptions>());
await stripeAdapter.Received(1).DetachPaymentMethodAsync("payment_method_1");
await stripeAdapter.Received(1).AttachPaymentMethodAsync("TOKEN", Arg.Is<PaymentMethodAttachOptions>(
options => options.Customer == provider.GatewayCustomerId));
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options =>
options.InvoiceSettings.DefaultPaymentMethod == "TOKEN" &&
options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == null));
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_Braintree_NullCustomer_ThrowsBillingException(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
Metadata = new Dictionary<string, string>
{
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
}
});
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
customerGateway.FindAsync(braintreeCustomerId).ReturnsNull();
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
await paymentMethodGateway.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<PaymentMethodRequest>());
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_Braintree_ReplacePaymentMethod_CreatePaymentMethodFails_ThrowsBillingException(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
Metadata = new Dictionary<string, string>
{
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
}
});
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var customer = Substitute.For<Braintree.Customer>();
customer.Id.Returns(braintreeCustomerId);
customerGateway.FindAsync(braintreeCustomerId).Returns(customer);
var createPaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
createPaymentMethodResult.IsSuccess().Returns(false);
paymentMethodGateway.CreateAsync(Arg.Is<PaymentMethodRequest>(
options => options.CustomerId == braintreeCustomerId && options.PaymentMethodNonce == "TOKEN"))
.Returns(createPaymentMethodResult);
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_Braintree_ReplacePaymentMethod_UpdateCustomerFails_DeletePaymentMethod_ThrowsBillingException(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
Metadata = new Dictionary<string, string>
{
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
}
});
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var customer = Substitute.For<Braintree.Customer>();
customer.Id.Returns(braintreeCustomerId);
customerGateway.FindAsync(braintreeCustomerId).Returns(customer);
var createPaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
var createdPaymentMethod = Substitute.For<Braintree.PaymentMethod>();
createdPaymentMethod.Token.Returns("TOKEN");
createPaymentMethodResult.IsSuccess().Returns(true);
createPaymentMethodResult.Target.Returns(createdPaymentMethod);
paymentMethodGateway.CreateAsync(Arg.Is<PaymentMethodRequest>(
options => options.CustomerId == braintreeCustomerId && options.PaymentMethodNonce == "TOKEN"))
.Returns(createPaymentMethodResult);
var updateCustomerResult = Substitute.For<Result<Braintree.Customer>>();
updateCustomerResult.IsSuccess().Returns(false);
customerGateway.UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(options =>
options.DefaultPaymentMethodToken == createPaymentMethodResult.Target.Token))
.Returns(updateCustomerResult);
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
await paymentMethodGateway.Received(1).DeleteAsync(createPaymentMethodResult.Target.Token);
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_Braintree_ReplacePaymentMethod_Success(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
Metadata = new Dictionary<string, string>
{
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
}
});
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var customer = Substitute.For<Braintree.Customer>();
var existingPaymentMethod = Substitute.For<Braintree.PaymentMethod>();
existingPaymentMethod.Token.Returns("OLD_TOKEN");
existingPaymentMethod.IsDefault.Returns(true);
customer.PaymentMethods.Returns([existingPaymentMethod]);
customer.Id.Returns(braintreeCustomerId);
customerGateway.FindAsync(braintreeCustomerId).Returns(customer);
var createPaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
var updatedPaymentMethod = Substitute.For<Braintree.PaymentMethod>();
updatedPaymentMethod.Token.Returns("TOKEN");
createPaymentMethodResult.IsSuccess().Returns(true);
createPaymentMethodResult.Target.Returns(updatedPaymentMethod);
paymentMethodGateway.CreateAsync(Arg.Is<PaymentMethodRequest>(
options => options.CustomerId == braintreeCustomerId && options.PaymentMethodNonce == "TOKEN"))
.Returns(createPaymentMethodResult);
var updateCustomerResult = Substitute.For<Result<Braintree.Customer>>();
updateCustomerResult.IsSuccess().Returns(true);
customerGateway.UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(options =>
options.DefaultPaymentMethodToken == createPaymentMethodResult.Target.Token))
.Returns(updateCustomerResult);
var deletePaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
deletePaymentMethodResult.IsSuccess().Returns(true);
paymentMethodGateway.DeleteAsync(existingPaymentMethod.Token).Returns(deletePaymentMethodResult);
await sutProvider.Sut.UpdatePaymentSource(provider,
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN"));
await paymentMethodGateway.Received(1).DeleteAsync(existingPaymentMethod.Token);
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_Braintree_CreateCustomer_CustomerUpdateFails_ThrowsBillingException(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer
{
Id = provider.GatewayCustomerId
});
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
.Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings())
{
CloudRegion = "US"
});
var (_, customerGateway, _) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var createCustomerResult = Substitute.For<Result<Braintree.Customer>>();
createCustomerResult.IsSuccess().Returns(false);
customerGateway.CreateAsync(Arg.Is<CustomerRequest>(
options =>
options.Id == braintreeCustomerId &&
options.CustomFields[provider.BraintreeIdField()] == provider.Id.ToString() &&
options.CustomFields[provider.BraintreeCloudRegionField()] == "US" &&
options.Email == provider.BillingEmailAddress() &&
options.PaymentMethodNonce == "TOKEN"))
.Returns(createCustomerResult);
await ThrowsBillingExceptionAsync(() =>
sutProvider.Sut.UpdatePaymentSource(provider,
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
}
[Theory, BitAutoData]
public async Task UpdatePaymentMethod_Braintree_CreateCustomer_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
.Returns(new Customer
{
Id = provider.GatewayCustomerId
});
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
.Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings())
{
CloudRegion = "US"
});
var (_, customerGateway, _) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
var createCustomerResult = Substitute.For<Result<Braintree.Customer>>();
var createdCustomer = Substitute.For<Braintree.Customer>();
createdCustomer.Id.Returns(braintreeCustomerId);
createCustomerResult.IsSuccess().Returns(true);
createCustomerResult.Target.Returns(createdCustomer);
customerGateway.CreateAsync(Arg.Is<CustomerRequest>(
options =>
options.CustomFields[provider.BraintreeIdField()] == provider.Id.ToString() &&
options.CustomFields[provider.BraintreeCloudRegionField()] == "US" &&
options.Email == provider.BillingEmailAddress() &&
options.PaymentMethodNonce == "TOKEN"))
.Returns(createCustomerResult);
await sutProvider.Sut.UpdatePaymentSource(provider,
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN"));
await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(
options => options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == braintreeCustomerId));
}
#endregion
#region UpdateTaxInformation
[Theory, BitAutoData]
public async Task UpdateTaxInformation_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(
() => sutProvider.Sut.UpdateTaxInformation(null, null));
[Theory, BitAutoData]
public async Task UpdateTaxInformation_NullTaxInformation_ThrowsArgumentNullException(
Provider provider,
SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(
() => sutProvider.Sut.UpdateTaxInformation(provider, null));
[Theory, BitAutoData]
public async Task UpdateTaxInformation_NonUser_MakesCorrectInvocations(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } };
stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("tax_ids"))).Returns(customer);
var taxInformation = new TaxInformation(
"US",
"12345",
"123456789",
"us_ein",
"123 Example St.",
null,
"Example Town",
"NY");
sutProvider.GetDependency<IStripeAdapter>()
.UpdateCustomerAsync(
Arg.Is<string>(p => p == provider.GatewayCustomerId),
Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Country == "US" &&
options.Address.PostalCode == "12345" &&
options.Address.Line1 == "123 Example St." &&
options.Address.Line2 == null &&
options.Address.City == "Example Town" &&
options.Address.State == "NY"))
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
Address = new Address
{
Country = "US",
PostalCode = "12345",
Line1 = "123 Example St.",
Line2 = null,
City = "Example Town",
State = "NY"
},
TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] },
Subscriptions = new StripeList<Subscription>
{
Data = [
new Subscription
{
Id = provider.GatewaySubscriptionId,
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
}
]
}
});
var subscription = new Subscription { Items = new StripeList<SubscriptionItem>() };
sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(Arg.Any<string>())
.Returns(subscription);
await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options =>
options.Address.Country == taxInformation.Country &&
options.Address.PostalCode == taxInformation.PostalCode &&
options.Address.Line1 == taxInformation.Line1 &&
options.Address.Line2 == taxInformation.Line2 &&
options.Address.City == taxInformation.City &&
options.Address.State == taxInformation.State));
await stripeAdapter.Received(1).DeleteTaxIdAsync(provider.GatewayCustomerId, "tax_id_1");
await stripeAdapter.Received(1).CreateTaxIdAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
options => options.Type == "us_ein" &&
options.Value == taxInformation.TaxId));
await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
[Theory, BitAutoData]
public async Task UpdateTaxInformation_NonUser_ReverseCharge_MakesCorrectInvocations(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } };
stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("tax_ids"))).Returns(customer);
var taxInformation = new TaxInformation(
"CA",
"12345",
"123456789",
"us_ein",
"123 Example St.",
null,
"Example Town",
"NY");
sutProvider.GetDependency<IStripeAdapter>()
.UpdateCustomerAsync(
Arg.Is<string>(p => p == provider.GatewayCustomerId),
Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Country == "CA" &&
options.Address.PostalCode == "12345" &&
options.Address.Line1 == "123 Example St." &&
options.Address.Line2 == null &&
options.Address.City == "Example Town" &&
options.Address.State == "NY"))
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
Address = new Address
{
Country = "CA",
PostalCode = "12345",
Line1 = "123 Example St.",
Line2 = null,
City = "Example Town",
State = "NY"
},
TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] },
Subscriptions = new StripeList<Subscription>
{
Data = [
new Subscription
{
Id = provider.GatewaySubscriptionId,
CustomerId = provider.GatewayCustomerId,
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
}
]
}
});
var subscription = new Subscription { Items = new StripeList<SubscriptionItem>() };
sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(Arg.Any<string>())
.Returns(subscription);
await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options =>
options.Address.Country == taxInformation.Country &&
options.Address.PostalCode == taxInformation.PostalCode &&
options.Address.Line1 == taxInformation.Line1 &&
options.Address.Line2 == taxInformation.Line2 &&
options.Address.City == taxInformation.City &&
options.Address.State == taxInformation.State));
await stripeAdapter.Received(1).DeleteTaxIdAsync(provider.GatewayCustomerId, "tax_id_1");
await stripeAdapter.Received(1).CreateTaxIdAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
options => options.Type == "us_ein" &&
options.Value == taxInformation.TaxId));
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == StripeConstants.TaxExempt.Reverse));
await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
#endregion
#region IsValidGatewayCustomerIdAsync
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
sutProvider.Sut.IsValidGatewayCustomerIdAsync(null));
}
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_NullGatewayCustomerId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewayCustomerId = null;
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.GetCustomerAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_EmptyGatewayCustomerId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewayCustomerId = "";
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.GetCustomerAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_ValidCustomerId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId).Returns(new Customer());
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.True(result);
await stripeAdapter.Received(1).GetCustomerAsync(organization.GatewayCustomerId);
}
[Theory, BitAutoData]
public async Task IsValidGatewayCustomerIdAsync_InvalidCustomerId_ReturnsFalse(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var stripeException = new StripeException { StripeError = new StripeError { Code = "resource_missing" } };
stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId).Throws(stripeException);
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.False(result);
await stripeAdapter.Received(1).GetCustomerAsync(organization.GatewayCustomerId);
}
#endregion
#region IsValidGatewaySubscriptionIdAsync
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider)
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(null));
}
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_NullGatewaySubscriptionId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewaySubscriptionId = null;
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.GetSubscriptionAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_EmptyGatewaySubscriptionId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
organization.GatewaySubscriptionId = "";
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.GetSubscriptionAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_ValidSubscriptionId_ReturnsTrue(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(new Subscription());
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.True(result);
await stripeAdapter.Received(1).GetSubscriptionAsync(organization.GatewaySubscriptionId);
}
[Theory, BitAutoData]
public async Task IsValidGatewaySubscriptionIdAsync_InvalidSubscriptionId_ReturnsFalse(
Organization organization,
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var stripeException = new StripeException { StripeError = new StripeError { Code = "resource_missing" } };
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Throws(stripeException);
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.False(result);
await stripeAdapter.Received(1).GetSubscriptionAsync(organization.GatewaySubscriptionId);
}
#endregion
}