diff --git a/CHANGELOG.md b/CHANGELOG.md index ee2e9bd8..ba7476dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Represents the **NuGet** versions. +## v3.18.1 +- *Fixed*: The `ITypedMappedHttpClient.MapResponse` was not validating the input HTTP response correctly before mapping; resulted in a `null` success value versus the originating error/exception. +- *Fixed*: The `HttpResult.ThrowOnError` was not correctly throwing the internal exception. + ## v3.18.0 - *Fixed*: Removed `Azure.Identity` dependency as no longer required; related to `https://github.com/advisories/GHSA-wvxc-855f-jvrv`. - *Fixed*: Removed `AspNetCore.HealthChecks.SqlServer` dependency as no longer required. diff --git a/Common.targets b/Common.targets index fa5005de..cb55d105 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 3.18.0 + 3.18.1 preview Avanade Avanade diff --git a/src/CoreEx/Http/Extended/ITypedMappedHttpClient.cs b/src/CoreEx/Http/Extended/ITypedMappedHttpClient.cs index ecd96f1e..e182be5e 100644 --- a/src/CoreEx/Http/Extended/ITypedMappedHttpClient.cs +++ b/src/CoreEx/Http/Extended/ITypedMappedHttpClient.cs @@ -23,7 +23,10 @@ public interface ITypedMappedHttpClient /// The response HTTP . /// The . /// The mapped . - public HttpResult MapResponse(HttpResult httpResult) => new(httpResult.Response, httpResult.BinaryContent, httpResult.IsSuccess && httpResult.Value is not null ? Mapper.Map(httpResult.Value, OperationTypes.Get) : default!); + public HttpResult MapResponse(HttpResult httpResult) + => httpResult.ThrowIfNull().IsSuccess + ? new(httpResult.Response, httpResult.BinaryContent, Mapper.Map(httpResult.Value, OperationTypes.Get)!) + : new(httpResult.Response, httpResult.BinaryContent, httpResult.Exception); /// /// Maps the to the . diff --git a/src/CoreEx/Http/HttpResult.cs b/src/CoreEx/Http/HttpResult.cs index 897507a1..e5eaf602 100644 --- a/src/CoreEx/Http/HttpResult.cs +++ b/src/CoreEx/Http/HttpResult.cs @@ -63,7 +63,7 @@ public static async Task> CreateAsync(HttpResponseMessage respo } catch (Exception ex) { - return new HttpResult(response, content, new InvalidOperationException($"Unable to convert the content '{content}' [{MediaTypeNames.Text.Plain}] to Type {typeof(T).Name}.", ex)); + return new HttpResult(response, content, new InvalidOperationException($"Unable to convert the content [{MediaTypeNames.Text.Plain}] content to Type {typeof(T).Name}.", ex)); } } @@ -84,7 +84,7 @@ public static async Task> CreateAsync(HttpResponseMessage respo } catch (Exception ex) { - return new HttpResult(response, content, new InvalidOperationException($"Unable to deserialize the JSON content '{content}' [{response.Content.Headers?.ContentType?.MediaType ?? "not specified"}] to Type {typeof(T).FullName}.", ex)); + return new HttpResult(response, content, new InvalidOperationException($"Unable to deserialize the JSON [{response.Content.Headers?.ContentType?.MediaType ?? "not specified"}] content to Type {typeof(T).FullName}.", ex)); } } diff --git a/src/CoreEx/Http/HttpResultT.cs b/src/CoreEx/Http/HttpResultT.cs index af91a6ed..96058fd5 100644 --- a/src/CoreEx/Http/HttpResultT.cs +++ b/src/CoreEx/Http/HttpResultT.cs @@ -30,7 +30,7 @@ public class HttpResult : HttpResultBase, IToResult /// The . /// The as (see ). /// The internal . - internal HttpResult(HttpResponseMessage response, BinaryData? content, Exception internalException) : this(response, content, default(T)!) => _internalException = internalException; + internal HttpResult(HttpResponseMessage response, BinaryData? content, Exception? internalException) : this(response, content, default(T)!) => _internalException = internalException; /// /// Gets the response value. @@ -45,6 +45,11 @@ public T Value } } + /// + /// Gets the internal exception where the request/response handling was not successful; i.e. JSON deserialization error. + /// + public Exception? Exception => _internalException; + /// public override bool IsSuccess => _internalException is null && base.IsSuccess; @@ -62,6 +67,9 @@ public HttpResult ThrowOnError(bool throwKnownException = true, bool useConte if (IsSuccess) return this; + if (_internalException is not null) + throw _internalException; + if (throwKnownException) { var eex = CreateExtendedException(Response, Content, useContentAsErrorMessage); diff --git a/src/CoreEx/Results/Result.cs b/src/CoreEx/Results/Result.cs index f9e22251..c8727110 100644 --- a/src/CoreEx/Results/Result.cs +++ b/src/CoreEx/Results/Result.cs @@ -41,7 +41,14 @@ public Result() { } public Result(Exception error) => _error = error.ThrowIfNull(nameof(error)); /// - object? IResult.Value => null; + object? IResult.Value + { + get + { + ThrowOnError(); + return null; + } + } /// public Exception Error { get => _error ?? throw new InvalidOperationException($"The {nameof(Error)} cannot be accessed as the {nameof(Result)} is in a successful state."); } diff --git a/tests/CoreEx.Test/Framework/Http/HttpResultTest.cs b/tests/CoreEx.Test/Framework/Http/HttpResultTest.cs new file mode 100644 index 00000000..1f0776eb --- /dev/null +++ b/tests/CoreEx.Test/Framework/Http/HttpResultTest.cs @@ -0,0 +1,30 @@ +using CoreEx.Http; +using NUnit.Framework; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace CoreEx.Test.Framework.Http +{ + [TestFixture] + public class HttpResultTest + { + [Test] + public async Task Create_InternalException() + { + var r = new System.Net.Http.HttpResponseMessage(HttpStatusCode.OK) { Content = new System.Net.Http.StringContent("[]") }; + var hr = await HttpResult.CreateAsync(r); + + Assert.That(hr.IsSuccess, Is.False); + Assert.Throws(() => hr.ThrowOnError()); + Assert.Throws(() => _ = hr.Value); + + var rr = hr.ToResult(); + Assert.Multiple(() => + { + Assert.That(rr.IsSuccess, Is.False); + Assert.That(rr.Error, Is.TypeOf()); + }); + } + } +} \ No newline at end of file diff --git a/tests/CoreEx.Test/Framework/Http/TypedMapperHttpClientBaseTest.cs b/tests/CoreEx.Test/Framework/Http/TypedMapperHttpClientBaseTest.cs new file mode 100644 index 00000000..8e3d3fb3 --- /dev/null +++ b/tests/CoreEx.Test/Framework/Http/TypedMapperHttpClientBaseTest.cs @@ -0,0 +1,135 @@ +using CoreEx.Http.Extended; +using CoreEx.Mapping; +using Moq; +using NUnit.Framework; +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web.Http; +using UnitTestEx.Mocking; + +namespace CoreEx.Test.Framework.Http +{ + [TestFixture] + public class TypedMapperHttpClientBaseTest + { + [Test] + public async Task MapSuccess() + { + var m = new Mapper(); + m.Register(new CustomerMapper()); + m.Register(new BackendMapper()); + + var mcf = UnitTestEx.NUnit.MockHttpClientFactory.Create(); + mcf.CreateDefaultClient().Request(HttpMethod.Post, "test").WithJsonBody(new Backend { First = "John", Last = "Doe" }).Respond.WithJson(new Backend { First = "John", Last = "Doe" }); + + var mc = new TypedMappedHttpClient(mcf.GetHttpClient()!, m); + var hr = await mc.PostMappedAsync("test", new Customer { FirstName = "John", LastName = "Doe" }); + + Assert.Multiple(() => + { + Assert.That(hr.IsSuccess, Is.True); + Assert.That(hr.Value, Is.Not.Null); + }); + Assert.Multiple(() => + { + Assert.That(hr.Value.FirstName, Is.EqualTo("John")); + Assert.That(hr.Value.LastName, Is.EqualTo("Doe")); + }); + + var r = hr.ToResult(); + Assert.That(r.IsSuccess, Is.True); + } + + [Test] + public async Task MapServerError() + { + var m = new Mapper(); + m.Register(new CustomerMapper()); + m.Register(new BackendMapper()); + + var mcf = UnitTestEx.NUnit.MockHttpClientFactory.Create(); + mcf.CreateDefaultClient().Request(HttpMethod.Post, "test").WithJsonBody(new Backend { First = "John", Last = "Doe" }).Respond.With(HttpStatusCode.InternalServerError); + + var mc = new TypedMappedHttpClient(mcf.GetHttpClient()!, m); + var hr = await mc.PostMappedAsync("test", new Customer { FirstName = "John", LastName = "Doe" }); + + Assert.Multiple(() => + { + Assert.That(hr.IsSuccess, Is.False); + Assert.That(hr.StatusCode, Is.EqualTo(HttpStatusCode.InternalServerError)); + }); + + var r = hr.ToResult(); + Assert.Multiple(() => + { + Assert.That(r.IsSuccess, Is.False); + Assert.That(r.Error, Is.TypeOf()); + }); + } + + [Test] + public async Task MapJsonError() + { + var m = new Mapper(); + m.Register(new CustomerMapper()); + m.Register(new BackendMapper()); + + var mcf = UnitTestEx.NUnit.MockHttpClientFactory.Create(); + mcf.CreateDefaultClient().Request(HttpMethod.Post, "test").WithJsonBody(new Backend { First = "John", Last = "Doe" }).Respond.WithJson("{\"first\":\"Dave\",\"age\":\"ten\"}"); + + var mc = new TypedMappedHttpClient(mcf.GetHttpClient()!, m); + var hr = await mc.PostMappedAsync("test", new Customer { FirstName = "John", LastName = "Doe" }); + + Assert.Multiple(() => + { + Assert.That(hr.IsSuccess, Is.False); + Assert.That(hr.StatusCode, Is.EqualTo(HttpStatusCode.InternalServerError)); + Assert.That(hr.Exception, Is.TypeOf()); + }); + + var r = hr.ToResult(); + Assert.Multiple(() => + { + Assert.That(r.IsSuccess, Is.False); + Assert.That(r.Error, Is.TypeOf()); + }); + } + } + + public class Customer + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + } + + public class Backend + { + public string? First { get; set; } + public string? Last { get; set; } + public int? Age { get; set; } + } + + public class CustomerMapper : CoreEx.Mapping.Mapper + { + protected override Backend? OnMap(Customer? source, Backend? destination, OperationTypes operationType) + { + destination ??= new Backend(); + destination.First = source?.FirstName; + destination.Last = source?.LastName; + return destination; + } + } + + public class BackendMapper : Mapper + { + protected override Customer? OnMap(Backend? source, Customer? destination, OperationTypes operationType) + { + destination ??= new Customer(); + destination.FirstName = source?.First; + destination.LastName = source?.Last; + return destination; + } + } +} \ No newline at end of file diff --git a/tests/CoreEx.Test/Framework/Results/ResultTTest.cs b/tests/CoreEx.Test/Framework/Results/ResultTTest.cs index 2f51d219..6e43d48d 100644 --- a/tests/CoreEx.Test/Framework/Results/ResultTTest.cs +++ b/tests/CoreEx.Test/Framework/Results/ResultTTest.cs @@ -219,5 +219,12 @@ public async Task AsTask() Assert.That(r.Value, Is.EqualTo(1)); }); } + + [Test] + public void Failure_Value() + { + var ir = (IResult)Result.Fail("On no!"); + Assert.Throws(() => _ = ir.Value); + } } } \ No newline at end of file diff --git a/tests/CoreEx.Test/Framework/Results/ResultTest.cs b/tests/CoreEx.Test/Framework/Results/ResultTest.cs index 2bc90530..190ab57d 100644 --- a/tests/CoreEx.Test/Framework/Results/ResultTest.cs +++ b/tests/CoreEx.Test/Framework/Results/ResultTest.cs @@ -141,5 +141,19 @@ public async Task AsTask() var r = await Result.Go().AsTask(); Assert.That(r, Is.EqualTo(Result.Success)); } + + [Test] + public void Success_Value() + { + var ir = (IResult)Result.Success; + Assert.That(ir.Value, Is.Null); + } + + [Test] + public void Failure_Value() + { + var ir = (IResult)Result.Fail("On no!"); + Assert.Throws(() => _ = ir.Value); + } } } \ No newline at end of file