1
0
mirror of https://github.com/bitwarden/server synced 2026-02-26 01:13:35 +00:00

Merge branch 'main' into billing/pm-29595/user-that-upgraded-from-premium-reverts-an-organization-upgrade-during-the-trial-period

This commit is contained in:
cyprain-okeke
2026-01-21 17:11:03 +01:00
committed by GitHub
19 changed files with 519 additions and 477 deletions

View File

@@ -715,6 +715,39 @@ public class RestoreOrganizationUserCommandTests
Arg.Is<OrganizationUserStatusType>(x => x != OrganizationUserStatusType.Revoked));
}
[Theory, BitAutoData]
public async Task RestoreUser_InvitedUserInFreeOrganization_Success(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
SutProvider<RestoreOrganizationUserCommand> sutProvider)
{
organization.PlanType = PlanType.Free;
organizationUser.UserId = null;
organizationUser.Key = null;
organizationUser.Status = OrganizationUserStatusType.Revoked;
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
{
Sponsored = 0,
Users = 1
});
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
await sutProvider.GetDependency<IPushNotificationService>()
.DidNotReceiveWithAnyArgs()
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task RestoreUsers_Success(Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,

View File

@@ -1,6 +1,7 @@
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Subscriptions.Models;
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -8,6 +9,7 @@ using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;
using static Bit.Core.Billing.Constants.StripeConstants;
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
@@ -15,6 +17,7 @@ namespace Bit.Core.Test.Billing.Premium.Commands;
public class UpdatePremiumStorageCommandTests
{
private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
private readonly IUserService _userService = Substitute.For<IUserService>();
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
@@ -33,13 +36,14 @@ public class UpdatePremiumStorageCommandTests
_pricingClient.ListPremiumPlans().Returns([premiumPlan]);
_command = new UpdatePremiumStorageCommand(
_braintreeService,
_stripeAdapter,
_userService,
_pricingClient,
Substitute.For<ILogger<UpdatePremiumStorageCommand>>());
}
private Subscription CreateMockSubscription(string subscriptionId, int? storageQuantity = null)
private Subscription CreateMockSubscription(string subscriptionId, int? storageQuantity = null, bool isPayPal = false)
{
var items = new List<SubscriptionItem>
{
@@ -63,9 +67,17 @@ public class UpdatePremiumStorageCommandTests
});
}
var customer = new Customer
{
Id = "cus_123",
Metadata = isPayPal ? new Dictionary<string, string> { { MetadataKeys.BraintreeCustomerId, "braintree_123" } } : new Dictionary<string, string>()
};
return new Subscription
{
Id = subscriptionId,
CustomerId = "cus_123",
Customer = customer,
Items = new StripeList<SubscriptionItem>
{
Data = items
@@ -97,7 +109,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, -5);
@@ -117,7 +129,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, 100);
@@ -154,7 +166,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 9);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, 0);
@@ -176,7 +188,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, 4);
@@ -185,7 +197,7 @@ public class UpdatePremiumStorageCommandTests
Assert.True(result.IsT0);
// Verify subscription was fetched but NOT updated
await _stripeAdapter.Received(1).GetSubscriptionAsync("sub_123");
await _stripeAdapter.Received(1).GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>());
await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
}
@@ -200,7 +212,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, 9);
@@ -233,7 +245,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123");
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, 9);
@@ -262,7 +274,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 9);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, 2);
@@ -291,7 +303,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 9);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, 0);
@@ -320,7 +332,7 @@ public class UpdatePremiumStorageCommandTests
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
// Act
var result = await _command.Run(user, 99);
@@ -335,4 +347,200 @@ public class UpdatePremiumStorageCommandTests
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 100));
}
[Theory, BitAutoData]
public async Task Run_IncreaseStorage_PayPal_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 5;
user.Storage = 2L * 1024 * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4, isPayPal: true);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var draftInvoice = new Invoice { Id = "in_draft" };
_stripeAdapter.CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>()).Returns(draftInvoice);
var finalizedInvoice = new Invoice
{
Id = "in_finalized",
Customer = new Customer { Id = "cus_123" }
};
_stripeAdapter.FinalizeInvoiceAsync("in_draft", Arg.Any<InvoiceFinalizeOptions>()).Returns(finalizedInvoice);
// Act
var result = await _command.Run(user, 9);
// Assert
Assert.True(result.IsT0);
// Verify subscription was updated with CreateProrations
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Count == 1 &&
opts.Items[0].Id == "si_storage" &&
opts.Items[0].Quantity == 9 &&
opts.ProrationBehavior == "create_prorations"));
// Verify draft invoice was created
await _stripeAdapter.Received(1).CreateInvoiceAsync(
Arg.Is<InvoiceCreateOptions>(opts =>
opts.Customer == "cus_123" &&
opts.Subscription == "sub_123" &&
opts.AutoAdvance == false &&
opts.CollectionMethod == "charge_automatically"));
// Verify invoice was finalized
await _stripeAdapter.Received(1).FinalizeInvoiceAsync(
"in_draft",
Arg.Is<InvoiceFinalizeOptions>(opts =>
opts.AutoAdvance == false &&
opts.Expand.Contains("customer")));
// Verify Braintree payment was processed
await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), finalizedInvoice);
// Verify user was saved
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
u.Id == user.Id &&
u.MaxStorageGb == 10));
}
[Theory, BitAutoData]
public async Task Run_AddStorageFromZero_PayPal_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 1;
user.Storage = 500L * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", isPayPal: true);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var draftInvoice = new Invoice { Id = "in_draft" };
_stripeAdapter.CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>()).Returns(draftInvoice);
var finalizedInvoice = new Invoice
{
Id = "in_finalized",
Customer = new Customer { Id = "cus_123" }
};
_stripeAdapter.FinalizeInvoiceAsync("in_draft", Arg.Any<InvoiceFinalizeOptions>()).Returns(finalizedInvoice);
// Act
var result = await _command.Run(user, 9);
// Assert
Assert.True(result.IsT0);
// Verify subscription was updated with new storage item
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Count == 1 &&
opts.Items[0].Price == "price_storage" &&
opts.Items[0].Quantity == 9 &&
opts.ProrationBehavior == "create_prorations"));
// Verify invoice creation and payment flow
await _stripeAdapter.Received(1).CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>());
await _stripeAdapter.Received(1).FinalizeInvoiceAsync("in_draft", Arg.Any<InvoiceFinalizeOptions>());
await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), finalizedInvoice);
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 10));
}
[Theory, BitAutoData]
public async Task Run_DecreaseStorage_PayPal_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 10;
user.Storage = 2L * 1024 * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 9, isPayPal: true);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var draftInvoice = new Invoice { Id = "in_draft" };
_stripeAdapter.CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>()).Returns(draftInvoice);
var finalizedInvoice = new Invoice
{
Id = "in_finalized",
Customer = new Customer { Id = "cus_123" }
};
_stripeAdapter.FinalizeInvoiceAsync("in_draft", Arg.Any<InvoiceFinalizeOptions>()).Returns(finalizedInvoice);
// Act
var result = await _command.Run(user, 2);
// Assert
Assert.True(result.IsT0);
// Verify subscription was updated
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Count == 1 &&
opts.Items[0].Id == "si_storage" &&
opts.Items[0].Quantity == 2 &&
opts.ProrationBehavior == "create_prorations"));
// Verify invoice creation and payment flow
await _stripeAdapter.Received(1).CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>());
await _stripeAdapter.Received(1).FinalizeInvoiceAsync("in_draft", Arg.Any<InvoiceFinalizeOptions>());
await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), finalizedInvoice);
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 3));
}
[Theory, BitAutoData]
public async Task Run_RemoveAllAdditionalStorage_PayPal_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 10;
user.Storage = 500L * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 9, isPayPal: true);
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var draftInvoice = new Invoice { Id = "in_draft" };
_stripeAdapter.CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>()).Returns(draftInvoice);
var finalizedInvoice = new Invoice
{
Id = "in_finalized",
Customer = new Customer { Id = "cus_123" }
};
_stripeAdapter.FinalizeInvoiceAsync("in_draft", Arg.Any<InvoiceFinalizeOptions>()).Returns(finalizedInvoice);
// Act
var result = await _command.Run(user, 0);
// Assert
Assert.True(result.IsT0);
// Verify subscription item was deleted
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Count == 1 &&
opts.Items[0].Id == "si_storage" &&
opts.Items[0].Deleted == true &&
opts.ProrationBehavior == "create_prorations"));
// Verify invoice creation and payment flow
await _stripeAdapter.Received(1).CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>());
await _stripeAdapter.Received(1).FinalizeInvoiceAsync("in_draft", Arg.Any<InvoiceFinalizeOptions>());
await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), finalizedInvoice);
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 1));
}
}