mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 05:00:19 -05:00

* Add endpoint to update a provider organization's seats for consolidated billing. * Fixed failing tests
340 lines
13 KiB
C#
340 lines
13 KiB
C#
using Bit.Core.AdminConsole.Entities;
|
|
using Bit.Core.AdminConsole.Entities.Provider;
|
|
using Bit.Core.Billing;
|
|
using Bit.Core.Billing.Commands.Implementations;
|
|
using Bit.Core.Billing.Entities;
|
|
using Bit.Core.Billing.Queries;
|
|
using Bit.Core.Billing.Repositories;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Models.StaticStore;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Utilities;
|
|
using Bit.Test.Common.AutoFixture;
|
|
using Bit.Test.Common.AutoFixture.Attributes;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
using static Bit.Core.Test.Billing.Utilities;
|
|
|
|
namespace Bit.Core.Test.Billing.Commands;
|
|
|
|
[SutProviderCustomize]
|
|
public class AssignSeatsToClientOrganizationCommandTests
|
|
{
|
|
[Theory, BitAutoData]
|
|
public Task AssignSeatsToClientOrganization_NullProvider_ArgumentNullException(
|
|
Organization organization,
|
|
int seats,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
=> Assert.ThrowsAsync<ArgumentNullException>(() =>
|
|
sutProvider.Sut.AssignSeatsToClientOrganization(null, organization, seats));
|
|
|
|
[Theory, BitAutoData]
|
|
public Task AssignSeatsToClientOrganization_NullOrganization_ArgumentNullException(
|
|
Provider provider,
|
|
int seats,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
=> Assert.ThrowsAsync<ArgumentNullException>(() =>
|
|
sutProvider.Sut.AssignSeatsToClientOrganization(provider, null, seats));
|
|
|
|
[Theory, BitAutoData]
|
|
public Task AssignSeatsToClientOrganization_NegativeSeats_BillingException(
|
|
Provider provider,
|
|
Organization organization,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
=> Assert.ThrowsAsync<BillingException>(() =>
|
|
sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, -5));
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task AssignSeatsToClientOrganization_CurrentSeatsMatchesNewSeats_NoOp(
|
|
Provider provider,
|
|
Organization organization,
|
|
int seats,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
{
|
|
organization.PlanType = PlanType.TeamsMonthly;
|
|
|
|
organization.Seats = seats;
|
|
|
|
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
|
|
|
|
await sutProvider.GetDependency<IProviderPlanRepository>().DidNotReceive().GetByProviderId(provider.Id);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task AssignSeatsToClientOrganization_OrganizationPlanTypeDoesNotSupportConsolidatedBilling_ContactSupport(
|
|
Provider provider,
|
|
Organization organization,
|
|
int seats,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
{
|
|
organization.PlanType = PlanType.FamiliesAnnually;
|
|
|
|
await ThrowsContactSupportAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats));
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task AssignSeatsToClientOrganization_ProviderPlanIsNotConfigured_ContactSupport(
|
|
Provider provider,
|
|
Organization organization,
|
|
int seats,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
{
|
|
organization.PlanType = PlanType.TeamsMonthly;
|
|
|
|
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(new List<ProviderPlan>
|
|
{
|
|
new ()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.TeamsMonthly,
|
|
ProviderId = provider.Id
|
|
}
|
|
});
|
|
|
|
await ThrowsContactSupportAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats));
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task AssignSeatsToClientOrganization_BelowToBelow_Succeeds(
|
|
Provider provider,
|
|
Organization organization,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
{
|
|
organization.Seats = 10;
|
|
|
|
organization.PlanType = PlanType.TeamsMonthly;
|
|
|
|
// Scale up 10 seats
|
|
const int seats = 20;
|
|
|
|
var providerPlans = new List<ProviderPlan>
|
|
{
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.TeamsMonthly,
|
|
ProviderId = provider.Id,
|
|
PurchasedSeats = 0,
|
|
// 100 minimum
|
|
SeatMinimum = 100,
|
|
AllocatedSeats = 50
|
|
},
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.EnterpriseMonthly,
|
|
ProviderId = provider.Id,
|
|
PurchasedSeats = 0,
|
|
SeatMinimum = 500,
|
|
AllocatedSeats = 0
|
|
}
|
|
};
|
|
|
|
var providerPlan = providerPlans.First();
|
|
|
|
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
|
|
|
// 50 seats currently assigned with a seat minimum of 100
|
|
sutProvider.GetDependency<IProviderBillingQueries>().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(50);
|
|
|
|
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
|
|
|
|
// 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum
|
|
await sutProvider.GetDependency<IPaymentService>().DidNotReceiveWithAnyArgs().AdjustSeats(
|
|
Arg.Any<Provider>(),
|
|
Arg.Any<Plan>(),
|
|
Arg.Any<int>(),
|
|
Arg.Any<int>());
|
|
|
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
|
|
org => org.Id == organization.Id && org.Seats == seats));
|
|
|
|
await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
|
pPlan => pPlan.AllocatedSeats == 60));
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task AssignSeatsToClientOrganization_BelowToAbove_Succeeds(
|
|
Provider provider,
|
|
Organization organization,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
{
|
|
organization.Seats = 10;
|
|
|
|
organization.PlanType = PlanType.TeamsMonthly;
|
|
|
|
// Scale up 10 seats
|
|
const int seats = 20;
|
|
|
|
var providerPlans = new List<ProviderPlan>
|
|
{
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.TeamsMonthly,
|
|
ProviderId = provider.Id,
|
|
PurchasedSeats = 0,
|
|
// 100 minimum
|
|
SeatMinimum = 100,
|
|
AllocatedSeats = 95
|
|
},
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.EnterpriseMonthly,
|
|
ProviderId = provider.Id,
|
|
PurchasedSeats = 0,
|
|
SeatMinimum = 500,
|
|
AllocatedSeats = 0
|
|
}
|
|
};
|
|
|
|
var providerPlan = providerPlans.First();
|
|
|
|
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
|
|
|
// 95 seats currently assigned with a seat minimum of 100
|
|
sutProvider.GetDependency<IProviderBillingQueries>().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(95);
|
|
|
|
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
|
|
|
|
// 95 current + 10 seat scale = 105 seats, 5 above the minimum
|
|
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSeats(
|
|
provider,
|
|
StaticStore.GetPlan(providerPlan.PlanType),
|
|
providerPlan.SeatMinimum!.Value,
|
|
105);
|
|
|
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
|
|
org => org.Id == organization.Id && org.Seats == seats));
|
|
|
|
// 105 total seats - 100 minimum = 5 purchased seats
|
|
await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
|
pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 5 && pPlan.AllocatedSeats == 105));
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task AssignSeatsToClientOrganization_AboveToAbove_Succeeds(
|
|
Provider provider,
|
|
Organization organization,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
{
|
|
organization.Seats = 10;
|
|
|
|
organization.PlanType = PlanType.TeamsMonthly;
|
|
|
|
// Scale up 10 seats
|
|
const int seats = 20;
|
|
|
|
var providerPlans = new List<ProviderPlan>
|
|
{
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.TeamsMonthly,
|
|
ProviderId = provider.Id,
|
|
// 10 additional purchased seats
|
|
PurchasedSeats = 10,
|
|
// 100 seat minimum
|
|
SeatMinimum = 100,
|
|
AllocatedSeats = 110
|
|
},
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.EnterpriseMonthly,
|
|
ProviderId = provider.Id,
|
|
PurchasedSeats = 0,
|
|
SeatMinimum = 500,
|
|
AllocatedSeats = 0
|
|
}
|
|
};
|
|
|
|
var providerPlan = providerPlans.First();
|
|
|
|
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
|
|
|
// 110 seats currently assigned with a seat minimum of 100
|
|
sutProvider.GetDependency<IProviderBillingQueries>().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(110);
|
|
|
|
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
|
|
|
|
// 110 current + 10 seat scale up = 120 seats
|
|
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSeats(
|
|
provider,
|
|
StaticStore.GetPlan(providerPlan.PlanType),
|
|
110,
|
|
120);
|
|
|
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
|
|
org => org.Id == organization.Id && org.Seats == seats));
|
|
|
|
// 120 total seats - 100 seat minimum = 20 purchased seats
|
|
await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
|
pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 20 && pPlan.AllocatedSeats == 120));
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task AssignSeatsToClientOrganization_AboveToBelow_Succeeds(
|
|
Provider provider,
|
|
Organization organization,
|
|
SutProvider<AssignSeatsToClientOrganizationCommand> sutProvider)
|
|
{
|
|
organization.Seats = 50;
|
|
|
|
organization.PlanType = PlanType.TeamsMonthly;
|
|
|
|
// Scale down 30 seats
|
|
const int seats = 20;
|
|
|
|
var providerPlans = new List<ProviderPlan>
|
|
{
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.TeamsMonthly,
|
|
ProviderId = provider.Id,
|
|
// 10 additional purchased seats
|
|
PurchasedSeats = 10,
|
|
// 100 seat minimum
|
|
SeatMinimum = 100,
|
|
AllocatedSeats = 110
|
|
},
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PlanType = PlanType.EnterpriseMonthly,
|
|
ProviderId = provider.Id,
|
|
PurchasedSeats = 0,
|
|
SeatMinimum = 500,
|
|
AllocatedSeats = 0
|
|
}
|
|
};
|
|
|
|
var providerPlan = providerPlans.First();
|
|
|
|
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
|
|
|
// 110 seats currently assigned with a seat minimum of 100
|
|
sutProvider.GetDependency<IProviderBillingQueries>().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(110);
|
|
|
|
await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats);
|
|
|
|
// 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum.
|
|
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSeats(
|
|
provider,
|
|
StaticStore.GetPlan(providerPlan.PlanType),
|
|
110,
|
|
providerPlan.SeatMinimum!.Value);
|
|
|
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
|
|
org => org.Id == organization.Id && org.Seats == seats));
|
|
|
|
// Being below the seat minimum means no purchased seats.
|
|
await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
|
pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 0 && pPlan.AllocatedSeats == 80));
|
|
}
|
|
}
|