mirror of
https://github.com/bitwarden/server.git
synced 2025-04-04 12:40:22 -05:00
Partial<T> for CommandResult<T> (#5482)
* Example of how a partial success/failure command result would look. * Fixed code. * Added Validator and ValidationResult * Moved errors into their own files. * Fixing tests * fixed import. * Forgot mock error.
This commit is contained in:
parent
7daf6cfad4
commit
488a9847ea
@ -12,7 +12,7 @@ public static class CommandResultExtensions
|
||||
NoRecordFoundFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
||||
BadRequestFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||
Failure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||
Success<T> success => new ObjectResult(success.Data) { StatusCode = StatusCodes.Status200OK },
|
||||
Success<T> success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK },
|
||||
_ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
||||
};
|
||||
}
|
||||
|
3
src/Core/AdminConsole/Errors/Error.cs
Normal file
3
src/Core/AdminConsole/Errors/Error.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record Error<T>(string Message, T ErroredValue);
|
11
src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs
Normal file
11
src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record InsufficientPermissionsError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
||||
{
|
||||
public const string Code = "Insufficient Permissions";
|
||||
|
||||
public InsufficientPermissionsError(T ErroredValue) : this(Code, ErroredValue)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
11
src/Core/AdminConsole/Errors/RecordNotFoundError.cs
Normal file
11
src/Core/AdminConsole/Errors/RecordNotFoundError.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.AdminConsole.Errors;
|
||||
|
||||
public record RecordNotFoundError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
||||
{
|
||||
public const string Code = "Record Not Found";
|
||||
|
||||
public RecordNotFoundError(T ErroredValue) : this(Code, ErroredValue)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
6
src/Core/AdminConsole/Shared/Validation/IValidator.cs
Normal file
6
src/Core/AdminConsole/Shared/Validation/IValidator.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.Shared.Validation;
|
||||
|
||||
public interface IValidator<T>
|
||||
{
|
||||
public Task<ValidationResult<T>> ValidateAsync(T value);
|
||||
}
|
15
src/Core/AdminConsole/Shared/Validation/ValidationResult.cs
Normal file
15
src/Core/AdminConsole/Shared/Validation/ValidationResult.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Shared.Validation;
|
||||
|
||||
public abstract record ValidationResult<T>;
|
||||
|
||||
public record Valid<T> : ValidationResult<T>
|
||||
{
|
||||
public T Value { get; init; }
|
||||
}
|
||||
|
||||
public record Invalid<T> : ValidationResult<T>
|
||||
{
|
||||
public IEnumerable<Error<T>> Errors { get; init; }
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
|
||||
namespace Bit.Core.Models.Commands;
|
||||
|
||||
public class CommandResult(IEnumerable<string> errors)
|
||||
@ -9,7 +11,6 @@ public class CommandResult(IEnumerable<string> errors)
|
||||
public bool Success => ErrorMessages.Count == 0;
|
||||
public bool HasErrors => ErrorMessages.Count > 0;
|
||||
public List<string> ErrorMessages { get; } = errors.ToList();
|
||||
|
||||
public CommandResult() : this(Array.Empty<string>()) { }
|
||||
}
|
||||
|
||||
@ -29,22 +30,30 @@ public class Success : CommandResult
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class CommandResult<T>
|
||||
{
|
||||
public abstract class CommandResult<T>;
|
||||
|
||||
public class Success<T>(T value) : CommandResult<T>
|
||||
{
|
||||
public T Value { get; } = value;
|
||||
}
|
||||
|
||||
public class Success<T>(T data) : CommandResult<T>
|
||||
public class Failure<T>(IEnumerable<string> errorMessages) : CommandResult<T>
|
||||
{
|
||||
public T? Data { get; init; } = data;
|
||||
public List<string> ErrorMessages { get; } = errorMessages.ToList();
|
||||
|
||||
public string ErrorMessage => string.Join(" ", ErrorMessages);
|
||||
|
||||
public Failure(string error) : this([error]) { }
|
||||
}
|
||||
|
||||
public class Failure<T>(IEnumerable<string> errorMessage) : CommandResult<T>
|
||||
public class Partial<T> : CommandResult<T>
|
||||
{
|
||||
public IEnumerable<string> ErrorMessages { get; init; } = errorMessage;
|
||||
public T[] Successes { get; set; } = [];
|
||||
public Error<T>[] Failures { get; set; } = [];
|
||||
|
||||
public Failure(string errorMessage) : this(new[] { errorMessage })
|
||||
public Partial(IEnumerable<T> successfulItems, IEnumerable<Error<T>> failedItems)
|
||||
{
|
||||
Successes = successfulItems.ToArray();
|
||||
Failures = failedItems.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
58
test/Core.Test/AdminConsole/Shared/IValidatorTests.cs
Normal file
58
test/Core.Test/AdminConsole/Shared/IValidatorTests.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.AdminConsole.Shared.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Shared;
|
||||
|
||||
public class IValidatorTests
|
||||
{
|
||||
public class TestClass
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public record InvalidRequestError<T>(T ErroredValue) : Error<T>(Code, ErroredValue)
|
||||
{
|
||||
public const string Code = "InvalidRequest";
|
||||
}
|
||||
|
||||
public class TestClassValidator : IValidator<TestClass>
|
||||
{
|
||||
public Task<ValidationResult<TestClass>> ValidateAsync(TestClass value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value.Name))
|
||||
{
|
||||
return Task.FromResult<ValidationResult<TestClass>>(new Invalid<TestClass>
|
||||
{
|
||||
Errors = [new InvalidRequestError<TestClass>(value)]
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult<ValidationResult<TestClass>>(new Valid<TestClass> { Value = value });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenSomethingIsInvalid_ReturnsInvalidWithError()
|
||||
{
|
||||
var example = new TestClass();
|
||||
|
||||
var result = await new TestClassValidator().ValidateAsync(example);
|
||||
|
||||
Assert.IsType<Invalid<TestClass>>(result);
|
||||
var invalidResult = result as Invalid<TestClass>;
|
||||
Assert.Equal(InvalidRequestError<TestClass>.Code, invalidResult.Errors.First().Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_WhenIsValid_ReturnsValid()
|
||||
{
|
||||
var example = new TestClass { Name = "Valid" };
|
||||
|
||||
var result = await new TestClassValidator().ValidateAsync(example);
|
||||
|
||||
Assert.IsType<Valid<TestClass>>(result);
|
||||
var validResult = result as Valid<TestClass>;
|
||||
Assert.Equal(example.Name, validResult.Value.Name);
|
||||
}
|
||||
}
|
53
test/Core.Test/Models/Commands/CommandResultTests.cs
Normal file
53
test/Core.Test/Models/Commands/CommandResultTests.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Commands;
|
||||
|
||||
public class CommandResultTests
|
||||
{
|
||||
public class TestItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public CommandResult<TestItem> BulkAction(IEnumerable<TestItem> items)
|
||||
{
|
||||
var itemList = items.ToList();
|
||||
var successfulItems = items.Where(x => x.Value == "SuccessfulRequest").ToArray();
|
||||
|
||||
var failedItems = itemList.Except(successfulItems).ToArray();
|
||||
|
||||
var notFound = failedItems.First(x => x.Value == "Failed due to not found");
|
||||
var invalidPermissions = failedItems.First(x => x.Value == "Failed due to invalid permissions");
|
||||
|
||||
var notFoundError = new RecordNotFoundError<TestItem>(notFound);
|
||||
var insufficientPermissionsError = new InsufficientPermissionsError<TestItem>(invalidPermissions);
|
||||
|
||||
return new Partial<TestItem>(successfulItems.ToArray(), [notFoundError, insufficientPermissionsError]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Partial_CommandResult_BulkRequestWithSuccessAndFailures(Guid successId1, Guid failureId1, Guid failureId2)
|
||||
{
|
||||
var listOfRecords = new List<TestItem>
|
||||
{
|
||||
new TestItem() { Id = successId1, Value = "SuccessfulRequest" },
|
||||
new TestItem() { Id = failureId1, Value = "Failed due to not found" },
|
||||
new TestItem() { Id = failureId2, Value = "Failed due to invalid permissions" }
|
||||
};
|
||||
|
||||
var result = BulkAction(listOfRecords);
|
||||
|
||||
Assert.IsType<Partial<TestItem>>(result);
|
||||
|
||||
var failures = (result as Partial<TestItem>).Failures.ToArray();
|
||||
var success = (result as Partial<TestItem>).Successes.First();
|
||||
|
||||
Assert.Equal(listOfRecords.First(), success);
|
||||
Assert.Equal(2, failures.Length);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user