From 2ebaed8bc2ba0f19ff8b8f10d16a9abde9a79a52 Mon Sep 17 00:00:00 2001 From: Chris C Date: Fri, 18 Oct 2024 10:46:05 -0500 Subject: [PATCH] Add Bind in addition to Map to support Railway Oriented Programming (#215) * removed some overloads * Added configure await --------- Co-authored-by: Christopher Calmes --- src/Ardalis.Result/ResultExtensions.cs | 357 +++++++- tests/Ardalis.Result.UnitTests/ResultBind.cs | 381 +++++++++ .../ResultBindAsync.cs | 802 ++++++++++++++++++ tests/Ardalis.Result.UnitTests/ResultMap.cs | 3 +- .../ResultMapAsync.cs | 210 +++++ .../ResultVoidMapAsync.cs | 163 ++++ 6 files changed, 1882 insertions(+), 34 deletions(-) create mode 100644 tests/Ardalis.Result.UnitTests/ResultBind.cs create mode 100644 tests/Ardalis.Result.UnitTests/ResultBindAsync.cs create mode 100644 tests/Ardalis.Result.UnitTests/ResultMapAsync.cs create mode 100644 tests/Ardalis.Result.UnitTests/ResultVoidMapAsync.cs diff --git a/src/Ardalis.Result/ResultExtensions.cs b/src/Ardalis.Result/ResultExtensions.cs index 5efca42..c8fb2d1 100644 --- a/src/Ardalis.Result/ResultExtensions.cs +++ b/src/Ardalis.Result/ResultExtensions.cs @@ -1,47 +1,338 @@ using System; using System.Linq; +using System.Threading.Tasks; namespace Ardalis.Result { public static class ResultExtensions { /// - /// Transforms a Result's type from a source type to a destination type. If the Result is successful, the func parameter is invoked on the Result's source value to map it to a destination type. + /// Transforms a Result's type from a source type to a destination type. + /// If the Result is successful, the func parameter is invoked on the Result's source value to map it to a destination type. /// - /// - /// - /// - /// - /// - /// + /// The type of the value contained in the source Result. + /// The type of the value to be returned in the destination Result. + /// The source Result to transform. + /// A function to transform the source value to the destination type. + /// A Result containing the transformed value or the appropriate error status. + /// Thrown when the Result status is not supported. public static Result Map(this Result result, Func func) { - switch (result.Status) - { - case ResultStatus.Ok: return func(result); - case ResultStatus.Created: return string.IsNullOrEmpty(result.Location) - ? Result.Created(func(result.Value)) - : Result.Created(func(result.Value), result.Location); - case ResultStatus.NotFound: return result.Errors.Any() - ? Result.NotFound(result.Errors.ToArray()) - : Result.NotFound(); - 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() - ? Result.Conflict(result.Errors.ToArray()) - : Result.Conflict(); - case ResultStatus.CriticalError: return Result.CriticalError(result.Errors.ToArray()); - case ResultStatus.Unavailable: return Result.Unavailable(result.Errors.ToArray()); - case ResultStatus.NoContent: return Result.NoContent(); - default: - throw new NotSupportedException($"Result {result.Status} conversion is not supported."); - } + return result.Status switch + { + ResultStatus.Ok => (Result)func(result), + ResultStatus.Created => string.IsNullOrEmpty(result.Location) + ? Result.Created(func(result.Value)) + : Result.Created(func(result.Value), result.Location), + _ => HandleNonSuccessStatus(result), + }; + } + + public static Result Map(this Result result, Func func) + { + return result.Status switch + { + ResultStatus.Ok => Result.Success(func()), + ResultStatus.Created => string.IsNullOrEmpty(result.Location) + ? Result.Created(func()) + : Result.Created(func(), result.Location), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task> MapAsync( + this Result result, + Func> func) + { + return result.Status switch + { + ResultStatus.Ok => Result.Success(await func(result.Value)), + ResultStatus.Created => string.IsNullOrEmpty(result.Location) + ? Result.Created(await func(result.Value)) + : Result.Created(await func(result.Value), result.Location), + _ => HandleNonSuccessStatus(result), + }; + } + + + public static async Task> MapAsync( + this Task> resultTask, + Func> func) + { + var result = await resultTask; + return result.Status switch + { + ResultStatus.Ok => Result.Success(await func(result.Value)), + ResultStatus.Created => string.IsNullOrEmpty(result.Location) + ? Result.Created(await func(result.Value)) + : Result.Created(await func(result.Value), result.Location), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task> MapAsync( + this Result result, + Func> func) + { + return result.Status switch + { + ResultStatus.Ok => Result.Success(await func()), + ResultStatus.Created => string.IsNullOrEmpty(result.Location) + ? Result.Created(await func()) + : Result.Created(await func(), result.Location), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task> MapAsync( + this Task resultTask, + Func> func) + { + var result = await resultTask; + return await result.MapAsync(func); + } + + + public static async Task> MapAsync( + this Task> resultTask, + Func func) + { + var result = await resultTask; + return result.Map(func); + } + + public static async Task> MapAsync( + this Task resultTask, + Func func) + { + var result = await resultTask; + return result.Map(func); + } + + /// + /// Transforms a Result's type from a source type to a destination type. + /// If the Result is successful, the func parameter is invoked on the Result's source value to map it to a destination type. + /// + /// The type of the value contained in the source Result. + /// The type of the value to be returned in the destination Result. + /// The source Result to transform. + /// A function to transform the source value to the destination type. + /// A Result containing the transformed value or the appropriate error status. + /// Thrown when the Result status is not supported. + public static Result Bind(this Result result, Func> bindFunc) + { + return result.Status switch + { + ResultStatus.Ok => bindFunc(result.Value), + ResultStatus.Created => bindFunc(result.Value), + _ => HandleNonSuccessStatus(result), + }; + } + + public static Result Bind(this Result result, Func> bindFunc) + { + return result.Status switch + { + ResultStatus.Ok => bindFunc(result.Value), + ResultStatus.Created => bindFunc(result.Value), + _ => HandleNonSuccessStatus(result), + }; + } + + public static Result Bind(this Result result, Func, Result> bindFunc) + { + return result.Status switch + { + ResultStatus.Ok => bindFunc(result.Value), + ResultStatus.Created => bindFunc(result.Value), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task> BindAsync( + this Task> resultTask, + Func>> bindFunc) + { + var result = await resultTask; + return result.Status switch + { + ResultStatus.Ok => await bindFunc(result.Value).ConfigureAwait(false), + ResultStatus.Created => await bindFunc(result.Value).ConfigureAwait(false), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task BindAsync( + this Task> resultTask, + Func> bindFunc) + { + var result = await resultTask; + return result.Status switch + { + ResultStatus.Ok => await bindFunc(result.Value).ConfigureAwait(false), + ResultStatus.Created => await bindFunc(result.Value).ConfigureAwait(false), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task BindAsync( + this Result result, + Func> bindFunc) + { + return result.Status switch + { + ResultStatus.Ok => await bindFunc(result.Value).ConfigureAwait(false), + ResultStatus.Created => await bindFunc(result.Value).ConfigureAwait(false), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task BindAsync( + this Result result, + Func> bindFunc) + { + return result.Status switch + { + ResultStatus.Ok => await bindFunc(result.Value).ConfigureAwait(false), + ResultStatus.Created => await bindFunc(result.Value).ConfigureAwait(false), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task> BindAsync( + this Task resultTask, + Func>> bindFunc) + { + var result = await resultTask; + return result.Status switch + { + ResultStatus.Ok => await bindFunc(result.Value).ConfigureAwait(false), + ResultStatus.Created => await bindFunc(result.Value).ConfigureAwait(false), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task> BindAsync( + this Result result, + Func>> bindFunc) + { + return result.Status switch + { + ResultStatus.Ok => await bindFunc(result.Value).ConfigureAwait(false), + ResultStatus.Created => await bindFunc(result.Value).ConfigureAwait(false), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task BindAsync( + this Result result, + Func> bindFunc) + { + return result.Status switch + { + ResultStatus.Ok => await bindFunc(result.Value).ConfigureAwait(false), + ResultStatus.Created => await bindFunc(result.Value).ConfigureAwait(false), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task BindAsync(this Task resultTask, Func> bindFunc) + { + var result = await resultTask; + return result.Status switch + { + ResultStatus.Ok => await bindFunc(result).ConfigureAwait(false), + ResultStatus.Created => await bindFunc(result).ConfigureAwait(false), + _ => HandleNonSuccessStatus(result), + }; + } + + public static async Task> BindAsync( + this Task> resultTask, + Func> bindFunc) + { + var result = await resultTask; + return result.Status switch + { + ResultStatus.Ok => bindFunc(result.Value), + ResultStatus.Created => bindFunc(result.Value), + _ => HandleNonSuccessStatus(result), + }; + } + + private static Result HandleNonSuccessStatus(Result result) + { + return result.Status switch + { + ResultStatus.NotFound => result.Errors.Any() + ? Result.NotFound(result.Errors.ToArray()) + : Result.NotFound(), + ResultStatus.Unauthorized => result.Errors.Any() + ? Result.Unauthorized(result.Errors.ToArray()) + : Result.Unauthorized(), + ResultStatus.Forbidden => result.Errors.Any() + ? Result.Forbidden(result.Errors.ToArray()) + : Result.Forbidden(), + ResultStatus.Invalid => Result.Invalid(result.ValidationErrors), + ResultStatus.Error => Result.Error(new ErrorList(result.Errors.ToArray(), result.CorrelationId)), + ResultStatus.Conflict => result.Errors.Any() + ? Result.Conflict(result.Errors.ToArray()) + : Result.Conflict(), + ResultStatus.CriticalError => Result.CriticalError(result.Errors.ToArray()), + ResultStatus.Unavailable => Result.Unavailable(result.Errors.ToArray()), + ResultStatus.NoContent => Result.NoContent(), + _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported."), + }; + } + + private static Result HandleNonSuccessStatus(Result result) + { + return result.Status switch + { + ResultStatus.NotFound => result.Errors.Any() + ? Result.NotFound(result.Errors.ToArray()) + : Result.NotFound(), + ResultStatus.Unauthorized => result.Errors.Any() + ? Result.Unauthorized(result.Errors.ToArray()) + : Result.Unauthorized(), + ResultStatus.Forbidden => result.Errors.Any() + ? Result.Forbidden(result.Errors.ToArray()) + : Result.Forbidden(), + ResultStatus.Invalid => Result.Invalid(result.ValidationErrors), + ResultStatus.Error => Result.Error(new ErrorList(result.Errors.ToArray(), result.CorrelationId)), + ResultStatus.Conflict => result.Errors.Any() + ? Result.Conflict(result.Errors.ToArray()) + : Result.Conflict(), + ResultStatus.CriticalError => Result.CriticalError(result.Errors.ToArray()), + ResultStatus.Unavailable => Result.Unavailable(result.Errors.ToArray()), + ResultStatus.NoContent => Result.NoContent(), + _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported."), + }; + } + + private static Result HandleNonSuccessStatus(Result result) + { + return result.Status switch + { + ResultStatus.NotFound => result.Errors.Any() + ? Result.NotFound(result.Errors.ToArray()) + : Result.NotFound(), + ResultStatus.Unauthorized => result.Errors.Any() + ? Result.Unauthorized(result.Errors.ToArray()) + : Result.Unauthorized(), + ResultStatus.Forbidden => result.Errors.Any() + ? Result.Forbidden(result.Errors.ToArray()) + : Result.Forbidden(), + ResultStatus.Invalid => Result.Invalid(result.ValidationErrors), + ResultStatus.Error => Result.Error(new ErrorList(result.Errors.ToArray(), result.CorrelationId)), + ResultStatus.Conflict => result.Errors.Any() + ? Result.Conflict(result.Errors.ToArray()) + : Result.Conflict(), + ResultStatus.CriticalError => Result.CriticalError(result.Errors.ToArray()), + ResultStatus.Unavailable => Result.Unavailable(result.Errors.ToArray()), + ResultStatus.NoContent => Result.NoContent(), + _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported."), + }; } } } diff --git a/tests/Ardalis.Result.UnitTests/ResultBind.cs b/tests/Ardalis.Result.UnitTests/ResultBind.cs new file mode 100644 index 0000000..febaab9 --- /dev/null +++ b/tests/Ardalis.Result.UnitTests/ResultBind.cs @@ -0,0 +1,381 @@ +using FluentAssertions; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Ardalis.Result.UnitTests +{ + public class ResultBind + { + [Fact] + public void ShouldProduceSuccessResultFromSuccess() + { + int successValue = 123; + var result = Result.Success(successValue); + var expected = Result.Success(successValue.ToString()); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void CanChainSeveralMethods() + { + var result = Result.Success(123); + var expected = Result.Success("125"); + + var actual = result.Bind(v => Result.Success(v + 1)) + .Bind(v => Result.Success(v + 1)) + .Bind(v => Result.Success(v.ToString())); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void CanChangeVoidResultToResult() + { + var result = Result.Success(); + var expected = Result.Success("Success"); + + var actual = result.Bind(_ => Result.Success("Success")); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void CanBindResultToVoidResult() + { + var result = Result.Success(123); + var expected = Result.Success(); + + var actual = result.Bind(_ => Result.Success()); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ShouldProduceComplexTypeResultFromSuccessAnonymousFunction() + { + var foo = new Foo("Bar"); + var result = Result.Success(foo); + var expected = Result.Success(new FooDto(foo.Bar)); + + var actual = result.Bind(foo => Result.Success(new FooDto(foo.Bar))); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ShouldProduceComplexTypeResultFromSuccessWithMethod() + { + var foo = new Foo("Bar"); + var result = Result.Success(foo); + var expected = Result.Success(FooDto.CreateFromFooResult(foo)); + + var actual = result.Bind(FooDto.CreateFromFooResult); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ShouldProduceCreatedResultFromCreated() + { + int createdValue = 123; + var result = Result.Created(createdValue); + var expected = Result.Created(createdValue.ToString()); + + var actual = result.Bind(val => Result.Created(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Created); + actual.Value.Should().Be(expected.Value); + } + + [Fact] + public void ShouldProduceComplexTypeResultFromCreatedAnonymously() + { + var foo = new Foo("Bar"); + var result = Result.Created(foo); + var expected = Result.Created(new FooDto(foo.Bar)); + + var actual = result.Bind(foo => Result.Created(new FooDto(foo.Bar))); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ShouldProduceComplexTypeResultFromCreatedWithMethod() + { + var foo = new Foo("Bar"); + var result = Result.Created(foo); + var expected = Result.Created(FooDto.CreateFromFooResult(foo)); + + var actual = result.Bind(FooDto.CreateFromFooCreatedResult); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ShouldProduceNotFound() + { + var result = Result.NotFound(); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.NotFound); + actual.Value.Should().BeNull(); + } + + [Fact] + public void ShouldProduceNotFoundWithError() + { + string expectedMessage = "Some integer not found"; + var result = Result.NotFound(expectedMessage); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.NotFound); + actual.Errors.Single().Should().Be(expectedMessage); + } + + [Fact] + public void ShouldProduceUnauthorized() + { + var result = Result.Unauthorized(); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Unauthorized); + } + + [Fact] + public void ShouldProduceForbidden() + { + var result = Result.Forbidden(); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Forbidden); + } + + [Fact] + public void ShouldProduceInvalidWithValidationErrors() + { + var validationErrors = new List + { + new() { ErrorMessage = "Validation Error 1" }, + new() { ErrorMessage = "Validation Error 2" } + }; + var result = Result.Invalid(validationErrors); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Invalid); + actual.ValidationErrors.Should().BeEquivalentTo(validationErrors); + } + + [Fact] + public void ShouldProduceInvalidWithoutValidationErrors() + { + var validationErrors = new List(); + var result = Result.Invalid(validationErrors); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Invalid); + actual.ValidationErrors.Should().BeEmpty(); + } + + [Fact] + public void ShouldProduceErrorResultWithErrors() + { + var errorList = new ErrorList(new[] { "Error 1", "Error 2" }, default); + var result = Result.Error(errorList); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Should().BeEquivalentTo(errorList.ErrorMessages); + } + + [Fact] + public void ShouldProduceErrorResultWithNoErrors() + { + var result = Result.Error(); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Should().BeEmpty(); + actual.CorrelationId.Should().BeEmpty(); + } + + [Fact] + public void ShouldProduceConflict() + { + var result = Result.Conflict(); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Conflict); + } + + [Fact] + public void ShouldProduceConflictWithError() + { + string expectedMessage = "Some conflict"; + var result = Result.Conflict(expectedMessage); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Conflict); + actual.Errors.Single().Should().Be(expectedMessage); + } + + [Fact] + public void ShouldProduceUnavailableWithError() + { + string expectedMessage = "Something unavailable"; + var result = Result.Unavailable(expectedMessage); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Unavailable); + actual.Errors.Single().Should().Be(expectedMessage); + } + + [Fact] + public void ShouldProduceCriticalErrorWithError() + { + string expectedMessage = "Some critical error"; + var result = Result.CriticalError(expectedMessage); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.CriticalError); + actual.Errors.Single().Should().Be(expectedMessage); + } + + [Fact] + public void ShouldProduceForbiddenWithError() + { + string expectedMessage = "You are forbidden"; + var result = Result.Forbidden(expectedMessage); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Forbidden); + actual.Errors.Single().Should().Be(expectedMessage); + } + + [Fact] + public void ShouldProduceUnauthorizedWithError() + { + string expectedMessage = "You are unauthorized"; + var result = Result.Unauthorized(expectedMessage); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.Unauthorized); + actual.Errors.Single().Should().Be(expectedMessage); + } + + [Fact] + public void ShouldProduceNoContentWithoutAnyContent() + { + var result = Result.NoContent(); + + var actual = result.Bind(val => Result.Success(val.ToString())); + + actual.Status.Should().Be(ResultStatus.NoContent); + actual.Value.Should().BeNull(); + actual.Errors.Should().BeEmpty(); + actual.ValidationErrors.Should().BeEmpty(); + } + + [Fact] + public void ShouldPropagateErrorFromBindFunction() + { + int successValue = 123; + var result = Result.Success(successValue); + string expectedError = "Bind function failed"; + + var actual = result.Bind(val => Result.Error(expectedError)); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(expectedError); + actual.Value.Should().BeNull(); + } + + [Fact] + public void ShouldHandleNestedResultsInBind() + { + int successValue = 123; + var result = Result.Success(successValue); + + var actual = result.Bind(val => + { + if (val > 1) + { + return Result.Success("Value is greater than 1"); + } + else + { + return Result.Error("Value is less than or equal to 1"); + } + }); + + actual.Status.Should().Be(ResultStatus.Ok); + actual.Value.Should().Be("Value is greater than 1"); + } + + [Fact] + public void ShouldHandleValidationErrorsFromBindFunction() + { + int successValue = 0; + var result = Result.Success(successValue); + var validationErrors = new List + { + new() { ErrorMessage = "Value must be greater than 1" } + }; + + var actual = result.Bind(val => + { + if (val > 1) + { + return Result.Success("Valid value"); + } + else + { + return Result.Invalid(validationErrors); + } + }); + + actual.Status.Should().Be(ResultStatus.Invalid); + actual.ValidationErrors.Should().BeEquivalentTo(validationErrors); + actual.Value.Should().BeNull(); + } + + private record Foo(string Bar); + + private class FooDto + { + public string Bar { get; set; } + + public FooDto(string bar) + { + Bar = bar; + } + + public static Result CreateFromFooResult(Foo foo) + { + return Result.Success(new FooDto(foo.Bar)); + } + + public static Result CreateFromFooCreatedResult(Foo foo) + { + return Result.Created(new FooDto(foo.Bar)); + } + } + } +} diff --git a/tests/Ardalis.Result.UnitTests/ResultBindAsync.cs b/tests/Ardalis.Result.UnitTests/ResultBindAsync.cs new file mode 100644 index 0000000..a60fe70 --- /dev/null +++ b/tests/Ardalis.Result.UnitTests/ResultBindAsync.cs @@ -0,0 +1,802 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Ardalis.Result.UnitTests +{ + public class ResultBindAsync + { + [Fact] + public async Task BindAsync_WithSuccessResultAndAsyncFunction_ReturnsSuccess() + { + int successValue = 123; + var result = Result.Success(successValue); + var expected = Result.Success(successValue.ToString()); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(val.ToString()); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithAsyncResultAndAsyncFunction_ReturnsSuccess() + { + int successValue = 123; + var resultTask = Task.FromResult(Result.Success(successValue)); + var expected = Result.Success(successValue.ToString()); + + var actual = await resultTask.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(val.ToString()); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_VoidAsyncResultToAsyncResult_ReturnsSuccess() + { + int successValue = 123; + var resultTask = Task.FromResult(Result.Success()); + var expected = Result.Success(successValue); + + var actual = await resultTask.BindAsync(async _ => + { + await Task.Delay(1); + return Result.Success(successValue); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_VoidResultToAsyncResult_ReturnsSuccess() + { + int successValue = 123; + var result = Result.Success(); + var expected = Result.Success(successValue); + + var actual = await result.BindAsync(async _ => + { + await Task.Delay(1); + return Result.Success(successValue); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithAsyncResultAndSyncFunction_ReturnsSuccess() + { + int successValue = 123; + var resultTask = Task.FromResult(Result.Success(successValue)); + var expected = Result.Success(successValue.ToString()); + + var actual = await resultTask.BindAsync(val => Result.Success(val.ToString())); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithComplexTypeAndAsyncFunction_ReturnsSuccess() + { + var foo = new Foo("Bar"); + var result = Result.Success(foo); + var expected = Result.Success(new FooDto(foo.Bar)); + + var actual = await result.BindAsync(async fooValue => + { + await Task.Delay(1); + return Result.Success(new FooDto(fooValue.Bar)); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithCreatedResultAndAsyncFunction_ReturnsCreated() + { + int createdValue = 123; + var result = Result.Created(createdValue); + var expected = Result.Created(createdValue.ToString()); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Created(val.ToString()); + }); + + actual.Status.Should().Be(ResultStatus.Created); + actual.Value.Should().Be(expected.Value); + } + + [Fact] + public async Task BindAsync_WithNotFoundResult_PropagatesNotFound() + { + var result = Result.NotFound(); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(val.ToString()); + }); + + actual.Status.Should().Be(ResultStatus.NotFound); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task BindAsync_WithErrorInBindFunction_PropagatesError() + { + int successValue = 123; + var result = Result.Success(successValue); + string expectedError = "Bind function failed"; + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Error(expectedError); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(expectedError); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task BindAsync_WithValidationErrorsInBindFunction_ReturnsInvalid() + { + int successValue = 1; + var result = Result.Success(successValue); + var validationErrors = new List + { + new() { ErrorMessage = "Value must be greater than 1" } + }; + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + if (val > 1) + { + return Result.Success("Valid value"); + } + else + { + return Result.Invalid(validationErrors); + } + }); + + actual.Status.Should().Be(ResultStatus.Invalid); + actual.ValidationErrors.Should().BeEquivalentTo(validationErrors); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task BindAsync_WithUnauthorizedResult_PropagatesUnauthorized() + { + var result = Result.Unauthorized(); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(val.ToString()); + }); + + actual.Status.Should().Be(ResultStatus.Unauthorized); + } + + [Fact] + public async Task BindAsync_WithForbiddenResult_PropagatesForbidden() + { + var result = Result.Forbidden(); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(val.ToString()); + }); + + actual.Status.Should().Be(ResultStatus.Forbidden); + } + + [Fact] + public async Task BindAsync_AsyncResultWithErrorInBindFunction_ReturnsError() + { + var resultTask = Task.FromResult(Result.Success(123)); + + var actual = await resultTask.BindAsync(async val => + { + await Task.Delay(1); + return Result.Error("Async error"); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be("Async error"); + } + + [Fact] + public async Task BindAsync_AsyncResultWithSyncBindFunction_ReturnsSuccess() + { + var resultTask = Task.FromResult(Result.Success(123)); + var expected = Result.Success("123"); + + var actual = await resultTask.BindAsync(val => Result.Success(val.ToString())); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_VoidAsyncResultToVoidAsyncResult_ReturnsSuccess() + { + Task resultTask = Task.FromResult(Result.Success()); + Result expected = Result.Success(); + + var actual = await resultTask.BindAsync(async r => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_VoidResultToAsyncResultWithValue_ReturnsSuccessWithValue() + { + int expectedValue = 123; + var result = Result.Success(); + + var actual = await result.BindAsync(async _ => + { + await Task.Delay(1); + return Result.Success(expectedValue); + }); + + actual.Status.Should().Be(ResultStatus.Ok); + actual.Value.Should().Be(expectedValue); + } + + [Fact] + public async Task BindAsync_AsyncResultWithValueToVoidAsyncResult_ReturnsSuccess() + { + var resultTask = Task.FromResult(Result.Success(123)); + + var actual = await resultTask.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Status.Should().Be(ResultStatus.Ok); + } + + [Fact] + public async Task BindAsync_AsyncResultWithValueToVoidResult_ReturnsSuccess() + { + var resultTask = Task.FromResult(Result.Success(123)); + + var actual = await resultTask.BindAsync(val => Result.Success()); + + actual.Status.Should().Be(ResultStatus.Ok); + } + + [Fact] + public async Task BindAsync_VoidAsyncResultWithoutReturnValue_ReturnsSuccess() + { + var resultTask = Task.FromResult(Result.Success()); + + var actual = await resultTask.BindAsync(async _ => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Status.Should().Be(ResultStatus.Ok); + } + + [Fact] + public async Task BindAsync_WithNoContentResult_PropagatesNoContent() + { + var result = Result.NoContent(); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(val.ToString()); + }); + + actual.Status.Should().Be(ResultStatus.NoContent); + actual.Value.Should().BeNull(); + actual.Errors.Should().BeEmpty(); + actual.ValidationErrors.Should().BeEmpty(); + } + + [Fact] + public async Task BindAsync_DoesNotInvokeBindFunctionWhenResultIsError() + { + var result = Result.Error("Initial error"); + + bool bindFunctionInvoked = false; + + var actual = await result.BindAsync(async val => + { + bindFunctionInvoked = true; + await Task.Delay(1); + return Result.Success(val.ToString()); + }); + + bindFunctionInvoked.Should().BeFalse(); + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be("Initial error"); + } + + [Fact] + public async Task BindAsync_AsyncResultWithValueToVoidResultUsingSyncBindFunction_ReturnsSuccess() + { + int inputValue = 123; + var resultTask = Task.FromResult(Result.Success(inputValue)); + var expected = Result.Success(); + + var actual = await resultTask.BindAsync(val => Result.Success()); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithAsyncResultError_DoesNotInvokeBindFunction() + { + string errorMessage = "Initial error"; + var resultTask = Task.FromResult(Result.Error(errorMessage)); + + var actual = await resultTask.BindAsync(val => Result.Success()); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public async Task BindAsync_SyncBindFunctionReturnsError_ReturnsError() + { + int inputValue = 123; + string bindErrorMessage = "Error in bind function"; + var resultTask = Task.FromResult(Result.Success(inputValue)); + + var actual = await resultTask.BindAsync(val => Result.Error(bindErrorMessage)); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(bindErrorMessage); + } + + [Fact] + public async Task BindAsync_SyncResultWithAsyncBindFunction_ReturnsSuccess() + { + int inputValue = 123; + var result = Result.Success(inputValue); + var expected = Result.Success(); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithSyncResultError_DoesNotInvokeBindFunction() + { + string errorMessage = "Initial error"; + var result = Result.Error(errorMessage); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public async Task BindAsync_AsyncBindFunctionReturnsError_ReturnsError() + { + int inputValue = 123; + string bindErrorMessage = "Error in bind function"; + var result = Result.Success(inputValue); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Error(bindErrorMessage); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(bindErrorMessage); + } + + [Fact] + public async Task BindAsync_VoidResultWithAsyncBindFunction_ReturnsSuccess() + { + var result = Result.Success(); + var expected = Result.Success(); + + var actual = await result.BindAsync(async res => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithVoidResultError_DoesNotInvokeBindFunction() + { + string errorMessage = "Initial error"; + var result = Result.Error(errorMessage); + + var actual = await result.BindAsync(async res => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public async Task BindAsync_AsyncBindFunctionReturnsErrorWithVoidResult_ReturnsError() + { + var result = Result.Success(); + string bindErrorMessage = "Error in bind function"; + + var actual = await result.BindAsync(async res => + { + await Task.Delay(1); + return Result.Error(bindErrorMessage); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(bindErrorMessage); + } + + [Fact] + public async Task BindAsync_AsyncVoidResultWithAsyncBindFunction_ReturnsSuccess() + { + var resultTask = Task.FromResult(Result.Success()); + var expected = Result.Success(); + + var actual = await resultTask.BindAsync(async res => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithAsyncVoidResultError_DoesNotInvokeBindFunction() + { + string errorMessage = "Initial error"; + var resultTask = Task.FromResult(Result.Error(errorMessage)); + + var actual = await resultTask.BindAsync(async res => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public async Task BindAsync_AsyncBindFunctionReturnsErrorWithAsyncVoidResult_ReturnsError() + { + var resultTask = Task.FromResult(Result.Success()); + string bindErrorMessage = "Error in bind function"; + + var actual = await resultTask.BindAsync(async res => + { + await Task.Delay(1); + return Result.Error(bindErrorMessage); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(bindErrorMessage); + } + + [Fact] + public async Task BindAsync_VoidAsyncResultWithAsyncBindFunction_ReturnsSuccess() + { + var resultTask = Task.FromResult(Result.Success()); + var expected = Result.Success(); + + var actual = await resultTask.BindAsync(async res => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithVoidAsyncResultError_DoesNotInvokeBindFunction() + { + var errorMessage = "Initial error"; + var resultTask = Task.FromResult(Result.Error(errorMessage)); + + var actual = await resultTask.BindAsync(async res => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public async Task BindAsync_SyncResultWithValueAndAsyncBindFunction_ReturnsSuccess() + { + int inputValue = 123; + var result = Result.Success(inputValue); + var expected = Result.Success(); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_TaskResultTSourceWithSyncBindFunction_ReturnsSuccess() + { + // Arrange + Task> resultTask = Task.FromResult(Result.Success(123)); + var expected = Result.Success(); + + // Act + var actual = await resultTask.BindAsync(val => Result.Success()); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_VoidResultToTaskVoidResult_ReturnsSuccess() + { + // Arrange + var result = Result.Success(123); + var expected = Result.Success(); + + // Act + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(); + }); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithSyncResultWithValueError_DoesNotInvokeBindFunction() + { + var errorMessage = "Initial error"; + var result = Result.Error(errorMessage); + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public async Task BindAsync_AsyncBindFunctionReturnsErrorOnResultWithValue_ReturnsError() + { + int inputValue = 123; + var result = Result.Success(inputValue); + var bindErrorMessage = "Error in bind function"; + + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Error(bindErrorMessage); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(bindErrorMessage); + } + + [Fact] + public async Task BindAsync_AsyncResultWithValueAndSyncBindFunction_ReturnsSuccess() + { + int inputValue = 123; + var resultTask = Task.FromResult(Result.Success(inputValue)); + var expected = Result.Success(); + + var actual = await resultTask.BindAsync(val => Result.Success()); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_WithAsyncResultWithValueError_DoesNotInvokeBindFunction() + { + var errorMessage = "Initial error"; + var resultTask = Task.FromResult(Result.Error(errorMessage)); + + var actual = await resultTask.BindAsync(val => Result.Success()); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public async Task BindAsync_SyncBindFunctionReturnsErrorOnAsyncResultWithValue_ReturnsError() + { + int inputValue = 123; + var resultTask = Task.FromResult(Result.Success(inputValue)); + var bindErrorMessage = "Error in bind function"; + + var actual = await resultTask.BindAsync(val => Result.Error(bindErrorMessage)); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(bindErrorMessage); + } + + + [Fact] + public async Task BindAsync_TaskResultTSourceWithSyncBindFunction_ReturnsError() + { + // Arrange + int successValue = 123; + var resultTask = Task.FromResult(Result.Success(successValue)); + string bindErrorMessage = "Error in bind function"; + + // Act + var actual = await resultTask.BindAsync(val => Result.Error(bindErrorMessage)); + + // Assert + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(bindErrorMessage); + } + + [Fact] + public async Task BindAsync_TaskResultTSourceError_DoesNotInvokeBindFunction() + { + // Arrange + string errorMessage = "Initial error"; + var resultTask = Task.FromResult(Result.Error(errorMessage)); + bool bindFunctionInvoked = false; + + // Act + var actual = await resultTask.BindAsync(val => + { + bindFunctionInvoked = true; + return Result.Success(); + }); + + // Assert + bindFunctionInvoked.Should().BeFalse(); + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + [Fact] + public async Task BindAsync_TaskResultTSourceCreatedWithSyncBindFunction_ReturnsResult() + { + // Arrange + int createdValue = 123; + var resultTask = Task.FromResult(Result.Created(createdValue)); + + // Act + var actual = await resultTask.BindAsync(val => Result.Success()); + + // Assert + actual.Status.Should().Be(ResultStatus.Ok); + } + + [Fact] + public async Task BindAsync_ResultTSourceWithAsyncBindFunction_ReturnsSuccess() + { + // Arrange + int successValue = 123; + var result = Result.Success(successValue); + var expected = Result.Success(); + + // Act + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Success(); + }); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task BindAsync_ResultTSourceWithAsyncBindFunction_ReturnsError() + { + // Arrange + int successValue = 123; + var result = Result.Success(successValue); + string bindErrorMessage = "Error in bind function"; + + // Act + var actual = await result.BindAsync(async val => + { + await Task.Delay(1); + return Result.Error(bindErrorMessage); + }); + + // Assert + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(bindErrorMessage); + } + + [Fact] + public async Task BindAsync_ResultTSourceError_DoesNotInvokeBindFunction() + { + // Arrange + string errorMessage = "Initial error"; + var result = Result.Error(errorMessage); + bool bindFunctionInvoked = false; + + // Act + var actual = await result.BindAsync(async val => + { + bindFunctionInvoked = true; + await Task.Delay(1); + return Result.Success(); + }); + + // Assert + bindFunctionInvoked.Should().BeFalse(); + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be(errorMessage); + } + + private record Foo(string Bar); + + private class FooDto + { + public string Bar { get; set; } + + public FooDto(string bar) + { + Bar = bar; + } + + public static async Task> CreateFromFooResultAsync(Foo foo) + { + await Task.Delay(1); + return Result.Success(new FooDto(foo.Bar)); + } + } + } + +} diff --git a/tests/Ardalis.Result.UnitTests/ResultMap.cs b/tests/Ardalis.Result.UnitTests/ResultMap.cs index 9877bd4..263b3cc 100644 --- a/tests/Ardalis.Result.UnitTests/ResultMap.cs +++ b/tests/Ardalis.Result.UnitTests/ResultMap.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace Ardalis.Result.UnitTests @@ -235,7 +236,7 @@ public void ShouldProduceForbiddenWithError() } [Fact] - public void ShouldProduceUnauthroizedWithError() + public void ShouldProduceUnauthorizedWithError() { string expectedMessage = "You are unauthorized"; var result = Result.Unauthorized(expectedMessage); diff --git a/tests/Ardalis.Result.UnitTests/ResultMapAsync.cs b/tests/Ardalis.Result.UnitTests/ResultMapAsync.cs new file mode 100644 index 0000000..1b7d534 --- /dev/null +++ b/tests/Ardalis.Result.UnitTests/ResultMapAsync.cs @@ -0,0 +1,210 @@ +using FluentAssertions; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Ardalis.Result.UnitTests +{ + public class ResultMapAsync + { + [Fact] + public async Task ShouldProduceReturnValueFromSuccessAsyncFunction() + { + int successValue = 123; + var result = Result.Success(successValue); + string expected = successValue.ToString(); + + var actual = await result.MapAsync(async val => + { + await Task.Delay(1); + return val.ToString(); + }); + + expected.Should().BeEquivalentTo(actual.Value); + actual.Status.Should().Be(ResultStatus.Ok); + } + + [Fact] + public async Task ShouldProduceReturnValueFromAsyncResultAndAsyncFunction() + { + int successValue = 123; + var resultTask = Task.FromResult(Result.Success(successValue)); + string expected = successValue.ToString(); + + var actual = await resultTask.MapAsync(async val => + { + await Task.Delay(1); + return val.ToString(); + }); + + expected.Should().BeEquivalentTo(actual.Value); + actual.Status.Should().Be(ResultStatus.Ok); + } + + [Fact] + public async Task ShouldProduceComplexTypeReturnValueFromSuccessAsyncFunction() + { + var foo = new Foo("Bar"); + var result = Result.Success(foo); + + var actual = await result.MapAsync(async fooValue => + { + await Task.Delay(1); + return new FooDto(fooValue.Bar); + }); + + actual.Value.Bar.Should().Be(foo.Bar); + actual.Status.Should().Be(ResultStatus.Ok); + } + + [Fact] + public async Task ShouldProduceReturnValueFromCreatedAsyncFunction() + { + int createdValue = 123; + var result = Result.Created(createdValue); + string expected = createdValue.ToString(); + + var actual = await result.MapAsync(async val => + { + await Task.Delay(1); + return val.ToString(); + }); + + expected.Should().BeEquivalentTo(actual.Value); + actual.Status.Should().Be(ResultStatus.Created); + } + + [Fact] + public async Task ShouldProduceNotFoundAsync() + { + var result = Result.NotFound(); + + var actual = await result.MapAsync(async val => + { + await Task.Delay(1); + return val.ToString(); + }); + + actual.Status.Should().Be(ResultStatus.NotFound); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceErrorResultWithErrorsAsync() + { + var errorList = new ErrorList(["Error 1", "Error 2"], default); + var result = Result.Error(errorList); + + var actual = await result.MapAsync(async val => + { + await Task.Delay(1); + return val.ToString(); + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Should().BeEquivalentTo(errorList.ErrorMessages); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldHandleExceptionInMapFunctionAsync() + { + int successValue = 123; + var result = Result.Success(successValue); + + Func action = async () => + { + await result.MapAsync(async val => + { + await Task.Delay(1); + throw new InvalidOperationException("Test exception"); + }); + }; + + await action.Should().ThrowAsync().WithMessage("Test exception"); + } + + [Fact] + public async Task ShouldNotInvokeMapFunctionWhenResultIsErrorAsync() + { + var result = Result.Error("Initial error"); + + bool mapFunctionInvoked = false; + + var actual = await result.MapAsync(async val => + { + mapFunctionInvoked = true; + await Task.Delay(1); + return val.ToString(); + }); + + mapFunctionInvoked.Should().BeFalse(); + actual.Status.Should().Be(ResultStatus.Error); + actual.Errors.Single().Should().Be("Initial error"); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldPreserveLocationInCreatedStatusAsync() + { + var foo = new Foo("Bar"); + var result = Result.Created(foo, location: "/foo/1"); + + var actual = await result.MapAsync(async val => + { + await Task.Delay(1); + return new FooDto(val.Bar); + }); + + actual.Status.Should().Be(ResultStatus.Created); + actual.Value.Bar.Should().Be(foo.Bar); + actual.Location.Should().Be("/foo/1"); + } + + [Fact] + public async Task ShouldHandleAsyncResultAndAsyncMapFunctionWithErrorStatus() + { + var resultTask = Task.FromResult(Result.NotFound("Item not found")); + + var actual = await resultTask.MapAsync(async val => + { + await Task.Delay(1); + return val.ToString(); + }); + + actual.Status.Should().Be(ResultStatus.NotFound); + actual.Errors.Single().Should().Be("Item not found"); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceNoContentAsync() + { + var result = Result.NoContent(); + + var actual = await result.MapAsync(async val => + { + await Task.Delay(1); + return val.ToString(); + }); + + actual.Status.Should().Be(ResultStatus.NoContent); + actual.Value.Should().BeNull(); + actual.Errors.Should().BeEmpty(); + actual.ValidationErrors.Should().BeEmpty(); + } + + private record Foo(string Bar); + + private class FooDto + { + public string Bar { get; set; } + + public FooDto(string bar) + { + Bar = bar; + } + } + } +} diff --git a/tests/Ardalis.Result.UnitTests/ResultVoidMapAsync.cs b/tests/Ardalis.Result.UnitTests/ResultVoidMapAsync.cs new file mode 100644 index 0000000..9d6284c --- /dev/null +++ b/tests/Ardalis.Result.UnitTests/ResultVoidMapAsync.cs @@ -0,0 +1,163 @@ +using FluentAssertions; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Ardalis.Result.UnitTests +{ + public class ResultVoidMapAsync + { + [Fact] + public async Task ShouldProduceSuccessWithValueGivenResultOfVoidAsync() + { + var result = Result.Success(); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // Simulate async operation + return "Success"; + }); + + actual.Value.Should().Be("Success"); + actual.Status.Should().Be(ResultStatus.Ok); + } + + [Fact] + public async Task ShouldProduceNotFoundAsync() + { + var result = Result.NotFound(); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.NotFound); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceUnauthorizedAsync() + { + var result = Result.Unauthorized(); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.Unauthorized); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceForbiddenAsync() + { + var result = Result.Forbidden(); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.Forbidden); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceInvalidWithEmptyListAsync() + { + var result = Result.Invalid(new List()); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.Invalid); + actual.ValidationErrors.Should().BeEmpty(); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceInvalidAsync() + { + var validationError = new ValidationError { ErrorMessage = "Invalid input" }; + var result = Result.Invalid(validationError); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.Invalid); + actual.ValidationErrors.Should().ContainSingle().Which.Should().BeEquivalentTo(validationError); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceErrorAsync() + { + var result = Result.Error(); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.Error); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceConflictAsync() + { + var result = Result.Conflict(); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.Conflict); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceUnavailableAsync() + { + var result = Result.Unavailable(); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.Unavailable); + actual.Value.Should().BeNull(); + } + + [Fact] + public async Task ShouldProduceCriticalErrorAsync() + { + var result = Result.CriticalError(); + + var actual = await result.MapAsync(async _ => + { + await Task.Delay(1); // This should not be called + return "This should be ignored"; + }); + + actual.Status.Should().Be(ResultStatus.CriticalError); + actual.Value.Should().BeNull(); + } + } +}