From 2656ccf31461cb9ca2a1c76e74046c35bbeb56d1 Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Tue, 1 Apr 2025 10:12:14 -0500 Subject: [PATCH] Created new errors and removed references in business code to ErrorMessages property. This aligns Invite User code to use Errors instead of ErrorMessages --- .../src/Scim/Users/PostUserCommand.cs | 5 ++- .../Errors/InvalidResultTypeError.cs | 6 +++ .../Errors/FailedToInviteUsersError.cs | 9 ++++ .../Errors/NoUsersToInviteError.cs | 9 ++++ .../InviteOrganizationUsersCommand.cs | 42 ++++++++++++------- .../Models/InviteOrganizationUsersResponse.cs | 17 +++++++- src/Core/Models/Commands/CommandResult.cs | 21 ++++++++++ .../InviteOrganizationUserCommandTests.cs | 10 +++-- 8 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 src/Core/AdminConsole/Errors/InvalidResultTypeError.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs diff --git a/bitwarden_license/src/Scim/Users/PostUserCommand.cs b/bitwarden_license/src/Scim/Users/PostUserCommand.cs index 288e2eae2b..822fcbb563 100644 --- a/bitwarden_license/src/Scim/Users/PostUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PostUserCommand.cs @@ -4,6 +4,7 @@ using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Enums; @@ -78,9 +79,9 @@ public class PostUserCommand( var invitedOrganizationUserId = result switch { Success success => success.Value.InvitedUser.Id, - Failure { ErrorMessage: InviteOrganizationUsersCommand.NoUsersToInvite } => (Guid?)null, + Failure failure when failure.Errors + .Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null, Failure failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors), - Failure failure when failure.ErrorMessages.Count != 0 => throw new BadRequestException(failure.ErrorMessage), _ => throw new InvalidOperationException() }; diff --git a/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs b/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs new file mode 100644 index 0000000000..67b5b634fb --- /dev/null +++ b/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.AdminConsole.Errors; + +public record InvalidResultTypeError(T Value) : Error(Code, Value) +{ + public const string Code = "Invalid result type."; +}; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs new file mode 100644 index 0000000000..810ef744c9 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs @@ -0,0 +1,9 @@ +using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; + +public record FailedToInviteUsersError(InviteOrganizationUsersResponse Response) : Error(Code, Response) +{ + public const string Code = "Failed to invite users"; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs new file mode 100644 index 0000000000..52697572e6 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs @@ -0,0 +1,9 @@ +using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; + +public record NoUsersToInviteError(InviteOrganizationUsersResponse Response) : Error(Code, Response) +{ + public const string Code = "No users to invite"; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 0ca9e66f51..9cffc20d4d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -1,13 +1,14 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.Interfaces; using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Shared.Validation; using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.Commands; @@ -40,9 +41,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, { public const string IssueNotifyingOwnersOfSeatLimitReached = "Error encountered notifying organization owners of seat limit reached."; - public const string FailedToInviteUsers = "Failed to invite user(s)."; - public const string NoUsersToInvite = "No users to invite."; - public const string InvalidResultType = "Invalid result type."; public async Task> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request) { @@ -50,11 +48,16 @@ public class InviteOrganizationUsersCommand(IEventService eventService, switch (result) { - case Failure> failure: - return new Failure(failure.ErrorMessage); + case Failure failure: + return new Failure( + failure.Errors.Select(error => new Error(error.Message, + new ScimInviteOrganizationUsersResponse + { + InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault() + }))); - case Success> success when success.Value.Any(): - var user = success.Value.First(); + case Success success when success.Value.InvitedUsers.Any(): + var user = success.Value.InvitedUsers.First(); await eventService.LogOrganizationUserEventAsync( organizationUser: user, @@ -68,17 +71,20 @@ public class InviteOrganizationUsersCommand(IEventService eventService, }); default: - return new Failure(InvalidResultType); + return new Failure( + new InvalidResultTypeError( + new ScimInviteOrganizationUsersResponse())); } } - private async Task>> InviteOrganizationUsersAsync(InviteOrganizationUsersRequest request) + private async Task> InviteOrganizationUsersAsync(InviteOrganizationUsersRequest request) { var invitesToSend = (await FilterExistingUsersAsync(request)).ToArray(); if (invitesToSend.Length == 0) { - return new Failure>(NoUsersToInvite); + return new Failure(new NoUsersToInviteError( + new InviteOrganizationUsersResponse(request.InviteOrganization.OrganizationId))); } var validationResult = await inviteUsersValidator.ValidateAsync(new InviteUserOrganizationValidationRequest @@ -93,7 +99,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService, if (validationResult is Invalid invalid) { - return new Failure>(invalid.ErrorMessageString); + return invalid.MapToFailure(r => new InviteOrganizationUsersResponse(r)); } var validatedRequest = validationResult as Valid; @@ -102,7 +108,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService, .Select(x => x.MapToDataModel(request.PerformedAt, validatedRequest!.Value.InviteOrganization)) .ToArray(); - var organization = await organizationRepository.GetByIdAsync(validatedRequest.Value.InviteOrganization.OrganizationId); + var organization = await organizationRepository.GetByIdAsync(validatedRequest!.Value.InviteOrganization.OrganizationId); try { @@ -120,7 +126,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService, } catch (Exception ex) { - logger.LogError(ex, FailedToInviteUsers); + logger.LogError(ex, FailedToInviteUsersError.Code); await organizationUserRepository.DeleteManyAsync(organizationUserToInviteEntities.Select(x => x.OrganizationUser.Id)); @@ -129,10 +135,14 @@ public class InviteOrganizationUsersCommand(IEventService eventService, await RevertPasswordManagerChangesAsync(validatedRequest, organization); - return new Failure>(FailedToInviteUsers); + return new Failure(new FailedToInviteUsersError( + new InviteOrganizationUsersResponse(validatedRequest.Value))); } - return new Success>(organizationUserToInviteEntities.Select(x => x.OrganizationUser)); + return new Success( + new InviteOrganizationUsersResponse( + invitedOrganizationUsers: organizationUserToInviteEntities.Select(x => x.OrganizationUser).ToArray(), + organizationId: organization!.Id)); } private async Task> FilterExistingUsersAsync(InviteOrganizationUsersRequest request) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs index 23d25b6c59..cc0bd23b2a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs @@ -2,9 +2,22 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; -public class InviteOrganizationUsersResponse +public class InviteOrganizationUsersResponse(Guid organizationId) { - public IEnumerable InvitedUsers { get; set; } = []; + public IEnumerable InvitedUsers { get; } = []; + public Guid OrganizationId { get; } = organizationId; + + public InviteOrganizationUsersResponse(InviteUserOrganizationValidationRequest validationRequest) + : this(validationRequest.InviteOrganization.OrganizationId) + { + InvitedUsers = validationRequest.Invites.Select(x => new OrganizationUser { Email = x.Email }); + } + + public InviteOrganizationUsersResponse(IEnumerable invitedOrganizationUsers, Guid organizationId) + : this(organizationId) + { + InvitedUsers = invitedOrganizationUsers; + } } public class ScimInviteOrganizationUsersResponse diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs index 26aac9504b..4a9477067e 100644 --- a/src/Core/Models/Commands/CommandResult.cs +++ b/src/Core/Models/Commands/CommandResult.cs @@ -1,6 +1,7 @@ #nullable enable using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Shared.Validation; namespace Bit.Core.Models.Commands; @@ -48,6 +49,11 @@ public class Failure(IEnumerable errorMessages) : CommandResult { } + public Failure(IEnumerable> errors) : this(errors.Select(e => e.Message)) + { + Errors = errors.ToArray(); + } + public Failure(Error error) : this([error.Message]) { Errors = [error]; @@ -65,3 +71,18 @@ public class Partial : CommandResult Failures = failedItems.ToArray(); } } + +public static class CommandResultExtensions +{ + /// + /// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types. + /// + /// + /// This is the invalid type from validating the object. + /// This function will map between the two types for the inner ErrorT + /// Invalid object's type + /// Failure object's type + /// + public static CommandResult MapToFailure(this Invalid invalidResult, Func mappingFunction) => + new Failure(invalidResult.Errors.Select(errorA => errorA.ToError(mappingFunction(errorA.ErroredValue)))); +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index 42df39d92d..2b79bb7dd7 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; @@ -79,7 +80,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - Assert.Equal(InviteOrganizationUsersCommand.NoUsersToInvite, (result as Failure).ErrorMessage); + Assert.Equal(NoUsersToInviteError.Code, (result as Failure).ErrorMessage); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -186,6 +187,8 @@ public class InviteOrganizationUserCommandTests performedBy: Guid.Empty, timeProvider.GetUtcNow()); + var validationRequest = GetInviteValidationRequestMock(request, inviteOrganization, organization); + sutProvider.GetDependency() .SelectKnownEmailsAsync(organization.Id, Arg.Any>(), false) .Returns([]); @@ -196,7 +199,8 @@ public class InviteOrganizationUserCommandTests sutProvider.GetDependency() .ValidateAsync(Arg.Any()) - .Returns(new Invalid(new Error(errorMessage, new InviteUserOrganizationValidationRequest()))); + .Returns(new Invalid( + new Error(errorMessage, validationRequest))); // Act var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request); @@ -496,7 +500,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - Assert.Equal(InviteOrganizationUsersCommand.FailedToInviteUsers, (result as Failure)!.ErrorMessage); + Assert.Equal(FailedToInviteUsersError.Code, (result as Failure)!.ErrorMessage); // org user revert await orgUserRepository.Received(1).DeleteManyAsync(Arg.Is>(x => x.Count() == 1));