mirror of
https://github.com/bitwarden/server
synced 2026-01-15 06:53:26 +00:00
[PM-29604] [PM-29605] [PM-29606] Support premium subscription page redesign (#6821)
* feat(get-subscription): Add EnumMemberJsonConverter * feat(get-subscription): Add BitwardenDiscount model * feat(get-subscription): Add Cart model * feat(get-subscription): Add Storage model * feat(get-subscription): Add BitwardenSubscription model * feat(get-subscription): Add DiscountExtensions * feat(get-subscription): Add error code to StripeConstants * feat(get-subscription): Add GetBitwardenSubscriptionQuery * feat(get-subscription): Expose GET /account/billing/vnext/subscription * feat(reinstate-subscription): Add ReinstateSubscriptionCommand * feat(reinstate-subscription): Expose POST /account/billing/vnext/subscription/reinstate * feat(pay-with-paypal-immediately): Add SubscriberId union * feat(pay-with-paypal-immediately): Add BraintreeService with PayInvoice method * feat(pay-with-paypal-immediately): Pay PayPal invoice immediately when starting premium subscription * feat(pay-with-paypal-immediately): Pay invoice with Braintree on invoice.created for subscription cycles only * fix(update-storage): Always invoice for premium storage update * fix(update-storage): Move endpoint to subscription path * docs: Note FF removal POIs * (format): Run dotnet format
This commit is contained in:
@@ -8,6 +8,7 @@ using Bit.Core.Billing.Payment.Queries;
|
||||
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.Platform.Push;
|
||||
using Bit.Core.Services;
|
||||
@@ -29,6 +30,7 @@ namespace Bit.Core.Test.Billing.Premium.Commands;
|
||||
public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
{
|
||||
private readonly IBraintreeGateway _braintreeGateway = Substitute.For<IBraintreeGateway>();
|
||||
private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();
|
||||
private readonly IGlobalSettings _globalSettings = Substitute.For<IGlobalSettings>();
|
||||
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
@@ -59,6 +61,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
|
||||
_command = new CreatePremiumCloudHostedSubscriptionCommand(
|
||||
_braintreeGateway,
|
||||
_braintreeService,
|
||||
_globalSettings,
|
||||
_setupIntentCache,
|
||||
_stripeAdapter,
|
||||
@@ -235,11 +238,15 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
mockCustomer.Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = "bt_customer_123"
|
||||
};
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
mockSubscription.LatestInvoiceId = "in_123";
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
@@ -258,6 +265,9 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
await _stripeAdapter.Received(1).CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
await _subscriberService.Received(1).CreateBraintreeCustomer(user, paymentMethod.Token);
|
||||
await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId,
|
||||
Arg.Is<InvoiceUpdateOptions>(opts => opts.AutoAdvance == false));
|
||||
await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), mockInvoice);
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
|
||||
}
|
||||
@@ -456,11 +466,15 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
mockCustomer.Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = "bt_customer_123"
|
||||
};
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "incomplete";
|
||||
mockSubscription.LatestInvoiceId = "in_123";
|
||||
mockSubscription.Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
@@ -487,6 +501,9 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
Assert.True(result.IsT0);
|
||||
Assert.True(user.Premium);
|
||||
Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);
|
||||
await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId,
|
||||
Arg.Is<InvoiceUpdateOptions>(opts => opts.AutoAdvance == false));
|
||||
await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), mockInvoice);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -559,11 +576,15 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
mockCustomer.Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = "bt_customer_123"
|
||||
};
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active"; // PayPal + active doesn't match pattern
|
||||
mockSubscription.LatestInvoiceId = "in_123";
|
||||
mockSubscription.Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
@@ -590,6 +611,9 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
Assert.True(result.IsT0);
|
||||
Assert.False(user.Premium);
|
||||
Assert.Null(user.PremiumExpirationDate);
|
||||
await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId,
|
||||
Arg.Is<InvoiceUpdateOptions>(opts => opts.AutoAdvance == false));
|
||||
await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), mockInvoice);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -18,13 +18,11 @@ 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
|
||||
var premiumPlan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
@@ -32,7 +30,7 @@ public class UpdatePremiumStorageCommandTests
|
||||
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 });
|
||||
_pricingClient.ListPremiumPlans().Returns([premiumPlan]);
|
||||
|
||||
_command = new UpdatePremiumStorageCommand(
|
||||
_stripeAdapter,
|
||||
@@ -43,18 +41,19 @@ public class UpdatePremiumStorageCommandTests
|
||||
|
||||
private Subscription CreateMockSubscription(string subscriptionId, int? storageQuantity = null)
|
||||
{
|
||||
var items = new List<SubscriptionItem>();
|
||||
|
||||
// Always add the seat item
|
||||
items.Add(new SubscriptionItem
|
||||
var items = new List<SubscriptionItem>
|
||||
{
|
||||
Id = "si_seat",
|
||||
Price = new Price { Id = "price_premium" },
|
||||
Quantity = 1
|
||||
});
|
||||
// Always add the seat item
|
||||
new()
|
||||
{
|
||||
Id = "si_seat",
|
||||
Price = new Price { Id = "price_premium" },
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
|
||||
// Add storage item if quantity is provided
|
||||
if (storageQuantity.HasValue && storageQuantity.Value > 0)
|
||||
if (storageQuantity is > 0)
|
||||
{
|
||||
items.Add(new SubscriptionItem
|
||||
{
|
||||
@@ -142,7 +141,7 @@ public class UpdatePremiumStorageCommandTests
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("No access to storage.", badRequest.Response);
|
||||
Assert.Equal("User has no access to storage.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -216,7 +215,7 @@ public class UpdatePremiumStorageCommandTests
|
||||
opts.Items.Count == 1 &&
|
||||
opts.Items[0].Id == "si_storage" &&
|
||||
opts.Items[0].Quantity == 9 &&
|
||||
opts.ProrationBehavior == "create_prorations"));
|
||||
opts.ProrationBehavior == "always_invoice"));
|
||||
|
||||
// Verify user was saved
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
|
||||
@@ -233,7 +232,7 @@ public class UpdatePremiumStorageCommandTests
|
||||
user.Storage = 500L * 1024 * 1024;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", null);
|
||||
var subscription = CreateMockSubscription("sub_123");
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Subscriptions.Models;
|
||||
using Bit.Core.Billing.Subscriptions.Queries;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Subscriptions.Queries;
|
||||
|
||||
using static StripeConstants;
|
||||
|
||||
public class GetBitwardenSubscriptionQueryTests
|
||||
{
|
||||
private readonly ILogger<GetBitwardenSubscriptionQuery> _logger = Substitute.For<ILogger<GetBitwardenSubscriptionQuery>>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly GetBitwardenSubscriptionQuery _query;
|
||||
|
||||
public GetBitwardenSubscriptionQueryTests()
|
||||
{
|
||||
_query = new GetBitwardenSubscriptionQuery(
|
||||
_logger,
|
||||
_pricingClient,
|
||||
_stripeAdapter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_IncompleteStatus_ReturnsBitwardenSubscriptionWithSuspension()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Incomplete);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.Incomplete, result.Status);
|
||||
Assert.NotNull(result.Suspension);
|
||||
Assert.Equal(subscription.Created.AddHours(23), result.Suspension);
|
||||
Assert.Equal(1, result.GracePeriod);
|
||||
Assert.Null(result.NextCharge);
|
||||
Assert.Null(result.CancelAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_IncompleteExpiredStatus_ReturnsBitwardenSubscriptionWithSuspension()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.IncompleteExpired);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.IncompleteExpired, result.Status);
|
||||
Assert.NotNull(result.Suspension);
|
||||
Assert.Equal(subscription.Created.AddHours(23), result.Suspension);
|
||||
Assert.Equal(1, result.GracePeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_TrialingStatus_ReturnsBitwardenSubscriptionWithNextCharge()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Trialing);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.Trialing, result.Status);
|
||||
Assert.NotNull(result.NextCharge);
|
||||
Assert.Equal(subscription.Items.First().CurrentPeriodEnd, result.NextCharge);
|
||||
Assert.Null(result.Suspension);
|
||||
Assert.Null(result.GracePeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_ActiveStatus_ReturnsBitwardenSubscriptionWithNextCharge()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.Active, result.Status);
|
||||
Assert.NotNull(result.NextCharge);
|
||||
Assert.Equal(subscription.Items.First().CurrentPeriodEnd, result.NextCharge);
|
||||
Assert.Null(result.Suspension);
|
||||
Assert.Null(result.GracePeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_ActiveStatusWithCancelAt_ReturnsCancelAt()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var cancelAt = DateTime.UtcNow.AddMonths(1);
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active, cancelAt: cancelAt);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.Active, result.Status);
|
||||
Assert.Equal(cancelAt, result.CancelAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_PastDueStatus_WithOpenInvoices_ReturnsSuspension()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.PastDue, collectionMethod: "charge_automatically");
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
var openInvoice = CreateInvoice();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
_stripeAdapter.SearchInvoiceAsync(Arg.Any<InvoiceSearchOptions>())
|
||||
.Returns([openInvoice]);
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.PastDue, result.Status);
|
||||
Assert.NotNull(result.Suspension);
|
||||
Assert.Equal(openInvoice.Created.AddDays(14), result.Suspension);
|
||||
Assert.Equal(14, result.GracePeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_PastDueStatus_WithoutOpenInvoices_ReturnsNoSuspension()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.PastDue);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
_stripeAdapter.SearchInvoiceAsync(Arg.Any<InvoiceSearchOptions>())
|
||||
.Returns([]);
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.PastDue, result.Status);
|
||||
Assert.Null(result.Suspension);
|
||||
Assert.Null(result.GracePeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_UnpaidStatus_WithOpenInvoices_ReturnsSuspension()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Unpaid, collectionMethod: "charge_automatically");
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
var openInvoice = CreateInvoice();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
_stripeAdapter.SearchInvoiceAsync(Arg.Any<InvoiceSearchOptions>())
|
||||
.Returns([openInvoice]);
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.Unpaid, result.Status);
|
||||
Assert.NotNull(result.Suspension);
|
||||
Assert.Equal(14, result.GracePeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_CanceledStatus_ReturnsCanceledDate()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var canceledAt = DateTime.UtcNow.AddDays(-5);
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Canceled, canceledAt: canceledAt);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(SubscriptionStatus.Canceled, result.Status);
|
||||
Assert.Equal(canceledAt, result.Canceled);
|
||||
Assert.Null(result.Suspension);
|
||||
Assert.Null(result.NextCharge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_UnmanagedStatus_ThrowsConflictException()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription("unmanaged_status");
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
await Assert.ThrowsAsync<ConflictException>(() => _query.Run(user));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_WithAdditionalStorage_IncludesStorageInCart()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active, includeStorage: true);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Cart.PasswordManager.AdditionalStorage);
|
||||
Assert.Equal("additionalStorageGB", result.Cart.PasswordManager.AdditionalStorage.TranslationKey);
|
||||
Assert.Equal(2, result.Cart.PasswordManager.AdditionalStorage.Quantity);
|
||||
Assert.NotNull(result.Storage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_WithoutAdditionalStorage_ExcludesStorageFromCart()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active, includeStorage: false);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.Cart.PasswordManager.AdditionalStorage);
|
||||
Assert.NotNull(result.Storage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_WithCartLevelDiscount_IncludesDiscountInCart()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active);
|
||||
subscription.Customer.Discount = CreateDiscount(discountType: "cart");
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Cart.Discount);
|
||||
Assert.Equal(BitwardenDiscountType.PercentOff, result.Cart.Discount.Type);
|
||||
Assert.Equal(20, result.Cart.Discount.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_WithProductLevelDiscount_IncludesDiscountInCartItem()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active);
|
||||
var productDiscount = CreateDiscount(discountType: "product", productId: "prod_premium_seat");
|
||||
subscription.Discounts = [productDiscount];
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Cart.PasswordManager.Seats.Discount);
|
||||
Assert.Equal(BitwardenDiscountType.PercentOff, result.Cart.PasswordManager.Seats.Discount.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_WithoutMaxStorageGb_ReturnsNullStorage()
|
||||
{
|
||||
var user = CreateUser();
|
||||
user.MaxStorageGb = null;
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.Storage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_CalculatesStorageCorrectly()
|
||||
{
|
||||
var user = CreateUser();
|
||||
user.Storage = 5368709120; // 5 GB in bytes
|
||||
user.MaxStorageGb = 10;
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active, includeStorage: true);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Storage);
|
||||
Assert.Equal(10, result.Storage.Available);
|
||||
Assert.Equal(5.0, result.Storage.Used);
|
||||
Assert.NotEmpty(result.Storage.ReadableUsed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_TaxEstimation_WithInvoiceUpcomingNoneError_ReturnsZeroTax()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.ThrowsAsync(new StripeException { StripeError = new StripeError { Code = ErrorCodes.InvoiceUpcomingNone } });
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0, result.Cart.EstimatedTax);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_MissingPasswordManagerSeatsItem_ThrowsConflictException()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active);
|
||||
subscription.Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = []
|
||||
};
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
|
||||
await Assert.ThrowsAsync<ConflictException>(() => _query.Run(user));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_IncludesEstimatedTax()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
var invoice = CreateInvoicePreview(totalTax: 500); // $5.00 tax
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(invoice);
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(5.0m, result.Cart.EstimatedTax);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_SetsCadenceToAnnually()
|
||||
{
|
||||
var user = CreateUser();
|
||||
var subscription = CreateSubscription(SubscriptionStatus.Active);
|
||||
var premiumPlans = CreatePremiumPlans();
|
||||
|
||||
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())
|
||||
.Returns(subscription);
|
||||
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(CreateInvoicePreview());
|
||||
|
||||
var result = await _query.Run(user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(PlanCadenceType.Annually, result.Cart.Cadence);
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static User CreateUser()
|
||||
{
|
||||
return new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GatewaySubscriptionId = "sub_test123",
|
||||
MaxStorageGb = 1,
|
||||
Storage = 1073741824 // 1 GB in bytes
|
||||
};
|
||||
}
|
||||
|
||||
private static Subscription CreateSubscription(
|
||||
string status,
|
||||
bool includeStorage = false,
|
||||
DateTime? cancelAt = null,
|
||||
DateTime? canceledAt = null,
|
||||
string collectionMethod = "charge_automatically")
|
||||
{
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
|
||||
var items = new List<SubscriptionItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "si_premium_seat",
|
||||
Price = new Price
|
||||
{
|
||||
Id = "price_premium_seat",
|
||||
UnitAmountDecimal = 1000,
|
||||
Product = new Product { Id = "prod_premium_seat" }
|
||||
},
|
||||
Quantity = 1,
|
||||
CurrentPeriodStart = DateTime.UtcNow,
|
||||
CurrentPeriodEnd = currentPeriodEnd
|
||||
}
|
||||
};
|
||||
|
||||
if (includeStorage)
|
||||
{
|
||||
items.Add(new SubscriptionItem
|
||||
{
|
||||
Id = "si_storage",
|
||||
Price = new Price
|
||||
{
|
||||
Id = "price_storage",
|
||||
UnitAmountDecimal = 400,
|
||||
Product = new Product { Id = "prod_storage" }
|
||||
},
|
||||
Quantity = 2,
|
||||
CurrentPeriodStart = DateTime.UtcNow,
|
||||
CurrentPeriodEnd = currentPeriodEnd
|
||||
});
|
||||
}
|
||||
|
||||
return new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = status,
|
||||
Created = DateTime.UtcNow.AddMonths(-1),
|
||||
Customer = new Customer
|
||||
{
|
||||
Id = "cus_test123",
|
||||
Discount = null
|
||||
},
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = items
|
||||
},
|
||||
CancelAt = cancelAt,
|
||||
CanceledAt = canceledAt,
|
||||
CollectionMethod = collectionMethod,
|
||||
Discounts = []
|
||||
};
|
||||
}
|
||||
|
||||
private static List<Bit.Core.Billing.Pricing.Premium.Plan> CreatePremiumPlans()
|
||||
{
|
||||
return
|
||||
[
|
||||
new()
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||
{
|
||||
StripePriceId = "price_premium_seat",
|
||||
Price = 10.0m,
|
||||
Provided = 1
|
||||
},
|
||||
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||
{
|
||||
StripePriceId = "price_storage",
|
||||
Price = 4.0m,
|
||||
Provided = 1
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private static Invoice CreateInvoice()
|
||||
{
|
||||
return new Invoice
|
||||
{
|
||||
Id = "in_test123",
|
||||
Created = DateTime.UtcNow.AddDays(-10),
|
||||
PeriodEnd = DateTime.UtcNow.AddDays(-5),
|
||||
Attempted = true,
|
||||
Status = "open"
|
||||
};
|
||||
}
|
||||
|
||||
private static Invoice CreateInvoicePreview(long totalTax = 0)
|
||||
{
|
||||
var taxes = totalTax > 0
|
||||
? new List<InvoiceTotalTax> { new() { Amount = totalTax } }
|
||||
: new List<InvoiceTotalTax>();
|
||||
|
||||
return new Invoice
|
||||
{
|
||||
Id = "in_preview",
|
||||
TotalTaxes = taxes
|
||||
};
|
||||
}
|
||||
|
||||
private static Discount CreateDiscount(string discountType = "cart", string? productId = null)
|
||||
{
|
||||
var coupon = new Coupon
|
||||
{
|
||||
Valid = true,
|
||||
PercentOff = 20,
|
||||
AppliesTo = discountType == "product" && productId != null
|
||||
? new CouponAppliesTo { Products = [productId] }
|
||||
: new CouponAppliesTo { Products = [] }
|
||||
};
|
||||
|
||||
return new Discount
|
||||
{
|
||||
Coupon = coupon
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
219
test/Core.Test/Utilities/EnumMemberJsonConverterTests.cs
Normal file
219
test/Core.Test/Utilities/EnumMemberJsonConverterTests.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class EnumMemberJsonConverterTests
|
||||
{
|
||||
[Fact]
|
||||
public void Serialize_WithEnumMemberAttribute_UsesAttributeValue()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new EnumConverterTestObject
|
||||
{
|
||||
Status = EnumConverterTestStatus.InProgress
|
||||
};
|
||||
const string expectedJsonString = "{\"Status\":\"in_progress\"}";
|
||||
|
||||
// Act
|
||||
var jsonString = JsonSerializer.Serialize(obj);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedJsonString, jsonString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_WithoutEnumMemberAttribute_UsesEnumName()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new EnumConverterTestObject
|
||||
{
|
||||
Status = EnumConverterTestStatus.Pending
|
||||
};
|
||||
const string expectedJsonString = "{\"Status\":\"Pending\"}";
|
||||
|
||||
// Act
|
||||
var jsonString = JsonSerializer.Serialize(obj);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedJsonString, jsonString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_MultipleValues_SerializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var obj = new EnumConverterTestObjectWithMultiple
|
||||
{
|
||||
Status1 = EnumConverterTestStatus.Active,
|
||||
Status2 = EnumConverterTestStatus.InProgress,
|
||||
Status3 = EnumConverterTestStatus.Pending
|
||||
};
|
||||
const string expectedJsonString = "{\"Status1\":\"active\",\"Status2\":\"in_progress\",\"Status3\":\"Pending\"}";
|
||||
|
||||
// Act
|
||||
var jsonString = JsonSerializer.Serialize(obj);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedJsonString, jsonString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_WithEnumMemberAttribute_ReturnsCorrectEnumValue()
|
||||
{
|
||||
// Arrange
|
||||
const string json = "{\"Status\":\"in_progress\"}";
|
||||
|
||||
// Act
|
||||
var obj = JsonSerializer.Deserialize<EnumConverterTestObject>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(EnumConverterTestStatus.InProgress, obj.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_WithoutEnumMemberAttribute_ReturnsCorrectEnumValue()
|
||||
{
|
||||
// Arrange
|
||||
const string json = "{\"Status\":\"Pending\"}";
|
||||
|
||||
// Act
|
||||
var obj = JsonSerializer.Deserialize<EnumConverterTestObject>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(EnumConverterTestStatus.Pending, obj.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MultipleValues_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
const string json = "{\"Status1\":\"active\",\"Status2\":\"in_progress\",\"Status3\":\"Pending\"}";
|
||||
|
||||
// Act
|
||||
var obj = JsonSerializer.Deserialize<EnumConverterTestObjectWithMultiple>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(EnumConverterTestStatus.Active, obj.Status1);
|
||||
Assert.Equal(EnumConverterTestStatus.InProgress, obj.Status2);
|
||||
Assert.Equal(EnumConverterTestStatus.Pending, obj.Status3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_InvalidEnumString_ThrowsJsonException()
|
||||
{
|
||||
// Arrange
|
||||
const string json = "{\"Status\":\"invalid_value\"}";
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<EnumConverterTestObject>(json));
|
||||
Assert.Contains("Unable to convert 'invalid_value' to EnumConverterTestStatus", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_EmptyString_ThrowsJsonException()
|
||||
{
|
||||
// Arrange
|
||||
const string json = "{\"Status\":\"\"}";
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<EnumConverterTestObject>(json));
|
||||
Assert.Contains("Unable to convert '' to EnumConverterTestStatus", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_WithEnumMemberAttribute_PreservesValue()
|
||||
{
|
||||
// Arrange
|
||||
var originalObj = new EnumConverterTestObject
|
||||
{
|
||||
Status = EnumConverterTestStatus.Completed
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(originalObj);
|
||||
var deserializedObj = JsonSerializer.Deserialize<EnumConverterTestObject>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(originalObj.Status, deserializedObj.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_WithoutEnumMemberAttribute_PreservesValue()
|
||||
{
|
||||
// Arrange
|
||||
var originalObj = new EnumConverterTestObject
|
||||
{
|
||||
Status = EnumConverterTestStatus.Pending
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(originalObj);
|
||||
var deserializedObj = JsonSerializer.Deserialize<EnumConverterTestObject>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(originalObj.Status, deserializedObj.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_AllEnumValues_ProducesExpectedStrings()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Equal("\"Pending\"", JsonSerializer.Serialize(EnumConverterTestStatus.Pending, CreateOptions()));
|
||||
Assert.Equal("\"active\"", JsonSerializer.Serialize(EnumConverterTestStatus.Active, CreateOptions()));
|
||||
Assert.Equal("\"in_progress\"", JsonSerializer.Serialize(EnumConverterTestStatus.InProgress, CreateOptions()));
|
||||
Assert.Equal("\"completed\"", JsonSerializer.Serialize(EnumConverterTestStatus.Completed, CreateOptions()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_AllEnumValues_ReturnsCorrectEnums()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Equal(EnumConverterTestStatus.Pending, JsonSerializer.Deserialize<EnumConverterTestStatus>("\"Pending\"", CreateOptions()));
|
||||
Assert.Equal(EnumConverterTestStatus.Active, JsonSerializer.Deserialize<EnumConverterTestStatus>("\"active\"", CreateOptions()));
|
||||
Assert.Equal(EnumConverterTestStatus.InProgress, JsonSerializer.Deserialize<EnumConverterTestStatus>("\"in_progress\"", CreateOptions()));
|
||||
Assert.Equal(EnumConverterTestStatus.Completed, JsonSerializer.Deserialize<EnumConverterTestStatus>("\"completed\"", CreateOptions()));
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add(new EnumMemberJsonConverter<EnumConverterTestStatus>());
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
public class EnumConverterTestObject
|
||||
{
|
||||
[JsonConverter(typeof(EnumMemberJsonConverter<EnumConverterTestStatus>))]
|
||||
public EnumConverterTestStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public class EnumConverterTestObjectWithMultiple
|
||||
{
|
||||
[JsonConverter(typeof(EnumMemberJsonConverter<EnumConverterTestStatus>))]
|
||||
public EnumConverterTestStatus Status1 { get; set; }
|
||||
|
||||
[JsonConverter(typeof(EnumMemberJsonConverter<EnumConverterTestStatus>))]
|
||||
public EnumConverterTestStatus Status2 { get; set; }
|
||||
|
||||
[JsonConverter(typeof(EnumMemberJsonConverter<EnumConverterTestStatus>))]
|
||||
public EnumConverterTestStatus Status3 { get; set; }
|
||||
}
|
||||
|
||||
public enum EnumConverterTestStatus
|
||||
{
|
||||
Pending, // No EnumMemberAttribute
|
||||
|
||||
[EnumMember(Value = "active")]
|
||||
Active,
|
||||
|
||||
[EnumMember(Value = "in_progress")]
|
||||
InProgress,
|
||||
|
||||
[EnumMember(Value = "completed")]
|
||||
Completed
|
||||
}
|
||||
Reference in New Issue
Block a user