using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Organizations.Commands; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Models.StaticStore; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; using OrganizationSubscriptionUpdate = Bit.Core.AdminConsole.Models.Data.Organizations.OrganizationSubscriptionUpdate; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; [SutProviderCustomize] public class BulkUpdateOrganizationSubscriptionsCommandTests { [Theory] [BitAutoData] public async Task BulkUpdateOrganizationSubscriptionsAsync_WhenNoSubscriptionsNeedToBeUpdated_ThenNoSyncsOccur( SutProvider sutProvider) { // Arrange OrganizationSubscriptionUpdate[] subscriptionsToUpdate = []; // Act await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate); await sutProvider.GetDependency() .DidNotReceive() .AdjustSeatsAsync(Arg.Any(), Arg.Any(), Arg.Any()); await sutProvider.GetDependency() .DidNotReceive() .UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any>(), Arg.Any()); } [Theory] [BitAutoData] public async Task BulkUpdateOrganizationSubscriptionsAsync_WhenOrgUpdatePassedIn_ThenSyncedThroughPaymentService( Organization organization, SutProvider sutProvider) { // Arrange organization.PlanType = PlanType.EnterpriseAnnually2023; organization.Seats = 2; OrganizationSubscriptionUpdate[] subscriptionsToUpdate = [new() { Organization = organization, Plan = new Enterprise2023Plan(true) }]; // Act await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate); await sutProvider.GetDependency() .Received(1) .AdjustSeatsAsync( Arg.Is(x => x.Id == organization.Id), Arg.Is(x => x.Type == organization.PlanType), organization.Seats!.Value); await sutProvider.GetDependency() .Received(1) .UpdateSuccessfulOrganizationSyncStatusAsync( Arg.Is>(x => x.Contains(organization.Id)), Arg.Any()); } [Theory] [BitAutoData] public async Task BulkUpdateOrganizationSubscriptionsAsync_WhenOrgUpdateFails_ThenSyncDoesNotOccur( Organization organization, Exception exception, SutProvider sutProvider) { // Arrange organization.PlanType = PlanType.EnterpriseAnnually2023; organization.Seats = 2; OrganizationSubscriptionUpdate[] subscriptionsToUpdate = [new() { Organization = organization, Plan = new Enterprise2023Plan(true) }]; sutProvider.GetDependency() .AdjustSeatsAsync( Arg.Is(x => x.Id == organization.Id), Arg.Is(x => x.Type == organization.PlanType), organization.Seats!.Value).ThrowsAsync(exception); // Act await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate); await sutProvider.GetDependency() .DidNotReceive() .UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any>(), Arg.Any()); } [Theory] [BitAutoData] public async Task BulkUpdateOrganizationSubscriptionsAsync_WhenOneOrgUpdateFailsAndAnotherSucceeds_ThenSyncOccursForTheSuccessfulOrg( Organization successfulOrganization, Organization failedOrganization, Exception exception, SutProvider sutProvider) { // Arrange successfulOrganization.PlanType = PlanType.EnterpriseAnnually2023; successfulOrganization.Seats = 2; failedOrganization.PlanType = PlanType.EnterpriseAnnually2023; failedOrganization.Seats = 2; OrganizationSubscriptionUpdate[] subscriptionsToUpdate = [ new() { Organization = successfulOrganization, Plan = new Enterprise2023Plan(true) }, new() { Organization = failedOrganization, Plan = new Enterprise2023Plan(true) } ]; sutProvider.GetDependency() .AdjustSeatsAsync( Arg.Is(x => x.Id == failedOrganization.Id), Arg.Is(x => x.Type == failedOrganization.PlanType), failedOrganization.Seats!.Value).ThrowsAsync(exception); // Act await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate); await sutProvider.GetDependency() .Received(1) .AdjustSeatsAsync( Arg.Is(x => x.Id == successfulOrganization.Id), Arg.Is(x => x.Type == successfulOrganization.PlanType), successfulOrganization.Seats!.Value); await sutProvider.GetDependency() .Received(1) .UpdateSuccessfulOrganizationSyncStatusAsync( Arg.Is>(x => x.Contains(successfulOrganization.Id)), Arg.Any()); await sutProvider.GetDependency() .DidNotReceive() .UpdateSuccessfulOrganizationSyncStatusAsync( Arg.Is>(x => x.Contains(failedOrganization.Id)), Arg.Any()); } [Theory] [BitAutoData] public async Task BulkUpdateOrganizationSubscriptionsAsync_WithFeatureFlag_WhenOrgUpdatePassedIn_ThenSyncedThroughCommand( Organization organization, SutProvider sutProvider) { // Arrange organization.PlanType = PlanType.EnterpriseAnnually2023; organization.Seats = 2; var plan = new Enterprise2023Plan(true); OrganizationSubscriptionUpdate[] subscriptionsToUpdate = [new() { Organization = organization, Plan = plan }]; sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand) .Returns(true); BillingCommandResult successResult = new Stripe.Subscription(); sutProvider.GetDependency() .Run(Arg.Is(x => x.Id == organization.Id), Arg.Any()) .Returns(successResult); // Act await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate); // Assert await sutProvider.GetDependency() .Received(1) .Run( Arg.Is(x => x.Id == organization.Id), Arg.Is(cs => cs.Changes.Count == 1 && !cs.ChargeImmediately && cs.Changes[0].AsT3.PriceId == plan.PasswordManager.StripeSeatPlanId && cs.Changes[0].AsT3.Quantity == organization.Seats!.Value)); await sutProvider.GetDependency() .DidNotReceive() .AdjustSeatsAsync(Arg.Any(), Arg.Any(), Arg.Any()); await sutProvider.GetDependency() .Received(1) .UpdateSuccessfulOrganizationSyncStatusAsync( Arg.Is>(x => x.Contains(organization.Id)), Arg.Any()); } [Theory] [BitAutoData] public async Task BulkUpdateOrganizationSubscriptionsAsync_WithFeatureFlag_WhenOrgUpdateFails_ThenSyncDoesNotOccur( Organization organization, SutProvider sutProvider) { // Arrange organization.PlanType = PlanType.EnterpriseAnnually2023; organization.Seats = 2; OrganizationSubscriptionUpdate[] subscriptionsToUpdate = [new() { Organization = organization, Plan = new Enterprise2023Plan(true) }]; sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand) .Returns(true); BillingCommandResult failureResult = new BadRequest("error"); sutProvider.GetDependency() .Run(Arg.Any(), Arg.Any()) .Returns(failureResult); // Act await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate); // Assert await sutProvider.GetDependency() .DidNotReceive() .UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any>(), Arg.Any()); } [Theory] [BitAutoData] public async Task BulkUpdateOrganizationSubscriptionsAsync_WithFeatureFlag_WhenOneFailsAndOneSucceeds_ThenSyncOccursForSuccessfulOrg( Organization successfulOrganization, Organization failedOrganization, SutProvider sutProvider) { // Arrange successfulOrganization.PlanType = PlanType.EnterpriseAnnually2023; successfulOrganization.Seats = 2; failedOrganization.PlanType = PlanType.EnterpriseAnnually2023; failedOrganization.Seats = 2; OrganizationSubscriptionUpdate[] subscriptionsToUpdate = [ new() { Organization = successfulOrganization, Plan = new Enterprise2023Plan(true) }, new() { Organization = failedOrganization, Plan = new Enterprise2023Plan(true) } ]; sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand) .Returns(true); BillingCommandResult successResult = new Stripe.Subscription(); sutProvider.GetDependency() .Run(Arg.Is(x => x.Id == successfulOrganization.Id), Arg.Any()) .Returns(successResult); BillingCommandResult failureResult = new BadRequest("error"); sutProvider.GetDependency() .Run(Arg.Is(x => x.Id == failedOrganization.Id), Arg.Any()) .Returns(failureResult); // Act await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate); // Assert await sutProvider.GetDependency() .Received(1) .UpdateSuccessfulOrganizationSyncStatusAsync( Arg.Is>(x => x.Contains(successfulOrganization.Id)), Arg.Any()); await sutProvider.GetDependency() .DidNotReceive() .UpdateSuccessfulOrganizationSyncStatusAsync( Arg.Is>(x => x.Contains(failedOrganization.Id)), Arg.Any()); } }