diff --git a/src/Api/Utilities/CommandResultExtensions.cs b/src/Api/Utilities/CommandResultExtensions.cs new file mode 100644 index 0000000000..39104db7ff --- /dev/null +++ b/src/Api/Utilities/CommandResultExtensions.cs @@ -0,0 +1,31 @@ +using Bit.Core.Models.Commands; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Utilities; + +public static class CommandResultExtensions +{ + public static IActionResult MapToActionResult(this CommandResult commandResult) + { + return commandResult switch + { + NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, + BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + Success success => new ObjectResult(success.Data) { StatusCode = StatusCodes.Status200OK }, + _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") + }; + } + + public static IActionResult MapToActionResult(this CommandResult commandResult) + { + return commandResult switch + { + NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, + BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + Success => new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK }, + _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") + }; + } +} diff --git a/src/Core/Models/Commands/BadRequestFailure.cs b/src/Core/Models/Commands/BadRequestFailure.cs new file mode 100644 index 0000000000..bd2753d4e4 --- /dev/null +++ b/src/Core/Models/Commands/BadRequestFailure.cs @@ -0,0 +1,23 @@ +namespace Bit.Core.Models.Commands; + +public class BadRequestFailure : Failure +{ + public BadRequestFailure(IEnumerable errorMessage) : base(errorMessage) + { + } + + public BadRequestFailure(string errorMessage) : base(errorMessage) + { + } +} + +public class BadRequestFailure : Failure +{ + public BadRequestFailure(IEnumerable errorMessage) : base(errorMessage) + { + } + + public BadRequestFailure(string errorMessage) : base(errorMessage) + { + } +} diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs index 9e5d91e09c..ae14b7d2f9 100644 --- a/src/Core/Models/Commands/CommandResult.cs +++ b/src/Core/Models/Commands/CommandResult.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.Models.Commands; +#nullable enable + +namespace Bit.Core.Models.Commands; public class CommandResult(IEnumerable errors) { @@ -10,3 +12,39 @@ public class CommandResult(IEnumerable errors) public CommandResult() : this(Array.Empty()) { } } + +public class Failure : CommandResult +{ + protected Failure(IEnumerable errorMessages) : base(errorMessages) + { + + } + public Failure(string errorMessage) : base(errorMessage) + { + + } +} + +public class Success : CommandResult +{ +} + +public abstract class CommandResult +{ + +} + +public class Success(T data) : CommandResult +{ + public T? Data { get; init; } = data; +} + +public class Failure(IEnumerable errorMessage) : CommandResult +{ + public IEnumerable ErrorMessages { get; init; } = errorMessage; + + public Failure(string errorMessage) : this(new[] { errorMessage }) + { + } +} + diff --git a/src/Core/Models/Commands/NoRecordFoundFailure.cs b/src/Core/Models/Commands/NoRecordFoundFailure.cs new file mode 100644 index 0000000000..a8a322b928 --- /dev/null +++ b/src/Core/Models/Commands/NoRecordFoundFailure.cs @@ -0,0 +1,24 @@ +namespace Bit.Core.Models.Commands; + +public class NoRecordFoundFailure : Failure +{ + public NoRecordFoundFailure(IEnumerable errorMessage) : base(errorMessage) + { + } + + public NoRecordFoundFailure(string errorMessage) : base(errorMessage) + { + } +} + +public class NoRecordFoundFailure : Failure +{ + public NoRecordFoundFailure(IEnumerable errorMessage) : base(errorMessage) + { + } + + public NoRecordFoundFailure(string errorMessage) : base(errorMessage) + { + } +} + diff --git a/test/Api.Test/Utilities/CommandResultExtensionTests.cs b/test/Api.Test/Utilities/CommandResultExtensionTests.cs new file mode 100644 index 0000000000..dafae10b5b --- /dev/null +++ b/test/Api.Test/Utilities/CommandResultExtensionTests.cs @@ -0,0 +1,107 @@ +using Bit.Api.Utilities; +using Bit.Core.Models.Commands; +using Bit.Core.Vault.Entities; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace Bit.Api.Test.Utilities; + +public class CommandResultExtensionTests +{ + public static IEnumerable WithGenericTypeTestCases() + { + yield return new object[] + { + new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }), + new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound } + }; + yield return new object[] + { + new BadRequestFailure("Error 3"), + new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest } + }; + yield return new object[] + { + new Failure("Error 4"), + new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest } + }; + var cipher = new Cipher() { Id = Guid.NewGuid() }; + + yield return new object[] + { + new Success(cipher), + new ObjectResult(cipher) { StatusCode = StatusCodes.Status200OK } + }; + } + + + [Theory] + [MemberData(nameof(WithGenericTypeTestCases))] + public void MapToActionResult_WithGenericType_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected) + { + var result = input.MapToActionResult(); + + Assert.Equivalent(expected, result); + } + + + [Fact] + public void MapToActionResult_WithGenericType_ShouldThrowExceptionForUnhandledCommandResult() + { + var result = new NotImplementedCommandResult(); + + Assert.Throws(() => result.MapToActionResult()); + } + + public static IEnumerable TestCases() + { + yield return new object[] + { + new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }), + new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound } + }; + yield return new object[] + { + new BadRequestFailure("Error 3"), + new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest } + }; + yield return new object[] + { + new Failure("Error 4"), + new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest } + }; + yield return new object[] + { + new Success(), + new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK } + }; + } + + [Theory] + [MemberData(nameof(TestCases))] + public void MapToActionResult_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected) + { + var result = input.MapToActionResult(); + + Assert.Equivalent(expected, result); + } + + [Fact] + public void MapToActionResult_ShouldThrowExceptionForUnhandledCommandResult() + { + var result = new NotImplementedCommandResult(); + + Assert.Throws(() => result.MapToActionResult()); + } +} + +public class NotImplementedCommandResult : CommandResult +{ + +} + +public class NotImplementedCommandResult : CommandResult +{ + +}