1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[AC-1904] Implement endpoint to retrieve Provider subscription (#3921)

* Refactor Core.Billing prior to adding new logic

* Add ProviderBillingQueries.GetSubscriptionData

* Add ProviderBillingController.GetSubscriptionAsync
This commit is contained in:
Alex Morask
2024-03-28 08:46:12 -04:00
committed by GitHub
parent 46dba15194
commit ffd988eeda
31 changed files with 786 additions and 238 deletions

View File

@ -56,7 +56,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
private readonly IPushNotificationService _pushNotificationService;
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
private readonly IGetSubscriptionQuery _getSubscriptionQuery;
private readonly ISubscriberQueries _subscriberQueries;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
@ -86,7 +86,7 @@ public class OrganizationsControllerTests : IDisposable
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_cancelSubscriptionCommand = Substitute.For<ICancelSubscriptionCommand>();
_getSubscriptionQuery = Substitute.For<IGetSubscriptionQuery>();
_subscriberQueries = Substitute.For<ISubscriberQueries>();
_referenceEventService = Substitute.For<IReferenceEventService>();
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
@ -113,7 +113,7 @@ public class OrganizationsControllerTests : IDisposable
_addSecretsManagerSubscriptionCommand,
_pushNotificationService,
_cancelSubscriptionCommand,
_getSubscriptionQuery,
_subscriberQueries,
_referenceEventService,
_organizationEnableCollectionEnhancementsCommand);
}

View File

@ -57,7 +57,7 @@ public class AccountsControllerTests : IDisposable
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
private readonly IFeatureService _featureService;
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
private readonly IGetSubscriptionQuery _getSubscriptionQuery;
private readonly ISubscriberQueries _subscriberQueries;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
@ -90,7 +90,7 @@ public class AccountsControllerTests : IDisposable
_rotateUserKeyCommand = Substitute.For<IRotateUserKeyCommand>();
_featureService = Substitute.For<IFeatureService>();
_cancelSubscriptionCommand = Substitute.For<ICancelSubscriptionCommand>();
_getSubscriptionQuery = Substitute.For<IGetSubscriptionQuery>();
_subscriberQueries = Substitute.For<ISubscriberQueries>();
_referenceEventService = Substitute.For<IReferenceEventService>();
_currentContext = Substitute.For<ICurrentContext>();
_cipherValidator =
@ -122,7 +122,7 @@ public class AccountsControllerTests : IDisposable
_rotateUserKeyCommand,
_featureService,
_cancelSubscriptionCommand,
_getSubscriptionQuery,
_subscriberQueries,
_referenceEventService,
_currentContext,
_cipherValidator,

View File

@ -1,13 +1,13 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Commands.Implementations;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
using BT = Braintree;
using S = Stripe;
@ -355,13 +355,4 @@ public class RemovePaymentMethodCommandTests
return (braintreeGateway, customerGateway, paymentMethodGateway);
}
private static async Task ThrowsContactSupportAsync(Func<Task> function)
{
const string message = "Could not remove your payment method. Please contact support for assistance.";
var exception = await Assert.ThrowsAsync<GatewayException>(function);
Assert.Equal(message, exception.Message);
}
}

View File

@ -1,104 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Queries.Implementations;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Queries;
[SutProviderCustomize]
public class GetSubscriptionQueryTests
{
[Theory, BitAutoData]
public async Task GetSubscription_NullSubscriber_ThrowsArgumentNullException(
SutProvider<GetSubscriptionQuery> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscription(null));
[Theory, BitAutoData]
public async Task GetSubscription_Organization_NoGatewaySubscriptionId_ThrowsGatewayException(
Organization organization,
SutProvider<GetSubscriptionQuery> sutProvider)
{
organization.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscription(organization));
}
[Theory, BitAutoData]
public async Task GetSubscription_Organization_NoSubscription_ThrowsGatewayException(
Organization organization,
SutProvider<GetSubscriptionQuery> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscription(organization));
}
[Theory, BitAutoData]
public async Task GetSubscription_Organization_Succeeds(
Organization organization,
SutProvider<GetSubscriptionQuery> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Equivalent(subscription, gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_User_NoGatewaySubscriptionId_ThrowsGatewayException(
User user,
SutProvider<GetSubscriptionQuery> sutProvider)
{
user.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscription(user));
}
[Theory, BitAutoData]
public async Task GetSubscription_User_NoSubscription_ThrowsGatewayException(
User user,
SutProvider<GetSubscriptionQuery> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscription(user));
}
[Theory, BitAutoData]
public async Task GetSubscription_User_Succeeds(
User user,
SutProvider<GetSubscriptionQuery> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(user);
Assert.Equivalent(subscription, gotSubscription);
}
private static async Task ThrowsContactSupportAsync(Func<Task> function)
{
const string message = "Something went wrong with your request. Please contact support.";
var exception = await Assert.ThrowsAsync<GatewayException>(function);
Assert.Equal(message, exception.Message);
}
}

View File

@ -0,0 +1,151 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Queries;
using Bit.Core.Billing.Queries.Implementations;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Queries;
[SutProviderCustomize]
public class ProviderBillingQueriesTests
{
#region GetSubscriptionData
[Theory, BitAutoData]
public async Task GetSubscriptionData_NullProvider_ReturnsNull(
SutProvider<ProviderBillingQueries> sutProvider,
Guid providerId)
{
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(providerId).ReturnsNull();
var subscriptionData = await sutProvider.Sut.GetSubscriptionData(providerId);
Assert.Null(subscriptionData);
await providerRepository.Received(1).GetByIdAsync(providerId);
}
[Theory, BitAutoData]
public async Task GetSubscriptionData_NullSubscription_ReturnsNull(
SutProvider<ProviderBillingQueries> sutProvider,
Guid providerId,
Provider provider)
{
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(providerId).Returns(provider);
var subscriberQueries = sutProvider.GetDependency<ISubscriberQueries>();
subscriberQueries.GetSubscription(provider).ReturnsNull();
var subscriptionData = await sutProvider.Sut.GetSubscriptionData(providerId);
Assert.Null(subscriptionData);
await providerRepository.Received(1).GetByIdAsync(providerId);
await subscriberQueries.Received(1).GetSubscription(
provider,
Arg.Is<SubscriptionGetOptions>(
options => options.Expand.Count == 1 && options.Expand.First() == "customer"));
}
[Theory, BitAutoData]
public async Task GetSubscriptionData_Success(
SutProvider<ProviderBillingQueries> sutProvider,
Guid providerId,
Provider provider)
{
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(providerId).Returns(provider);
var subscriberQueries = sutProvider.GetDependency<ISubscriberQueries>();
var subscription = new Subscription();
subscriberQueries.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(
options => options.Expand.Count == 1 && options.Expand.First() == "customer")).Returns(subscription);
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
var enterprisePlan = new ProviderPlan
{
Id = Guid.NewGuid(),
ProviderId = providerId,
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100,
PurchasedSeats = 0
};
var teamsPlan = new ProviderPlan
{
Id = Guid.NewGuid(),
ProviderId = providerId,
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 50,
PurchasedSeats = 10
};
var providerPlans = new List<ProviderPlan>
{
enterprisePlan,
teamsPlan,
};
providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans);
var subscriptionData = await sutProvider.Sut.GetSubscriptionData(providerId);
Assert.NotNull(subscriptionData);
Assert.Equivalent(subscriptionData.Subscription, subscription);
Assert.Equal(2, subscriptionData.ProviderPlans.Count);
var configuredEnterprisePlan =
subscriptionData.ProviderPlans.FirstOrDefault(configuredPlan =>
configuredPlan.PlanType == PlanType.EnterpriseMonthly);
var configuredTeamsPlan =
subscriptionData.ProviderPlans.FirstOrDefault(configuredPlan =>
configuredPlan.PlanType == PlanType.TeamsMonthly);
Compare(enterprisePlan, configuredEnterprisePlan);
Compare(teamsPlan, configuredTeamsPlan);
await providerRepository.Received(1).GetByIdAsync(providerId);
await subscriberQueries.Received(1).GetSubscription(
provider,
Arg.Is<SubscriptionGetOptions>(
options => options.Expand.Count == 1 && options.Expand.First() == "customer"));
await providerPlanRepository.Received(1).GetByProviderId(providerId);
return;
void Compare(ProviderPlan providerPlan, ConfiguredProviderPlan configuredProviderPlan)
{
Assert.NotNull(configuredProviderPlan);
Assert.Equal(providerPlan.Id, configuredProviderPlan.Id);
Assert.Equal(providerPlan.ProviderId, configuredProviderPlan.ProviderId);
Assert.Equal(providerPlan.SeatMinimum!.Value, configuredProviderPlan.SeatMinimum);
Assert.Equal(providerPlan.PurchasedSeats!.Value, configuredProviderPlan.PurchasedSeats);
}
}
#endregion
}

View File

@ -0,0 +1,263 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Queries.Implementations;
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
using static Bit.Core.Test.Billing.Utilities;
namespace Bit.Core.Test.Billing.Queries;
[SutProviderCustomize]
public class SubscriberQueriesTests
{
#region GetSubscription
[Theory, BitAutoData]
public async Task GetSubscription_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberQueries> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscription(null));
[Theory, BitAutoData]
public async Task GetSubscription_Organization_NoGatewaySubscriptionId_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
organization.GatewaySubscriptionId = null;
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Organization_NoSubscription_ReturnsNull(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Null(gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Organization_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
Assert.Equivalent(subscription, gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_User_NoGatewaySubscriptionId_ReturnsNull(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
user.GatewaySubscriptionId = null;
var gotSubscription = await sutProvider.Sut.GetSubscription(user);
Assert.Null(gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_User_NoSubscription_ReturnsNull(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.ReturnsNull();
var gotSubscription = await sutProvider.Sut.GetSubscription(user);
Assert.Null(gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_User_Succeeds(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(user);
Assert.Equivalent(subscription, gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Provider_NoGatewaySubscriptionId_ReturnsNull(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
provider.GatewaySubscriptionId = null;
var gotSubscription = await sutProvider.Sut.GetSubscription(provider);
Assert.Null(gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Provider_NoSubscription_ReturnsNull(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(provider.GatewaySubscriptionId)
.ReturnsNull();
var gotSubscription = await sutProvider.Sut.GetSubscription(provider);
Assert.Null(gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscription_Provider_Succeeds(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(provider.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(provider);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
#region GetSubscriptionOrThrow
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberQueries> sutProvider)
=> await Assert.ThrowsAsync<ArgumentNullException>(
async () => await sutProvider.Sut.GetSubscriptionOrThrow(null));
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Organization_NoGatewaySubscriptionId_ThrowsGatewayException(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
organization.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Organization_NoSubscription_ThrowsGatewayException(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Organization_Succeeds(
Organization organization,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(organization);
Assert.Equivalent(subscription, gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_User_NoGatewaySubscriptionId_ThrowsGatewayException(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
user.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(user));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_User_NoSubscription_ThrowsGatewayException(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(user));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_User_Succeeds(
User user,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(user.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(user);
Assert.Equivalent(subscription, gotSubscription);
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Provider_NoGatewaySubscriptionId_ThrowsGatewayException(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
provider.GatewaySubscriptionId = null;
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(provider));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Provider_NoSubscription_ThrowsGatewayException(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(provider.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsContactSupportAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(provider));
}
[Theory, BitAutoData]
public async Task GetSubscriptionOrThrow_Provider_Succeeds(
Provider provider,
SutProvider<SubscriberQueries> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(provider.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(provider);
Assert.Equivalent(subscription, gotSubscription);
}
#endregion
}

View File

@ -1,4 +1,4 @@
using Bit.Core.Exceptions;
using Bit.Core.Billing;
using Xunit;
using static Bit.Core.Billing.Utilities;
@ -11,7 +11,7 @@ public static class Utilities
{
var contactSupport = ContactSupport();
var exception = await Assert.ThrowsAsync<GatewayException>(function);
var exception = await Assert.ThrowsAsync<BillingException>(function);
Assert.Equal(contactSupport.Message, exception.Message);
}