From 71a8116d4c793ceec9da8d82916bf5f1bba24930 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:57:29 -0600 Subject: [PATCH] [PM-29089] Remove FF: `pm-26793-fetch-premium-price-from-pricing-service` - Logic (#6989) * refactor: [PM-39087] remove PM-26793 feature flag from PricingClient * test: add ListPremiumPlans and GetAvailablePremiumPlan coverage to PricingClientTests --- src/Core/Billing/Pricing/PricingClient.cs | 18 -- .../Billing/Pricing/PricingClientTests.cs | 204 ++++++++++++++++++ 2 files changed, 204 insertions(+), 18 deletions(-) diff --git a/src/Core/Billing/Pricing/PricingClient.cs b/src/Core/Billing/Pricing/PricingClient.cs index ecb85ed7e8..127eab1fbc 100644 --- a/src/Core/Billing/Pricing/PricingClient.cs +++ b/src/Core/Billing/Pricing/PricingClient.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Http.Json; -using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing.Organizations; using Bit.Core.Exceptions; @@ -12,7 +11,6 @@ namespace Bit.Core.Billing.Pricing; using OrganizationPlan = Bit.Core.Models.StaticStore.Plan; using PremiumPlan = Premium.Plan; -using Purchasable = Premium.Purchasable; public class PricingClient( IFeatureService featureService, @@ -99,14 +97,6 @@ public class PricingClient( return []; } - var fetchPremiumPriceFromPricingService = - featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService); - - if (!fetchPremiumPriceFromPricingService) - { - return [CurrentPremiumPlan]; - } - var response = await httpClient.GetAsync("plans/premium"); if (response.IsSuccessStatusCode) @@ -164,12 +154,4 @@ public class PricingClient( return plan; } - private static PremiumPlan CurrentPremiumPlan => new() - { - Name = "Premium", - Available = true, - LegacyYear = null, - Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually }, - Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 } - }; } diff --git a/test/Core.Test/Billing/Pricing/PricingClientTests.cs b/test/Core.Test/Billing/Pricing/PricingClientTests.cs index 43329e9c2e..caca997ed1 100644 --- a/test/Core.Test/Billing/Pricing/PricingClientTests.cs +++ b/test/Core.Test/Billing/Pricing/PricingClientTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Billing; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; +using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -445,6 +446,181 @@ public class PricingClientTests #endregion + #region ListPremiumPlans Tests + + [Fact] + public async Task ListPremiumPlans_Success_ReturnsPremiumPlans() + { + // Arrange + var mockHttp = new MockHttpMessageHandler(); + var plansJson = $@"[ + {CreatePremiumPlanJson("Premium", true, null, 10M, "price_premium", 4M, "price_storage", 1)}, + {CreatePremiumPlanJson("Premium Legacy", false, 2019, 10M, "price_premium_legacy", 4M, "price_storage_legacy", 1)} + ]"; + + mockHttp.When(HttpMethod.Get, "*/plans/premium") + .Respond("application/json", plansJson); + + var featureService = Substitute.For(); + var globalSettings = new GlobalSettings { SelfHosted = false }; + + var httpClient = new HttpClient(mockHttp) + { + BaseAddress = new Uri("https://test.com/") + }; + + var logger = Substitute.For>(); + var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger); + + // Act + var result = await pricingClient.ListPremiumPlans(); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.Equal("Premium", result[0].Name); + Assert.True(result[0].Available); + Assert.Null(result[0].LegacyYear); + Assert.Equal(10M, result[0].Seat.Price); + Assert.Equal("price_premium", result[0].Seat.StripePriceId); + Assert.Equal(4M, result[0].Storage.Price); + Assert.Equal("price_storage", result[0].Storage.StripePriceId); + Assert.Equal(1, result[0].Storage.Provided); + Assert.Equal("Premium Legacy", result[1].Name); + Assert.False(result[1].Available); + Assert.Equal(2019, result[1].LegacyYear); + } + + [Theory, BitAutoData] + public async Task ListPremiumPlans_WhenSelfHosted_ReturnsEmptyList( + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().SelfHosted = true; + + // Act + var result = await sutProvider.Sut.ListPremiumPlans(); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public async Task ListPremiumPlans_WhenPricingServiceReturnsError_ThrowsBillingException() + { + // Arrange + var mockHttp = new MockHttpMessageHandler(); + mockHttp.When(HttpMethod.Get, "*/plans/premium") + .Respond(HttpStatusCode.InternalServerError); + + var featureService = Substitute.For(); + var globalSettings = new GlobalSettings { SelfHosted = false }; + + var httpClient = new HttpClient(mockHttp) + { + BaseAddress = new Uri("https://test.com/") + }; + + var logger = Substitute.For>(); + var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger); + + // Act & Assert + await Assert.ThrowsAsync(() => + pricingClient.ListPremiumPlans()); + } + + #endregion + + #region GetAvailablePremiumPlan Tests + + [Fact] + public async Task GetAvailablePremiumPlan_WithAvailablePlan_ReturnsIt() + { + // Arrange + var mockHttp = new MockHttpMessageHandler(); + var plansJson = $@"[ + {CreatePremiumPlanJson("Premium Legacy", false, 2019, 10M, "price_legacy", 4M, "price_storage_legacy", 1)}, + {CreatePremiumPlanJson("Premium", true, null, 10M, "price_premium", 4M, "price_storage", 1)} + ]"; + + mockHttp.When(HttpMethod.Get, "*/plans/premium") + .Respond("application/json", plansJson); + + var featureService = Substitute.For(); + var globalSettings = new GlobalSettings { SelfHosted = false }; + + var httpClient = new HttpClient(mockHttp) + { + BaseAddress = new Uri("https://test.com/") + }; + + var logger = Substitute.For>(); + var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger); + + // Act + var result = await pricingClient.GetAvailablePremiumPlan(); + + // Assert + Assert.NotNull(result); + Assert.Equal("Premium", result.Name); + Assert.True(result.Available); + } + + [Fact] + public async Task GetAvailablePremiumPlan_WithNoAvailablePlan_ThrowsNotFoundException() + { + // Arrange + var mockHttp = new MockHttpMessageHandler(); + var plansJson = $@"[ + {CreatePremiumPlanJson("Premium Legacy", false, 2019, 10M, "price_legacy", 4M, "price_storage_legacy", 1)} + ]"; + + mockHttp.When(HttpMethod.Get, "*/plans/premium") + .Respond("application/json", plansJson); + + var featureService = Substitute.For(); + var globalSettings = new GlobalSettings { SelfHosted = false }; + + var httpClient = new HttpClient(mockHttp) + { + BaseAddress = new Uri("https://test.com/") + }; + + var logger = Substitute.For>(); + var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger); + + // Act & Assert + await Assert.ThrowsAsync(() => + pricingClient.GetAvailablePremiumPlan()); + } + + [Fact] + public async Task GetAvailablePremiumPlan_WithEmptyList_ThrowsNotFoundException() + { + // Arrange + var mockHttp = new MockHttpMessageHandler(); + mockHttp.When(HttpMethod.Get, "*/plans/premium") + .Respond("application/json", "[]"); + + var featureService = Substitute.For(); + var globalSettings = new GlobalSettings { SelfHosted = false }; + + var httpClient = new HttpClient(mockHttp) + { + BaseAddress = new Uri("https://test.com/") + }; + + var logger = Substitute.For>(); + var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger); + + // Act & Assert + await Assert.ThrowsAsync(() => + pricingClient.GetAvailablePremiumPlan()); + } + + #endregion + private static string CreatePlanJson( string lookupKey, string name, @@ -471,4 +647,32 @@ public class PricingClientTests }} }}"; } + + private static string CreatePremiumPlanJson( + string name, + bool available, + int? legacyYear, + decimal seatPrice, + string seatStripePriceId, + decimal storagePrice, + string storageStripePriceId, + int storageProvided) + { + var legacyYearJson = legacyYear.HasValue ? legacyYear.Value.ToString() : "null"; + return $@"{{ + ""name"": ""{name}"", + ""available"": {available.ToString().ToLower()}, + ""legacyYear"": {legacyYearJson}, + ""seat"": {{ + ""stripePriceId"": ""{seatStripePriceId}"", + ""price"": {seatPrice}, + ""provided"": 0 + }}, + ""storage"": {{ + ""stripePriceId"": ""{storageStripePriceId}"", + ""price"": {storagePrice}, + ""provided"": {storageProvided} + }} + }}"; + } }