mirror of
https://github.com/bitwarden/server
synced 2025-12-14 15:23:42 +00:00
[PM-15621] Add functionality to map command results to HTTP responses. (#5467)
This commit is contained in:
31
src/Api/Utilities/CommandResultExtensions.cs
Normal file
31
src/Api/Utilities/CommandResultExtensions.cs
Normal file
@@ -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<T>(this CommandResult<T> commandResult)
|
||||||
|
{
|
||||||
|
return commandResult switch
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
_ => 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}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Core/Models/Commands/BadRequestFailure.cs
Normal file
23
src/Core/Models/Commands/BadRequestFailure.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace Bit.Core.Models.Commands;
|
||||||
|
|
||||||
|
public class BadRequestFailure<T> : Failure<T>
|
||||||
|
{
|
||||||
|
public BadRequestFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BadRequestFailure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BadRequestFailure : Failure
|
||||||
|
{
|
||||||
|
public BadRequestFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BadRequestFailure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Bit.Core.Models.Commands;
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Commands;
|
||||||
|
|
||||||
public class CommandResult(IEnumerable<string> errors)
|
public class CommandResult(IEnumerable<string> errors)
|
||||||
{
|
{
|
||||||
@@ -10,3 +12,39 @@ public class CommandResult(IEnumerable<string> errors)
|
|||||||
|
|
||||||
public CommandResult() : this(Array.Empty<string>()) { }
|
public CommandResult() : this(Array.Empty<string>()) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Failure : CommandResult
|
||||||
|
{
|
||||||
|
protected Failure(IEnumerable<string> errorMessages) : base(errorMessages)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public Failure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Success : CommandResult
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class CommandResult<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Success<T>(T data) : CommandResult<T>
|
||||||
|
{
|
||||||
|
public T? Data { get; init; } = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Failure<T>(IEnumerable<string> errorMessage) : CommandResult<T>
|
||||||
|
{
|
||||||
|
public IEnumerable<string> ErrorMessages { get; init; } = errorMessage;
|
||||||
|
|
||||||
|
public Failure(string errorMessage) : this(new[] { errorMessage })
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
24
src/Core/Models/Commands/NoRecordFoundFailure.cs
Normal file
24
src/Core/Models/Commands/NoRecordFoundFailure.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace Bit.Core.Models.Commands;
|
||||||
|
|
||||||
|
public class NoRecordFoundFailure<T> : Failure<T>
|
||||||
|
{
|
||||||
|
public NoRecordFoundFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoRecordFoundFailure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NoRecordFoundFailure : Failure
|
||||||
|
{
|
||||||
|
public NoRecordFoundFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoRecordFoundFailure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
107
test/Api.Test/Utilities/CommandResultExtensionTests.cs
Normal file
107
test/Api.Test/Utilities/CommandResultExtensionTests.cs
Normal file
@@ -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<object[]> WithGenericTypeTestCases()
|
||||||
|
{
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new NoRecordFoundFailure<Cipher>(new[] { "Error 1", "Error 2" }),
|
||||||
|
new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound }
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new BadRequestFailure<Cipher>("Error 3"),
|
||||||
|
new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest }
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new Failure<Cipher>("Error 4"),
|
||||||
|
new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest }
|
||||||
|
};
|
||||||
|
var cipher = new Cipher() { Id = Guid.NewGuid() };
|
||||||
|
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new Success<Cipher>(cipher),
|
||||||
|
new ObjectResult(cipher) { StatusCode = StatusCodes.Status200OK }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(WithGenericTypeTestCases))]
|
||||||
|
public void MapToActionResult_WithGenericType_ShouldMapToHttpResponse(CommandResult<Cipher> input, ObjectResult expected)
|
||||||
|
{
|
||||||
|
var result = input.MapToActionResult();
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapToActionResult_WithGenericType_ShouldThrowExceptionForUnhandledCommandResult()
|
||||||
|
{
|
||||||
|
var result = new NotImplementedCommandResult();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => result.MapToActionResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> 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<Cipher>();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => result.MapToActionResult());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotImplementedCommandResult<T> : CommandResult<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotImplementedCommandResult : CommandResult
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user