mirror of
https://github.com/bitwarden/server
synced 2025-12-22 19:23:45 +00:00
[PM-24284] - milestone 3 (#6543)
* new feature flag * first pass at changes * safeguard against billing-pricing not being deployed yet * handle families pre migration plan * wrong stripe id * tests * unit tests
This commit is contained in:
@@ -97,6 +97,8 @@ public class ConfirmOrganizationUserCommandTests
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2025, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2025, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)]
|
||||
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)]
|
||||
|
||||
@@ -23,6 +23,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2025)]
|
||||
public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
|
||||
{
|
||||
signup.Plan = planType;
|
||||
@@ -65,6 +66,7 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2025)]
|
||||
public async Task SignUp_AssignsOwnerToDefaultCollection
|
||||
(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
|
||||
{
|
||||
|
||||
527
test/Core.Test/Billing/Pricing/PricingClientTests.cs
Normal file
527
test/Core.Test/Billing/Pricing/PricingClientTests.cs
Normal file
@@ -0,0 +1,527 @@
|
||||
using System.Net;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using RichardSzalay.MockHttp;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Pricing;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PricingClientTests
|
||||
{
|
||||
#region GetLookupKey Tests (via GetPlan)
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagEnabled_UsesFamilies2025LookupKey()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("families-2025", "Families 2025", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/families-2025")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagDisabled_UsesFamiliesLookupKey()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/families")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
// PreProcessFamiliesPreMigrationPlan should change "families" to "families-2025" when FF is disabled
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PreProcessFamiliesPreMigrationPlan Tests (via GetPlan)
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagDisabled_ReturnsFamiliesAnnually2025PlanType()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
// billing-pricing returns "families" lookup key because the flag is off
|
||||
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
// PreProcessFamiliesPreMigrationPlan should convert the families lookup key to families-2025
|
||||
// and the PlanAdapter should assign the correct FamiliesAnnually2025 plan type
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagEnabled_ReturnsFamiliesAnnually2025PlanType()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("families-2025", "Families", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
// PreProcessFamiliesPreMigrationPlan should ignore the lookup key because the flag is on
|
||||
// and the PlanAdapter should assign the correct FamiliesAnnually2025 plan type
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithFamiliesAnnuallyAndFeatureFlagEnabled_ReturnsFamiliesAnnuallyPlanType()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
// PreProcessFamiliesPreMigrationPlan should ignore the lookup key because the flag is on
|
||||
// and the PlanAdapter should assign the correct FamiliesAnnually plan type
|
||||
Assert.Equal(PlanType.FamiliesAnnually, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WithOtherLookupKey_KeepsLookupKeyUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var planJson = CreatePlanJson("enterprise-annually", "Enterprise", "enterprise", 144M, "price_id");
|
||||
|
||||
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/enterprise-annually")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond("application/json", planJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(PlanType.EnterpriseAnnually, result.Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ListPlans Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ListPlans_WithFeatureFlagDisabled_ReturnsListWithPreProcessing()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
// biling-pricing would return "families" because the flag is disabled
|
||||
var plansJson = $@"[
|
||||
{CreatePlanJson("families", "Families", "families", 40M, "price_id")},
|
||||
{CreatePlanJson("enterprise-annually", "Enterprise", "enterprise", 144M, "price_id")}
|
||||
]";
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization")
|
||||
.Respond("application/json", plansJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.ListPlans();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Count);
|
||||
// First plan should have been preprocessed from "families" to "families-2025"
|
||||
Assert.Equal(PlanType.FamiliesAnnually2025, result[0].Type);
|
||||
// Second plan should remain unchanged
|
||||
Assert.Equal(PlanType.EnterpriseAnnually, result[1].Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListPlans_WithFeatureFlagEnabled_ReturnsListWithoutPreProcessing()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
var plansJson = $@"[
|
||||
{CreatePlanJson("families", "Families", "families", 40M, "price_id")}
|
||||
]";
|
||||
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization")
|
||||
.Respond("application/json", plansJson);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.ListPlans();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
// Plan should remain as FamiliesAnnually when FF is enabled
|
||||
Assert.Equal(PlanType.FamiliesAnnually, result[0].Type);
|
||||
mockHttp.VerifyNoOutstandingExpectation();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetPlan - Additional Coverage
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPlan_WhenSelfHosted_ReturnsNull(
|
||||
SutProvider<PricingClient> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var globalSettings = sutProvider.GetDependency<GlobalSettings>();
|
||||
globalSettings.SelfHosted = true;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPlan_WhenPricingServiceDisabled_ReturnsStaticStorePlan(
|
||||
SutProvider<PricingClient> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.UsePricingService)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPlan(PlanType.FamiliesAnnually);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(PlanType.FamiliesAnnually, result.Type);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPlan_WhenLookupKeyNotFound_ReturnsNull(
|
||||
SutProvider<PricingClient> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.UsePricingService)
|
||||
.Returns(true);
|
||||
|
||||
// Act - Using PlanType that doesn't have a lookup key mapping
|
||||
var result = await sutProvider.Sut.GetPlan(unchecked((PlanType)999));
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WhenPricingServiceReturnsNotFound_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond(HttpStatusCode.NotFound);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act
|
||||
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlan_WhenPricingServiceReturnsError_ThrowsBillingException()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
|
||||
.Respond(HttpStatusCode.InternalServerError);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BillingException>(() =>
|
||||
pricingClient.GetPlan(PlanType.FamiliesAnnually2025));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ListPlans - Additional Coverage
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ListPlans_WhenSelfHosted_ReturnsEmptyList(
|
||||
SutProvider<PricingClient> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var globalSettings = sutProvider.GetDependency<GlobalSettings>();
|
||||
globalSettings.SelfHosted = true;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ListPlans();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ListPlans_WhenPricingServiceDisabled_ReturnsStaticStorePlans(
|
||||
SutProvider<PricingClient> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.UsePricingService)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ListPlans();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEmpty(result);
|
||||
Assert.Equal(StaticStore.Plans.Count(), result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListPlans_WhenPricingServiceReturnsError_ThrowsBillingException()
|
||||
{
|
||||
// Arrange
|
||||
var mockHttp = new MockHttpMessageHandler();
|
||||
mockHttp.When(HttpMethod.Get, "*/plans/organization")
|
||||
.Respond(HttpStatusCode.InternalServerError);
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
|
||||
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
var httpClient = new HttpClient(mockHttp)
|
||||
{
|
||||
BaseAddress = new Uri("https://test.com/")
|
||||
};
|
||||
|
||||
var logger = Substitute.For<ILogger<PricingClient>>();
|
||||
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BillingException>(() =>
|
||||
pricingClient.ListPlans());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string CreatePlanJson(
|
||||
string lookupKey,
|
||||
string name,
|
||||
string tier,
|
||||
decimal seatsPrice,
|
||||
string seatsStripePriceId,
|
||||
int seatsQuantity = 1)
|
||||
{
|
||||
return $@"{{
|
||||
""lookupKey"": ""{lookupKey}"",
|
||||
""name"": ""{name}"",
|
||||
""tier"": ""{tier}"",
|
||||
""features"": [],
|
||||
""seats"": {{
|
||||
""type"": ""packaged"",
|
||||
""quantity"": {seatsQuantity},
|
||||
""price"": {seatsPrice},
|
||||
""stripePriceId"": ""{seatsStripePriceId}""
|
||||
}},
|
||||
""canUpgradeTo"": [],
|
||||
""additionalData"": {{
|
||||
""nameLocalizationKey"": ""{lookupKey}Name"",
|
||||
""descriptionLocalizationKey"": ""{lookupKey}Description""
|
||||
}}
|
||||
}}";
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public class SecretsManagerSubscriptionUpdateTests
|
||||
}
|
||||
|
||||
public static TheoryData<Plan> NonSmPlans =>
|
||||
ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019]);
|
||||
ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2025, PlanType.FamiliesAnnually2019]);
|
||||
|
||||
public static TheoryData<Plan> SmPlans => ToPlanTheory([
|
||||
PlanType.EnterpriseAnnually2019,
|
||||
|
||||
@@ -13,7 +13,7 @@ public class StaticStoreTests
|
||||
var plans = StaticStore.Plans.ToList();
|
||||
Assert.NotNull(plans);
|
||||
Assert.NotEmpty(plans);
|
||||
Assert.Equal(22, plans.Count);
|
||||
Assert.Equal(23, plans.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -34,8 +34,8 @@ public class StaticStoreTests
|
||||
{
|
||||
// Ref: https://daniel.haxx.se/blog/2025/05/16/detecting-malicious-unicode/
|
||||
// URLs can contain unicode characters that to a computer would point to completely seperate domains but to the
|
||||
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
|
||||
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
|
||||
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
|
||||
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
|
||||
// url update that could be missed in code review and then if they got a user to that URL Bitwarden could
|
||||
// consider it equivalent with a cipher in the users vault and offer autofill when we should not.
|
||||
// GitHub does now show a warning on non-ascii characters but it could still be missed.
|
||||
|
||||
Reference in New Issue
Block a user