1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-07 19:50:32 -05:00

Merge branch 'main' into billing/license-refactor

This commit is contained in:
Conner Turnbull 2025-06-06 09:57:31 -04:00
commit 71cb4a4ea8
No known key found for this signature in database
27 changed files with 111 additions and 40 deletions

View File

@ -31,6 +31,7 @@ public record PlanAdapter : Plan
HasScim = HasFeature("scim"); HasScim = HasFeature("scim");
HasResetPassword = HasFeature("resetPassword"); HasResetPassword = HasFeature("resetPassword");
UsersGetPremium = HasFeature("usersGetPremium"); UsersGetPremium = HasFeature("usersGetPremium");
HasCustomPermissions = HasFeature("customPermissions");
UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder) UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder)
? int.Parse(upgradeSortOrder) ? int.Parse(upgradeSortOrder)
: 0; : 0;
@ -141,6 +142,7 @@ public record PlanAdapter : Plan
var stripeSeatPlanId = GetStripeSeatPlanId(seats); var stripeSeatPlanId = GetStripeSeatPlanId(seats);
var hasAdditionalSeatsOption = seats.IsScalable; var hasAdditionalSeatsOption = seats.IsScalable;
var seatPrice = GetSeatPrice(seats); var seatPrice = GetSeatPrice(seats);
var baseSeats = GetBaseSeats(seats);
var maxSeats = GetMaxSeats(seats); var maxSeats = GetMaxSeats(seats);
var allowSeatAutoscale = seats.IsScalable; var allowSeatAutoscale = seats.IsScalable;
var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0; var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
@ -156,6 +158,7 @@ public record PlanAdapter : Plan
StripeSeatPlanId = stripeSeatPlanId, StripeSeatPlanId = stripeSeatPlanId,
HasAdditionalSeatsOption = hasAdditionalSeatsOption, HasAdditionalSeatsOption = hasAdditionalSeatsOption,
SeatPrice = seatPrice, SeatPrice = seatPrice,
BaseSeats = baseSeats,
MaxSeats = maxSeats, MaxSeats = maxSeats,
AllowSeatAutoscale = allowSeatAutoscale, AllowSeatAutoscale = allowSeatAutoscale,
MaxProjects = maxProjects MaxProjects = maxProjects
@ -168,8 +171,16 @@ public record PlanAdapter : Plan
private static decimal GetBasePrice(PurchasableDTO purchasable) private static decimal GetBasePrice(PurchasableDTO purchasable)
=> purchasable.FromPackaged(x => x.Price); => 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) 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) private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.Match( => freeOrScalable.Match(

View File

@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class BadRequestException : Exception public class BadRequestException : Exception
{ {
public BadRequestException() : base() public BadRequestException() : base()
@ -41,5 +43,5 @@ public class BadRequestException : Exception
} }
} }
public ModelStateDictionary ModelState { get; set; } public ModelStateDictionary? ModelState { get; set; }
} }

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class ConflictException : Exception public class ConflictException : Exception
{ {
public ConflictException() : base("Conflict.") { } public ConflictException() : base("Conflict.") { }

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class DnsQueryException : Exception public class DnsQueryException : Exception
{ {
public DnsQueryException(string message) public DnsQueryException(string message)

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class DomainClaimedException : Exception public class DomainClaimedException : Exception
{ {
public DomainClaimedException() public DomainClaimedException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class DomainVerifiedException : Exception public class DomainVerifiedException : Exception
{ {
public DomainVerifiedException() public DomainVerifiedException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class DuplicateDomainException : Exception public class DuplicateDomainException : Exception
{ {
public DuplicateDomainException() public DuplicateDomainException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
/// <summary> /// <summary>
/// Exception to throw when a requested feature is not yet enabled/available for the requesting context. /// Exception to throw when a requested feature is not yet enabled/available for the requesting context.
/// </summary> /// </summary>

View File

@ -1,8 +1,10 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class GatewayException : Exception public class GatewayException : Exception
{ {
public GatewayException(string message, Exception innerException = null) public GatewayException(string message, Exception? innerException = null)
: base(message, innerException) : base(message, innerException)
{ } { }
} }

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class InvalidEmailException : Exception public class InvalidEmailException : Exception
{ {
public InvalidEmailException() public InvalidEmailException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class InvalidGatewayCustomerIdException : Exception public class InvalidGatewayCustomerIdException : Exception
{ {
public InvalidGatewayCustomerIdException() public InvalidGatewayCustomerIdException()

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Exceptions; namespace Bit.Core.Exceptions;
#nullable enable
public class NotFoundException : Exception public class NotFoundException : Exception
{ {
public NotFoundException() : base() public NotFoundException() : base()

View File

@ -10,9 +10,11 @@ using Microsoft.Extensions.Logging;
namespace Bit.Core.HostedServices; namespace Bit.Core.HostedServices;
#nullable enable
public class ApplicationCacheHostedService : IHostedService, IDisposable public class ApplicationCacheHostedService : IHostedService, IDisposable
{ {
private readonly InMemoryServiceBusApplicationCacheService _applicationCacheService; private readonly InMemoryServiceBusApplicationCacheService? _applicationCacheService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
protected readonly ILogger<ApplicationCacheHostedService> _logger; protected readonly ILogger<ApplicationCacheHostedService> _logger;
private readonly ServiceBusClient _serviceBusClient; private readonly ServiceBusClient _serviceBusClient;
@ -20,8 +22,8 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable
private readonly ServiceBusAdministrationClient _serviceBusAdministrationClient; private readonly ServiceBusAdministrationClient _serviceBusAdministrationClient;
private readonly string _subName; private readonly string _subName;
private readonly string _topicName; private readonly string _topicName;
private CancellationTokenSource _cts; private CancellationTokenSource? _cts;
private Task _executingTask; private Task? _executingTask;
public ApplicationCacheHostedService( public ApplicationCacheHostedService(
@ -67,14 +69,18 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable
{ {
await _subscriptionReceiver.CloseAsync(cancellationToken); await _subscriptionReceiver.CloseAsync(cancellationToken);
await _serviceBusClient.DisposeAsync(); await _serviceBusClient.DisposeAsync();
_cts.Cancel(); _cts?.Cancel();
try try
{ {
await _serviceBusAdministrationClient.DeleteSubscriptionAsync(_topicName, _subName, cancellationToken); await _serviceBusAdministrationClient.DeleteSubscriptionAsync(_topicName, _subName, cancellationToken);
} }
catch { } catch { }
if (_executingTask != null)
{
await _executingTask; await _executingTask;
} }
}
public virtual void Dispose() public virtual void Dispose()
{ } { }

View File

@ -3,6 +3,8 @@ using Microsoft.Extensions.Hosting;
namespace Bit.Core.HostedServices; namespace Bit.Core.HostedServices;
#nullable enable
/// <summary> /// <summary>
/// A startup service that will seed the IP rate limiting stores with any values in the /// A startup service that will seed the IP rate limiting stores with any values in the
/// GlobalSettings configuration. /// GlobalSettings configuration.

View File

@ -3,6 +3,8 @@ using Quartz;
namespace Bit.Core.Jobs; namespace Bit.Core.Jobs;
#nullable enable
public abstract class BaseJob : IJob public abstract class BaseJob : IJob
{ {
protected readonly ILogger _logger; protected readonly ILogger _logger;

View File

@ -8,6 +8,8 @@ using Quartz.Impl.Matchers;
namespace Bit.Core.Jobs; namespace Bit.Core.Jobs;
#nullable enable
public abstract class BaseJobsHostedService : IHostedService, IDisposable public abstract class BaseJobsHostedService : IHostedService, IDisposable
{ {
private const int MaximumJobRetries = 10; private const int MaximumJobRetries = 10;
@ -16,7 +18,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
private readonly ILogger<JobListener> _listenerLogger; private readonly ILogger<JobListener> _listenerLogger;
protected readonly ILogger _logger; protected readonly ILogger _logger;
private IScheduler _scheduler; private IScheduler? _scheduler;
protected GlobalSettings _globalSettings; protected GlobalSettings _globalSettings;
public BaseJobsHostedService( public BaseJobsHostedService(
@ -31,7 +33,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }
public IEnumerable<Tuple<Type, ITrigger>> Jobs { get; protected set; } public IEnumerable<Tuple<Type, ITrigger>>? Jobs { get; protected set; }
public virtual async Task StartAsync(CancellationToken cancellationToken) public virtual async Task StartAsync(CancellationToken cancellationToken)
{ {
@ -61,10 +63,19 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
_scheduler.ListenerManager.AddJobListener(new JobListener(_listenerLogger), _scheduler.ListenerManager.AddJobListener(new JobListener(_listenerLogger),
GroupMatcher<JobKey>.AnyGroup()); GroupMatcher<JobKey>.AnyGroup());
await _scheduler.Start(cancellationToken); await _scheduler.Start(cancellationToken);
var jobKeys = new List<JobKey>();
var triggerKeys = new List<TriggerKey>();
if (Jobs != null) if (Jobs != null)
{ {
foreach (var (job, trigger) in Jobs) 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++) for (var retry = 0; retry < MaximumJobRetries; retry++)
{ {
// There's a race condition when starting multiple containers simultaneously, retry until it succeeds.. // 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) var jobDetail = JobBuilder.Create(job)
.WithIdentity(job.FullName) .WithIdentity(job.FullName!)
.Build(); .Build();
var dupeJ = await _scheduler.GetJobDetail(jobDetail.Key); var dupeJ = await _scheduler.GetJobDetail(jobDetail.Key);
@ -106,13 +117,6 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
// Delete old Jobs and Triggers // Delete old Jobs and Triggers
var existingJobKeys = await _scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup()); var existingJobKeys = await _scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup());
var jobKeys = Jobs.Select(j =>
{
var job = j.Item1;
return JobBuilder.Create(job)
.WithIdentity(job.FullName)
.Build().Key;
});
foreach (var key in existingJobKeys) foreach (var key in existingJobKeys)
{ {
@ -126,7 +130,6 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
} }
var existingTriggerKeys = await _scheduler.GetTriggerKeys(GroupMatcher<TriggerKey>.AnyGroup()); var existingTriggerKeys = await _scheduler.GetTriggerKeys(GroupMatcher<TriggerKey>.AnyGroup());
var triggerKeys = Jobs.Select(j => j.Item2.Key);
foreach (var key in existingTriggerKeys) foreach (var key in existingTriggerKeys)
{ {
@ -142,7 +145,10 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable
public virtual async Task StopAsync(CancellationToken cancellationToken) public virtual async Task StopAsync(CancellationToken cancellationToken)
{ {
await _scheduler?.Shutdown(cancellationToken); if (_scheduler is not null)
{
await _scheduler.Shutdown(cancellationToken);
}
} }
public virtual void Dispose() public virtual void Dispose()

View File

@ -4,6 +4,8 @@ using Quartz.Spi;
namespace Bit.Core.Jobs; namespace Bit.Core.Jobs;
#nullable enable
public class JobFactory : IJobFactory public class JobFactory : IJobFactory
{ {
private readonly IServiceProvider _container; private readonly IServiceProvider _container;
@ -16,7 +18,7 @@ public class JobFactory : IJobFactory
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{ {
var scope = _container.CreateScope(); 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) public void ReturnJob(IJob job)

View File

@ -3,6 +3,8 @@ using Quartz;
namespace Bit.Core.Jobs; namespace Bit.Core.Jobs;
#nullable enable
public class JobListener : IJobListener public class JobListener : IJobListener
{ {
private readonly ILogger<JobListener> _logger; private readonly ILogger<JobListener> _logger;
@ -28,7 +30,7 @@ public class JobListener : IJobListener
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException,
CancellationToken cancellationToken = default(CancellationToken)) CancellationToken cancellationToken = default(CancellationToken))
{ {
_logger.LogInformation(Constants.BypassFiltersEventId, null, "Finished job {0} at {1}.", _logger.LogInformation(Constants.BypassFiltersEventId, null, "Finished job {0} at {1}.",

View File

@ -2,6 +2,8 @@
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public interface INotificationHubProxy public interface INotificationHubProxy
{ {
Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression); Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression);

View File

@ -2,6 +2,8 @@
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public interface INotificationHubPool public interface INotificationHubPool
{ {
NotificationHubConnection ConnectionFor(Guid comb); NotificationHubConnection ConnectionFor(Guid comb);

View File

@ -2,6 +2,8 @@
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public class NotificationHubClientProxy : INotificationHubProxy public class NotificationHubClientProxy : INotificationHubProxy
{ {
private readonly IEnumerable<INotificationHubClient> _clients; private readonly IEnumerable<INotificationHubClient> _clients;

View File

@ -1,4 +1,5 @@
using System.Security.Cryptography; using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Web; using System.Web;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -7,16 +8,18 @@ using Microsoft.Azure.NotificationHubs;
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public class NotificationHubConnection public class NotificationHubConnection
{ {
public string HubName { get; init; } public string? HubName { get; init; }
public string ConnectionString { get; init; } public string? ConnectionString { get; init; }
private Lazy<NotificationHubConnectionStringBuilder> _parsedConnectionString; private Lazy<NotificationHubConnectionStringBuilder> _parsedConnectionString;
public Uri Endpoint => _parsedConnectionString.Value.Endpoint; public Uri Endpoint => _parsedConnectionString.Value.Endpoint;
private string SasKey => _parsedConnectionString.Value.SharedAccessKey; private string SasKey => _parsedConnectionString.Value.SharedAccessKey;
private string SasKeyName => _parsedConnectionString.Value.SharedAccessKeyName; private string SasKeyName => _parsedConnectionString.Value.SharedAccessKeyName;
public bool EnableSendTracing { get; init; } public bool EnableSendTracing { get; init; }
private NotificationHubClient _hubClient; private NotificationHubClient? _hubClient;
/// <summary> /// <summary>
/// Gets the NotificationHubClient for this connection. /// Gets the NotificationHubClient for this connection.
/// ///
@ -155,9 +158,10 @@ public class NotificationHubConnection
}; };
} }
[MemberNotNull(nameof(_hubClient))]
private NotificationHubConnection Init() private NotificationHubConnection Init()
{ {
HubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing); _hubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing);
return this; return this;
} }

View File

@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging;
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public class NotificationHubPool : INotificationHubPool public class NotificationHubPool : INotificationHubPool
{ {
private List<NotificationHubConnection> _connections { get; } private List<NotificationHubConnection> _connections { get; }

View File

@ -19,6 +19,8 @@ using Notification = Bit.Core.NotificationCenter.Entities.Notification;
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
/// <summary> /// <summary>
/// Sends mobile push notifications to the Azure Notification Hub. /// Sends mobile push notifications to the Azure Notification Hub.
/// Used by Cloud-Hosted environments. /// Used by Cloud-Hosted environments.

View File

@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public class NotificationHubPushRegistrationService : IPushRegistrationService public class NotificationHubPushRegistrationService : IPushRegistrationService
{ {
private static readonly JsonSerializerOptions webPushSerializationOptions = new() private static readonly JsonSerializerOptions webPushSerializationOptions = new()
@ -37,7 +39,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId, public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId,
string identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId) string? identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId)
{ {
var orgIds = organizationIds.ToList(); var orgIds = organizationIds.ToList();
var clientType = DeviceTypes.ToClientType(type); var clientType = DeviceTypes.ToClientType(type);
@ -79,7 +81,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId, private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId,
string identifier, ClientType clientType, List<string> organizationIds, DeviceType type, Guid installationId) string? identifier, ClientType clientType, List<string> organizationIds, DeviceType type, Guid installationId)
{ {
if (string.IsNullOrWhiteSpace(installation.PushChannel)) 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, private async Task CreateOrUpdateWebRegistrationAsync(string endpoint, string p256dh, string auth, Installation installation, string userId,
string identifier, ClientType clientType, List<string> organizationIds, Guid installationId) string? identifier, ClientType clientType, List<string> organizationIds, Guid installationId)
{ {
// The Azure SDK is currently lacking support for web push registrations. // The Azure SDK is currently lacking support for web push registrations.
// We need to use the REST API directly. // We need to use the REST API directly.
@ -187,7 +189,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
private static KeyValuePair<string, InstallationTemplate> BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody, private static KeyValuePair<string, InstallationTemplate> BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody,
string userId, string identifier, ClientType clientType, List<string> organizationIds, Guid installationId) string userId, string? identifier, ClientType clientType, List<string> organizationIds, Guid installationId)
{ {
var fullTemplateId = $"template:{templateId}"; var fullTemplateId = $"template:{templateId}";

View File

@ -1,5 +1,7 @@
namespace Bit.Core.NotificationHub; namespace Bit.Core.NotificationHub;
#nullable enable
public record struct WebPushRegistrationData public record struct WebPushRegistrationData
{ {
public string Endpoint { get; init; } public string Endpoint { get; init; }
@ -9,9 +11,9 @@ public record struct WebPushRegistrationData
public record class PushRegistrationData public record class PushRegistrationData
{ {
public string Token { get; set; } public string? Token { get; set; }
public WebPushRegistrationData? WebPush { get; set; } public WebPushRegistrationData? WebPush { get; set; }
public PushRegistrationData(string token) public PushRegistrationData(string? token)
{ {
Token = token; Token = token;
} }

View File

@ -175,7 +175,7 @@ public class AccountsKeyManagementControllerTests
} }
catch (BadRequestException ex) catch (BadRequestException ex)
{ {
Assert.NotEmpty(ex.ModelState.Values); Assert.NotEmpty(ex.ModelState!.Values);
} }
} }
@ -210,7 +210,7 @@ public class AccountsKeyManagementControllerTests
var badRequestException = var badRequestException =
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data)); await Assert.ThrowsAsync<BadRequestException>(() => 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); Assert.Equal("set key connector key error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
await sutProvider.GetDependency<IUserService>().Received(1) await sutProvider.GetDependency<IUserService>().Received(1)
.SetKeyConnectorKeyAsync(Arg.Do<User>(user => .SetKeyConnectorKeyAsync(Arg.Do<User>(user =>
@ -284,7 +284,7 @@ public class AccountsKeyManagementControllerTests
var badRequestException = var badRequestException =
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync()); await Assert.ThrowsAsync<BadRequestException>(() => 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); Assert.Equal("convert to key connector error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
await sutProvider.GetDependency<IUserService>().Received(1) await sutProvider.GetDependency<IUserService>().Received(1)
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser)); .ConvertToKeyConnectorAsync(Arg.Is(expectedUser));