From ceb0ea907f4ad727cc4a18a935d0d9e40bb4feb5 Mon Sep 17 00:00:00 2001 From: Cy Okeke Date: Wed, 25 Feb 2026 12:41:52 +0100 Subject: [PATCH 1/3] Resolve the bug --- .../VNext/AccountBillingVNextController.cs | 4 +- .../UpgradePremiumToOrganizationRequest.cs | 12 +- .../UpgradePremiumToOrganizationCommand.cs | 53 +++- ...pgradePremiumToOrganizationCommandTests.cs | 294 +++++++++++++++++- 4 files changed, 336 insertions(+), 27 deletions(-) diff --git a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs index 579804df0f04..dc8ad9c0ca27 100644 --- a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs @@ -132,8 +132,8 @@ public async Task UpgradePremiumToOrganizationAsync( [BindNever] User user, [FromBody] UpgradePremiumToOrganizationRequest request) { - var (organizationName, key, planType, billingAddress) = request.ToDomain(); - var result = await upgradePremiumToOrganizationCommand.Run(user, organizationName, key, planType, billingAddress); + var (organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress) = request.ToDomain(); + var result = await upgradePremiumToOrganizationCommand.Run(user, organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress); return Handle(result); } } diff --git a/src/Api/Billing/Models/Requests/Premium/UpgradePremiumToOrganizationRequest.cs b/src/Api/Billing/Models/Requests/Premium/UpgradePremiumToOrganizationRequest.cs index 00b1da4bba59..62a5c2adff56 100644 --- a/src/Api/Billing/Models/Requests/Premium/UpgradePremiumToOrganizationRequest.cs +++ b/src/Api/Billing/Models/Requests/Premium/UpgradePremiumToOrganizationRequest.cs @@ -13,6 +13,14 @@ public class UpgradePremiumToOrganizationRequest [Required] public string Key { get; set; } = null!; + [Required] + public string PublicKey { get; set; } = null!; + + [Required] + public string EncryptedPrivateKey { get; set; } = null!; + + public string? CollectionName { get; set; } + [Required] [JsonConverter(typeof(JsonStringEnumConverter))] public required ProductTierType TargetProductTierType { get; set; } @@ -39,6 +47,6 @@ private PlanType PlanType } } - public (string OrganizationName, string Key, PlanType PlanType, Core.Billing.Payment.Models.BillingAddress BillingAddress) ToDomain() => - (OrganizationName, Key, PlanType, BillingAddress.ToDomain()); + public (string OrganizationName, string Key, string PublicKey, string EncryptedPrivateKey, string? CollectionName, PlanType PlanType, Core.Billing.Payment.Models.BillingAddress BillingAddress) ToDomain() => + (OrganizationName, Key, PublicKey, EncryptedPrivateKey, CollectionName, PlanType, BillingAddress.ToDomain()); } diff --git a/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs b/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs index 803674120a8b..434c9c58e9ec 100644 --- a/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs +++ b/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs @@ -6,11 +6,11 @@ using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; -using OneOf.Types; using Stripe; namespace Bit.Core.Billing.Premium.Commands; @@ -26,13 +26,19 @@ public interface IUpgradePremiumToOrganizationCommand /// The user with an active Premium subscription to upgrade. /// The name for the new organization. /// The encrypted organization key for the owner. + /// The organization's public key. + /// The organization's encrypted private key. + /// Optional name for the default collection. /// The target organization plan type to upgrade to. /// The billing address for tax calculation. - /// A billing command result indicating success or failure with appropriate error details. - Task> Run( + /// A billing command result containing the new organization ID on success, or error details on failure. + Task> Run( User user, string organizationName, string key, + string publicKey, + string encryptedPrivateKey, + string? collectionName, PlanType targetPlanType, Payment.Models.BillingAddress billingAddress); } @@ -45,15 +51,21 @@ public class UpgradePremiumToOrganizationCommand( IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationApiKeyRepository organizationApiKeyRepository, + ICollectionRepository collectionRepository, IApplicationCacheService applicationCacheService) : BaseBillingCommand(logger), IUpgradePremiumToOrganizationCommand { - public Task> Run( + private readonly ILogger _logger = logger; + + public Task> Run( User user, string organizationName, string key, + string publicKey, + string encryptedPrivateKey, + string? collectionName, PlanType targetPlanType, - Payment.Models.BillingAddress billingAddress) => HandleAsync(async () => + Payment.Models.BillingAddress billingAddress) => HandleAsync(async () => { // Validate that the user has an active Premium subscription if (user is not { Premium: true, GatewaySubscriptionId: not null and not "" }) @@ -164,6 +176,8 @@ public Task> Run( Gateway = GatewayType.Stripe, Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), + PublicKey = publicKey, + PrivateKey = encryptedPrivateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, @@ -217,6 +231,33 @@ await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey organizationUser.SetNewId(); await organizationUserRepository.CreateAsync(organizationUser); + // Create default collection if collection name is provided + if (!string.IsNullOrWhiteSpace(collectionName)) + { + try + { + // Give the owner Can Manage access over the default collection + List defaultOwnerAccess = + [new CollectionAccessSelection { Id = organizationUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }]; + + var defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = organization.Id, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "{Command}: Failed to create default collection for organization {OrganizationId}. Organization upgrade will continue.", + CommandName, organization.Id); + // Continue - organization is fully functional without default collection + } + } + // Remove subscription from user user.Premium = false; user.PremiumExpirationDate = null; @@ -225,6 +266,6 @@ await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey user.RevisionDate = DateTime.UtcNow; await userService.SaveUserAsync(user); - return new None(); + return organization.Id; }); } diff --git a/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs index b4fd0e2d21d0..aa98c6ed7665 100644 --- a/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture.Attributes; @@ -131,6 +132,7 @@ private static List CreateTestPremiumPlansList() private readonly IOrganizationRepository _organizationRepository = Substitute.For(); private readonly IOrganizationUserRepository _organizationUserRepository = Substitute.For(); private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository = Substitute.For(); + private readonly ICollectionRepository _collectionRepository = Substitute.For(); private readonly IApplicationCacheService _applicationCacheService = Substitute.For(); private readonly ILogger _logger = Substitute.For>(); private readonly UpgradePremiumToOrganizationCommand _command; @@ -145,6 +147,7 @@ public UpgradePremiumToOrganizationCommandTests() _organizationRepository, _organizationUserRepository, _organizationApiKeyRepository, + _collectionRepository, _applicationCacheService); } @@ -158,7 +161,7 @@ public async Task Run_UserNotPremium_ReturnsBadRequest(User user) user.Premium = false; // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT1); @@ -174,7 +177,7 @@ public async Task Run_UserNoGatewaySubscriptionId_ReturnsBadRequest(User user) user.GatewaySubscriptionId = null; // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT1); @@ -190,7 +193,7 @@ public async Task Run_UserEmptyGatewaySubscriptionId_ReturnsBadRequest(User user user.GatewaySubscriptionId = ""; // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT1); @@ -245,10 +248,12 @@ public async Task Run_SuccessfulUpgrade_SeatBasedPlan_ReturnsSuccess(User user) _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _stripeAdapter.Received(1).UpdateSubscriptionAsync( "sub_123", @@ -319,10 +324,12 @@ public async Task Run_SuccessfulUpgrade_NonSeatBasedPlan_ReturnsSuccess(User use _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Families Org", "encrypted-key", PlanType.FamiliesAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Families Org", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.FamiliesAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _stripeAdapter.Received(1).UpdateSubscriptionAsync( "sub_123", @@ -381,10 +388,12 @@ public async Task Run_AddsMetadataWithOriginalPremiumPriceId(User user) _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _stripeAdapter.Received(1).UpdateSubscriptionAsync( "sub_123", @@ -446,10 +455,12 @@ public async Task Run_UserOnLegacyPremiumPlan_SuccessfullyDeletesLegacyItems(Use _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); // Verify that legacy password manager item is modified and legacy storage is deleted await _stripeAdapter.Received(1).UpdateSubscriptionAsync( @@ -512,10 +523,12 @@ public async Task Run_UserHasPremiumPlusOtherProducts_OnlyDeletesPremiumItems(Us _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); // Verify that ONLY the premium password manager item is modified (not other products) // Note: We modify the specific premium item by ID, so other products are untouched @@ -580,10 +593,12 @@ public async Task Run_UserHasAdditionalStorage_CapturesStorageInMetadata(User us _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); // Verify that the additional storage quantity (5) is captured in metadata await _stripeAdapter.Received(1).UpdateSubscriptionAsync( @@ -625,7 +640,7 @@ public async Task Run_NoPremiumSubscriptionItemFound_ReturnsBadRequest(User user _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT1); @@ -675,10 +690,12 @@ public async Task Run_UpdatesCustomerBillingAddress(User user) var billingAddress = new Core.Billing.Payment.Models.BillingAddress { Country = "US", PostalCode = "12345" }; // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, billingAddress); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, billingAddress); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _stripeAdapter.Received(1).UpdateCustomerAsync( "cus_123", @@ -727,10 +744,12 @@ public async Task Run_EnablesAutomaticTaxOnSubscription(User user) _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _stripeAdapter.Received(1).UpdateSubscriptionAsync( "sub_123", @@ -779,10 +798,12 @@ public async Task Run_UsesAlwaysInvoiceProrationBehavior(User user) _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _stripeAdapter.Received(1).UpdateSubscriptionAsync( "sub_123", @@ -830,10 +851,12 @@ public async Task Run_ModifiesExistingSubscriptionItem_NotDeleteAndRecreate(User _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); // Verify that the subscription item was modified, not deleted await _stripeAdapter.Received(1).UpdateSubscriptionAsync( @@ -887,10 +910,12 @@ public async Task Run_CreatesOrganizationWithCorrectSettings(User user) _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _organizationRepository.Received(1).CreateAsync( Arg.Is(org => @@ -944,10 +969,12 @@ public async Task Run_CreatesOrganizationApiKeyWithCorrectType(User user) _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _organizationApiKeyRepository.Received(1).CreateAsync( Arg.Is(apiKey => @@ -995,10 +1022,12 @@ public async Task Run_CreatesOrganizationUserAsOwnerWithAllPermissions(User user _userService.SaveUserAsync(user).Returns(Task.CompletedTask); // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress()); + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); // Assert Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); await _organizationUserRepository.Received(1).CreateAsync( Arg.Is(orgUser => @@ -1006,4 +1035,235 @@ await _organizationUserRepository.Received(1).CreateAsync( orgUser.Type == OrganizationUserType.Owner && orgUser.Status == OrganizationUserStatusType.Confirmed)); } + + [Theory, BitAutoData] + public async Task Run_SetsOrganizationPublicAndPrivateKeys(User user) + { + // Arrange + user.Premium = true; + user.GatewaySubscriptionId = "sub_123"; + user.GatewayCustomerId = "cus_123"; + + var mockSubscription = new Subscription + { + Id = "sub_123", + Items = new StripeList + { + Data = new List + { + new SubscriptionItem + { + Id = "si_premium", + Price = new Price { Id = "premium-annually" } + } + } + }, + Metadata = new Dictionary() + }; + + var mockPremiumPlans = CreateTestPremiumPlansList(); + var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually"); + + _stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription); + _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans); + _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan); + _stripeAdapter.UpdateSubscriptionAsync(Arg.Any(), Arg.Any()).Returns(mockSubscription); + _stripeAdapter.UpdateCustomerAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(new Customer())); + _organizationRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _organizationApiKeyRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _organizationUserRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any()).Returns(Task.CompletedTask); + _userService.SaveUserAsync(user).Returns(Task.CompletedTask); + + // Act + var result = await _command.Run(user, "My Organization", "encrypted-key", "test-public-key", "test-encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); + + // Assert + Assert.True(result.IsT0); + + await _organizationRepository.Received(1).CreateAsync( + Arg.Is(org => + org.PublicKey == "test-public-key" && + org.PrivateKey == "test-encrypted-private-key")); + } + + [Theory, BitAutoData] + public async Task Run_WithCollectionName_CreatesDefaultCollection(User user) + { + // Arrange + user.Premium = true; + user.GatewaySubscriptionId = "sub_123"; + user.GatewayCustomerId = "cus_123"; + + var mockSubscription = new Subscription + { + Id = "sub_123", + Items = new StripeList + { + Data = new List + { + new SubscriptionItem + { + Id = "si_premium", + Price = new Price { Id = "premium-annually" } + } + } + }, + Metadata = new Dictionary() + }; + + var mockPremiumPlans = CreateTestPremiumPlansList(); + var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually"); + + _stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription); + _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans); + _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan); + _stripeAdapter.UpdateSubscriptionAsync(Arg.Any(), Arg.Any()).Returns(mockSubscription); + _stripeAdapter.UpdateCustomerAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(new Customer())); + _organizationRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _organizationApiKeyRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _organizationUserRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _collectionRepository.CreateAsync(Arg.Any(), Arg.Any>(), Arg.Any>()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any()).Returns(Task.CompletedTask); + _userService.SaveUserAsync(user).Returns(Task.CompletedTask); + + // Act + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", "Default Collection", PlanType.TeamsAnnually, CreateTestBillingAddress()); + + // Assert + Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); + + await _collectionRepository.Received(1).CreateAsync( + Arg.Is(c => c.Name == "Default Collection"), + Arg.Is>(x => x == null), + Arg.Is>(access => + access.Count() == 1 && + access.First().Manage == true && + access.First().ReadOnly == false && + access.First().HidePasswords == false)); + } + + [Theory, BitAutoData] + public async Task Run_WithoutCollectionName_DoesNotCreateCollection(User user) + { + // Arrange + user.Premium = true; + user.GatewaySubscriptionId = "sub_123"; + user.GatewayCustomerId = "cus_123"; + + var mockSubscription = new Subscription + { + Id = "sub_123", + Items = new StripeList + { + Data = new List + { + new SubscriptionItem + { + Id = "si_premium", + Price = new Price { Id = "premium-annually" } + } + } + }, + Metadata = new Dictionary() + }; + + var mockPremiumPlans = CreateTestPremiumPlansList(); + var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually"); + + _stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription); + _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans); + _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan); + _stripeAdapter.UpdateSubscriptionAsync(Arg.Any(), Arg.Any()).Returns(mockSubscription); + _stripeAdapter.UpdateCustomerAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(new Customer())); + _organizationRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _organizationApiKeyRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _organizationUserRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any()).Returns(Task.CompletedTask); + _userService.SaveUserAsync(user).Returns(Task.CompletedTask); + + // Act + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); + + // Assert + Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); + + await _collectionRepository.DidNotReceive().CreateAsync( + Arg.Any(), + Arg.Any>(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task Run_CollectionCreationFails_UpgradeStillSucceeds(User user) + { + // Arrange + user.Premium = true; + user.GatewaySubscriptionId = "sub_123"; + user.GatewayCustomerId = "cus_123"; + + var mockSubscription = new Subscription + { + Id = "sub_123", + Items = new StripeList + { + Data = new List + { + new SubscriptionItem + { + Id = "si_premium", + Price = new Price { Id = "premium-annually" } + } + } + }, + Metadata = new Dictionary() + }; + + var mockPremiumPlans = CreateTestPremiumPlansList(); + var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually"); + + _stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription); + _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans); + _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan); + _stripeAdapter.UpdateSubscriptionAsync(Arg.Any(), Arg.Any()).Returns(mockSubscription); + _stripeAdapter.UpdateCustomerAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(new Customer())); + _organizationRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _organizationApiKeyRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _organizationUserRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); + _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any()).Returns(Task.CompletedTask); + _userService.SaveUserAsync(user).Returns(Task.CompletedTask); + + // Mock collection repository to throw exception + _collectionRepository + .When(x => x.CreateAsync( + Arg.Any(), + Arg.Any>(), + Arg.Any>())) + .Do(_ => throw new InvalidOperationException("Database error")); + + // Act + var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", "Default Collection", PlanType.TeamsAnnually, CreateTestBillingAddress()); + + // Assert + Assert.True(result.IsT0); + var organizationId = result.AsT0; + Assert.NotEqual(Guid.Empty, organizationId); + + // Verify that core upgrade operations still completed successfully + await _organizationRepository.Received(1).CreateAsync(Arg.Any()); + await _organizationUserRepository.Received(1).CreateAsync(Arg.Any()); + await _userService.Received(1).SaveUserAsync(Arg.Is(u => + u.Premium == false && + u.GatewaySubscriptionId == null)); + + // Verify collection creation was attempted + await _collectionRepository.Received(1).CreateAsync( + Arg.Any(), + Arg.Any>(), + Arg.Any>()); + } } From 1df9a5676ca1ca0e875c280988b1538e0b9db3a3 Mon Sep 17 00:00:00 2001 From: Cy Okeke Date: Wed, 25 Feb 2026 13:47:23 +0100 Subject: [PATCH 2/3] Fix the failing test --- .../UpgradePremiumToOrganizationRequestTests.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/Api.Test/Billing/Models/Requests/UpgradePremiumToOrganizationRequestTests.cs b/test/Api.Test/Billing/Models/Requests/UpgradePremiumToOrganizationRequestTests.cs index 2d3bdb7b1483..b9cb7754d8aa 100644 --- a/test/Api.Test/Billing/Models/Requests/UpgradePremiumToOrganizationRequestTests.cs +++ b/test/Api.Test/Billing/Models/Requests/UpgradePremiumToOrganizationRequestTests.cs @@ -18,6 +18,9 @@ public void ToDomain_ValidTierTypes_ReturnsPlanType(ProductTierType tierType, Pl { OrganizationName = "Test Organization", Key = "encrypted-key", + PublicKey = "public-key", + EncryptedPrivateKey = "encrypted-private-key", + CollectionName = "Default Collection", TargetProductTierType = tierType, BillingAddress = new MinimalBillingAddressRequest { @@ -27,11 +30,14 @@ public void ToDomain_ValidTierTypes_ReturnsPlanType(ProductTierType tierType, Pl }; // Act - var (organizationName, key, planType, billingAddress) = sut.ToDomain(); + var (organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress) = sut.ToDomain(); // Assert Assert.Equal("Test Organization", organizationName); Assert.Equal("encrypted-key", key); + Assert.Equal("public-key", publicKey); + Assert.Equal("encrypted-private-key", encryptedPrivateKey); + Assert.Equal("Default Collection", collectionName); Assert.Equal(expectedPlanType, planType); Assert.Equal("US", billingAddress.Country); Assert.Equal("12345", billingAddress.PostalCode); @@ -47,6 +53,8 @@ public void ToDomain_InvalidTierTypes_ThrowsInvalidOperationException(ProductTie { OrganizationName = "Test Organization", Key = "encrypted-key", + PublicKey = "public-key", + EncryptedPrivateKey = "encrypted-private-key", TargetProductTierType = tierType, BillingAddress = new MinimalBillingAddressRequest { From 2e802c26748a76268be620c4382b26c7e662a89d Mon Sep 17 00:00:00 2001 From: Cy Okeke Date: Wed, 25 Feb 2026 16:56:26 +0100 Subject: [PATCH 3/3] removed --- ...pgradePremiumToOrganizationCommandTests.cs | 68 ------------------- 1 file changed, 68 deletions(-) diff --git a/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs index aa98c6ed7665..0406c39bf452 100644 --- a/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs @@ -540,74 +540,6 @@ await _stripeAdapter.Received(1).UpdateSubscriptionAsync( opts.Items.Count(i => i.Id == "si_other_product") == 0)); // Other product NOT in update (untouched) } - [Theory, BitAutoData] - public async Task Run_UserHasAdditionalStorage_CapturesStorageInMetadata(User user) - { - // Arrange - user.Premium = true; - user.GatewaySubscriptionId = "sub_123"; - user.GatewayCustomerId = "cus_123"; - - var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); - var mockSubscription = new Subscription - { - Id = "sub_123", - Items = new StripeList - { - Data = new List - { - new SubscriptionItem - { - Id = "si_premium", - Price = new Price { Id = "premium-annually" }, - CurrentPeriodEnd = currentPeriodEnd - }, - new SubscriptionItem - { - Id = "si_storage", - Price = new Price { Id = "personal-storage-gb-annually" }, - Quantity = 5, // User has 5GB additional storage - CurrentPeriodEnd = currentPeriodEnd - } - } - }, - Metadata = new Dictionary() - }; - - var mockPremiumPlans = CreateTestPremiumPlansList(); - var mockPlan = CreateTestPlan( - PlanType.TeamsAnnually, - stripeSeatPlanId: "teams-seat-annually" - ); - - _stripeAdapter.GetSubscriptionAsync("sub_123") - .Returns(mockSubscription); - _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans); - _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan); - _stripeAdapter.UpdateSubscriptionAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(mockSubscription)); - _organizationRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); - _organizationApiKeyRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); - _organizationUserRepository.CreateAsync(Arg.Any()).Returns(callInfo => Task.FromResult(callInfo.Arg())); - _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any()).Returns(Task.CompletedTask); - _userService.SaveUserAsync(user).Returns(Task.CompletedTask); - - // Act - var result = await _command.Run(user, "My Organization", "encrypted-key", "public-key", "encrypted-private-key", null, PlanType.TeamsAnnually, CreateTestBillingAddress()); - - // Assert - Assert.True(result.IsT0); - var organizationId = result.AsT0; - Assert.NotEqual(Guid.Empty, organizationId); - - // Verify that the additional storage quantity (5) is captured in metadata - await _stripeAdapter.Received(1).UpdateSubscriptionAsync( - "sub_123", - Arg.Is(opts => - opts.Items.Count == 2 && // 1 modified (premium to new price) + 1 deleted (storage) - opts.Items.Count(i => i.Deleted == true) == 1)); - } - [Theory, BitAutoData] public async Task Run_NoPremiumSubscriptionItemFound_ReturnsBadRequest(User user) {