From 43f0b4c77cba95c0a1e196e23e10eb8172a19aae Mon Sep 17 00:00:00 2001 From: Ahmad Hamdan Date: Sat, 25 May 2024 23:41:08 +0300 Subject: [PATCH] Add error messages to forbidden and unauthorized (#190) --- .../MinimalApiResultExtensions.cs | 48 ++++++++++++++++++- src/Ardalis.Result/Result.Void.cs | 24 +++++++++- src/Ardalis.Result/Result.cs | 24 +++++++++- src/Ardalis.Result/ResultExtensions.cs | 8 +++- .../ResultConstructor.cs | 28 +++++++++++ tests/Ardalis.Result.UnitTests/ResultMap.cs | 24 ++++++++++ .../ResultVoidConstructor.cs | 24 +++++++++- 7 files changed, 173 insertions(+), 7 deletions(-) diff --git a/src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs b/src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs index 3988a2f..29df941 100644 --- a/src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs +++ b/src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using System.Text; + +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -36,8 +38,8 @@ internal static Microsoft.AspNetCore.Http.IResult ToMinimalApiResult(this IResul ResultStatus.Ok => result is Result ? Results.Ok() : Results.Ok(result.GetValue()), ResultStatus.Created => Results.Created("", result.GetValue()), ResultStatus.NotFound => NotFoundEntity(result), - ResultStatus.Unauthorized => Results.Unauthorized(), - ResultStatus.Forbidden => Results.Forbid(), + ResultStatus.Unauthorized => UnAuthorized(result), + ResultStatus.Forbidden => Forbidden(result), ResultStatus.Invalid => Results.BadRequest(result.ValidationErrors), ResultStatus.Error => UnprocessableEntity(result), ResultStatus.Conflict => ConflictEntity(result), @@ -140,5 +142,47 @@ private static Microsoft.AspNetCore.Http.IResult UnavailableEntity(IResult resul return Results.StatusCode(StatusCodes.Status503ServiceUnavailable); } } + + private static Microsoft.AspNetCore.Http.IResult Forbidden(IResult result) + { + var details = new StringBuilder("Next error(s) occurred:"); + + if (result.Errors.Any()) + { + foreach (var error in result.Errors) details.Append("* ").Append(error).AppendLine(); + + return Results.Problem(new ProblemDetails + { + Title = "Forbidden.", + Detail = details.ToString(), + Status = StatusCodes.Status403Forbidden + }); + } + else + { + return Results.Forbid(); + } + } + + private static Microsoft.AspNetCore.Http.IResult UnAuthorized(IResult result) + { + var details = new StringBuilder("Next error(s) occurred:"); + + if (result.Errors.Any()) + { + foreach (var error in result.Errors) details.Append("* ").Append(error).AppendLine(); + + return Results.Problem(new ProblemDetails + { + Title = "Unauthorized.", + Detail = details.ToString(), + Status = StatusCodes.Status401Unauthorized + }); + } + else + { + return Results.Unauthorized(); + } + } } #endif diff --git a/src/Ardalis.Result/Result.Void.cs b/src/Ardalis.Result/Result.Void.cs index 8486536..b9ba543 100644 --- a/src/Ardalis.Result/Result.Void.cs +++ b/src/Ardalis.Result/Result.Void.cs @@ -136,6 +136,17 @@ public static Result Error(string errorMessage) return new Result(ResultStatus.Forbidden); } + /// + /// The parameters to the call were correct, but the user does not have permission to perform some action. + /// See also HTTP 403 Forbidden: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors + /// + /// A list of string error messages. + /// A Result + public new static Result Forbidden(params string[] errorMessages) + { + return new Result(ResultStatus.Forbidden) { Errors = errorMessages }; + } + /// /// This is similar to Forbidden, but should be used when the user has not authenticated or has attempted to authenticate but failed. /// See also HTTP 401 Unauthorized: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors @@ -145,7 +156,18 @@ public static Result Error(string errorMessage) { return new Result(ResultStatus.Unauthorized); } - + + /// + /// This is similar to Forbidden, but should be used when the user has not authenticated or has attempted to authenticate but failed. + /// See also HTTP 401 Unauthorized: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors + /// + /// A list of string error messages. + /// A Result + public new static Result Unauthorized(params string[] errorMessages) + { + return new Result(ResultStatus.Unauthorized) { Errors = errorMessages }; + } + /// /// Represents a situation where a service is in conflict due to the current state of a resource, /// such as an edit conflict between multiple concurrent updates. diff --git a/src/Ardalis.Result/Result.cs b/src/Ardalis.Result/Result.cs index 9176e5b..79b2569 100644 --- a/src/Ardalis.Result/Result.cs +++ b/src/Ardalis.Result/Result.cs @@ -217,6 +217,17 @@ public static Result Forbidden() return new Result(ResultStatus.Forbidden); } + /// + /// The parameters to the call were correct, but the user does not have permission to perform some action. + /// See also HTTP 403 Forbidden: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors + /// + /// A list of string error messages. + /// A Result + public static Result Forbidden(params string[] errorMessages) + { + return new Result(ResultStatus.Forbidden) { Errors = errorMessages }; + } + /// /// This is similar to Forbidden, but should be used when the user has not authenticated or has attempted to authenticate but failed. /// See also HTTP 401 Unauthorized: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors @@ -226,7 +237,18 @@ public static Result Unauthorized() { return new Result(ResultStatus.Unauthorized); } - + + /// + /// This is similar to Forbidden, but should be used when the user has not authenticated or has attempted to authenticate but failed. + /// See also HTTP 401 Unauthorized: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors + /// + /// A list of string error messages. + /// A Result + public static Result Unauthorized(params string[] errorMessages) + { + return new Result(ResultStatus.Unauthorized) { Errors = errorMessages }; + } + /// /// Represents a situation where a service is in conflict due to the current state of a resource, /// such as an edit conflict between multiple concurrent updates. diff --git a/src/Ardalis.Result/ResultExtensions.cs b/src/Ardalis.Result/ResultExtensions.cs index ad96f5f..5efca42 100644 --- a/src/Ardalis.Result/ResultExtensions.cs +++ b/src/Ardalis.Result/ResultExtensions.cs @@ -25,8 +25,12 @@ public static Result Map(this Result.NotFound(result.Errors.ToArray()) : Result.NotFound(); - case ResultStatus.Unauthorized: return Result.Unauthorized(); - case ResultStatus.Forbidden: return Result.Forbidden(); + case ResultStatus.Unauthorized: return result.Errors.Any() + ? Result.Unauthorized(result.Errors.ToArray()) + : Result.Unauthorized(); + case ResultStatus.Forbidden: return result.Errors.Any() + ? Result.Forbidden(result.Errors.ToArray()) + : Result.Forbidden(); case ResultStatus.Invalid: return Result.Invalid(result.ValidationErrors); case ResultStatus.Error: return Result.Error(new ErrorList(result.Errors.ToArray(), result.CorrelationId)); case ResultStatus.Conflict: return result.Errors.Any() diff --git a/tests/Ardalis.Result.UnitTests/ResultConstructor.cs b/tests/Ardalis.Result.UnitTests/ResultConstructor.cs index 8f4fde1..44deaf6 100644 --- a/tests/Ardalis.Result.UnitTests/ResultConstructor.cs +++ b/tests/Ardalis.Result.UnitTests/ResultConstructor.cs @@ -235,6 +235,34 @@ public void InitializesStatusToForbiddenGivenForbiddenFactoryCall() Assert.Equal(ResultStatus.Forbidden, result.Status); } + [Fact] + public void InitializesStatusToForbiddenGivenForbiddenFactoryCallWithString() + { + var errorMessage = "You are forbidden"; + var result = Result.Forbidden(errorMessage); + + result.Status.Should().Be(ResultStatus.Forbidden); + result.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public void InitializesStatusToUnauthorizedGivenUnauthorizedFactoryCall() + { + var result = Result.Unauthorized(); + + Assert.Equal(ResultStatus.Unauthorized, result.Status); + } + + [Fact] + public void InitializesStatusToUnauthorizedGivenUnauthorizedFactoryCallWithString() + { + var errorMessage = "You are unauthorized"; + var result = Result.Unauthorized(errorMessage); + + result.Status.Should().Be(ResultStatus.Unauthorized); + result.Errors.Single().Should().Be(errorMessage); + } + [Fact] public void InitializesStatusToUnavailableGivenUnavailableFactoryCallWithString() { diff --git a/tests/Ardalis.Result.UnitTests/ResultMap.cs b/tests/Ardalis.Result.UnitTests/ResultMap.cs index d425688..9877bd4 100644 --- a/tests/Ardalis.Result.UnitTests/ResultMap.cs +++ b/tests/Ardalis.Result.UnitTests/ResultMap.cs @@ -222,6 +222,30 @@ public void ShouldProduceCriticalErrorWithError() actual.Errors.Single().Should().Be(expectedMessage); } + [Fact] + public void ShouldProduceForbiddenWithError() + { + string expectedMessage = "You are forbidden"; + var result = Result.Forbidden(expectedMessage); + + var actual = result.Map(val => val.ToString()); + + actual.Status.Should().Be(ResultStatus.Forbidden); + actual.Errors.Single().Should().Be(expectedMessage); + } + + [Fact] + public void ShouldProduceUnauthroizedWithError() + { + string expectedMessage = "You are unauthorized"; + var result = Result.Unauthorized(expectedMessage); + + var actual = result.Map(val => val.ToString()); + + actual.Status.Should().Be(ResultStatus.Unauthorized); + actual.Errors.Single().Should().Be(expectedMessage); + } + [Fact] public void ShouldProductNoContentWithoutAnyContent() { diff --git a/tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs b/tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs index 5ca96d9..4308559 100644 --- a/tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs +++ b/tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs @@ -143,6 +143,17 @@ public void InitializesForbiddenResultWithFactoryMethod() Assert.Equal(ResultStatus.Forbidden, result.Status); } + [Fact] + public void InitializesStatusToForbiddenGivenForbiddenFactoryCallWithString() + { + var errorMessage = "You are forbidden"; + var result = Result.Forbidden(errorMessage); + + Assert.Null(result.Value); + result.Status.Should().Be(ResultStatus.Forbidden); + result.Errors.Single().Should().Be(errorMessage); + } + [Fact] public void InitializesUnauthorizedResultWithFactoryMethod() { @@ -151,7 +162,18 @@ public void InitializesUnauthorizedResultWithFactoryMethod() Assert.Null(result.Value); Assert.Equal(ResultStatus.Unauthorized, result.Status); } - + + [Fact] + public void InitializesUnauthorizedResultWithFactoryMethodWithString() + { + var errorMessage = "You are unauthorized"; + var result = Result.Unauthorized(errorMessage); + + result.Value.Should().BeNull(); + result.Status.Should().Be(ResultStatus.Unauthorized); + result.Errors.Single().Should().Be(errorMessage); + } + [Fact] public void InitializesConflictResultWithFactoryMethod() {