1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 13:08:17 -05:00

[PM-12490] Extract OrganizationService.EnableAsync into commands (#5321)

* Add organization enable command implementation

* Add unit tests for OrganizationEnableCommand

* Add organization enable command registration for dependency injection

* Refactor payment and subscription handlers to use IOrganizationEnableCommand for organization enabling

* Remove EnableAsync methods from IOrganizationService and OrganizationService

* Add xmldoc to IOrganizationEnableCommand

* Refactor OrganizationEnableCommand to consolidate enable logic and add optional expiration
This commit is contained in:
Rui Tomé 2025-02-14 11:25:29 +00:00 committed by GitHub
parent f4341b2f3b
commit f4c37df883
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 213 additions and 31 deletions

View File

@ -1,4 +1,5 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Context; using Bit.Core.Context;
@ -17,7 +18,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
{ {
private readonly ILogger<PaymentSucceededHandler> _logger; private readonly ILogger<PaymentSucceededHandler> _logger;
private readonly IStripeEventService _stripeEventService; private readonly IStripeEventService _stripeEventService;
private readonly IOrganizationService _organizationService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IStripeFacade _stripeFacade; private readonly IStripeFacade _stripeFacade;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
@ -27,6 +27,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationEnableCommand _organizationEnableCommand;
public PaymentSucceededHandler( public PaymentSucceededHandler(
ILogger<PaymentSucceededHandler> logger, ILogger<PaymentSucceededHandler> logger,
@ -39,8 +40,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
IUserRepository userRepository, IUserRepository userRepository,
IStripeEventUtilityService stripeEventUtilityService, IStripeEventUtilityService stripeEventUtilityService,
IUserService userService, IUserService userService,
IOrganizationService organizationService, IPushNotificationService pushNotificationService,
IPushNotificationService pushNotificationService) IOrganizationEnableCommand organizationEnableCommand)
{ {
_logger = logger; _logger = logger;
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
@ -52,8 +53,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
_userRepository = userRepository; _userRepository = userRepository;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
_userService = userService; _userService = userService;
_organizationService = organizationService;
_pushNotificationService = pushNotificationService; _pushNotificationService = pushNotificationService;
_organizationEnableCommand = organizationEnableCommand;
} }
/// <summary> /// <summary>
@ -142,7 +143,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
return; return;
} }
await _organizationService.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);

View File

@ -1,6 +1,7 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Billing.Jobs; using Bit.Billing.Jobs;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -24,6 +25,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly ISchedulerFactory _schedulerFactory; private readonly ISchedulerFactory _schedulerFactory;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IOrganizationEnableCommand _organizationEnableCommand;
public SubscriptionUpdatedHandler( public SubscriptionUpdatedHandler(
IStripeEventService stripeEventService, IStripeEventService stripeEventService,
@ -35,7 +37,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
ISchedulerFactory schedulerFactory, ISchedulerFactory schedulerFactory,
IFeatureService featureService) IFeatureService featureService,
IOrganizationEnableCommand organizationEnableCommand)
{ {
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
@ -47,6 +50,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_schedulerFactory = schedulerFactory; _schedulerFactory = schedulerFactory;
_featureService = featureService; _featureService = featureService;
_organizationEnableCommand = organizationEnableCommand;
} }
/// <summary> /// <summary>
@ -90,7 +94,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
} }
case StripeSubscriptionStatus.Active when organizationId.HasValue: case StripeSubscriptionStatus.Active when organizationId.HasValue:
{ {
await _organizationService.EnableAsync(organizationId.Value); await _organizationEnableCommand.EnableAsync(organizationId.Value);
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
break; break;

View File

@ -0,0 +1,11 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
public interface IOrganizationEnableCommand
{
/// <summary>
/// Enables an organization that is currently disabled and has a gateway configured.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization to enable.</param>
/// <param name="expirationDate">When provided, sets the date the organization's subscription will expire. If not provided, no expiration date will be set.</param>
Task EnableAsync(Guid organizationId, DateTime? expirationDate = null);
}

View File

@ -0,0 +1,39 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationEnableCommand : IOrganizationEnableCommand
{
private readonly IApplicationCacheService _applicationCacheService;
private readonly IOrganizationRepository _organizationRepository;
public OrganizationEnableCommand(
IApplicationCacheService applicationCacheService,
IOrganizationRepository organizationRepository)
{
_applicationCacheService = applicationCacheService;
_organizationRepository = organizationRepository;
}
public async Task EnableAsync(Guid organizationId, DateTime? expirationDate = null)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
if (organization is null || organization.Enabled || expirationDate is not null && organization.Gateway is null)
{
return;
}
organization.Enabled = true;
if (expirationDate is not null && organization.Gateway is not null)
{
organization.ExpirationDate = expirationDate;
organization.RevisionDate = DateTime.UtcNow;
}
await _organizationRepository.ReplaceAsync(organization);
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
}
}

View File

@ -28,10 +28,8 @@ public interface IOrganizationService
/// </summary> /// </summary>
Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner,
string ownerKey, string collectionName, string publicKey, string privateKey); string ownerKey, string collectionName, string publicKey, string privateKey);
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate);
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
Task EnableAsync(Guid organizationId);
Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated); Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated);
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);

View File

@ -686,18 +686,6 @@ public class OrganizationService : IOrganizationService
} }
} }
public async Task EnableAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await GetOrgById(organizationId);
if (org != null && !org.Enabled && org.Gateway.HasValue)
{
org.Enabled = true;
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCacheAsync(org);
}
}
public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)
{ {
var org = await GetOrgById(organizationId); var org = await GetOrgById(organizationId);
@ -723,16 +711,6 @@ public class OrganizationService : IOrganizationService
} }
} }
public async Task EnableAsync(Guid organizationId)
{
var org = await GetOrgById(organizationId);
if (org != null && !org.Enabled)
{
org.Enabled = true;
await ReplaceAndUpdateCacheAsync(org);
}
}
public async Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated) public async Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated)
{ {
if (organization.Id == default(Guid)) if (organization.Id == default(Guid))

View File

@ -54,6 +54,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationSignUpCommands(); services.AddOrganizationSignUpCommands();
services.AddOrganizationDeleteCommands(); services.AddOrganizationDeleteCommands();
services.AddOrganizationEnableCommands();
services.AddOrganizationAuthCommands(); services.AddOrganizationAuthCommands();
services.AddOrganizationUserCommands(); services.AddOrganizationUserCommands();
services.AddOrganizationUserCommandsQueries(); services.AddOrganizationUserCommandsQueries();
@ -69,6 +70,9 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IOrganizationInitiateDeleteCommand, OrganizationInitiateDeleteCommand>(); services.AddScoped<IOrganizationInitiateDeleteCommand, OrganizationInitiateDeleteCommand>();
} }
private static void AddOrganizationEnableCommands(this IServiceCollection services) =>
services.AddScoped<IOrganizationEnableCommand, OrganizationEnableCommand>();
private static void AddOrganizationConnectionCommands(this IServiceCollection services) private static void AddOrganizationConnectionCommands(this IServiceCollection services)
{ {
services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>(); services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>();

View File

@ -0,0 +1,147 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
[SutProviderCustomize]
public class OrganizationEnableCommandTests
{
[Theory, BitAutoData]
public async Task EnableAsync_WhenOrganizationDoesNotExist_DoesNothing(
Guid organizationId,
SutProvider<OrganizationEnableCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organizationId)
.Returns((Organization)null);
await sutProvider.Sut.EnableAsync(organizationId);
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive()
.ReplaceAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IApplicationCacheService>()
.DidNotReceive()
.UpsertOrganizationAbilityAsync(Arg.Any<Organization>());
}
[Theory, BitAutoData]
public async Task EnableAsync_WhenOrganizationAlreadyEnabled_DoesNothing(
Organization organization,
SutProvider<OrganizationEnableCommand> sutProvider)
{
organization.Enabled = true;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await sutProvider.Sut.EnableAsync(organization.Id);
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive()
.ReplaceAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IApplicationCacheService>()
.DidNotReceive()
.UpsertOrganizationAbilityAsync(Arg.Any<Organization>());
}
[Theory, BitAutoData]
public async Task EnableAsync_WhenOrganizationDisabled_EnablesAndSaves(
Organization organization,
SutProvider<OrganizationEnableCommand> sutProvider)
{
organization.Enabled = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await sutProvider.Sut.EnableAsync(organization.Id);
Assert.True(organization.Enabled);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.ReplaceAsync(organization);
await sutProvider.GetDependency<IApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(organization);
}
[Theory, BitAutoData]
public async Task EnableAsync_WithExpiration_WhenOrganizationHasNoGateway_DoesNothing(
Organization organization,
DateTime expirationDate,
SutProvider<OrganizationEnableCommand> sutProvider)
{
organization.Enabled = false;
organization.Gateway = null;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await sutProvider.Sut.EnableAsync(organization.Id, expirationDate);
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive()
.ReplaceAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IApplicationCacheService>()
.DidNotReceive()
.UpsertOrganizationAbilityAsync(Arg.Any<Organization>());
}
[Theory, BitAutoData]
public async Task EnableAsync_WithExpiration_WhenValid_EnablesAndSetsExpiration(
Organization organization,
DateTime expirationDate,
SutProvider<OrganizationEnableCommand> sutProvider)
{
organization.Enabled = false;
organization.Gateway = GatewayType.Stripe;
organization.RevisionDate = DateTime.UtcNow.AddDays(-1);
var originalRevisionDate = organization.RevisionDate;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await sutProvider.Sut.EnableAsync(organization.Id, expirationDate);
Assert.True(organization.Enabled);
Assert.Equal(expirationDate, organization.ExpirationDate);
Assert.True(organization.RevisionDate > originalRevisionDate);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.ReplaceAsync(organization);
await sutProvider.GetDependency<IApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(organization);
}
[Theory, BitAutoData]
public async Task EnableAsync_WithoutExpiration_DoesNotUpdateRevisionDate(
Organization organization,
SutProvider<OrganizationEnableCommand> sutProvider)
{
organization.Enabled = false;
var originalRevisionDate = organization.RevisionDate;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await sutProvider.Sut.EnableAsync(organization.Id);
Assert.True(organization.Enabled);
Assert.Equal(originalRevisionDate, organization.RevisionDate);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.ReplaceAsync(organization);
await sutProvider.GetDependency<IApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(organization);
}
}