1
0
mirror of https://github.com/bitwarden/server synced 2025-12-15 07:43:54 +00:00

[AC-1495] Extract UpgradePlanAsync into a command (#3081)

* This is a pure lift & shift with no refactors

* Only register subscription commands in Api

---------

Co-authored-by: cyprain-okeke <cokeke@bitwarden.com>
This commit is contained in:
Thomas Rittson
2023-07-11 19:01:20 +10:00
committed by GitHub
parent ce03d3859d
commit a5efec301e
17 changed files with 493 additions and 406 deletions

View File

@@ -42,8 +42,6 @@ public class Startup
// Repositories
services.AddDatabaseRepositories(globalSettings);
services.AddOosServices();
// Context
services.AddScoped<ICurrentContext, CurrentContext>();
services.AddScoped<IScimContext, ScimContext>();

View File

@@ -9,8 +9,6 @@ using Stripe;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Admin.Services;
using Bit.Core.Repositories.Noop;
using Bit.Core.SecretsManager.Repositories;
#if !OSS
using Bit.Commercial.Core.Utilities;
@@ -88,7 +86,6 @@ public class Startup
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddScoped<IAccessControlService, AccessControlService>();
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
#if OSS
services.AddOosServices();

View File

@@ -18,7 +18,7 @@ using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -52,6 +52,7 @@ public class OrganizationsController : Controller
private readonly GlobalSettings _globalSettings;
private readonly ILicensingService _licensingService;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@@ -73,7 +74,8 @@ public class OrganizationsController : Controller
IFeatureService featureService,
GlobalSettings globalSettings,
ILicensingService licensingService,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@@ -95,6 +97,7 @@ public class OrganizationsController : Controller
_globalSettings = globalSettings;
_licensingService = licensingService;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_upgradeOrganizationPlanCommand = upgradeOrganizationPlanCommand;
}
[HttpGet("{id}")]
@@ -310,7 +313,7 @@ public class OrganizationsController : Controller
throw new NotFoundException();
}
var result = await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
var result = await _upgradeOrganizationPlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 };
}

View File

@@ -15,6 +15,7 @@ using Bit.SharedWeb.Utilities;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Core.Auth.Identity;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
#if !OSS
using Bit.Commercial.Core.SecretsManager;
@@ -133,6 +134,7 @@ public class Startup
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddOrganizationSubscriptionServices();
services.AddCoreLocalizationServices();
//health check

View File

@@ -17,8 +17,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager;
using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces;
using Bit.Core.Services;
@@ -45,7 +43,6 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationGroupCommands();
services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationSubscriptionUpdateCommandsQueries();
services.AddOrganizationAuthCommands();
}
@@ -116,11 +113,6 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
}
private static void AddOrganizationSubscriptionUpdateCommandsQueries(this IServiceCollection services)
{
services.AddScoped<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();
}
private static void AddOrganizationAuthCommands(this IServiceCollection services)
{
services.AddScoped<IUpdateOrganizationAuthRequestCommand, UpdateOrganizationAuthRequestCommand>();

View File

@@ -1,6 +1,6 @@
using Bit.Core.Models.Business;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
public interface IUpdateSecretsManagerSubscriptionCommand
{

View File

@@ -0,0 +1,6 @@
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
public interface IUpgradeOrganizationPlanCommand
{
Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, Models.Business.OrganizationUpgrade upgrade);
}

View File

@@ -0,0 +1,13 @@
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
public static class OrganizationSubscriptionServiceCollectionExtensions
{
public static void AddOrganizationSubscriptionServices(this IServiceCollection services)
{
services.AddScoped<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();
services.AddScoped<IUpgradeOrganizationPlanCommand, UpgradeOrganizationPlanCommand>();
}
}

View File

@@ -4,14 +4,14 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubscriptionCommand
{

View File

@@ -0,0 +1,339 @@
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.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.OrganizationFeatures.OrganizationSubscriptions;
public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
{
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository;
private readonly IPaymentService _paymentService;
private readonly IPolicyRepository _policyRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ICurrentContext _currentContext;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
public UpgradeOrganizationPlanCommand(
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository,
IGroupRepository groupRepository,
IPaymentService paymentService,
IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository,
IReferenceEventService referenceEventService,
IOrganizationConnectionRepository organizationConnectionRepository,
ICurrentContext currentContext,
IServiceAccountRepository serviceAccountRepository,
IOrganizationRepository organizationRepository,
IOrganizationService organizationService)
{
_organizationUserRepository = organizationUserRepository;
_collectionRepository = collectionRepository;
_groupRepository = groupRepository;
_paymentService = paymentService;
_policyRepository = policyRepository;
_ssoConfigRepository = ssoConfigRepository;
_referenceEventService = referenceEventService;
_organizationConnectionRepository = organizationConnectionRepository;
_currentContext = currentContext;
_serviceAccountRepository = serviceAccountRepository;
_organizationRepository = organizationRepository;
_organizationService = organizationService;
}
public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{
throw new BadRequestException("Your account has no payment method available.");
}
var existingPasswordManagerPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
if (existingPasswordManagerPlan == null)
{
throw new BadRequestException("Existing plan not found.");
}
var newPasswordManagerPlan =
StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
if (newPasswordManagerPlan == null)
{
throw new BadRequestException("Plan not found.");
}
if (existingPasswordManagerPlan.Type == newPasswordManagerPlan.Type)
{
throw new BadRequestException("Organization is already on this plan.");
}
if (existingPasswordManagerPlan.UpgradeSortOrder >= newPasswordManagerPlan.UpgradeSortOrder)
{
throw new BadRequestException("You cannot upgrade to this plan.");
}
if (existingPasswordManagerPlan.Type != PlanType.Free)
{
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
}
_organizationService.ValidatePasswordManagerPlan(newPasswordManagerPlan, upgrade);
var newSecretsManagerPlan =
StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
if (upgrade.UseSecretsManager)
{
_organizationService.ValidateSecretsManagerPlan(newSecretsManagerPlan, upgrade);
}
var newPasswordManagerPlanSeats = (short)(newPasswordManagerPlan.BaseSeats +
(newPasswordManagerPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
if (!organization.Seats.HasValue || organization.Seats.Value > newPasswordManagerPlanSeats)
{
var occupiedSeats =
await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
if (occupiedSeats > newPasswordManagerPlanSeats)
{
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
$"Your new plan only has ({newPasswordManagerPlanSeats}) seats. Remove some users.");
}
}
if (newPasswordManagerPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
organization.MaxCollections.Value >
newPasswordManagerPlan.MaxCollections.Value))
{
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
if (collectionCount > newPasswordManagerPlan.MaxCollections.Value)
{
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
$"Your new plan allows for a maximum of ({newPasswordManagerPlan.MaxCollections.Value}) collections. " +
"Remove some collections.");
}
}
if (!newPasswordManagerPlan.HasGroups && organization.UseGroups)
{
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
if (groups.Any())
{
throw new BadRequestException($"Your new plan does not allow the groups feature. " +
$"Remove your groups.");
}
}
if (!newPasswordManagerPlan.HasPolicies && organization.UsePolicies)
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
if (policies.Any(p => p.Enabled))
{
throw new BadRequestException($"Your new plan does not allow the policies feature. " +
$"Disable your policies.");
}
}
if (!newPasswordManagerPlan.HasSso && organization.UseSso)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.Enabled)
{
throw new BadRequestException($"Your new plan does not allow the SSO feature. " +
$"Disable your SSO configuration.");
}
}
if (!newPasswordManagerPlan.HasKeyConnector && organization.UseKeyConnector)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector)
{
throw new BadRequestException("Your new plan does not allow the Key Connector feature. " +
"Disable your Key Connector.");
}
}
if (!newPasswordManagerPlan.HasResetPassword && organization.UseResetPassword)
{
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled)
{
throw new BadRequestException("Your new plan does not allow the Password Reset feature. " +
"Disable your Password Reset policy.");
}
}
if (!newPasswordManagerPlan.HasScim && organization.UseScim)
{
var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
OrganizationConnectionType.Scim);
if (scimConnections != null && scimConnections.Any(c => c.GetConfig<ScimConfig>()?.Enabled == true))
{
throw new BadRequestException("Your new plan does not allow the SCIM feature. " +
"Disable your SCIM configuration.");
}
}
if (!newPasswordManagerPlan.HasCustomPermissions && organization.UseCustomPermissions)
{
var organizationCustomUsers =
await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id,
OrganizationUserType.Custom);
if (organizationCustomUsers.Any())
{
throw new BadRequestException("Your new plan does not allow the Custom Permissions feature. " +
"Disable your Custom Permissions configuration.");
}
}
if (upgrade.UseSecretsManager && newSecretsManagerPlan != null)
{
await ValidateSecretsManagerSeatsAndServiceAccountAsync(upgrade, organization, newSecretsManagerPlan);
}
// TODO: Check storage?
string paymentIntentClientSecret = null;
var success = true;
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
var organizationUpgradePlan = upgrade.UseSecretsManager
? StaticStore.Plans.Where(p => p.Type == upgrade.Plan).ToList()
: StaticStore.Plans.Where(p => p.Type == upgrade.Plan && p.BitwardenProduct == BitwardenProductType.PasswordManager).ToList();
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization,
organizationUpgradePlan,
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo
, upgrade.AdditionalSmSeats.GetValueOrDefault(), upgrade.AdditionalServiceAccounts.GetValueOrDefault());
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
}
else
{
// TODO: Update existing sub
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
}
organization.BusinessName = upgrade.BusinessName;
organization.PlanType = newPasswordManagerPlan.Type;
organization.Seats = (short)(newPasswordManagerPlan.BaseSeats + upgrade.AdditionalSeats);
organization.MaxCollections = newPasswordManagerPlan.MaxCollections;
organization.UseGroups = newPasswordManagerPlan.HasGroups;
organization.UseDirectory = newPasswordManagerPlan.HasDirectory;
organization.UseEvents = newPasswordManagerPlan.HasEvents;
organization.UseTotp = newPasswordManagerPlan.HasTotp;
organization.Use2fa = newPasswordManagerPlan.Has2fa;
organization.UseApi = newPasswordManagerPlan.HasApi;
organization.SelfHost = newPasswordManagerPlan.HasSelfHost;
organization.UsePolicies = newPasswordManagerPlan.HasPolicies;
organization.MaxStorageGb = !newPasswordManagerPlan.BaseStorageGb.HasValue
? (short?)null
: (short)(newPasswordManagerPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
organization.UseGroups = newPasswordManagerPlan.HasGroups;
organization.UseDirectory = newPasswordManagerPlan.HasDirectory;
organization.UseEvents = newPasswordManagerPlan.HasEvents;
organization.UseTotp = newPasswordManagerPlan.HasTotp;
organization.Use2fa = newPasswordManagerPlan.Has2fa;
organization.UseApi = newPasswordManagerPlan.HasApi;
organization.UseSso = newPasswordManagerPlan.HasSso;
organization.UseKeyConnector = newPasswordManagerPlan.HasKeyConnector;
organization.UseScim = newPasswordManagerPlan.HasScim;
organization.UseResetPassword = newPasswordManagerPlan.HasResetPassword;
organization.SelfHost = newPasswordManagerPlan.HasSelfHost;
organization.UsersGetPremium = newPasswordManagerPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
organization.UseCustomPermissions = newPasswordManagerPlan.HasCustomPermissions;
organization.Plan = newPasswordManagerPlan.Name;
organization.Enabled = success;
organization.PublicKey = upgrade.PublicKey;
organization.PrivateKey = upgrade.PrivateKey;
organization.UsePasswordManager = true;
organization.SmSeats = (short)(newSecretsManagerPlan.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault());
organization.SmServiceAccounts = upgrade.AdditionalServiceAccounts.GetValueOrDefault();
organization.UseSecretsManager = upgrade.UseSecretsManager;
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
if (success)
{
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext)
{
PlanName = newPasswordManagerPlan.Name,
PlanType = newPasswordManagerPlan.Type,
OldPlanName = existingPasswordManagerPlan.Name,
OldPlanType = existingPasswordManagerPlan.Type,
Seats = organization.Seats,
Storage = organization.MaxStorageGb,
SmSeats = organization.SmSeats,
ServiceAccounts = organization.SmServiceAccounts,
UseSecretsManager = organization.UseSecretsManager
});
}
return new Tuple<bool, string>(success, paymentIntentClientSecret);
}
private async Task ValidateSecretsManagerSeatsAndServiceAccountAsync(OrganizationUpgrade upgrade, Organization organization,
Models.StaticStore.Plan newSecretsManagerPlan)
{
var newPlanSmSeats = (short)(newSecretsManagerPlan.BaseSeats +
(newSecretsManagerPlan.HasAdditionalSeatsOption
? upgrade.AdditionalSmSeats
: 0));
var occupiedSmSeats =
await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
if (!organization.SmSeats.HasValue || organization.SmSeats.Value > newPlanSmSeats)
{
if (occupiedSmSeats > newPlanSmSeats)
{
throw new BadRequestException(
$"Your organization currently has {occupiedSmSeats} Secrets Manager seats filled. " +
$"Your new plan only has ({newPlanSmSeats}) seats. Remove some users.");
}
}
if (newSecretsManagerPlan.BaseServiceAccount != null)
{
if (!organization.SmServiceAccounts.HasValue ||
organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccounts)
{
var currentServiceAccounts =
await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccounts)
{
throw new BadRequestException(
$"Your organization currently has {currentServiceAccounts} service account seats filled. " +
$"Your new plan only has ({newSecretsManagerPlan.MaxServiceAccounts}) service accounts. Remove some service accounts.");
}
}
}
}
private async Task<Organization> GetOrgById(Guid id)
{
return await _organizationRepository.GetByIdAsync(id);
}
}

View File

@@ -13,7 +13,6 @@ public interface IOrganizationService
TaxInfo taxInfo);
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
Task ReinstateSubscriptionAsync(Guid organizationId);
Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade);
Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats);
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd, DateTime? prorationDate = null);
@@ -79,4 +78,7 @@ public interface IOrganizationService
/// </remarks>
Task InitPendingOrganization(Guid userId, Guid organizationId, string publicKey, string privateKey, string collectionName);
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
}

View File

@@ -1,6 +1,5 @@
using System.Security.Claims;
using System.Text.Json;
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Business;
using Bit.Core.Auth.Repositories;
@@ -13,7 +12,6 @@ using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Settings;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
@@ -39,7 +37,6 @@ public class OrganizationService : IOrganizationService
private readonly IDeviceRepository _deviceRepository;
private readonly ILicensingService _licensingService;
private readonly IEventService _eventService;
private readonly IInstallationRepository _installationRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IPaymentService _paymentService;
private readonly IPolicyRepository _policyRepository;
@@ -49,12 +46,10 @@ public class OrganizationService : IOrganizationService
private readonly IReferenceEventService _referenceEventService;
private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ICurrentContext _currentContext;
private readonly ILogger<OrganizationService> _logger;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public OrganizationService(
IOrganizationRepository organizationRepository,
@@ -69,7 +64,6 @@ public class OrganizationService : IOrganizationService
IDeviceRepository deviceRepository,
ILicensingService licensingService,
IEventService eventService,
IInstallationRepository installationRepository,
IApplicationCacheService applicationCacheService,
IPaymentService paymentService,
IPolicyRepository policyRepository,
@@ -79,12 +73,10 @@ public class OrganizationService : IOrganizationService
IReferenceEventService referenceEventService,
IGlobalSettings globalSettings,
IOrganizationApiKeyRepository organizationApiKeyRepository,
IOrganizationConnectionRepository organizationConnectionRepository,
ICurrentContext currentContext,
ILogger<OrganizationService> logger,
IProviderOrganizationRepository providerOrganizationRepository,
IProviderUserRepository providerUserRepository,
IServiceAccountRepository serviceAccountRepository)
IProviderUserRepository providerUserRepository)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@@ -98,7 +90,6 @@ public class OrganizationService : IOrganizationService
_deviceRepository = deviceRepository;
_licensingService = licensingService;
_eventService = eventService;
_installationRepository = installationRepository;
_applicationCacheService = applicationCacheService;
_paymentService = paymentService;
_policyRepository = policyRepository;
@@ -108,12 +99,10 @@ public class OrganizationService : IOrganizationService
_referenceEventService = referenceEventService;
_globalSettings = globalSettings;
_organizationApiKeyRepository = organizationApiKeyRepository;
_organizationConnectionRepository = organizationConnectionRepository;
_currentContext = currentContext;
_logger = logger;
_providerOrganizationRepository = providerOrganizationRepository;
_providerUserRepository = providerUserRepository;
_serviceAccountRepository = serviceAccountRepository;
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@@ -170,277 +159,6 @@ public class OrganizationService : IOrganizationService
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization, _currentContext));
}
public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{
throw new BadRequestException("Your account has no payment method available.");
}
var existingPasswordManagerPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
if (existingPasswordManagerPlan == null)
{
throw new BadRequestException("Existing plan not found.");
}
var newPasswordManagerPlan =
StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
if (newPasswordManagerPlan == null)
{
throw new BadRequestException("Plan not found.");
}
if (existingPasswordManagerPlan.Type == newPasswordManagerPlan.Type)
{
throw new BadRequestException("Organization is already on this plan.");
}
if (existingPasswordManagerPlan.UpgradeSortOrder >= newPasswordManagerPlan.UpgradeSortOrder)
{
throw new BadRequestException("You cannot upgrade to this plan.");
}
if (existingPasswordManagerPlan.Type != PlanType.Free)
{
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
}
ValidatePasswordManagerPlan(newPasswordManagerPlan, upgrade);
var newSecretsManagerPlan =
StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
if (upgrade.UseSecretsManager)
{
ValidateSecretsManagerPlan(newSecretsManagerPlan, upgrade);
}
var newPasswordManagerPlanSeats = (short)(newPasswordManagerPlan.BaseSeats +
(newPasswordManagerPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
if (!organization.Seats.HasValue || organization.Seats.Value > newPasswordManagerPlanSeats)
{
var occupiedSeats =
await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
if (occupiedSeats > newPasswordManagerPlanSeats)
{
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
$"Your new plan only has ({newPasswordManagerPlanSeats}) seats. Remove some users.");
}
}
if (newPasswordManagerPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
organization.MaxCollections.Value >
newPasswordManagerPlan.MaxCollections.Value))
{
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
if (collectionCount > newPasswordManagerPlan.MaxCollections.Value)
{
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
$"Your new plan allows for a maximum of ({newPasswordManagerPlan.MaxCollections.Value}) collections. " +
"Remove some collections.");
}
}
if (!newPasswordManagerPlan.HasGroups && organization.UseGroups)
{
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
if (groups.Any())
{
throw new BadRequestException($"Your new plan does not allow the groups feature. " +
$"Remove your groups.");
}
}
if (!newPasswordManagerPlan.HasPolicies && organization.UsePolicies)
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
if (policies.Any(p => p.Enabled))
{
throw new BadRequestException($"Your new plan does not allow the policies feature. " +
$"Disable your policies.");
}
}
if (!newPasswordManagerPlan.HasSso && organization.UseSso)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.Enabled)
{
throw new BadRequestException($"Your new plan does not allow the SSO feature. " +
$"Disable your SSO configuration.");
}
}
if (!newPasswordManagerPlan.HasKeyConnector && organization.UseKeyConnector)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector)
{
throw new BadRequestException("Your new plan does not allow the Key Connector feature. " +
"Disable your Key Connector.");
}
}
if (!newPasswordManagerPlan.HasResetPassword && organization.UseResetPassword)
{
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled)
{
throw new BadRequestException("Your new plan does not allow the Password Reset feature. " +
"Disable your Password Reset policy.");
}
}
if (!newPasswordManagerPlan.HasScim && organization.UseScim)
{
var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
OrganizationConnectionType.Scim);
if (scimConnections != null && scimConnections.Any(c => c.GetConfig<ScimConfig>()?.Enabled == true))
{
throw new BadRequestException("Your new plan does not allow the SCIM feature. " +
"Disable your SCIM configuration.");
}
}
if (!newPasswordManagerPlan.HasCustomPermissions && organization.UseCustomPermissions)
{
var organizationCustomUsers =
await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id,
OrganizationUserType.Custom);
if (organizationCustomUsers.Any())
{
throw new BadRequestException("Your new plan does not allow the Custom Permissions feature. " +
"Disable your Custom Permissions configuration.");
}
}
if (upgrade.UseSecretsManager && newSecretsManagerPlan != null)
{
await ValidateSecretsManagerSeatsAndServiceAccountAsync(upgrade, organization, newSecretsManagerPlan);
}
// TODO: Check storage?
string paymentIntentClientSecret = null;
var success = true;
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
var organizationUpgradePlan = upgrade.UseSecretsManager
? StaticStore.Plans.Where(p => p.Type == upgrade.Plan).ToList()
: StaticStore.Plans.Where(p => p.Type == upgrade.Plan && p.BitwardenProduct == BitwardenProductType.PasswordManager).ToList();
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization,
organizationUpgradePlan,
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo
, upgrade.AdditionalSmSeats.GetValueOrDefault(), upgrade.AdditionalServiceAccounts.GetValueOrDefault());
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
}
else
{
// TODO: Update existing sub
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
}
organization.BusinessName = upgrade.BusinessName;
organization.PlanType = newPasswordManagerPlan.Type;
organization.Seats = (short)(newPasswordManagerPlan.BaseSeats + upgrade.AdditionalSeats);
organization.MaxCollections = newPasswordManagerPlan.MaxCollections;
organization.UseGroups = newPasswordManagerPlan.HasGroups;
organization.UseDirectory = newPasswordManagerPlan.HasDirectory;
organization.UseEvents = newPasswordManagerPlan.HasEvents;
organization.UseTotp = newPasswordManagerPlan.HasTotp;
organization.Use2fa = newPasswordManagerPlan.Has2fa;
organization.UseApi = newPasswordManagerPlan.HasApi;
organization.SelfHost = newPasswordManagerPlan.HasSelfHost;
organization.UsePolicies = newPasswordManagerPlan.HasPolicies;
organization.MaxStorageGb = !newPasswordManagerPlan.BaseStorageGb.HasValue
? (short?)null
: (short)(newPasswordManagerPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
organization.UseGroups = newPasswordManagerPlan.HasGroups;
organization.UseDirectory = newPasswordManagerPlan.HasDirectory;
organization.UseEvents = newPasswordManagerPlan.HasEvents;
organization.UseTotp = newPasswordManagerPlan.HasTotp;
organization.Use2fa = newPasswordManagerPlan.Has2fa;
organization.UseApi = newPasswordManagerPlan.HasApi;
organization.UseSso = newPasswordManagerPlan.HasSso;
organization.UseKeyConnector = newPasswordManagerPlan.HasKeyConnector;
organization.UseScim = newPasswordManagerPlan.HasScim;
organization.UseResetPassword = newPasswordManagerPlan.HasResetPassword;
organization.SelfHost = newPasswordManagerPlan.HasSelfHost;
organization.UsersGetPremium = newPasswordManagerPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
organization.UseCustomPermissions = newPasswordManagerPlan.HasCustomPermissions;
organization.Plan = newPasswordManagerPlan.Name;
organization.Enabled = success;
organization.PublicKey = upgrade.PublicKey;
organization.PrivateKey = upgrade.PrivateKey;
organization.UsePasswordManager = true;
organization.SmSeats = (short)(newSecretsManagerPlan.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault());
organization.SmServiceAccounts = upgrade.AdditionalServiceAccounts.GetValueOrDefault();
organization.UseSecretsManager = upgrade.UseSecretsManager;
await ReplaceAndUpdateCacheAsync(organization);
if (success)
{
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext)
{
PlanName = newPasswordManagerPlan.Name,
PlanType = newPasswordManagerPlan.Type,
OldPlanName = existingPasswordManagerPlan.Name,
OldPlanType = existingPasswordManagerPlan.Type,
Seats = organization.Seats,
Storage = organization.MaxStorageGb,
SmSeats = organization.SmSeats,
ServiceAccounts = organization.SmServiceAccounts,
UseSecretsManager = organization.UseSecretsManager
});
}
return new Tuple<bool, string>(success, paymentIntentClientSecret);
}
private async Task ValidateSecretsManagerSeatsAndServiceAccountAsync(OrganizationUpgrade upgrade, Organization organization,
Models.StaticStore.Plan newSecretsManagerPlan)
{
var newPlanSmSeats = (short)(newSecretsManagerPlan.BaseSeats +
(newSecretsManagerPlan.HasAdditionalSeatsOption
? upgrade.AdditionalSmSeats
: 0));
var occupiedSmSeats =
await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
if (!organization.SmSeats.HasValue || organization.SmSeats.Value > newPlanSmSeats)
{
if (occupiedSmSeats > newPlanSmSeats)
{
throw new BadRequestException(
$"Your organization currently has {occupiedSmSeats} Secrets Manager seats filled. " +
$"Your new plan only has ({newPlanSmSeats}) seats. Remove some users.");
}
}
if (newSecretsManagerPlan.BaseServiceAccount != null)
{
if (!organization.SmServiceAccounts.HasValue ||
organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccounts)
{
var currentServiceAccounts =
await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccounts)
{
throw new BadRequestException(
$"Your organization currently has {currentServiceAccounts} service account seats filled. " +
$"Your new plan only has ({newSecretsManagerPlan.MaxServiceAccounts}) service accounts. Remove some service accounts.");
}
}
}
}
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
{
var organization = await GetOrgById(organizationId);
@@ -2162,7 +1880,7 @@ public class OrganizationService : IOrganizationService
}
}
private static void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
public void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
{
ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager");
@@ -2204,7 +1922,7 @@ public class OrganizationService : IOrganizationService
}
}
private static void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
public void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
{
ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager");

View File

@@ -4,8 +4,6 @@ using AspNetCoreRateLimit;
using Bit.Core;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Repositories.Noop;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Identity.Utilities;
@@ -48,8 +46,6 @@ public class Startup
// Repositories
services.AddDatabaseRepositories(globalSettings);
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
// Context
services.AddScoped<ICurrentContext, CurrentContext>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
@@ -155,7 +151,6 @@ public class Startup
});
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,

View File

@@ -11,7 +11,7 @@ using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate.Interface;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -42,6 +42,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IFeatureService _featureService;
private readonly ILicensingService _licensingService;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
private readonly OrganizationsController _sut;
@@ -67,13 +68,14 @@ public class OrganizationsControllerTests : IDisposable
_featureService = Substitute.For<IFeatureService>();
_licensingService = Substitute.For<ILicensingService>();
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
_policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext,
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand,
_cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService,
_updateSecretsManagerSubscriptionCommand);
_updateSecretsManagerSubscriptionCommand, _upgradeOrganizationPlanCommand);
}
public void Dispose()

View File

@@ -3,7 +3,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptionUpdate;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;

View File

@@ -0,0 +1,113 @@
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using Organization = Bit.Core.Entities.Organization;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate;
[SutProviderCustomize]
public class UpgradeOrganizationPlanCommandTests
{
[Theory, BitAutoData]
public async Task UpgradePlan_OrganizationIsNull_Throws(Guid organizationId, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(Task.FromResult<Organization>(null));
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade));
}
[Theory, BitAutoData]
public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
organization.GatewayCustomerId = string.Empty;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("no payment method", exception.Message);
}
[Theory, BitAutoData]
public async Task UpgradePlan_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
upgrade.Plan = organization.PlanType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("already on this plan", exception.Message);
}
[Theory, BitAutoData]
public async Task UpgradePlan_SM_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
upgrade.Plan = organization.PlanType;
upgrade.UseSecretsManager = true;
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalServiceAccounts = 10;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("already on this plan", exception.Message);
}
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("can only upgrade", exception.Message);
}
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
public async Task UpgradePlan_SM_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
upgrade.UseSecretsManager = true;
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalServiceAccounts = 10;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("can only upgrade", exception.Message);
}
[Theory]
[FreeOrganizationUpgradeCustomize, BitAutoData]
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalSeats = 10;
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
}
[Theory]
[FreeOrganizationUpgradeCustomize, BitAutoData]
public async Task UpgradePlan_SM_Passes(Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalSeats = 10;
var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
Assert.True(result.Item1);
Assert.NotNull(result.Item2);
}
}

View File

@@ -145,85 +145,6 @@ public class OrganizationServiceTests
referenceEvent.Users == expectedNewUsersCount));
}
[Theory, BitAutoData]
public async Task UpgradePlan_OrganizationIsNull_Throws(Guid organizationId, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(Task.FromResult<Organization>(null));
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade));
}
[Theory, BitAutoData]
public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
organization.GatewayCustomerId = string.Empty;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("no payment method", exception.Message);
}
[Theory, BitAutoData]
public async Task UpgradePlan_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
upgrade.Plan = organization.PlanType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("already on this plan", exception.Message);
}
[Theory, BitAutoData]
public async Task UpgradePlan_SM_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
upgrade.Plan = organization.PlanType;
upgrade.UseSecretsManager = true;
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalServiceAccounts = 10;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("already on this plan", exception.Message);
}
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("can only upgrade", exception.Message);
}
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
public async Task UpgradePlan_SM_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
upgrade.UseSecretsManager = true;
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalServiceAccounts = 10;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("can only upgrade", exception.Message);
}
[Theory]
[FreeOrganizationUpgradeCustomize, BitAutoData]
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalSeats = 10;
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
}
[Theory]
[BitAutoData]
@@ -327,20 +248,6 @@ public class OrganizationServiceTests
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
}
[Theory]
[FreeOrganizationUpgradeCustomize, BitAutoData]
public async Task UpgradePlan_SM_Passes(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalSeats = 10;
var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
Assert.True(result.Item1);
Assert.NotNull(result.Item2);
}
[Theory]
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
InvitorUserType = OrganizationUserType.Owner), BitAutoData]