From 20d3911b8015fbe9b447bcfb2a82cea3661bea16 Mon Sep 17 00:00:00 2001 From: Ben Bryant <30258875+benbryant0@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:59:57 +0100 Subject: [PATCH 1/2] [PM-22380] Enable NRT for some Core project files (#5912) * Enable NRT for Core/Jobs files * Enable NRT for Core/HostedServices files * Enable NRT for Core/Exceptions files * Enable NRT for Core/NotificationHub files --------- Co-authored-by: Bernd Schoolmann --- src/Core/Exceptions/BadRequestException.cs | 4 ++- src/Core/Exceptions/ConflictException.cs | 2 ++ src/Core/Exceptions/DnsQueryException.cs | 2 ++ src/Core/Exceptions/DomainClaimedException.cs | 2 ++ .../Exceptions/DomainVerifiedException.cs | 2 ++ .../Exceptions/DuplicateDomainException.cs | 2 ++ .../Exceptions/FeatureUnavailableException.cs | 2 ++ src/Core/Exceptions/GatewayException.cs | 4 ++- src/Core/Exceptions/InvalidEmailException.cs | 2 ++ .../InvalidGatewayCustomerIdException.cs | 2 ++ src/Core/Exceptions/NotFoundException.cs | 2 ++ .../ApplicationCacheHostedService.cs | 16 ++++++---- .../IpRateLimitSeedStartupService.cs | 2 ++ src/Core/Jobs/BaseJob.cs | 2 ++ src/Core/Jobs/BaseJobsHostedService.cs | 30 +++++++++++-------- src/Core/Jobs/JobFactory.cs | 4 ++- src/Core/Jobs/JobListener.cs | 4 ++- .../INotificationHubClientProxy.cs | 2 ++ .../NotificationHub/INotificationHubPool.cs | 2 ++ .../NotificationHubClientProxy.cs | 2 ++ .../NotificationHubConnection.cs | 22 ++++++++------ .../NotificationHub/NotificationHubPool.cs | 2 ++ .../NotificationHubPushNotificationService.cs | 2 ++ .../NotificationHubPushRegistrationService.cs | 10 ++++--- .../NotificationHub/PushRegistrationData.cs | 6 ++-- .../AccountsKeyManagementControllerTests.cs | 6 ++-- 26 files changed, 99 insertions(+), 39 deletions(-) diff --git a/src/Core/Exceptions/BadRequestException.cs b/src/Core/Exceptions/BadRequestException.cs index 042f853a57..b27bc7510f 100644 --- a/src/Core/Exceptions/BadRequestException.cs +++ b/src/Core/Exceptions/BadRequestException.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Bit.Core.Exceptions; +#nullable enable + public class BadRequestException : Exception { public BadRequestException() : base() @@ -41,5 +43,5 @@ public class BadRequestException : Exception } } - public ModelStateDictionary ModelState { get; set; } + public ModelStateDictionary? ModelState { get; set; } } diff --git a/src/Core/Exceptions/ConflictException.cs b/src/Core/Exceptions/ConflictException.cs index 27b90a657f..92fcc52d7f 100644 --- a/src/Core/Exceptions/ConflictException.cs +++ b/src/Core/Exceptions/ConflictException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class ConflictException : Exception { public ConflictException() : base("Conflict.") { } diff --git a/src/Core/Exceptions/DnsQueryException.cs b/src/Core/Exceptions/DnsQueryException.cs index 57b2c56daa..e3f605dec4 100644 --- a/src/Core/Exceptions/DnsQueryException.cs +++ b/src/Core/Exceptions/DnsQueryException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class DnsQueryException : Exception { public DnsQueryException(string message) diff --git a/src/Core/Exceptions/DomainClaimedException.cs b/src/Core/Exceptions/DomainClaimedException.cs index 09ccb3d0d8..9ac6972fa1 100644 --- a/src/Core/Exceptions/DomainClaimedException.cs +++ b/src/Core/Exceptions/DomainClaimedException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class DomainClaimedException : Exception { public DomainClaimedException() diff --git a/src/Core/Exceptions/DomainVerifiedException.cs b/src/Core/Exceptions/DomainVerifiedException.cs index d3a3fd4de4..1fb704bd55 100644 --- a/src/Core/Exceptions/DomainVerifiedException.cs +++ b/src/Core/Exceptions/DomainVerifiedException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class DomainVerifiedException : Exception { public DomainVerifiedException() diff --git a/src/Core/Exceptions/DuplicateDomainException.cs b/src/Core/Exceptions/DuplicateDomainException.cs index 8d347dda55..4f61f333f5 100644 --- a/src/Core/Exceptions/DuplicateDomainException.cs +++ b/src/Core/Exceptions/DuplicateDomainException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class DuplicateDomainException : Exception { public DuplicateDomainException() diff --git a/src/Core/Exceptions/FeatureUnavailableException.cs b/src/Core/Exceptions/FeatureUnavailableException.cs index 7bea350956..80fd7d0635 100644 --- a/src/Core/Exceptions/FeatureUnavailableException.cs +++ b/src/Core/Exceptions/FeatureUnavailableException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + /// /// Exception to throw when a requested feature is not yet enabled/available for the requesting context. /// diff --git a/src/Core/Exceptions/GatewayException.cs b/src/Core/Exceptions/GatewayException.cs index 73e8cd7613..4b24c8d107 100644 --- a/src/Core/Exceptions/GatewayException.cs +++ b/src/Core/Exceptions/GatewayException.cs @@ -1,8 +1,10 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class GatewayException : Exception { - public GatewayException(string message, Exception innerException = null) + public GatewayException(string message, Exception? innerException = null) : base(message, innerException) { } } diff --git a/src/Core/Exceptions/InvalidEmailException.cs b/src/Core/Exceptions/InvalidEmailException.cs index 1f17acf62e..c38ec0ac38 100644 --- a/src/Core/Exceptions/InvalidEmailException.cs +++ b/src/Core/Exceptions/InvalidEmailException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class InvalidEmailException : Exception { public InvalidEmailException() diff --git a/src/Core/Exceptions/InvalidGatewayCustomerIdException.cs b/src/Core/Exceptions/InvalidGatewayCustomerIdException.cs index cfc7c56c1c..6ec15da308 100644 --- a/src/Core/Exceptions/InvalidGatewayCustomerIdException.cs +++ b/src/Core/Exceptions/InvalidGatewayCustomerIdException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class InvalidGatewayCustomerIdException : Exception { public InvalidGatewayCustomerIdException() diff --git a/src/Core/Exceptions/NotFoundException.cs b/src/Core/Exceptions/NotFoundException.cs index 70769d41ed..6a61e35868 100644 --- a/src/Core/Exceptions/NotFoundException.cs +++ b/src/Core/Exceptions/NotFoundException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class NotFoundException : Exception { public NotFoundException() : base() diff --git a/src/Core/HostedServices/ApplicationCacheHostedService.cs b/src/Core/HostedServices/ApplicationCacheHostedService.cs index 9021782d20..a699a26fcc 100644 --- a/src/Core/HostedServices/ApplicationCacheHostedService.cs +++ b/src/Core/HostedServices/ApplicationCacheHostedService.cs @@ -10,9 +10,11 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.HostedServices; +#nullable enable + public class ApplicationCacheHostedService : IHostedService, IDisposable { - private readonly InMemoryServiceBusApplicationCacheService _applicationCacheService; + private readonly InMemoryServiceBusApplicationCacheService? _applicationCacheService; private readonly IOrganizationRepository _organizationRepository; protected readonly ILogger _logger; private readonly ServiceBusClient _serviceBusClient; @@ -20,8 +22,8 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable private readonly ServiceBusAdministrationClient _serviceBusAdministrationClient; private readonly string _subName; private readonly string _topicName; - private CancellationTokenSource _cts; - private Task _executingTask; + private CancellationTokenSource? _cts; + private Task? _executingTask; public ApplicationCacheHostedService( @@ -67,13 +69,17 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable { await _subscriptionReceiver.CloseAsync(cancellationToken); await _serviceBusClient.DisposeAsync(); - _cts.Cancel(); + _cts?.Cancel(); try { await _serviceBusAdministrationClient.DeleteSubscriptionAsync(_topicName, _subName, cancellationToken); } catch { } - await _executingTask; + + if (_executingTask != null) + { + await _executingTask; + } } public virtual void Dispose() diff --git a/src/Core/HostedServices/IpRateLimitSeedStartupService.cs b/src/Core/HostedServices/IpRateLimitSeedStartupService.cs index a6869d929c..827dd94806 100644 --- a/src/Core/HostedServices/IpRateLimitSeedStartupService.cs +++ b/src/Core/HostedServices/IpRateLimitSeedStartupService.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Hosting; namespace Bit.Core.HostedServices; +#nullable enable + /// /// A startup service that will seed the IP rate limiting stores with any values in the /// GlobalSettings configuration. diff --git a/src/Core/Jobs/BaseJob.cs b/src/Core/Jobs/BaseJob.cs index 56c39014a7..a56045f659 100644 --- a/src/Core/Jobs/BaseJob.cs +++ b/src/Core/Jobs/BaseJob.cs @@ -3,6 +3,8 @@ using Quartz; namespace Bit.Core.Jobs; +#nullable enable + public abstract class BaseJob : IJob { protected readonly ILogger _logger; diff --git a/src/Core/Jobs/BaseJobsHostedService.cs b/src/Core/Jobs/BaseJobsHostedService.cs index 897a382a2b..2ade53c6bb 100644 --- a/src/Core/Jobs/BaseJobsHostedService.cs +++ b/src/Core/Jobs/BaseJobsHostedService.cs @@ -8,6 +8,8 @@ using Quartz.Impl.Matchers; namespace Bit.Core.Jobs; +#nullable enable + public abstract class BaseJobsHostedService : IHostedService, IDisposable { private const int MaximumJobRetries = 10; @@ -16,7 +18,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable private readonly ILogger _listenerLogger; protected readonly ILogger _logger; - private IScheduler _scheduler; + private IScheduler? _scheduler; protected GlobalSettings _globalSettings; public BaseJobsHostedService( @@ -31,7 +33,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable _globalSettings = globalSettings; } - public IEnumerable> Jobs { get; protected set; } + public IEnumerable>? Jobs { get; protected set; } public virtual async Task StartAsync(CancellationToken cancellationToken) { @@ -61,10 +63,19 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable _scheduler.ListenerManager.AddJobListener(new JobListener(_listenerLogger), GroupMatcher.AnyGroup()); await _scheduler.Start(cancellationToken); + + var jobKeys = new List(); + var triggerKeys = new List(); + if (Jobs != null) { foreach (var (job, trigger) in Jobs) { + jobKeys.Add(JobBuilder.Create(job) + .WithIdentity(job.FullName!) + .Build().Key); + triggerKeys.Add(trigger.Key); + for (var retry = 0; retry < MaximumJobRetries; retry++) { // There's a race condition when starting multiple containers simultaneously, retry until it succeeds.. @@ -77,7 +88,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable } var jobDetail = JobBuilder.Create(job) - .WithIdentity(job.FullName) + .WithIdentity(job.FullName!) .Build(); var dupeJ = await _scheduler.GetJobDetail(jobDetail.Key); @@ -106,13 +117,6 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable // Delete old Jobs and Triggers var existingJobKeys = await _scheduler.GetJobKeys(GroupMatcher.AnyGroup()); - var jobKeys = Jobs.Select(j => - { - var job = j.Item1; - return JobBuilder.Create(job) - .WithIdentity(job.FullName) - .Build().Key; - }); foreach (var key in existingJobKeys) { @@ -126,7 +130,6 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable } var existingTriggerKeys = await _scheduler.GetTriggerKeys(GroupMatcher.AnyGroup()); - var triggerKeys = Jobs.Select(j => j.Item2.Key); foreach (var key in existingTriggerKeys) { @@ -142,7 +145,10 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable public virtual async Task StopAsync(CancellationToken cancellationToken) { - await _scheduler?.Shutdown(cancellationToken); + if (_scheduler is not null) + { + await _scheduler.Shutdown(cancellationToken); + } } public virtual void Dispose() diff --git a/src/Core/Jobs/JobFactory.cs b/src/Core/Jobs/JobFactory.cs index 6529443d97..8289a90322 100644 --- a/src/Core/Jobs/JobFactory.cs +++ b/src/Core/Jobs/JobFactory.cs @@ -4,6 +4,8 @@ using Quartz.Spi; namespace Bit.Core.Jobs; +#nullable enable + public class JobFactory : IJobFactory { private readonly IServiceProvider _container; @@ -16,7 +18,7 @@ public class JobFactory : IJobFactory public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { var scope = _container.CreateScope(); - return scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob; + return (scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob)!; } public void ReturnJob(IJob job) diff --git a/src/Core/Jobs/JobListener.cs b/src/Core/Jobs/JobListener.cs index e5e05e4b6b..0dc865655d 100644 --- a/src/Core/Jobs/JobListener.cs +++ b/src/Core/Jobs/JobListener.cs @@ -3,6 +3,8 @@ using Quartz; namespace Bit.Core.Jobs; +#nullable enable + public class JobListener : IJobListener { private readonly ILogger _logger; @@ -28,7 +30,7 @@ public class JobListener : IJobListener return Task.FromResult(0); } - public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, + public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default(CancellationToken)) { _logger.LogInformation(Constants.BypassFiltersEventId, null, "Finished job {0} at {1}.", diff --git a/src/Core/NotificationHub/INotificationHubClientProxy.cs b/src/Core/NotificationHub/INotificationHubClientProxy.cs index 82b4d39591..78eb0206d6 100644 --- a/src/Core/NotificationHub/INotificationHubClientProxy.cs +++ b/src/Core/NotificationHub/INotificationHubClientProxy.cs @@ -2,6 +2,8 @@ namespace Bit.Core.NotificationHub; +#nullable enable + public interface INotificationHubProxy { Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary properties, string tagExpression); diff --git a/src/Core/NotificationHub/INotificationHubPool.cs b/src/Core/NotificationHub/INotificationHubPool.cs index 3981598118..25a31d62f4 100644 --- a/src/Core/NotificationHub/INotificationHubPool.cs +++ b/src/Core/NotificationHub/INotificationHubPool.cs @@ -2,6 +2,8 @@ namespace Bit.Core.NotificationHub; +#nullable enable + public interface INotificationHubPool { NotificationHubConnection ConnectionFor(Guid comb); diff --git a/src/Core/NotificationHub/NotificationHubClientProxy.cs b/src/Core/NotificationHub/NotificationHubClientProxy.cs index 815ac88363..b47069fe21 100644 --- a/src/Core/NotificationHub/NotificationHubClientProxy.cs +++ b/src/Core/NotificationHub/NotificationHubClientProxy.cs @@ -2,6 +2,8 @@ namespace Bit.Core.NotificationHub; +#nullable enable + public class NotificationHubClientProxy : INotificationHubProxy { private readonly IEnumerable _clients; diff --git a/src/Core/NotificationHub/NotificationHubConnection.cs b/src/Core/NotificationHub/NotificationHubConnection.cs index a68134450e..a61f2ded8f 100644 --- a/src/Core/NotificationHub/NotificationHubConnection.cs +++ b/src/Core/NotificationHub/NotificationHubConnection.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; using System.Text; using System.Web; using Bit.Core.Settings; @@ -7,21 +8,23 @@ using Microsoft.Azure.NotificationHubs; namespace Bit.Core.NotificationHub; +#nullable enable + public class NotificationHubConnection { - public string HubName { get; init; } - public string ConnectionString { get; init; } + public string? HubName { get; init; } + public string? ConnectionString { get; init; } private Lazy _parsedConnectionString; public Uri Endpoint => _parsedConnectionString.Value.Endpoint; private string SasKey => _parsedConnectionString.Value.SharedAccessKey; private string SasKeyName => _parsedConnectionString.Value.SharedAccessKeyName; public bool EnableSendTracing { get; init; } - private NotificationHubClient _hubClient; + private NotificationHubClient? _hubClient; /// /// Gets the NotificationHubClient for this connection. - /// + /// /// If the client is null, it will be initialized. - /// + /// /// Exception if the connection is invalid. /// public NotificationHubClient HubClient @@ -45,13 +48,13 @@ public class NotificationHubConnection } /// /// Gets the start date for registration. - /// + /// /// If null, registration is always disabled. /// public DateTime? RegistrationStartDate { get; init; } /// /// Gets the end date for registration. - /// + /// /// If null, registration has no end date. /// public DateTime? RegistrationEndDate { get; init; } @@ -155,9 +158,10 @@ public class NotificationHubConnection }; } + [MemberNotNull(nameof(_hubClient))] private NotificationHubConnection Init() { - HubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing); + _hubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing); return this; } diff --git a/src/Core/NotificationHub/NotificationHubPool.cs b/src/Core/NotificationHub/NotificationHubPool.cs index 6b48e82f88..38192c11fc 100644 --- a/src/Core/NotificationHub/NotificationHubPool.cs +++ b/src/Core/NotificationHub/NotificationHubPool.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.NotificationHub; +#nullable enable + public class NotificationHubPool : INotificationHubPool { private List _connections { get; } diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index bb3de80977..368c0f731b 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -19,6 +19,8 @@ using Notification = Bit.Core.NotificationCenter.Entities.Notification; namespace Bit.Core.NotificationHub; +#nullable enable + /// /// Sends mobile push notifications to the Azure Notification Hub. /// Used by Cloud-Hosted environments. diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index f44fcf91a0..dc494eecd6 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.NotificationHub; +#nullable enable + public class NotificationHubPushRegistrationService : IPushRegistrationService { private static readonly JsonSerializerOptions webPushSerializationOptions = new() @@ -37,7 +39,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) + string? identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) { var orgIds = organizationIds.ToList(); var clientType = DeviceTypes.ToClientType(type); @@ -79,7 +81,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId, - string identifier, ClientType clientType, List organizationIds, DeviceType type, Guid installationId) + string? identifier, ClientType clientType, List organizationIds, DeviceType type, Guid installationId) { if (string.IsNullOrWhiteSpace(installation.PushChannel)) { @@ -137,7 +139,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private async Task CreateOrUpdateWebRegistrationAsync(string endpoint, string p256dh, string auth, Installation installation, string userId, - string identifier, ClientType clientType, List organizationIds, Guid installationId) + string? identifier, ClientType clientType, List organizationIds, Guid installationId) { // The Azure SDK is currently lacking support for web push registrations. // We need to use the REST API directly. @@ -187,7 +189,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private static KeyValuePair BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody, - string userId, string identifier, ClientType clientType, List organizationIds, Guid installationId) + string userId, string? identifier, ClientType clientType, List organizationIds, Guid installationId) { var fullTemplateId = $"template:{templateId}"; diff --git a/src/Core/NotificationHub/PushRegistrationData.cs b/src/Core/NotificationHub/PushRegistrationData.cs index 20e1cf0936..c11ee7be23 100644 --- a/src/Core/NotificationHub/PushRegistrationData.cs +++ b/src/Core/NotificationHub/PushRegistrationData.cs @@ -1,5 +1,7 @@ namespace Bit.Core.NotificationHub; +#nullable enable + public record struct WebPushRegistrationData { public string Endpoint { get; init; } @@ -9,9 +11,9 @@ public record struct WebPushRegistrationData public record class PushRegistrationData { - public string Token { get; set; } + public string? Token { get; set; } public WebPushRegistrationData? WebPush { get; set; } - public PushRegistrationData(string token) + public PushRegistrationData(string? token) { Token = token; } diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index d2775762e8..05b1aa5a4d 100644 --- a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -175,7 +175,7 @@ public class AccountsKeyManagementControllerTests } catch (BadRequestException ex) { - Assert.NotEmpty(ex.ModelState.Values); + Assert.NotEmpty(ex.ModelState!.Values); } } @@ -210,7 +210,7 @@ public class AccountsKeyManagementControllerTests var badRequestException = await Assert.ThrowsAsync(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data)); - Assert.Equal(1, badRequestException.ModelState.ErrorCount); + Assert.Equal(1, badRequestException.ModelState!.ErrorCount); Assert.Equal("set key connector key error", badRequestException.ModelState.Root.Errors[0].ErrorMessage); await sutProvider.GetDependency().Received(1) .SetKeyConnectorKeyAsync(Arg.Do(user => @@ -284,7 +284,7 @@ public class AccountsKeyManagementControllerTests var badRequestException = await Assert.ThrowsAsync(() => sutProvider.Sut.PostConvertToKeyConnectorAsync()); - Assert.Equal(1, badRequestException.ModelState.ErrorCount); + Assert.Equal(1, badRequestException.ModelState!.ErrorCount); Assert.Equal("convert to key connector error", badRequestException.ModelState.Root.Errors[0].ErrorMessage); await sutProvider.GetDependency().Received(1) .ConvertToKeyConnectorAsync(Arg.Is(expectedUser)); From dcf97a577236d3db5bb8dc5965462f64f4fb258f Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 6 Jun 2025 08:04:10 -0500 Subject: [PATCH 2/2] [PM-20485] [PM-20486] Add missing mappings to PlanAdapter (#5791) * Add missing mappings to PlanAdapter * PM-20485: Fix SM base seats --- src/Core/Billing/Pricing/PlanAdapter.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Core/Billing/Pricing/PlanAdapter.cs b/src/Core/Billing/Pricing/PlanAdapter.cs index f719fd1e87..45a48c3f80 100644 --- a/src/Core/Billing/Pricing/PlanAdapter.cs +++ b/src/Core/Billing/Pricing/PlanAdapter.cs @@ -31,6 +31,7 @@ public record PlanAdapter : Plan HasScim = HasFeature("scim"); HasResetPassword = HasFeature("resetPassword"); UsersGetPremium = HasFeature("usersGetPremium"); + HasCustomPermissions = HasFeature("customPermissions"); UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder) ? int.Parse(upgradeSortOrder) : 0; @@ -141,6 +142,7 @@ public record PlanAdapter : Plan var stripeSeatPlanId = GetStripeSeatPlanId(seats); var hasAdditionalSeatsOption = seats.IsScalable; var seatPrice = GetSeatPrice(seats); + var baseSeats = GetBaseSeats(seats); var maxSeats = GetMaxSeats(seats); var allowSeatAutoscale = seats.IsScalable; var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0; @@ -156,6 +158,7 @@ public record PlanAdapter : Plan StripeSeatPlanId = stripeSeatPlanId, HasAdditionalSeatsOption = hasAdditionalSeatsOption, SeatPrice = seatPrice, + BaseSeats = baseSeats, MaxSeats = maxSeats, AllowSeatAutoscale = allowSeatAutoscale, MaxProjects = maxProjects @@ -168,8 +171,16 @@ public record PlanAdapter : Plan private static decimal GetBasePrice(PurchasableDTO purchasable) => purchasable.FromPackaged(x => x.Price); + private static int GetBaseSeats(FreeOrScalableDTO freeOrScalable) + => freeOrScalable.Match( + free => free.Quantity, + scalable => scalable.Provided); + private static int GetBaseSeats(PurchasableDTO purchasable) - => purchasable.FromPackaged(x => x.Quantity); + => purchasable.Match( + free => free.Quantity, + packaged => packaged.Quantity, + scalable => scalable.Provided); private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable) => freeOrScalable.Match(