mirror of
https://github.com/bitwarden/server
synced 2026-01-13 05:53:37 +00:00
Merge branch 'main' into auth/pm-22975/client-version-validator
This commit is contained in:
@@ -1,57 +1,245 @@
|
||||
using Bit.Api.Billing.Controllers.VNext;
|
||||
using Bit.Api.Billing.Models.Requests.Storage;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Licenses.Queries;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NSubstitute;
|
||||
using OneOf.Types;
|
||||
using Xunit;
|
||||
using BadRequest = Bit.Core.Billing.Commands.BadRequest;
|
||||
|
||||
namespace Bit.Api.Test.Billing.Controllers.VNext;
|
||||
|
||||
public class AccountBillingVNextControllerTests
|
||||
{
|
||||
private readonly ICreateBitPayInvoiceForCreditCommand _createBitPayInvoiceForCreditCommand;
|
||||
private readonly ICreatePremiumCloudHostedSubscriptionCommand _createPremiumCloudHostedSubscriptionCommand;
|
||||
private readonly IGetCreditQuery _getCreditQuery;
|
||||
private readonly IGetPaymentMethodQuery _getPaymentMethodQuery;
|
||||
private readonly IUpdatePremiumStorageCommand _updatePremiumStorageCommand;
|
||||
private readonly IGetUserLicenseQuery _getUserLicenseQuery;
|
||||
private readonly IUpdatePaymentMethodCommand _updatePaymentMethodCommand;
|
||||
private readonly AccountBillingVNextController _sut;
|
||||
|
||||
public AccountBillingVNextControllerTests()
|
||||
{
|
||||
_createBitPayInvoiceForCreditCommand = Substitute.For<ICreateBitPayInvoiceForCreditCommand>();
|
||||
_createPremiumCloudHostedSubscriptionCommand = Substitute.For<ICreatePremiumCloudHostedSubscriptionCommand>();
|
||||
_getCreditQuery = Substitute.For<IGetCreditQuery>();
|
||||
_getPaymentMethodQuery = Substitute.For<IGetPaymentMethodQuery>();
|
||||
_updatePremiumStorageCommand = Substitute.For<IUpdatePremiumStorageCommand>();
|
||||
_getUserLicenseQuery = Substitute.For<IGetUserLicenseQuery>();
|
||||
_updatePaymentMethodCommand = Substitute.For<IUpdatePaymentMethodCommand>();
|
||||
|
||||
_sut = new AccountBillingVNextController(
|
||||
_createBitPayInvoiceForCreditCommand,
|
||||
_createPremiumCloudHostedSubscriptionCommand,
|
||||
_getCreditQuery,
|
||||
_getPaymentMethodQuery,
|
||||
Substitute.For<Core.Billing.Payment.Commands.ICreateBitPayInvoiceForCreditCommand>(),
|
||||
Substitute.For<Core.Billing.Premium.Commands.ICreatePremiumCloudHostedSubscriptionCommand>(),
|
||||
Substitute.For<Core.Billing.Payment.Queries.IGetCreditQuery>(),
|
||||
Substitute.For<Core.Billing.Payment.Queries.IGetPaymentMethodQuery>(),
|
||||
_getUserLicenseQuery,
|
||||
_updatePaymentMethodCommand);
|
||||
Substitute.For<Core.Billing.Payment.Commands.IUpdatePaymentMethodCommand>(),
|
||||
_updatePremiumStorageCommand);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetLicenseAsync_ValidUser_ReturnsLicenseResponse(User user,
|
||||
public async Task GetLicenseAsync_ValidUser_ReturnsLicenseResponse(
|
||||
User user,
|
||||
Core.Billing.Licenses.Models.Api.Response.LicenseResponseModel licenseResponse)
|
||||
{
|
||||
// Arrange
|
||||
_getUserLicenseQuery.Run(user).Returns(licenseResponse);
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetLicenseAsync(user);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _getUserLicenseQuery.Received(1).Run(user);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_Success_ReturnsOk(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 10))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 10);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_UserNotPremium_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };
|
||||
var errorMessage = "User does not have a premium subscription.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 10))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 10);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_NoPaymentMethod_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };
|
||||
var errorMessage = "No payment method found.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 10))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 10);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_StorageLessThanBase_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 1 };
|
||||
var errorMessage = "Storage cannot be less than the base amount of 1 GB.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 1))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 1);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_StorageExceedsMaximum_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 100 };
|
||||
var errorMessage = "Maximum storage is 100 GB.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 100))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 100);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_StorageExceedsCurrentUsage_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 2 };
|
||||
var errorMessage = "You are currently using 5.00 GB of storage. Delete some stored data first.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 2))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 2);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_IncreaseStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 15 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 15))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 15);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_DecreaseStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 3 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 3))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 3);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_MaximumStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 100 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 100))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 100);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_NullPaymentSecret_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 5 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 5))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,4 +407,85 @@ public class UpdateBillingAddressCommandTests
|
||||
options => options.Type == TaxIdType.SpanishNIF &&
|
||||
options.Value == input.TaxId.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_BusinessOrganization_UpdatingWithSameTaxId_DeletesBeforeCreating()
|
||||
{
|
||||
var organization = new Organization
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
GatewayCustomerId = "cus_123",
|
||||
GatewaySubscriptionId = "sub_123"
|
||||
};
|
||||
|
||||
var input = new BillingAddress
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345",
|
||||
Line1 = "123 Main St.",
|
||||
Line2 = "Suite 100",
|
||||
City = "New York",
|
||||
State = "NY",
|
||||
TaxId = new TaxID("us_ein", "987654321")
|
||||
};
|
||||
|
||||
var existingTaxId = new TaxId { Id = "tax_id_123", Type = "us_ein", Value = "987654321" };
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345",
|
||||
Line1 = "123 Main St.",
|
||||
Line2 = "Suite 100",
|
||||
City = "New York",
|
||||
State = "NY"
|
||||
},
|
||||
Id = organization.GatewayCustomerId,
|
||||
Subscriptions = new StripeList<Subscription>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new Subscription
|
||||
{
|
||||
Id = organization.GatewaySubscriptionId,
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
|
||||
}
|
||||
]
|
||||
},
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [existingTaxId]
|
||||
}
|
||||
};
|
||||
|
||||
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Address.Matches(input) &&
|
||||
options.HasExpansions("subscriptions", "tax_ids") &&
|
||||
options.TaxExempt == TaxExempt.None
|
||||
)).Returns(customer);
|
||||
|
||||
var newTaxId = new TaxId { Id = "tax_id_456", Type = "us_ein", Value = "987654321" };
|
||||
_stripeAdapter.CreateTaxIdAsync(customer.Id, Arg.Is<TaxIdCreateOptions>(
|
||||
options => options.Type == "us_ein" && options.Value == "987654321"
|
||||
)).Returns(newTaxId);
|
||||
|
||||
var result = await _command.Run(organization, input);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
var output = result.AsT0;
|
||||
Assert.Equivalent(input, output);
|
||||
|
||||
// Verify that deletion happens before creation
|
||||
Received.InOrder(() =>
|
||||
{
|
||||
_stripeAdapter.DeleteTaxIdAsync(customer.Id, existingTaxId.Id);
|
||||
_stripeAdapter.CreateTaxIdAsync(customer.Id, Arg.Any<TaxIdCreateOptions>());
|
||||
});
|
||||
|
||||
await _stripeAdapter.Received(1).DeleteTaxIdAsync(customer.Id, existingTaxId.Id);
|
||||
await _stripeAdapter.Received(1).CreateTaxIdAsync(customer.Id, Arg.Is<TaxIdCreateOptions>(
|
||||
options => options.Type == "us_ein" && options.Value == "987654321"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
|
||||
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Premium.Commands;
|
||||
|
||||
public class UpdatePremiumStorageCommandTests
|
||||
{
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly PremiumPlan _premiumPlan;
|
||||
private readonly UpdatePremiumStorageCommand _command;
|
||||
|
||||
public UpdatePremiumStorageCommandTests()
|
||||
{
|
||||
// Setup default premium plan with standard pricing
|
||||
_premiumPlan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = "price_premium", Provided = 1 },
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = "price_storage", Provided = 1 }
|
||||
};
|
||||
_pricingClient.ListPremiumPlans().Returns(new List<PremiumPlan> { _premiumPlan });
|
||||
|
||||
_command = new UpdatePremiumStorageCommand(
|
||||
_stripeAdapter,
|
||||
_userService,
|
||||
_pricingClient,
|
||||
Substitute.For<ILogger<UpdatePremiumStorageCommand>>());
|
||||
}
|
||||
|
||||
private Subscription CreateMockSubscription(string subscriptionId, int? storageQuantity = null)
|
||||
{
|
||||
var items = new List<SubscriptionItem>();
|
||||
|
||||
// Always add the seat item
|
||||
items.Add(new SubscriptionItem
|
||||
{
|
||||
Id = "si_seat",
|
||||
Price = new Price { Id = "price_premium" },
|
||||
Quantity = 1
|
||||
});
|
||||
|
||||
// Add storage item if quantity is provided
|
||||
if (storageQuantity.HasValue && storageQuantity.Value > 0)
|
||||
{
|
||||
items.Add(new SubscriptionItem
|
||||
{
|
||||
Id = "si_storage",
|
||||
Price = new Price { Id = "price_storage" },
|
||||
Quantity = storageQuantity.Value
|
||||
});
|
||||
}
|
||||
|
||||
return new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = items
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_UserNotPremium_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 5);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("User does not have a premium subscription.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NegativeStorage_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 5;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 4);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, -5);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("Additional storage cannot be negative.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_StorageExceedsMaximum_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 5;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 4);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 100);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("Maximum storage is 100 GB.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NoMaxStorageGb_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = null;
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 5);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("No access to storage.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_StorageExceedsCurrentUsage_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 10;
|
||||
user.Storage = 5L * 1024 * 1024 * 1024; // 5 GB currently used
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 9);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Contains("You are currently using", badRequest.Response);
|
||||
Assert.Contains("Delete some stored data first", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_SameStorageAmount_Idempotent(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);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 4);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
// Verify subscription was fetched but NOT updated
|
||||
await _stripeAdapter.Received(1).GetSubscriptionAsync("sub_123");
|
||||
await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_IncreaseStorage_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);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 9);
|
||||
|
||||
// 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 == 9 &&
|
||||
opts.ProrationBehavior == "create_prorations"));
|
||||
|
||||
// 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_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 1;
|
||||
user.Storage = 500L * 1024 * 1024;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", null);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// 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));
|
||||
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 10));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_DecreaseStorage_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);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// 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));
|
||||
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 3));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_RemoveAllAdditionalStorage_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);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// 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));
|
||||
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 1));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_MaximumStorage_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);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 99);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||
opts.Items[0].Quantity == 99));
|
||||
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 100));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user