From a2ba5289fa77b79f5f117279eda271978dfb208a Mon Sep 17 00:00:00 2001
From: Ike <137194738+ike-kottlowski@users.noreply.github.com>
Date: Fri, 2 Jan 2026 12:02:57 -0500
Subject: [PATCH 1/5] [PM-29568] Footer Links lack correct styling (#6784)
* fix: color weight of links in footer.
* fix: css styling
---
.../Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs | 4 ++--
.../organization-confirmation-enterprise-teams.html.hbs | 4 ++--
.../organization-confirmation-family-free.html.hbs | 4 ++--
.../MJML/Auth/Onboarding/welcome-family-user.html.hbs | 4 ++--
.../MJML/Auth/Onboarding/welcome-individual-user.html.hbs | 4 ++--
.../Handlebars/MJML/Auth/Onboarding/welcome-org-user.html.hbs | 4 ++--
src/Core/MailTemplates/Mjml/components/footer.mjml | 4 ++--
7 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs
index 352bb447c8..7d30fdcbe4 100644
--- a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs
@@ -660,8 +660,8 @@
Always confirm you are on a trusted Bitwarden domain before logging
in:
- bitwarden.com |
- Learn why we include this
+ bitwarden.com |
+ Learn why we include this
diff --git a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs
index be1a3854b5..29977724d4 100644
--- a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs
@@ -784,8 +784,8 @@
Always confirm you are on a trusted Bitwarden domain before logging
in:
- bitwarden.com |
- Learn why we include this
+ bitwarden.com |
+ Learn why we include this
diff --git a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs
index b9984343d5..93d4b9cd9c 100644
--- a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs
@@ -952,8 +952,8 @@
Always confirm you are on a trusted Bitwarden domain before logging
in:
- bitwarden.com |
- Learn why we include this
+ bitwarden.com |
+ Learn why we include this
diff --git a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs
index 1998cf10ba..b2b957f849 100644
--- a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs
@@ -873,8 +873,8 @@
Always confirm you are on a trusted Bitwarden domain before logging
in:
- bitwarden.com |
- Learn why we include this
+ bitwarden.com |
+ Learn why we include this
diff --git a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-individual-user.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-individual-user.html.hbs
index 2ad670383b..4cdf153c30 100644
--- a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-individual-user.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-individual-user.html.hbs
@@ -872,8 +872,8 @@
Always confirm you are on a trusted Bitwarden domain before logging
in:
- bitwarden.com |
- Learn why we include this
+ bitwarden.com |
+ Learn why we include this
diff --git a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-org-user.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-org-user.html.hbs
index efcacf1866..5a8dfb7374 100644
--- a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-org-user.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-org-user.html.hbs
@@ -873,8 +873,8 @@
Always confirm you are on a trusted Bitwarden domain before logging
in:
- bitwarden.com |
- Learn why we include this
+ bitwarden.com |
+ Learn why we include this
diff --git a/src/Core/MailTemplates/Mjml/components/footer.mjml b/src/Core/MailTemplates/Mjml/components/footer.mjml
index 4037d6c9ba..ddaf3f493b 100644
--- a/src/Core/MailTemplates/Mjml/components/footer.mjml
+++ b/src/Core/MailTemplates/Mjml/components/footer.mjml
@@ -45,8 +45,8 @@
Always confirm you are on a trusted Bitwarden domain before logging
in:
- bitwarden.com |
- Learn why we include this
+ bitwarden.com |
+ Learn why we include this
From e9d53c0c6b9c81bfc4a638ff63083af41cea9c3e Mon Sep 17 00:00:00 2001
From: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Date: Sat, 3 Jan 2026 07:48:34 +1000
Subject: [PATCH 2/5] [PM-30298] Initial documentation for OrganizationAbility
pattern (#6781)
---
.../OrganizationAbility.cs | 0
.../OrganizationAbility/README.md | 141 ++++++++++++++++++
2 files changed, 141 insertions(+)
rename src/Core/AdminConsole/{Models/Data/Organizations => OrganizationFeatures/OrganizationAbility}/OrganizationAbility.cs (100%)
create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md
diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs
similarity index 100%
rename from src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs
rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs
diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md
new file mode 100644
index 0000000000..7b92ba3fef
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md
@@ -0,0 +1,141 @@
+# Organization Ability Flags
+
+## Overview
+
+Many Bitwarden features are tied to specific subscription plans. For example, SCIM and SSO are Enterprise features,
+while Event Logs are available to Teams and Enterprise plans. When developing features that require plan-based access
+control, we use **Organization Ability Flags** (or simply _abilities_) — explicit boolean properties on the Organization
+entity that indicate whether an organization can use a specific feature.
+
+## The Rule
+
+**Never check plan types to control feature access.** Always use a dedicated ability flag on the Organization entity.
+
+### ❌ Don't Do This
+
+```csharp
+// Checking plan type directly
+if (organization.PlanType == PlanType.Enterprise ||
+ organization.PlanType == PlanType.Teams ||
+ organization.PlanType == PlanType.Family)
+{
+ // allow feature...
+}
+```
+
+### ❌ Don't Do This
+
+```csharp
+// Piggybacking off another feature's ability
+if (organization.PlanType == PlanType.Enterprise && organization.UseEvents)
+{
+ // assume they can use some other feature...
+}
+```
+
+### ✅ Do This Instead
+
+```csharp
+// Check the explicit ability flag
+if (organization.UseEvents)
+{
+ // allow UseEvents feature...
+}
+```
+
+## Why This Pattern Matters
+
+Using explicit ability flags instead of plan type checks provides several benefits:
+
+1. **Simplicity** — A single boolean check is cleaner and less error-prone than maintaining lists of plan types.
+
+2. **Centralized Control** — Feature access is managed in one place: the ability assignment during organization
+ creation/upgrade. No need to hunt through the codebase for scattered plan type checks.
+
+3. **Flexibility** — Abilities can be set independently of plan type, enabling:
+
+ - Early access programs for features not yet tied to a plan
+ - Trial access to help customers evaluate a feature before upgrading
+ - Custom arrangements for specific customers
+ - A/B testing of features across different cohorts
+
+4. **Safe Refactoring** — When plans change (e.g., adding a new plan tier, renaming plans, or moving features between
+ tiers), we only update the ability assignment logic—not every place the feature is used.
+
+5. **Graceful Downgrades** — When an organization downgrades, we update their abilities. All feature checks
+ automatically respect the new access level.
+
+## How It Works
+
+### Ability Assignment at Signup/Upgrade
+
+When an organization is created or changes plans, the ability flags are set based on the plan's capabilities:
+
+```csharp
+// During organization creation or plan change
+organization.UseGroups = plan.HasGroups;
+organization.UseSso = plan.HasSso;
+organization.UseScim = plan.HasScim;
+organization.UsePolicies = plan.HasPolicies;
+organization.UseEvents = plan.HasEvents;
+// ... etc
+```
+
+### Modifying Abilities for Existing Organizations
+
+To change abilities for existing organizations (e.g., rolling out a feature to a new plan tier), create a database
+migration that updates the relevant flag:
+
+```sql
+-- Example: Enable UseEvents for all Teams organizations
+UPDATE [dbo].[Organization]
+SET UseEvents = 1
+WHERE PlanType IN (17, 18) -- TeamsMonthly = 17, TeamsAnnually = 18
+```
+
+Then update the plan-to-ability assignment code so new organizations get the correct value.
+
+## Adding a New Ability
+
+When developing a new plan-gated feature:
+
+1. **Add the ability to the Organization and OrganizationAbility entities** — Create a `Use[FeatureName]` boolean
+ property.
+
+2. **Add a database migration** — Add the new column to the Organization table.
+
+3. **Update plan definitions** — Add a corresponding `Has[FeatureName]` property to the Plan model and configure which
+ plans include it.
+
+4. **Update organization creation/upgrade logic** — Ensure the ability is set based on the plan.
+
+5. **Update the organization license claims** (if applicable) - to make the feature available on self-hosted instances.
+
+6. **Implement checks throughout client and server** — Use the ability consistently everywhere the feature is accessed.
+ - Clients: get the organization object from `OrganizationService`.
+ - Server: if you already have the full `Organization` object in scope, you can use it directly. If not, use the
+ `IApplicationCacheService` to retrieve the `OrganizationAbility`, which is a simplified, cached representation
+ of the organization ability flags. Note that some older flags may be missing from `OrganizationAbility` but
+ can be added if needed.
+
+## Existing Abilities
+
+For reference, here are some current organization ability flags (not a complete list):
+
+| Ability | Description | Plans |
+|--------------------------|-------------------------------|-------------------|
+| `UseGroups` | Group-based collection access | Teams, Enterprise |
+| `UseDirectory` | Directory Connector sync | Teams, Enterprise |
+| `UseEvents` | Event logging | Teams, Enterprise |
+| `UseTotp` | Authenticator (TOTP) | Teams, Enterprise |
+| `UseSso` | Single Sign-On | Enterprise |
+| `UseScim` | SCIM provisioning | Teams, Enterprise |
+| `UsePolicies` | Enterprise policies | Enterprise |
+| `UseResetPassword` | Admin password reset | Enterprise |
+| `UseOrganizationDomains` | Domain verification/claiming | Enterprise |
+
+## Questions?
+
+If you're unsure whether your feature needs a new ability or which existing ability to use, reach out to your team lead
+or members of the Admin Console or Architecture teams. When in doubt, adding an explicit ability is almost always the
+right choice—it's easy to do and keeps our access control clean and maintainable.
From 76a8f0fd95a903801f8ccb5b389e70f874b025ff Mon Sep 17 00:00:00 2001
From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:52:52 +0100
Subject: [PATCH 3/5] [PM 29610]Update Account Storage Endpoint (#6750)
* update account storage endpoint
* Fix the failing test
* Added flag and refactor base on pr comments
* fix the lint error
* Resolve the pr comments
* Fix the failing test
* Fix the failing test
* Return none
* Resolve the lint error
* Fix the failing test
* Add the missing test
* Formatting issues fixed
---
.../VNext/AccountBillingVNextController.cs | 15 +-
.../Requests/Storage/StorageUpdateRequest.cs | 35 ++
.../Extensions/ServiceCollectionExtensions.cs | 1 +
.../Commands/UpdatePremiumStorageCommand.cs | 144 ++++++++
src/Core/Constants.cs | 1 +
.../AccountBillingVNextControllerTests.cs | 228 ++++++++++--
.../UpdateBillingAddressCommandTests.cs | 81 +++++
.../UpdatePremiumStorageCommandTests.cs | 339 ++++++++++++++++++
8 files changed, 823 insertions(+), 21 deletions(-)
create mode 100644 src/Api/Billing/Models/Requests/Storage/StorageUpdateRequest.cs
create mode 100644 src/Core/Billing/Premium/Commands/UpdatePremiumStorageCommand.cs
create mode 100644 test/Core.Test/Billing/Premium/Commands/UpdatePremiumStorageCommandTests.cs
diff --git a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs
index 7d970aef8b..489241bd55 100644
--- a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs
+++ b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs
@@ -1,6 +1,7 @@
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Api.Billing.Models.Requests.Premium;
+using Bit.Api.Billing.Models.Requests.Storage;
using Bit.Core;
using Bit.Core.Billing.Licenses.Queries;
using Bit.Core.Billing.Payment.Commands;
@@ -23,7 +24,8 @@ public class AccountBillingVNextController(
IGetCreditQuery getCreditQuery,
IGetPaymentMethodQuery getPaymentMethodQuery,
IGetUserLicenseQuery getUserLicenseQuery,
- IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
+ IUpdatePaymentMethodCommand updatePaymentMethodCommand,
+ IUpdatePremiumStorageCommand updatePremiumStorageCommand) : BaseBillingController
{
[HttpGet("credit")]
[InjectUser]
@@ -88,4 +90,15 @@ public class AccountBillingVNextController(
var response = await getUserLicenseQuery.Run(user);
return TypedResults.Ok(response);
}
+
+ [HttpPut("storage")]
+ [RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)]
+ [InjectUser]
+ public async Task UpdateStorageAsync(
+ [BindNever] User user,
+ [FromBody] StorageUpdateRequest request)
+ {
+ var result = await updatePremiumStorageCommand.Run(user, request.AdditionalStorageGb);
+ return Handle(result);
+ }
}
diff --git a/src/Api/Billing/Models/Requests/Storage/StorageUpdateRequest.cs b/src/Api/Billing/Models/Requests/Storage/StorageUpdateRequest.cs
new file mode 100644
index 0000000000..0b18fc1e6f
--- /dev/null
+++ b/src/Api/Billing/Models/Requests/Storage/StorageUpdateRequest.cs
@@ -0,0 +1,35 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Bit.Api.Billing.Models.Requests.Storage;
+
+///
+/// Request model for updating storage allocation on a user's premium subscription.
+/// Allows for both increasing and decreasing storage in an idempotent manner.
+///
+public class StorageUpdateRequest : IValidatableObject
+{
+ ///
+ /// The additional storage in GB beyond the base storage.
+ /// Must be between 0 and the maximum allowed (minus base storage).
+ ///
+ [Required]
+ [Range(0, 99)]
+ public short AdditionalStorageGb { get; set; }
+
+ public IEnumerable Validate(ValidationContext validationContext)
+ {
+ if (AdditionalStorageGb < 0)
+ {
+ yield return new ValidationResult(
+ "Additional storage cannot be negative.",
+ new[] { nameof(AdditionalStorageGb) });
+ }
+
+ if (AdditionalStorageGb > 99)
+ {
+ yield return new ValidationResult(
+ "Maximum additional storage is 99 GB.",
+ new[] { nameof(AdditionalStorageGb) });
+ }
+ }
+}
diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
index 905f797bb4..3d63a35406 100644
--- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
@@ -53,6 +53,7 @@ public static class ServiceCollectionExtensions
services.AddScoped();
services.AddScoped();
services.AddTransient();
+ services.AddScoped();
}
private static void AddPremiumQueries(this IServiceCollection services)
diff --git a/src/Core/Billing/Premium/Commands/UpdatePremiumStorageCommand.cs b/src/Core/Billing/Premium/Commands/UpdatePremiumStorageCommand.cs
new file mode 100644
index 0000000000..610c112e08
--- /dev/null
+++ b/src/Core/Billing/Premium/Commands/UpdatePremiumStorageCommand.cs
@@ -0,0 +1,144 @@
+using Bit.Core.Billing.Commands;
+using Bit.Core.Billing.Pricing;
+using Bit.Core.Billing.Services;
+using Bit.Core.Entities;
+using Bit.Core.Services;
+using Bit.Core.Utilities;
+using Microsoft.Extensions.Logging;
+using OneOf.Types;
+using Stripe;
+
+namespace Bit.Core.Billing.Premium.Commands;
+
+///
+/// Updates the storage allocation for a premium user's subscription.
+/// Handles both increases and decreases in storage in an idempotent manner.
+///
+public interface IUpdatePremiumStorageCommand
+{
+ ///
+ /// Updates the user's storage by the specified additional amount.
+ ///
+ /// The premium user whose storage should be updated.
+ /// The additional storage amount in GB beyond base storage.
+ /// A billing command result indicating success or failure.
+ Task> Run(User user, short additionalStorageGb);
+}
+
+public class UpdatePremiumStorageCommand(
+ IStripeAdapter stripeAdapter,
+ IUserService userService,
+ IPricingClient pricingClient,
+ ILogger logger)
+ : BaseBillingCommand(logger), IUpdatePremiumStorageCommand
+{
+ public Task> Run(User user, short additionalStorageGb) => HandleAsync(async () =>
+ {
+ if (!user.Premium)
+ {
+ return new BadRequest("User does not have a premium subscription.");
+ }
+
+ if (!user.MaxStorageGb.HasValue)
+ {
+ return new BadRequest("No access to storage.");
+ }
+
+ // Fetch all premium plans and the user's subscription to find which plan they're on
+ var premiumPlans = await pricingClient.ListPremiumPlans();
+ var subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);
+
+ // Find the password manager subscription item (seat, not storage) and match it to a plan
+ var passwordManagerItem = subscription.Items.Data.FirstOrDefault(i =>
+ premiumPlans.Any(p => p.Seat.StripePriceId == i.Price.Id));
+
+ if (passwordManagerItem == null)
+ {
+ return new BadRequest("Premium subscription item not found.");
+ }
+
+ var premiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id);
+
+ var baseStorageGb = (short)premiumPlan.Storage.Provided;
+
+ if (additionalStorageGb < 0)
+ {
+ return new BadRequest("Additional storage cannot be negative.");
+ }
+
+ var newTotalStorageGb = (short)(baseStorageGb + additionalStorageGb);
+
+ if (newTotalStorageGb > 100)
+ {
+ return new BadRequest("Maximum storage is 100 GB.");
+ }
+
+ // Idempotency check: if user already has the requested storage, return success
+ if (user.MaxStorageGb == newTotalStorageGb)
+ {
+ return new None();
+ }
+
+ var remainingStorage = user.StorageBytesRemaining(newTotalStorageGb);
+ if (remainingStorage < 0)
+ {
+ return new BadRequest(
+ $"You are currently using {CoreHelpers.ReadableBytesSize(user.Storage.GetValueOrDefault(0))} of storage. " +
+ "Delete some stored data first.");
+ }
+
+ // Find the storage line item in the subscription
+ var storageItem = subscription.Items.Data.FirstOrDefault(i => i.Price.Id == premiumPlan.Storage.StripePriceId);
+
+ var subscriptionItemOptions = new List();
+
+ if (additionalStorageGb > 0)
+ {
+ if (storageItem != null)
+ {
+ // Update existing storage item
+ subscriptionItemOptions.Add(new SubscriptionItemOptions
+ {
+ Id = storageItem.Id,
+ Price = premiumPlan.Storage.StripePriceId,
+ Quantity = additionalStorageGb
+ });
+ }
+ else
+ {
+ // Add new storage item
+ subscriptionItemOptions.Add(new SubscriptionItemOptions
+ {
+ Price = premiumPlan.Storage.StripePriceId,
+ Quantity = additionalStorageGb
+ });
+ }
+ }
+ else if (storageItem != null)
+ {
+ // Remove storage item if setting to 0
+ subscriptionItemOptions.Add(new SubscriptionItemOptions
+ {
+ Id = storageItem.Id,
+ Deleted = true
+ });
+ }
+
+ // Update subscription with prorations
+ // Storage is billed annually, so we create prorations and invoice immediately
+ var subscriptionUpdateOptions = new SubscriptionUpdateOptions
+ {
+ Items = subscriptionItemOptions,
+ ProrationBehavior = Core.Constants.CreateProrations
+ };
+
+ await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, subscriptionUpdateOptions);
+
+ // Update the user's max storage
+ user.MaxStorageGb = newTotalStorageGb;
+ await userService.SaveUserAsync(user);
+
+ // No payment intent needed - the subscription update will automatically create and finalize the invoice
+ return new None();
+ });
+}
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 1002d2eb74..61c8d7931f 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -196,6 +196,7 @@ public static class FeatureFlagKeys
public const string PM26462_Milestone_3 = "pm-26462-milestone-3";
public const string PM28265_EnableReconcileAdditionalStorageJob = "pm-28265-enable-reconcile-additional-storage-job";
public const string PM28265_ReconcileAdditionalStorageJobEnableLiveMode = "pm-28265-reconcile-additional-storage-job-enable-live-mode";
+ public const string PM29594_UpdateIndividualSubscriptionPage = "pm-29594-update-individual-subscription-page";
/* Key Management Team */
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
diff --git a/test/Api.Test/Billing/Controllers/VNext/AccountBillingVNextControllerTests.cs b/test/Api.Test/Billing/Controllers/VNext/AccountBillingVNextControllerTests.cs
index b087a0fd6d..66d1a4d3e1 100644
--- a/test/Api.Test/Billing/Controllers/VNext/AccountBillingVNextControllerTests.cs
+++ b/test/Api.Test/Billing/Controllers/VNext/AccountBillingVNextControllerTests.cs
@@ -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();
- _createPremiumCloudHostedSubscriptionCommand = Substitute.For();
- _getCreditQuery = Substitute.For();
- _getPaymentMethodQuery = Substitute.For();
+ _updatePremiumStorageCommand = Substitute.For();
_getUserLicenseQuery = Substitute.For();
- _updatePaymentMethodCommand = Substitute.For();
_sut = new AccountBillingVNextController(
- _createBitPayInvoiceForCreditCommand,
- _createPremiumCloudHostedSubscriptionCommand,
- _getCreditQuery,
- _getPaymentMethodQuery,
+ Substitute.For(),
+ Substitute.For(),
+ Substitute.For(),
+ Substitute.For(),
_getUserLicenseQuery,
- _updatePaymentMethodCommand);
+ Substitute.For(),
+ _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(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 10))
+ .Returns(new BillingCommandResult(new None()));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var okResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 10))
+ .Returns(new BadRequest(errorMessage));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var badRequestResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 10))
+ .Returns(new BadRequest(errorMessage));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var badRequestResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 1))
+ .Returns(new BadRequest(errorMessage));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var badRequestResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 100))
+ .Returns(new BadRequest(errorMessage));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var badRequestResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 2))
+ .Returns(new BadRequest(errorMessage));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var badRequestResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 15))
+ .Returns(new BillingCommandResult(new None()));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var okResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 3))
+ .Returns(new BillingCommandResult(new None()));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var okResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 100))
+ .Returns(new BillingCommandResult(new None()));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var okResult = Assert.IsAssignableFrom(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(u => u.Id == user.Id),
+ Arg.Is(s => s == 5))
+ .Returns(new BillingCommandResult(new None()));
+
+ // Act
+ var result = await _sut.UpdateStorageAsync(user, request);
+
+ // Assert
+ var okResult = Assert.IsAssignableFrom(result);
+ await _updatePremiumStorageCommand.Received(1).Run(user, 5);
+ }
}
diff --git a/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs
index 5854d1c3b5..88728d4839 100644
--- a/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs
+++ b/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs
@@ -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
+ {
+ Data =
+ [
+ new Subscription
+ {
+ Id = organization.GatewaySubscriptionId,
+ AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
+ }
+ ]
+ },
+ TaxIds = new StripeList
+ {
+ Data = [existingTaxId]
+ }
+ };
+
+ _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(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(
+ 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());
+ });
+
+ await _stripeAdapter.Received(1).DeleteTaxIdAsync(customer.Id, existingTaxId.Id);
+ await _stripeAdapter.Received(1).CreateTaxIdAsync(customer.Id, Arg.Is(
+ options => options.Type == "us_ein" && options.Value == "987654321"));
+ }
}
diff --git a/test/Core.Test/Billing/Premium/Commands/UpdatePremiumStorageCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/UpdatePremiumStorageCommandTests.cs
new file mode 100644
index 0000000000..7e3ea562d6
--- /dev/null
+++ b/test/Core.Test/Billing/Premium/Commands/UpdatePremiumStorageCommandTests.cs
@@ -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();
+ private readonly IUserService _userService = Substitute.For();
+ private readonly IPricingClient _pricingClient = Substitute.For();
+ 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 });
+
+ _command = new UpdatePremiumStorageCommand(
+ _stripeAdapter,
+ _userService,
+ _pricingClient,
+ Substitute.For>());
+ }
+
+ private Subscription CreateMockSubscription(string subscriptionId, int? storageQuantity = null)
+ {
+ var items = new List();
+
+ // 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
+ {
+ 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(), Arg.Any());
+ await _userService.DidNotReceive().SaveUserAsync(Arg.Any());
+ }
+
+ [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(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(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(opts =>
+ opts.Items.Count == 1 &&
+ opts.Items[0].Price == "price_storage" &&
+ opts.Items[0].Quantity == 9));
+
+ await _userService.Received(1).SaveUserAsync(Arg.Is(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(opts =>
+ opts.Items.Count == 1 &&
+ opts.Items[0].Id == "si_storage" &&
+ opts.Items[0].Quantity == 2));
+
+ await _userService.Received(1).SaveUserAsync(Arg.Is(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(opts =>
+ opts.Items.Count == 1 &&
+ opts.Items[0].Id == "si_storage" &&
+ opts.Items[0].Deleted == true));
+
+ await _userService.Received(1).SaveUserAsync(Arg.Is(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(opts =>
+ opts.Items[0].Quantity == 99));
+
+ await _userService.Received(1).SaveUserAsync(Arg.Is(u => u.MaxStorageGb == 100));
+ }
+}
From 2442d2dabc5a0e81a271e8439b825fa68601bfc3 Mon Sep 17 00:00:00 2001
From: Kyle Spearrin
Date: Mon, 5 Jan 2026 11:56:31 -0500
Subject: [PATCH 4/5] [PM-30391] fix for org context on sso provisioning
(#6797)
* fix for org context on sso provisioning
* tests are no longer needed since there is no logic on feature flag
* lint fixes
---
.../src/Sso/Controllers/AccountController.cs | 20 +--
.../Controllers/AccountControllerTest.cs | 129 ------------------
2 files changed, 4 insertions(+), 145 deletions(-)
diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs
index 7141f8429d..afbef321a9 100644
--- a/bitwarden_license/src/Sso/Controllers/AccountController.cs
+++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs
@@ -680,22 +680,10 @@ public class AccountController : Controller
ApiKey = CoreHelpers.SecureRandomString(30)
};
- /*
- The feature flag is checked here so that we can send the new MJML welcome email templates.
- The other organization invites flows have an OrganizationUser allowing the RegisterUserCommand the ability
- to fetch the Organization. The old method RegisterUser(User) here does not have that context, so we need
- to use a new method RegisterSSOAutoProvisionedUserAsync(User, Organization) to send the correct email.
- [PM-28057]: Prefer RegisterSSOAutoProvisionedUserAsync for SSO auto-provisioned users.
- TODO: Remove Feature flag: PM-28221
- */
- if (_featureService.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates))
- {
- await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
- }
- else
- {
- await _registerUserCommand.RegisterUser(newUser);
- }
+ // Always use RegisterSSOAutoProvisionedUserAsync to ensure organization context is available
+ // for domain validation (BlockClaimedDomainAccountCreation policy) and welcome emails.
+ // The feature flag logic for welcome email templates is handled internally by RegisterUserCommand.
+ await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
var twoFactorPolicy =
diff --git a/bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs b/bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs
index b276174814..66cb018923 100644
--- a/bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs
+++ b/bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs
@@ -6,7 +6,6 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
-using Bit.Core.Auth.UserFeatures.Registration;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
@@ -21,7 +20,6 @@ using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
@@ -1013,133 +1011,6 @@ public class AccountControllerTest
}
}
- [Theory, BitAutoData]
- public async Task AutoProvisionUserAsync_WithFeatureFlagEnabled_CallsRegisterSSOAutoProvisionedUser(
- SutProvider sutProvider)
- {
- // Arrange
- var orgId = Guid.NewGuid();
- var providerUserId = "ext-new-user";
- var email = "newuser@example.com";
- var organization = new Organization { Id = orgId, Name = "Test Org", Seats = null };
-
- // No existing user (JIT provisioning scenario)
- sutProvider.GetDependency().GetByEmailAsync(email).Returns((User?)null);
- sutProvider.GetDependency().GetByIdAsync(orgId).Returns(organization);
- sutProvider.GetDependency().GetByOrganizationEmailAsync(orgId, email)
- .Returns((OrganizationUser?)null);
-
- // Feature flag enabled
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
- .Returns(true);
-
- // Mock the RegisterSSOAutoProvisionedUserAsync to return success
- sutProvider.GetDependency()
- .RegisterSSOAutoProvisionedUserAsync(Arg.Any(), Arg.Any())
- .Returns(IdentityResult.Success);
-
- var claims = new[]
- {
- new Claim(JwtClaimTypes.Email, email),
- new Claim(JwtClaimTypes.Name, "New User")
- } as IEnumerable;
- var config = new SsoConfigurationData();
-
- var method = typeof(AccountController).GetMethod(
- "CreateUserAndOrgUserConditionallyAsync",
- BindingFlags.Instance | BindingFlags.NonPublic);
- Assert.NotNull(method);
-
- // Act
- var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method!.Invoke(
- sutProvider.Sut,
- new object[]
- {
- orgId.ToString(),
- providerUserId,
- claims,
- null!,
- config
- })!;
-
- var result = await task;
-
- // Assert
- await sutProvider.GetDependency().Received(1)
- .RegisterSSOAutoProvisionedUserAsync(
- Arg.Is(u => u.Email == email && u.Name == "New User"),
- Arg.Is(o => o.Id == orgId && o.Name == "Test Org"));
-
- Assert.NotNull(result.user);
- Assert.Equal(email, result.user.Email);
- Assert.Equal(organization.Id, result.organization.Id);
- }
-
- [Theory, BitAutoData]
- public async Task AutoProvisionUserAsync_WithFeatureFlagDisabled_CallsRegisterUserInstead(
- SutProvider sutProvider)
- {
- // Arrange
- var orgId = Guid.NewGuid();
- var providerUserId = "ext-legacy-user";
- var email = "legacyuser@example.com";
- var organization = new Organization { Id = orgId, Name = "Test Org", Seats = null };
-
- // No existing user (JIT provisioning scenario)
- sutProvider.GetDependency().GetByEmailAsync(email).Returns((User?)null);
- sutProvider.GetDependency().GetByIdAsync(orgId).Returns(organization);
- sutProvider.GetDependency().GetByOrganizationEmailAsync(orgId, email)
- .Returns((OrganizationUser?)null);
-
- // Feature flag disabled
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
- .Returns(false);
-
- // Mock the RegisterUser to return success
- sutProvider.GetDependency()
- .RegisterUser(Arg.Any())
- .Returns(IdentityResult.Success);
-
- var claims = new[]
- {
- new Claim(JwtClaimTypes.Email, email),
- new Claim(JwtClaimTypes.Name, "Legacy User")
- } as IEnumerable;
- var config = new SsoConfigurationData();
-
- var method = typeof(AccountController).GetMethod(
- "CreateUserAndOrgUserConditionallyAsync",
- BindingFlags.Instance | BindingFlags.NonPublic);
- Assert.NotNull(method);
-
- // Act
- var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method!.Invoke(
- sutProvider.Sut,
- new object[]
- {
- orgId.ToString(),
- providerUserId,
- claims,
- null!,
- config
- })!;
-
- var result = await task;
-
- // Assert
- await sutProvider.GetDependency().Received(1)
- .RegisterUser(Arg.Is(u => u.Email == email && u.Name == "Legacy User"));
-
- // Verify the new method was NOT called
- await sutProvider.GetDependency().DidNotReceive()
- .RegisterSSOAutoProvisionedUserAsync(Arg.Any(), Arg.Any());
-
- Assert.NotNull(result.user);
- Assert.Equal(email, result.user.Email);
- }
-
[Theory, BitAutoData]
public void ExternalChallenge_WithMatchingOrgId_Succeeds(
SutProvider sutProvider,
From 35868c2a65e4f3d55898570d9bafa12a2c8b8cc9 Mon Sep 17 00:00:00 2001
From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:27:17 -0600
Subject: [PATCH 5/5] [PM-22434] Remove CreateDefaultLocation feature flag
references (#6758)
* chore: remove ff ref from PoliciesController
* chore: remove ff ref from OrganizationExportController, refs PM-22434
* chore: remove ff ref from CiphersController, refs PM-22434
* chore: remove ff ref from ConfirmOrganizationUserCommand, refs PM-22434
* chore: remove ff refs from OrganizationDataOwnershipPolicyValidator, refs PM-22434
* chore: remove ff ref from OrganizationUserControllerTests, refs PM-22434
* chore: remove ff refs from ConfirmOrganizationUserCommandTests, refs PM-22434
* chore: remove ff refs from OrganizationDataOwnershipPolicyValidatorTests, refs PM-22434
* chore: format, refs PM-22434
---
.../Controllers/PoliciesController.cs | 2 -
.../OrganizationExportController.cs | 32 ++------
.../Vault/Controllers/CiphersController.cs | 12 +--
.../ConfirmOrganizationUserCommand.cs | 10 ---
...rganizationDataOwnershipPolicyValidator.cs | 9 +--
.../OrganizationUserControllerTests.cs | 9 ---
.../ConfirmOrganizationUserCommandTests.cs | 11 +--
...zationDataOwnershipPolicyValidatorTests.cs | 79 +------------------
8 files changed, 15 insertions(+), 149 deletions(-)
diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs
index ae1d12e887..bce0332d67 100644
--- a/src/Api/AdminConsole/Controllers/PoliciesController.cs
+++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs
@@ -7,7 +7,6 @@ using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.AdminConsole.Models.Response.Helpers;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
-using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@@ -212,7 +211,6 @@ public class PoliciesController : Controller
}
[HttpPut("{type}/vnext")]
- [RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)]
[Authorize]
public async Task PutVNext(Guid orgId, PolicyType type, [FromBody] SavePolicyRequest model)
{
diff --git a/src/Api/Tools/Controllers/OrganizationExportController.cs b/src/Api/Tools/Controllers/OrganizationExportController.cs
index dd039bc4a5..578220075a 100644
--- a/src/Api/Tools/Controllers/OrganizationExportController.cs
+++ b/src/Api/Tools/Controllers/OrganizationExportController.cs
@@ -1,6 +1,5 @@
using Bit.Api.Tools.Authorization;
using Bit.Api.Tools.Models.Response;
-using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@@ -21,7 +20,6 @@ public class OrganizationExportController : Controller
private readonly IAuthorizationService _authorizationService;
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
private readonly ICollectionRepository _collectionRepository;
- private readonly IFeatureService _featureService;
public OrganizationExportController(
IUserService userService,
@@ -36,7 +34,6 @@ public class OrganizationExportController : Controller
_authorizationService = authorizationService;
_organizationCiphersQuery = organizationCiphersQuery;
_collectionRepository = collectionRepository;
- _featureService = featureService;
}
[HttpGet("export")]
@@ -46,33 +43,20 @@ public class OrganizationExportController : Controller
VaultExportOperations.ExportWholeVault);
var canExportManaged = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId),
VaultExportOperations.ExportManagedCollections);
- var createDefaultLocationEnabled = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation);
if (canExportAll.Succeeded)
{
- if (createDefaultLocationEnabled)
- {
- var allOrganizationCiphers =
- await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(
- organizationId);
+ var allOrganizationCiphers =
+ await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(
+ organizationId);
- var allCollections = await _collectionRepository
- .GetManySharedCollectionsByOrganizationIdAsync(
- organizationId);
+ var allCollections = await _collectionRepository
+ .GetManySharedCollectionsByOrganizationIdAsync(
+ organizationId);
- return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections,
- _globalSettings));
- }
- else
- {
- var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId);
-
- var allCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId);
-
- return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections,
- _globalSettings));
- }
+ return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections,
+ _globalSettings));
}
if (canExportManaged.Succeeded)
diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs
index 6a506cc01f..d40cb1c410 100644
--- a/src/Api/Vault/Controllers/CiphersController.cs
+++ b/src/Api/Vault/Controllers/CiphersController.cs
@@ -10,7 +10,6 @@ using Bit.Api.Utilities;
using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Models.Response;
using Bit.Core;
-using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -43,7 +42,6 @@ public class CiphersController : Controller
private readonly ICipherService _cipherService;
private readonly IUserService _userService;
private readonly IAttachmentStorageService _attachmentStorageService;
- private readonly IProviderService _providerService;
private readonly ICurrentContext _currentContext;
private readonly ILogger _logger;
private readonly GlobalSettings _globalSettings;
@@ -52,7 +50,6 @@ public class CiphersController : Controller
private readonly ICollectionRepository _collectionRepository;
private readonly IArchiveCiphersCommand _archiveCiphersCommand;
private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand;
- private readonly IFeatureService _featureService;
public CiphersController(
ICipherRepository cipherRepository,
@@ -60,7 +57,6 @@ public class CiphersController : Controller
ICipherService cipherService,
IUserService userService,
IAttachmentStorageService attachmentStorageService,
- IProviderService providerService,
ICurrentContext currentContext,
ILogger logger,
GlobalSettings globalSettings,
@@ -68,15 +64,13 @@ public class CiphersController : Controller
IApplicationCacheService applicationCacheService,
ICollectionRepository collectionRepository,
IArchiveCiphersCommand archiveCiphersCommand,
- IUnarchiveCiphersCommand unarchiveCiphersCommand,
- IFeatureService featureService)
+ IUnarchiveCiphersCommand unarchiveCiphersCommand)
{
_cipherRepository = cipherRepository;
_collectionCipherRepository = collectionCipherRepository;
_cipherService = cipherService;
_userService = userService;
_attachmentStorageService = attachmentStorageService;
- _providerService = providerService;
_currentContext = currentContext;
_logger = logger;
_globalSettings = globalSettings;
@@ -85,7 +79,6 @@ public class CiphersController : Controller
_collectionRepository = collectionRepository;
_archiveCiphersCommand = archiveCiphersCommand;
_unarchiveCiphersCommand = unarchiveCiphersCommand;
- _featureService = featureService;
}
[HttpGet("{id}")]
@@ -344,8 +337,7 @@ public class CiphersController : Controller
throw new NotFoundException();
}
- bool excludeDefaultUserCollections = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) && !includeMemberItems;
- var allOrganizationCiphers = excludeDefaultUserCollections
+ var allOrganizationCiphers = !includeMemberItems
?
await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId)
:
diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
index b6b49e93e9..2fbe1e27f4 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs
@@ -280,11 +280,6 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
/// The encrypted default user collection name.
private async Task CreateDefaultCollectionAsync(OrganizationUser organizationUser, string defaultUserCollectionName)
{
- if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation))
- {
- return;
- }
-
// Skip if no collection name provided (backwards compatibility)
if (string.IsNullOrWhiteSpace(defaultUserCollectionName))
{
@@ -323,11 +318,6 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
private async Task CreateManyDefaultCollectionsAsync(Guid organizationId,
IEnumerable confirmedOrganizationUsers, string defaultUserCollectionName)
{
- if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation))
- {
- return;
- }
-
// Skip if no collection name provided (backwards compatibility)
if (string.IsNullOrWhiteSpace(defaultUserCollectionName))
{
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs
index 0bee2a55af..7a47baa65a 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs
@@ -6,15 +6,13 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Repositories;
-using Bit.Core.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
public class OrganizationDataOwnershipPolicyValidator(
IPolicyRepository policyRepository,
ICollectionRepository collectionRepository,
- IEnumerable> factories,
- IFeatureService featureService)
+ IEnumerable> factories)
: OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect, IOnPolicyPostUpdateEvent
{
public PolicyType Type => PolicyType.OrganizationDataOwnership;
@@ -32,11 +30,6 @@ public class OrganizationDataOwnershipPolicyValidator(
Policy postUpdatedPolicy,
Policy? previousPolicyState)
{
- if (!featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation))
- {
- return;
- }
-
if (policyRequest.Metadata is not OrganizationModelOwnershipPolicyModel metadata)
{
return;
diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs
index 0fef4a0cd0..343178e7a2 100644
--- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs
+++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs
@@ -5,7 +5,6 @@ using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
-using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.Repositories;
@@ -14,8 +13,6 @@ using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
-using Bit.Core.Services;
-using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
@@ -28,12 +25,6 @@ public class OrganizationUserControllerTests : IClassFixture(featureService =>
- {
- featureService
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(true);
- });
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs
index 5528ecb2a2..7ec26cf882 100644
--- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs
@@ -462,7 +462,7 @@ public class ConfirmOrganizationUserCommandTests
}
[Theory, BitAutoData]
- public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection(
+ public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection(
Organization organization, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
string key, string collectionName, SutProvider sutProvider)
@@ -475,8 +475,6 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
-
var policyDetails = new PolicyDetails
{
OrganizationId = organization.Id,
@@ -506,7 +504,7 @@ public class ConfirmOrganizationUserCommandTests
}
[Theory, BitAutoData]
- public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection(
+ public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
string key, SutProvider sutProvider)
@@ -519,8 +517,6 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
-
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, "");
await sutProvider.GetDependency()
@@ -529,7 +525,7 @@ public class ConfirmOrganizationUserCommandTests
}
[Theory, BitAutoData]
- public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
+ public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Owner)] OrganizationUser orgUser, User user,
string key, string collectionName, SutProvider sutProvider)
@@ -541,7 +537,6 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
- sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
var policyDetails = new PolicyDetails
{
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs
index e6677c8a23..93cbde89ec 100644
--- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs
@@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Repositories;
-using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -20,29 +19,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
{
private const string _defaultUserCollectionName = "Default";
- [Theory, BitAutoData]
- public async Task ExecuteSideEffectsAsync_FeatureFlagDisabled_DoesNothing(
- [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
- [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
- [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
- SutProvider sutProvider)
- {
- // Arrange
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(false);
-
- var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
-
- // Act
- await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
-
- // Assert
- await sutProvider.GetDependency()
- .DidNotReceive()
- .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any());
- }
-
[Theory, BitAutoData]
public async Task ExecuteSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
@@ -54,10 +30,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(true);
-
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
@@ -80,10 +52,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(true);
-
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
@@ -234,10 +202,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true;
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(true);
-
var policyRequest = new SavePolicyModel(policyUpdate, metadata);
// Act
@@ -264,39 +228,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests
IPolicyRepository policyRepository,
ICollectionRepository collectionRepository)
{
-
- var featureService = Substitute.For();
- featureService
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(true);
-
- var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService);
+ var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory]);
return sut;
}
- [Theory, BitAutoData]
- public async Task ExecutePostUpsertSideEffectAsync_FeatureFlagDisabled_DoesNothing(
- [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
- [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
- [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
- SutProvider sutProvider)
- {
- // Arrange
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(false);
-
- var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
-
- // Act
- await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
-
- // Assert
- await sutProvider.GetDependency()
- .DidNotReceiveWithAnyArgs()
- .UpsertDefaultCollectionsAsync(default, default, default);
- }
-
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_PolicyAlreadyEnabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
@@ -308,10 +243,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(true);
-
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
@@ -334,10 +265,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(true);
-
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
@@ -432,10 +359,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true;
- sutProvider.GetDependency()
- .IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
- .Returns(true);
-
var policyRequest = new SavePolicyModel(policyUpdate, metadata);
// Act