diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 0ac750e665..6ffd60e425 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; @@ -52,11 +53,11 @@ public class OrganizationsController : Controller private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; - private readonly IPushNotificationService _pushNotificationService; private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -73,11 +74,11 @@ public class OrganizationsController : Controller IOrganizationApiKeyRepository organizationApiKeyRepository, IFeatureService featureService, GlobalSettings globalSettings, - IPushNotificationService pushNotificationService, IProviderRepository providerRepository, IProviderBillingService providerBillingService, IDataProtectorTokenFactory orgDeleteTokenDataFactory, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -93,11 +94,11 @@ public class OrganizationsController : Controller _organizationApiKeyRepository = organizationApiKeyRepository; _featureService = featureService; _globalSettings = globalSettings; - _pushNotificationService = pushNotificationService; _providerRepository = providerRepository; _providerBillingService = providerBillingService; _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; } [HttpGet("{id}")] @@ -175,8 +176,8 @@ public class OrganizationsController : Controller } var organizationSignup = model.ToOrganizationSignup(user); - var result = await _organizationService.SignUpAsync(organizationSignup); - return new OrganizationResponseModel(result.Item1); + var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); + return new OrganizationResponseModel(result.Organization); } [HttpPost("create-without-payment")] @@ -190,8 +191,8 @@ public class OrganizationsController : Controller } var organizationSignup = model.ToOrganizationSignup(user); - var result = await _organizationService.SignUpAsync(organizationSignup); - return new OrganizationResponseModel(result.Item1); + var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); + return new OrganizationResponseModel(result.Organization); } [HttpPut("{id}")] diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs new file mode 100644 index 0000000000..3eb4d35ef1 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -0,0 +1,368 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public record SignUpOrganizationResponse( + Organization Organization, + OrganizationUser OrganizationUser, + Collection DefaultCollection); + +public interface ICloudOrganizationSignUpCommand +{ + Task SignUpOrganizationAsync(OrganizationSignup signup); +} + +public class CloudOrganizationSignUpCommand( + IOrganizationUserRepository organizationUserRepository, + IFeatureService featureService, + IOrganizationBillingService organizationBillingService, + IPaymentService paymentService, + IPolicyService policyService, + IReferenceEventService referenceEventService, + ICurrentContext currentContext, + IOrganizationRepository organizationRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository, + IApplicationCacheService applicationCacheService, + IPushRegistrationService pushRegistrationService, + IPushNotificationService pushNotificationService, + ICollectionRepository collectionRepository, + IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand +{ + public async Task SignUpOrganizationAsync(OrganizationSignup signup) + { + var plan = StaticStore.GetPlan(signup.Plan); + + ValidatePasswordManagerPlan(plan, signup); + + if (signup.UseSecretsManager) + { + if (signup.IsFromProvider) + { + throw new BadRequestException( + "Organizations with a Managed Service Provider do not support Secrets Manager."); + } + ValidateSecretsManagerPlan(plan, signup); + } + + if (!signup.IsFromProvider) + { + await ValidateSignUpPoliciesAsync(signup.Owner.Id); + } + + var organization = new Organization + { + // Pre-generate the org id so that we can save it with the Stripe subscription + Id = CoreHelpers.GenerateComb(), + Name = signup.Name, + BillingEmail = signup.BillingEmail, + BusinessName = signup.BusinessName, + PlanType = plan!.Type, + Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats), + MaxCollections = plan.PasswordManager.MaxCollections, + MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ? + (short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb), + UsePolicies = plan.HasPolicies, + UseSso = plan.HasSso, + UseGroups = plan.HasGroups, + UseEvents = plan.HasEvents, + UseDirectory = plan.HasDirectory, + UseTotp = plan.HasTotp, + Use2fa = plan.Has2fa, + UseApi = plan.HasApi, + UseResetPassword = plan.HasResetPassword, + SelfHost = plan.HasSelfHost, + UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon, + UseCustomPermissions = plan.HasCustomPermissions, + UseScim = plan.HasScim, + Plan = plan.Name, + Gateway = null, + ReferenceData = signup.Owner.ReferenceData, + Enabled = true, + LicenseKey = CoreHelpers.SecureRandomString(20), + PublicKey = signup.PublicKey, + PrivateKey = signup.PrivateKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Status = OrganizationStatusType.Created, + UsePasswordManager = true, + UseSecretsManager = signup.UseSecretsManager + }; + + if (signup.UseSecretsManager) + { + organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault(); + organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount + + signup.AdditionalServiceAccounts.GetValueOrDefault(); + } + + if (plan.Type == PlanType.Free && !signup.IsFromProvider) + { + var adminCount = + await organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id); + if (adminCount > 0) + { + throw new BadRequestException("You can only be an admin of one free organization."); + } + } + else if (plan.Type != PlanType.Free) + { + if (featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) + { + var sale = OrganizationSale.From(organization, signup); + await organizationBillingService.Finalize(sale); + } + else + { + if (signup.PaymentMethodType != null) + { + await paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, + signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, + signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); + } + else + { + await paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats, + signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); + } + + } + } + + var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; + var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true); + await referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.Signup, organization, currentContext) + { + PlanName = plan.Name, + PlanType = plan.Type, + Seats = returnValue.Item1.Seats, + SignupInitiationPath = signup.InitiationPath, + Storage = returnValue.Item1.MaxStorageGb, + // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 + }); + + return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser, returnValue.defaultCollection); + } + + public void ValidatePasswordManagerPlan(Plan plan, OrganizationUpgrade upgrade) + { + ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager"); + + if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0) + { + throw new BadRequestException($"You do not have any Password Manager seats!"); + } + + if (upgrade.AdditionalSeats < 0) + { + throw new BadRequestException($"You can't subtract Password Manager seats!"); + } + + if (!plan.PasswordManager.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0) + { + throw new BadRequestException("Plan does not allow additional storage."); + } + + if (upgrade.AdditionalStorageGb < 0) + { + throw new BadRequestException("You can't subtract storage!"); + } + + if (!plan.PasswordManager.HasPremiumAccessOption && upgrade.PremiumAccessAddon) + { + throw new BadRequestException("This plan does not allow you to buy the premium access addon."); + } + + if (!plan.PasswordManager.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0) + { + throw new BadRequestException("Plan does not allow additional users."); + } + + if (plan.PasswordManager.HasAdditionalSeatsOption && plan.PasswordManager.MaxAdditionalSeats.HasValue && + upgrade.AdditionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value) + { + throw new BadRequestException($"Selected plan allows a maximum of " + + $"{plan.PasswordManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users."); + } + } + + public void ValidateSecretsManagerPlan(Plan plan, OrganizationUpgrade upgrade) + { + if (plan.SupportsSecretsManager == false) + { + throw new BadRequestException("Invalid Secrets Manager plan selected."); + } + + ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager"); + + if (plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats <= 0) + { + throw new BadRequestException($"You do not have any Secrets Manager seats!"); + } + + if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0) + { + throw new BadRequestException("Plan does not allow additional Machine Accounts."); + } + + if ((plan.ProductTier == ProductTierType.TeamsStarter && + upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) || + (plan.ProductTier != ProductTierType.TeamsStarter && + upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats)) + { + throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats."); + } + + if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0) + { + throw new BadRequestException("You can't subtract Machine Accounts!"); + } + + switch (plan.SecretsManager.HasAdditionalSeatsOption) + { + case false when upgrade.AdditionalSmSeats > 0: + throw new BadRequestException("Plan does not allow additional users."); + case true when plan.SecretsManager.MaxAdditionalSeats.HasValue && + upgrade.AdditionalSmSeats > plan.SecretsManager.MaxAdditionalSeats.Value: + throw new BadRequestException($"Selected plan allows a maximum of " + + $"{plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users."); + } + } + + private static void ValidatePlan(Plan plan, int additionalSeats, string productType) + { + if (plan is null) + { + throw new BadRequestException($"{productType} Plan was null."); + } + + if (plan.Disabled) + { + throw new BadRequestException($"{productType} Plan not found."); + } + + if (additionalSeats < 0) + { + throw new BadRequestException($"You can't subtract {productType} seats!"); + } + } + + private async Task ValidateSignUpPoliciesAsync(Guid ownerId) + { + var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); + if (anySingleOrgPolicies) + { + throw new BadRequestException("You may not create an organization. You belong to an organization " + + "which has a policy that prohibits you from being a member of any other organization."); + } + } + + private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization, + Guid ownerId, string ownerKey, string collectionName, bool withPayment) + { + try + { + await organizationRepository.CreateAsync(organization); + await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey + { + OrganizationId = organization.Id, + ApiKey = CoreHelpers.SecureRandomString(30), + Type = OrganizationApiKeyType.Default, + RevisionDate = DateTime.UtcNow, + }); + await applicationCacheService.UpsertOrganizationAbilityAsync(organization); + + // ownerId == default if the org is created by a provider - in this case it's created without an + // owner and the first owner is immediately invited afterwards + OrganizationUser orgUser = null; + if (ownerId != default) + { + orgUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = ownerId, + Key = ownerKey, + AccessSecretsManager = organization.UseSecretsManager, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + orgUser.SetNewId(); + + await organizationUserRepository.CreateAsync(orgUser); + + var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value); + await pushRegistrationService.AddUserRegistrationOrganizationAsync(devices, organization.Id.ToString()); + await pushNotificationService.PushSyncOrgKeysAsync(ownerId); + } + + Collection defaultCollection = null; + if (!string.IsNullOrWhiteSpace(collectionName)) + { + defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = organization.Id, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + + // Give the owner Can Manage access over the default collection + List defaultOwnerAccess = null; + if (orgUser != null) + { + defaultOwnerAccess = + [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }]; + } + + await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); + } + + return (organization, orgUser, defaultCollection); + } + catch + { + if (withPayment) + { + await paymentService.CancelAndRecoverChargesAsync(organization); + } + + if (organization.Id != default(Guid)) + { + await organizationRepository.DeleteAsync(organization); + await applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + } + + throw; + } + } + + private async Task> GetUserDeviceIdsAsync(Guid userId) + { + var devices = await deviceRepository.GetManyByUserIdAsync(userId); + return devices + .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) + .Select(d => d.Id.ToString()); + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 646ae66166..0495c4c76e 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -20,13 +20,7 @@ public interface IOrganizationService Task AutoAddSeatsAsync(Organization organization, int seatsToAdd); Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); - /// - /// Create a new organization in a cloud environment - /// - /// A tuple containing the new organization, the initial organizationUser (if any) and the default collection (if any) #nullable enable - Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup); - Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup); #nullable disable /// diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 862b566c91..1ca047aa47 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -16,7 +16,6 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; -using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -502,129 +501,6 @@ public class OrganizationService : IOrganizationService return returnValue; } - /// - /// Create a new organization in a cloud environment - /// - public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(OrganizationSignup signup) - { - var plan = StaticStore.GetPlan(signup.Plan); - - ValidatePasswordManagerPlan(plan, signup); - - if (signup.UseSecretsManager) - { - if (signup.IsFromProvider) - { - throw new BadRequestException( - "Organizations with a Managed Service Provider do not support Secrets Manager."); - } - ValidateSecretsManagerPlan(plan, signup); - } - - if (!signup.IsFromProvider) - { - await ValidateSignUpPoliciesAsync(signup.Owner.Id); - } - - var organization = new Organization - { - // Pre-generate the org id so that we can save it with the Stripe subscription.. - Id = CoreHelpers.GenerateComb(), - Name = signup.Name, - BillingEmail = signup.BillingEmail, - BusinessName = signup.BusinessName, - PlanType = plan!.Type, - Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats), - MaxCollections = plan.PasswordManager.MaxCollections, - MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ? - (short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb), - UsePolicies = plan.HasPolicies, - UseSso = plan.HasSso, - UseGroups = plan.HasGroups, - UseEvents = plan.HasEvents, - UseDirectory = plan.HasDirectory, - UseTotp = plan.HasTotp, - Use2fa = plan.Has2fa, - UseApi = plan.HasApi, - UseResetPassword = plan.HasResetPassword, - SelfHost = plan.HasSelfHost, - UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon, - UseCustomPermissions = plan.HasCustomPermissions, - UseScim = plan.HasScim, - Plan = plan.Name, - Gateway = null, - ReferenceData = signup.Owner.ReferenceData, - Enabled = true, - LicenseKey = CoreHelpers.SecureRandomString(20), - PublicKey = signup.PublicKey, - PrivateKey = signup.PrivateKey, - CreationDate = DateTime.UtcNow, - RevisionDate = DateTime.UtcNow, - Status = OrganizationStatusType.Created, - UsePasswordManager = true, - UseSecretsManager = signup.UseSecretsManager - }; - - if (signup.UseSecretsManager) - { - organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault(); - organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount + - signup.AdditionalServiceAccounts.GetValueOrDefault(); - } - - if (plan.Type == PlanType.Free && !signup.IsFromProvider) - { - var adminCount = - await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id); - if (adminCount > 0) - { - throw new BadRequestException("You can only be an admin of one free organization."); - } - } - else if (plan.Type != PlanType.Free) - { - var deprecateStripeSourcesAPI = _featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI); - - if (deprecateStripeSourcesAPI) - { - var sale = OrganizationSale.From(organization, signup); - await _organizationBillingService.Finalize(sale); - } - else - { - if (signup.PaymentMethodType != null) - { - await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, - signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, - signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); - } - else - { - await _paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats, - signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); - } - - } - } - - var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; - var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Seats = returnValue.Item1.Seats, - SignupInitiationPath = signup.InitiationPath, - Storage = returnValue.Item1.MaxStorageGb, - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - }); - - return returnValue; - } - private async Task ValidateSignUpPoliciesAsync(Guid ownerId) { var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 96fdefcfad..5586273520 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -50,12 +51,16 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationGroupCommands(); services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationDomainCommandsQueries(); + services.AddOrganizationSignUpCommands(); services.AddOrganizationAuthCommands(); services.AddOrganizationUserCommands(); services.AddOrganizationUserCommandsQueries(); services.AddBaseOrganizationSubscriptionCommandsQueries(); } + private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) => + services.AddScoped(); + private static void AddOrganizationConnectionCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index 64f719e82e..dd514803fe 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -1,13 +1,13 @@ using System.Diagnostics; using Bit.Api.IntegrationTest.Factories; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.IntegrationTestCommon.Factories; namespace Bit.Api.IntegrationTest.Helpers; @@ -24,11 +24,11 @@ public static class OrganizationTestHelpers PaymentMethodType paymentMethod = PaymentMethodType.None) where T : class { var userRepository = factory.GetService(); - var organizationService = factory.GetService(); + var organizationSignUpCommand = factory.GetService(); var owner = await userRepository.GetByEmailAsync(ownerEmail); - var signUpResult = await organizationService.SignUpAsync(new OrganizationSignup + var signUpResult = await organizationSignUpCommand.SignUpOrganizationAsync(new OrganizationSignup { Name = name, BillingEmail = billingEmail, @@ -39,9 +39,9 @@ public static class OrganizationTestHelpers PaymentMethodType = paymentMethod }); - Debug.Assert(signUpResult.organizationUser is not null); + Debug.Assert(signUpResult.OrganizationUser is not null); - return new Tuple(signUpResult.organization, signUpResult.organizationUser); + return new Tuple(signUpResult.Organization, signUpResult.OrganizationUser); } /// diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 27c0f7a7c3..e58cd05b9d 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; @@ -46,11 +47,11 @@ public class OrganizationsControllerTests : IDisposable private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly IFeatureService _featureService; - private readonly IPushNotificationService _pushNotificationService; private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly OrganizationsController _sut; public OrganizationsControllerTests() @@ -69,11 +70,11 @@ public class OrganizationsControllerTests : IDisposable _userService = Substitute.For(); _createOrganizationApiKeyCommand = Substitute.For(); _featureService = Substitute.For(); - _pushNotificationService = Substitute.For(); _providerRepository = Substitute.For(); _providerBillingService = Substitute.For(); _orgDeleteTokenDataFactory = Substitute.For>(); _removeOrganizationUserCommand = Substitute.For(); + _cloudOrganizationSignUpCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -90,11 +91,11 @@ public class OrganizationsControllerTests : IDisposable _organizationApiKeyRepository, _featureService, _globalSettings, - _pushNotificationService, _providerRepository, _providerBillingService, _orgDeleteTokenDataFactory, - _removeOrganizationUserCommand); + _removeOrganizationUserCommand, + _cloudOrganizationSignUpCommand); } public void Dispose() diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs new file mode 100644 index 0000000000..2c32f0504b --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -0,0 +1,238 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp; + +[SutProviderCustomize] +public class CloudICloudOrganizationSignUpCommandTests +{ + [Theory] + [BitAutoData(PlanType.FamiliesAnnually)] + public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = planType; + + var plan = StaticStore.GetPlan(signup.Plan); + + signup.AdditionalSeats = 0; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.UseSecretsManager = false; + signup.IsFromSecretsManagerTrial = false; + + var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); + + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(o => + o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats + && o.SmSeats == null + && o.SmServiceAccounts == null)); + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); + + await sutProvider.GetDependency().Received(1) + .RaiseEventAsync(Arg.Is(referenceEvent => + referenceEvent.Type == ReferenceEventType.Signup && + referenceEvent.PlanName == plan.Name && + referenceEvent.PlanType == plan.Type && + referenceEvent.Seats == result.Organization.Seats && + referenceEvent.Storage == result.Organization.MaxStorageGb)); + // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 + + Assert.NotNull(result.Organization); + Assert.NotNull(result.OrganizationUser); + + await sutProvider.GetDependency().Received(1).PurchaseOrganizationAsync( + Arg.Any(), + signup.PaymentMethodType.Value, + signup.PaymentToken, + plan, + signup.AdditionalStorageGb, + signup.AdditionalSeats, + signup.PremiumAccessAddon, + signup.TaxInfo, + false, + signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), + signup.UseSecretsManager + ); + } + + [Theory] + [BitAutoData(PlanType.FamiliesAnnually)] + public async Task SignUp_AssignsOwnerToDefaultCollection + (PlanType planType, OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = planType; + signup.AdditionalSeats = 0; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.UseSecretsManager = false; + + // Extract orgUserId when created + Guid? orgUserId = null; + await sutProvider.GetDependency() + .CreateAsync(Arg.Do(ou => orgUserId = ou.Id)); + + var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); + + // Assert: created a Can Manage association for the default collection + Assert.NotNull(orgUserId); + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Any(), + Arg.Is>(cas => cas == null), + Arg.Is>(cas => + cas.Count() == 1 && + cas.All(c => + c.Id == orgUserId && + !c.ReadOnly && + !c.HidePasswords && + c.Manage))); + + Assert.NotNull(result.Organization); + Assert.NotNull(result.OrganizationUser); + } + + [Theory] + [BitAutoData(PlanType.EnterpriseAnnually)] + [BitAutoData(PlanType.EnterpriseMonthly)] + [BitAutoData(PlanType.TeamsAnnually)] + [BitAutoData(PlanType.TeamsMonthly)] + public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = planType; + + var plan = StaticStore.GetPlan(signup.Plan); + + signup.UseSecretsManager = true; + signup.AdditionalSeats = 15; + signup.AdditionalSmSeats = 10; + signup.AdditionalServiceAccounts = 20; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.IsFromSecretsManagerTrial = false; + + var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); + + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(o => + o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats + && o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats + && o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts)); + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); + + await sutProvider.GetDependency().Received(1) + .RaiseEventAsync(Arg.Is(referenceEvent => + referenceEvent.Type == ReferenceEventType.Signup && + referenceEvent.PlanName == plan.Name && + referenceEvent.PlanType == plan.Type && + referenceEvent.Seats == result.Organization.Seats && + referenceEvent.Storage == result.Organization.MaxStorageGb)); + // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 + + Assert.NotNull(result.Organization); + Assert.NotNull(result.OrganizationUser); + + await sutProvider.GetDependency().Received(1).PurchaseOrganizationAsync( + Arg.Any(), + signup.PaymentMethodType.Value, + signup.PaymentToken, + Arg.Is(plan), + signup.AdditionalStorageGb, + signup.AdditionalSeats, + signup.PremiumAccessAddon, + signup.TaxInfo, + false, + signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), + signup.IsFromSecretsManagerTrial + ); + } + + [Theory] + [BitAutoData(PlanType.EnterpriseAnnually)] + public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = planType; + signup.UseSecretsManager = true; + signup.AdditionalSeats = 15; + signup.AdditionalSmSeats = 10; + signup.AdditionalServiceAccounts = 20; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.IsFromProvider = true; + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) + { + signup.AdditionalSmSeats = 0; + signup.AdditionalSeats = 0; + signup.Plan = PlanType.Free; + signup.UseSecretsManager = true; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.AdditionalServiceAccounts = 10; + signup.AdditionalStorageGb = 0; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) + { + signup.AdditionalSmSeats = 100; + signup.AdditionalSeats = 10; + signup.Plan = PlanType.EnterpriseAnnually; + signup.UseSecretsManager = true; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.AdditionalServiceAccounts = 10; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) + { + signup.AdditionalSmSeats = 10; + signup.AdditionalSeats = 10; + signup.Plan = PlanType.EnterpriseAnnually; + signup.UseSecretsManager = true; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.AdditionalServiceAccounts = -10; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("You can't subtract Machine Accounts!", exception.Message); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index f0b7084fe9..fc839030aa 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -20,7 +20,6 @@ using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Mail; -using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; @@ -200,221 +199,6 @@ public class OrganizationServiceTests referenceEvent.Users == expectedNewUsersCount)); } - [Theory] - [BitAutoData(PlanType.FamiliesAnnually)] - public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = planType; - - var plan = StaticStore.GetPlan(signup.Plan); - - signup.AdditionalSeats = 0; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.UseSecretsManager = false; - signup.IsFromSecretsManagerTrial = false; - - var purchaseOrganizationPlan = StaticStore.GetPlan(signup.Plan); - - var result = await sutProvider.Sut.SignUpAsync(signup); - - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Is(o => - o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats - && o.SmSeats == null - && o.SmServiceAccounts == null)); - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); - - await sutProvider.GetDependency().Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.Signup && - referenceEvent.PlanName == plan.Name && - referenceEvent.PlanType == plan.Type && - referenceEvent.Seats == result.Item1.Seats && - referenceEvent.Storage == result.Item1.MaxStorageGb)); - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - - Assert.NotNull(result.Item1); - Assert.NotNull(result.Item2); - - await sutProvider.GetDependency().Received(1).PurchaseOrganizationAsync( - Arg.Any(), - signup.PaymentMethodType.Value, - signup.PaymentToken, - plan, - signup.AdditionalStorageGb, - signup.AdditionalSeats, - signup.PremiumAccessAddon, - signup.TaxInfo, - false, - signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), - signup.UseSecretsManager - ); - } - - [Theory] - [BitAutoData(PlanType.FamiliesAnnually)] - public async Task SignUp_AssignsOwnerToDefaultCollection - (PlanType planType, OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = planType; - signup.AdditionalSeats = 0; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.UseSecretsManager = false; - - // Extract orgUserId when created - Guid? orgUserId = null; - await sutProvider.GetDependency() - .CreateAsync(Arg.Do(ou => orgUserId = ou.Id)); - - var result = await sutProvider.Sut.SignUpAsync(signup); - - // Assert: created a Can Manage association for the default collection - Assert.NotNull(orgUserId); - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Any(), - Arg.Is>(cas => cas == null), - Arg.Is>(cas => - cas.Count() == 1 && - cas.All(c => - c.Id == orgUserId && - !c.ReadOnly && - !c.HidePasswords && - c.Manage))); - - Assert.NotNull(result.Item1); - Assert.NotNull(result.Item2); - } - - [Theory] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsMonthly)] - public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = planType; - - var plan = StaticStore.GetPlan(signup.Plan); - - signup.UseSecretsManager = true; - signup.AdditionalSeats = 15; - signup.AdditionalSmSeats = 10; - signup.AdditionalServiceAccounts = 20; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.IsFromSecretsManagerTrial = false; - - var result = await sutProvider.Sut.SignUpAsync(signup); - - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Is(o => - o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats - && o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats - && o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts)); - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); - - await sutProvider.GetDependency().Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.Signup && - referenceEvent.PlanName == plan.Name && - referenceEvent.PlanType == plan.Type && - referenceEvent.Seats == result.Item1.Seats && - referenceEvent.Storage == result.Item1.MaxStorageGb)); - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - - Assert.NotNull(result.Item1); - Assert.NotNull(result.Item2); - - await sutProvider.GetDependency().Received(1).PurchaseOrganizationAsync( - Arg.Any(), - signup.PaymentMethodType.Value, - signup.PaymentToken, - Arg.Is(plan), - signup.AdditionalStorageGb, - signup.AdditionalSeats, - signup.PremiumAccessAddon, - signup.TaxInfo, - false, - signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), - signup.IsFromSecretsManagerTrial - ); - } - - [Theory] - [BitAutoData(PlanType.EnterpriseAnnually)] - public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = planType; - signup.UseSecretsManager = true; - signup.AdditionalSeats = 15; - signup.AdditionalSmSeats = 10; - signup.AdditionalServiceAccounts = 20; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.IsFromProvider = true; - - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpAsync(signup)); - Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message); - } - - [Theory] - [BitAutoData] - public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) - { - signup.AdditionalSmSeats = 0; - signup.AdditionalSeats = 0; - signup.Plan = PlanType.Free; - signup.UseSecretsManager = true; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.AdditionalServiceAccounts = 10; - signup.AdditionalStorageGb = 0; - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SignUpAsync(signup)); - Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message); - } - - [Theory] - [BitAutoData] - public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) - { - signup.AdditionalSmSeats = 100; - signup.AdditionalSeats = 10; - signup.Plan = PlanType.EnterpriseAnnually; - signup.UseSecretsManager = true; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.AdditionalServiceAccounts = 10; - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SignUpAsync(signup)); - Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message); - } - - [Theory] - [BitAutoData] - public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) - { - signup.AdditionalSmSeats = 10; - signup.AdditionalSeats = 10; - signup.Plan = PlanType.EnterpriseAnnually; - signup.UseSecretsManager = true; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.AdditionalServiceAccounts = -10; - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SignUpAsync(signup)); - Assert.Contains("You can't subtract Machine Accounts!", exception.Message); - } - [Theory, BitAutoData] public async Task SignupClientAsync_Succeeds( OrganizationSignup signup,