1
0
mirror of https://github.com/bitwarden/server synced 2026-01-09 03:53:42 +00:00

[PM-26194] Fix: Provider Portal not automatically disabled, when subscription is cancelled (#6480)

* Add the fix for the bug

* Move the org disable to job
This commit is contained in:
cyprain-okeke
2025-10-27 13:19:42 +01:00
committed by GitHub
parent 9b313d9c0a
commit 427600d0cc
4 changed files with 500 additions and 2 deletions

View File

@@ -1,10 +1,15 @@
using Bit.Billing.Constants;
using Bit.Billing.Jobs;
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Services;
using NSubstitute;
using Quartz;
using Stripe;
using Xunit;
@@ -16,6 +21,10 @@ public class SubscriptionDeletedHandlerTests
private readonly IUserService _userService;
private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
private readonly IProviderRepository _providerRepository;
private readonly IProviderService _providerService;
private readonly ISchedulerFactory _schedulerFactory;
private readonly IScheduler _scheduler;
private readonly SubscriptionDeletedHandler _sut;
public SubscriptionDeletedHandlerTests()
@@ -24,11 +33,19 @@ public class SubscriptionDeletedHandlerTests
_userService = Substitute.For<IUserService>();
_stripeEventUtilityService = Substitute.For<IStripeEventUtilityService>();
_organizationDisableCommand = Substitute.For<IOrganizationDisableCommand>();
_providerRepository = Substitute.For<IProviderRepository>();
_providerService = Substitute.For<IProviderService>();
_schedulerFactory = Substitute.For<ISchedulerFactory>();
_scheduler = Substitute.For<IScheduler>();
_schedulerFactory.GetScheduler().Returns(_scheduler);
_sut = new SubscriptionDeletedHandler(
_stripeEventService,
_userService,
_stripeEventUtilityService,
_organizationDisableCommand);
_organizationDisableCommand,
_providerRepository,
_providerService,
_schedulerFactory);
}
[Fact]
@@ -59,6 +76,7 @@ public class SubscriptionDeletedHandlerTests
// Assert
await _organizationDisableCommand.DidNotReceiveWithAnyArgs().DisableAsync(default, default);
await _userService.DidNotReceiveWithAnyArgs().DisablePremiumAsync(default, default);
await _providerService.DidNotReceiveWithAnyArgs().UpdateAsync(default);
}
[Fact]
@@ -192,4 +210,120 @@ public class SubscriptionDeletedHandlerTests
await _organizationDisableCommand.DidNotReceiveWithAnyArgs()
.DisableAsync(default, default);
}
[Fact]
public async Task HandleAsync_ProviderSubscriptionCanceled_DisablesProviderAndQueuesJob()
{
// Arrange
var stripeEvent = new Event();
var providerId = Guid.NewGuid();
var provider = new Provider
{
Id = providerId,
Enabled = true
};
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Canceled,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } }
};
_stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository.GetByIdAsync(providerId).Returns(provider);
// Act
await _sut.HandleAsync(stripeEvent);
// Assert
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
await _scheduler.Received(1).ScheduleJob(
Arg.Is<IJobDetail>(j => j.JobType == typeof(ProviderOrganizationDisableJob)),
Arg.Any<ITrigger>());
}
[Fact]
public async Task HandleAsync_ProviderSubscriptionCanceled_ProviderNotFound_DoesNotThrow()
{
// Arrange
var stripeEvent = new Event();
var providerId = Guid.NewGuid();
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Canceled,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } }
};
_stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository.GetByIdAsync(providerId).Returns((Provider)null);
// Act & Assert - Should not throw
await _sut.HandleAsync(stripeEvent);
// Assert
await _providerService.DidNotReceiveWithAnyArgs().UpdateAsync(default);
await _scheduler.DidNotReceiveWithAnyArgs().ScheduleJob(default, default);
}
[Fact]
public async Task HandleAsync_ProviderSubscriptionCanceled_QueuesJobWithCorrectParameters()
{
// Arrange
var stripeEvent = new Event();
var providerId = Guid.NewGuid();
var expirationDate = DateTime.UtcNow.AddDays(30);
var provider = new Provider
{
Id = providerId,
Enabled = true
};
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Canceled,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = expirationDate }
]
},
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } }
};
_stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository.GetByIdAsync(providerId).Returns(provider);
// Act
await _sut.HandleAsync(stripeEvent);
// Assert
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
await _scheduler.Received(1).ScheduleJob(
Arg.Is<IJobDetail>(j =>
j.JobType == typeof(ProviderOrganizationDisableJob) &&
j.JobDataMap.GetString("providerId") == providerId.ToString() &&
j.JobDataMap.GetString("expirationDate") == expirationDate.ToString("O")),
Arg.Is<ITrigger>(t => t.Key.Name == $"disable-trigger-{providerId}"));
}
}