diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 5d39af91..4698d0ba 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -19,7 +19,7 @@ jobs: - name: Install NET 9 uses: actions/setup-dotnet@v4.0.1 with: - dotnet-version: '9.0.x' + dotnet-version: '9.0.306' - name: Restore Nuget Packages run: dotnet restore Shared.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} @@ -67,7 +67,7 @@ jobs: - name: Install NET 9 uses: actions/setup-dotnet@v4.0.1 with: - dotnet-version: '9.0.x' + dotnet-version: '9.0.306' - name: Build Windows SQL Server run: | diff --git a/ClientProxyBase/AssemblyInfo.cs b/ClientProxyBase/AssemblyInfo.cs new file mode 100644 index 00000000..106bd54f --- /dev/null +++ b/ClientProxyBase/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Shared.Tests")] \ No newline at end of file diff --git a/ClientProxyBase/ClientProxyBase.cs b/ClientProxyBase/ClientProxyBase.cs index f849efea..65ac2975 100644 --- a/ClientProxyBase/ClientProxyBase.cs +++ b/ClientProxyBase/ClientProxyBase.cs @@ -16,7 +16,7 @@ namespace ClientProxyBase; using System.Threading; using System.Threading.Tasks; -public abstract class ClientProxyBase { +public abstract partial class ClientProxyBase { #region Fields /// @@ -106,6 +106,7 @@ protected virtual ResponseData HandleResponseContent(String content) return JsonConvert.DeserializeObject>(content); } + /* protected virtual async Task> SendGetRequest(String uri, String accessToken, CancellationToken cancellationToken) => @@ -151,9 +152,9 @@ protected virtual async Task> SendGetRequest(String return Result.Success(responseData); } - protected virtual async Task> SendPostRequest(String uri, String accessToken, TRequest content, CancellationToken cancellationToken) => await this.SendPostRequest(uri, accessToken, content, cancellationToken, null); + protected virtual async Task> SendPostRequest(String uri, String accessToken, TRequest content, CancellationToken cancellationToken) => await this.SendPostRequest(uri, accessToken, content, null, cancellationToken); - protected virtual async Task> SendPostRequest(String uri, String accessToken, TRequest content, CancellationToken cancellationToken, List<(String header, String value)> additionalHeaders) + protected virtual async Task> SendPostRequest(String uri, String accessToken, TRequest content, List<(String header, String value)> additionalHeaders, CancellationToken cancellationToken) { HttpRequestMessage requestMessage = new(HttpMethod.Post, uri); @@ -184,6 +185,30 @@ protected virtual async Task> SendPostRequest(responseData); } + protected virtual async Task SendPostRequest(string uri, + string accessToken, + List<(string header, string value)> additionalHeaders, + CancellationToken cancellationToken) { + Result result = await this.SendPostRequest(uri, accessToken, additionalHeaders, cancellationToken); + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + return Result.Success(); + } + + protected virtual async Task SendPostRequest(string uri, + string accessToken, + CancellationToken cancellationToken) => await this.SendPostRequest(uri, accessToken, null, cancellationToken); + + //protected virtual async Task SendPostRequest(string uri, + // string accessToken = null, + // List<(string header, string value)> additionalHeaders = null) + //{ + // Result result = await SendPostRequest(uri, accessToken, null, cancellationToken, additionalHeaders); + // if (result.IsFailed) + // return ResultHelpers.CreateFailure(result); + // return Result.Success(); + //} + protected virtual async Task> SendPutRequest(String uri, String accessToken, TRequest content, CancellationToken cancellationToken) => await this.SendPutRequest(uri, accessToken, content, cancellationToken, null); @@ -278,7 +303,7 @@ protected virtual async Task> SendDeleteRequest(Str TResponse responseData = JsonConvert.DeserializeObject(result.Data); return Result.Success(responseData); - } + }*/ static void AddAdditionalHeaders(HttpRequestMessage requestMessage, List<(String header, String value)> additionalHeaders) { @@ -289,7 +314,6 @@ static void AddAdditionalHeaders(HttpRequestMessage requestMessage, } } - #endregion } diff --git a/ClientProxyBase/ClientProxyBase_DELETE.cs b/ClientProxyBase/ClientProxyBase_DELETE.cs new file mode 100644 index 00000000..650e1300 --- /dev/null +++ b/ClientProxyBase/ClientProxyBase_DELETE.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Shared.Results; +using SimpleResults; + +namespace ClientProxyBase +{ + public abstract partial class ClientProxyBase + { + internal virtual async Task SendHttpDeleteRequest(String uri, + String accessToken, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) + { + HttpRequestMessage requestMessage = new(HttpMethod.Delete, uri); + if (String.IsNullOrEmpty(accessToken) == false) + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationSchemes.Bearer, accessToken); + + AddAdditionalHeaders(requestMessage, additionalHeaders); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + + // Process the response + Result result = await this.HandleResponseX(httpResponse, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + return Result.Success(); + } + + internal virtual async Task SendHttpDeleteRequest(String uri, + String accessToken, + CancellationToken cancellationToken) => + await this.SendHttpDeleteRequest(uri, accessToken,null, cancellationToken); + + internal virtual async Task SendHttpDeleteRequest(String uri, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) => + await this.SendHttpDeleteRequest(uri, null, additionalHeaders, cancellationToken); + + internal virtual async Task SendHttpDeleteRequest(String uri, + CancellationToken cancellationToken) => + await this.SendHttpDeleteRequest(uri, null, null, cancellationToken); + } +} diff --git a/ClientProxyBase/ClientProxyBase_GET.cs b/ClientProxyBase/ClientProxyBase_GET.cs new file mode 100644 index 00000000..55341371 --- /dev/null +++ b/ClientProxyBase/ClientProxyBase_GET.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using Shared.Results; +using SimpleResults; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ClientProxyBase +{ + public abstract partial class ClientProxyBase + { + internal virtual async Task> SendHttpGetRequest(String uri, + String accessToken, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) + { + HttpRequestMessage requestMessage = new(HttpMethod.Get, uri); + if (String.IsNullOrEmpty(accessToken) == false) + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationSchemes.Bearer, accessToken); + + AddAdditionalHeaders(requestMessage, additionalHeaders); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + + // Process the response + Result result = await this.HandleResponseX(httpResponse, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + TResponse responseData = JsonConvert.DeserializeObject(result.Data); + + return Result.Success(responseData); + } + + internal virtual async Task> SendHttpGetRequest(String uri, + String accessToken, + CancellationToken cancellationToken) => + await this.SendHttpGetRequest(uri, accessToken, null, cancellationToken); + + internal virtual async Task> SendHttpGetRequest(String uri, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) => + await this.SendHttpGetRequest(uri, null, additionalHeaders, cancellationToken); + + internal virtual async Task> SendHttpGetRequest(String uri, + CancellationToken cancellationToken) => + await this.SendHttpGetRequest(uri, null, null, cancellationToken); + } +} diff --git a/ClientProxyBase/ClientProxyBase_PATCH.cs b/ClientProxyBase/ClientProxyBase_PATCH.cs new file mode 100644 index 00000000..3a2cac65 --- /dev/null +++ b/ClientProxyBase/ClientProxyBase_PATCH.cs @@ -0,0 +1,69 @@ +using Newtonsoft.Json; +using Shared.Results; +using SimpleResults; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ClientProxyBase +{ + public abstract partial class ClientProxyBase + { + internal virtual async Task> SendHttpPatchRequest(String uri, + TRequest request, + String accessToken, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) + { + HttpRequestMessage requestMessage = new(HttpMethod.Patch, uri); + if (String.IsNullOrEmpty(accessToken) == false) + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationSchemes.Bearer, accessToken); + + AddAdditionalHeaders(requestMessage, additionalHeaders); + + if (request.GetType() == typeof(FormUrlEncodedContent)) + { + // Treat this specially + requestMessage.Content = request as FormUrlEncodedContent; + } + else + { + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + } + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + + // Process the response + Result result = await this.HandleResponseX(httpResponse, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + return Result.Success(); + } + + internal virtual async Task> SendHttpPatchRequest(String uri, + TRequest request, + String accessToken, + CancellationToken cancellationToken) => + await this.SendHttpPatchRequest(uri, request, accessToken, null, cancellationToken); + + internal virtual async Task> SendHttpPatchRequest(String uri, + TRequest request, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) => + await this.SendHttpPatchRequest(uri, request, null, additionalHeaders, cancellationToken); + + internal virtual async Task> SendHttpPatchRequest(String uri, + TRequest request, + CancellationToken cancellationToken) => + await this.SendHttpPatchRequest(uri, request, null, null, cancellationToken); + + } +} diff --git a/ClientProxyBase/ClientProxyBase_POST.cs b/ClientProxyBase/ClientProxyBase_POST.cs new file mode 100644 index 00000000..cbcb3c91 --- /dev/null +++ b/ClientProxyBase/ClientProxyBase_POST.cs @@ -0,0 +1,129 @@ +using Newtonsoft.Json; +using Shared.Results; +using SimpleResults; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ClientProxyBase +{ + public abstract partial class ClientProxyBase + { + internal virtual async Task> SendHttpPostRequest(String uri, + String accessToken, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) { + + HttpRequestMessage requestMessage = new(HttpMethod.Post, uri); + if (String.IsNullOrEmpty(accessToken) == false) + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationSchemes.Bearer, accessToken); + + AddAdditionalHeaders(requestMessage, additionalHeaders); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + + // Process the response + Result result = await this.HandleResponseX(httpResponse, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + TResponse responseData = JsonConvert.DeserializeObject(result.Data); + + return Result.Success(responseData); + + } + + internal virtual async Task> SendHttpPostRequest(String uri, String accessToken, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri, accessToken, null, cancellationToken); + + internal virtual async Task> SendHttpPostRequest(String uri, List<(String header, String value)> additionalHeaders, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri, null, additionalHeaders, cancellationToken); + + internal virtual async Task> SendHttpPostRequest(String uri, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri, null, null, cancellationToken); + + + internal virtual async Task> SendHttpPostRequest(String uri, + TRequest request, + String accessToken, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) { + HttpRequestMessage requestMessage = new(HttpMethod.Post, uri); + if (String.IsNullOrEmpty(accessToken) == false) + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationSchemes.Bearer, accessToken); + + AddAdditionalHeaders(requestMessage, additionalHeaders); + + if (request.GetType() == typeof(FormUrlEncodedContent)) + { + // Treat this specially + requestMessage.Content = request as FormUrlEncodedContent; + } + else + { + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + } + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + + // Process the response + Result result = await this.HandleResponseX(httpResponse, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + TResponse responseData = JsonConvert.DeserializeObject(result.Data); + + return Result.Success(responseData); + } + + internal virtual async Task> SendHttpPostRequest(String uri, TRequest request, String accessToken, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri,request, accessToken, null, cancellationToken); + + internal virtual async Task> SendHttpPostRequest(String uri, TRequest request, List<(String header, String value)> additionalHeaders, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri, request, null, additionalHeaders, cancellationToken); + + internal virtual async Task> SendHttpPostRequest(String uri, TRequest request, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri, request, null, null, cancellationToken); + + internal virtual async Task SendHttpPostRequest(String uri, + String accessToken, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) + { + HttpRequestMessage requestMessage = new(HttpMethod.Post, uri); + if (String.IsNullOrEmpty(accessToken) == false) + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationSchemes.Bearer, accessToken); + + AddAdditionalHeaders(requestMessage, additionalHeaders); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + + // Process the response + Result result = await this.HandleResponseX(httpResponse, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + return Result.Success(); + } + + internal virtual async Task SendHttpPostRequest(String uri, String accessToken, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri, accessToken, null, cancellationToken); + + internal virtual async Task SendHttpPostRequest(String uri, List<(String header, String value)> additionalHeaders, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri, null, additionalHeaders, cancellationToken); + + internal virtual async Task SendHttpPostRequest(String uri, CancellationToken cancellationToken) => + await this.SendHttpPostRequest(uri, null, null, cancellationToken); + } +} diff --git a/ClientProxyBase/ClientProxyBase_PUT.cs b/ClientProxyBase/ClientProxyBase_PUT.cs new file mode 100644 index 00000000..b5b144c8 --- /dev/null +++ b/ClientProxyBase/ClientProxyBase_PUT.cs @@ -0,0 +1,68 @@ +using Newtonsoft.Json; +using Shared.Results; +using SimpleResults; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ClientProxyBase +{ + public abstract partial class ClientProxyBase + { + internal virtual async Task> SendHttpPutRequest(String uri, + TRequest request, + String accessToken, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) + { + HttpRequestMessage requestMessage = new(HttpMethod.Put, uri); + if (String.IsNullOrEmpty(accessToken) == false) + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationSchemes.Bearer, accessToken); + + AddAdditionalHeaders(requestMessage, additionalHeaders); + + if (request.GetType() == typeof(FormUrlEncodedContent)) + { + // Treat this specially + requestMessage.Content = request as FormUrlEncodedContent; + } + else + { + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + } + + // Make the Http Call here; + HttpResponseMessage httpResponse = await this.HttpClient.SendAsync(requestMessage, cancellationToken); + + // Process the response + Result result = await this.HandleResponseX(httpResponse, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + return Result.Success(); + } + + internal virtual async Task> SendHttpPutRequest(String uri, + TRequest request, + String accessToken, + CancellationToken cancellationToken) => + await this.SendHttpPutRequest(uri, request, accessToken, null, cancellationToken); + + internal virtual async Task> SendHttpPutRequest(String uri, + TRequest request, + List<(String header, String value)> additionalHeaders, + CancellationToken cancellationToken) => + await this.SendHttpPutRequest(uri, request, null, additionalHeaders, cancellationToken); + + internal virtual async Task> SendHttpPutRequest(String uri, + TRequest request, + CancellationToken cancellationToken) => + await this.SendHttpPutRequest(uri, request, null, null, cancellationToken); + } +} diff --git a/Shared.Tests/HttpMethodTests.cs b/Shared.Tests/HttpMethodTests.cs new file mode 100644 index 00000000..b82a5f69 --- /dev/null +++ b/Shared.Tests/HttpMethodTests.cs @@ -0,0 +1,926 @@ +using Newtonsoft.Json; +using RichardSzalay.MockHttp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using SimpleResults; +using Xunit; + +namespace Shared.Tests +{ + public class HttpService : ClientProxyBase.ClientProxyBase { + public HttpService(HttpClient httpClient) : base(httpClient) { + } + } + public class SampleResponse + { + public string Message { get; set; } + public int Value { get; set; } + } + + public class SampleRequest + { + public string Name { get; set; } + public int Id { get; set; } + } + + public class HttpServiceTests + { + private readonly MockHttpMessageHandler _mockHttp; + private readonly HttpService _service; + + public HttpServiceTests() + { + _mockHttp = new MockHttpMessageHandler(); + var client = _mockHttp.ToHttpClient(); + _service = new HttpService(client); + } + + [Fact] + public async Task SendHttpGetRequest_SuccessfulResponse_ShouldReturnTypedObject() + { + var payload = new SampleResponse { Message = "Hello", Value = 42 }; + _mockHttp.When(HttpMethod.Get, "https://api/success") + .Respond("application/json", JsonConvert.SerializeObject(payload)); + + var result = await _service.SendHttpGetRequest( + "https://api/success", "token", CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.Message.ShouldBe("Hello"); + result.Data.Value.ShouldBe(42); + } + + [Fact] + public async Task SendHttpGetRequest_ShouldAddBearerHeader_WhenAccessTokenPresent() + { + // Arrange + const string expectedToken = "token123"; + const string expectedUri = "https://api/auth"; + + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Get, expectedUri) + .WithHeaders("Authorization", $"Bearer {expectedToken}") + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new SampleResponse + { + Message = "Authorized", + Value = 1 + }), System.Text.Encoding.UTF8, "application/json") + }; + }); + + // Act + var result = await _service.SendHttpGetRequest( + expectedUri, expectedToken, CancellationToken.None); + + // Assert + result.IsSuccess.ShouldBeTrue(); + _mockHttp.VerifyNoOutstandingExpectation(); + + capturedRequest.ShouldNotBeNull(); + capturedRequest.Headers.Authorization.ShouldNotBeNull(); + capturedRequest.Headers.Authorization.Scheme.ShouldBe("Bearer"); + capturedRequest.Headers.Authorization.Parameter.ShouldBe(expectedToken); + } + + [Fact] + public async Task SendHttpGetRequest_ShouldAddAllAdditionalHeaders_ToRequest() + { + const string expectedUri = "https://api/headers"; + var headers = new List<(string, string)> + { + ("X-Test-Header", "Value1"), + ("X-Custom", "12345") + }; + + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Get, expectedUri) + .WithHeaders("X-Test-Header", "Value1") + .WithHeaders("X-Custom", "12345") + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new SampleResponse + { + Message = "Header check", + Value = 1 + }), System.Text.Encoding.UTF8, "application/json") + }; + }); + + var result = await _service.SendHttpGetRequest( + expectedUri, headers, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.Message.ShouldBe("Header check"); + _mockHttp.VerifyNoOutstandingExpectation(); + + capturedRequest.ShouldNotBeNull(); + capturedRequest.Headers.Contains("X-Test-Header").ShouldBeTrue(); + capturedRequest.Headers.GetValues("X-Test-Header").ShouldContain("Value1"); + capturedRequest.Headers.Contains("X-Custom").ShouldBeTrue(); + capturedRequest.Headers.GetValues("X-Custom").ShouldContain("12345"); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest, ResultStatus.Invalid)] + [InlineData(HttpStatusCode.Forbidden, ResultStatus.Forbidden)] + [InlineData(HttpStatusCode.Unauthorized, ResultStatus.Unauthorized)] + [InlineData(HttpStatusCode.NotFound, ResultStatus.NotFound)] + [InlineData(HttpStatusCode.Conflict, ResultStatus.Conflict)] + [InlineData(HttpStatusCode.InternalServerError, ResultStatus.CriticalError)] + [InlineData(HttpStatusCode.GatewayTimeout, ResultStatus.Failure)] + public async Task SendHttpGetRequest_FailureBranches_ShouldReturnFailure(HttpStatusCode code, ResultStatus expectedStatus) + { + _mockHttp.When(HttpMethod.Get, "https://api/fail") + .Respond(code, "application/json", JsonConvert.SerializeObject(new { Text = "Error" })); + + var result = await _service.SendHttpGetRequest("https://api/fail", CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(expectedStatus); + } + + [Fact] + public async Task SendHttpGetRequest_SuccessfulResponse_ShouldReturnSuccess() + { + var data = new SampleResponse { Message = "OK", Value = 10 }; + _mockHttp.When(HttpMethod.Get, "https://api/ok") + .Respond(HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(data)); + + var result = await _service.SendHttpGetRequest( + "https://api/ok", CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.Message.ShouldBe("OK"); + result.Data.Value.ShouldBe(10); + } + + [Fact] + public async Task SendHttpGetRequest_AllOverloads_ShouldCallBaseMethod() + { + var data = new SampleResponse { Message = "World", Value = 1 }; + _mockHttp.When(HttpMethod.Get, "https://api/base") + .Respond("application/json", JsonConvert.SerializeObject(data)); + + var r1 = await _service.SendHttpGetRequest("https://api/base", "token", CancellationToken.None); + var r2 = await _service.SendHttpGetRequest("https://api/base", new List<(string, string)>(), CancellationToken.None); + var r3 = await _service.SendHttpGetRequest("https://api/base", CancellationToken.None); + + r1.IsSuccess.ShouldBeTrue(); + r2.IsSuccess.ShouldBeTrue(); + r3.IsSuccess.ShouldBeTrue(); + + r1.Data.Message.ShouldBe("World"); + r2.Data.Message.ShouldBe("World"); + r3.Data.Message.ShouldBe("World"); + } + + [Fact] + public async Task SendHttpDeleteRequest_SuccessfulResponse_ShouldReturnSuccess() + { + _mockHttp.When(HttpMethod.Delete, "https://api/delete-success") + .Respond(HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { message = "deleted" })); + + var result = await _service.SendHttpDeleteRequest("https://api/delete-success", CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public async Task SendHttpDeleteRequest_ShouldAddBearerHeader_WhenAccessTokenPresent() + { + const string expectedToken = "delete-token-123"; + const string expectedUri = "https://api/delete-auth"; + + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Delete, expectedUri) + .WithHeaders("Authorization", $"Bearer {expectedToken}") + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { deleted = true }), System.Text.Encoding.UTF8, "application/json") + }; + }); + + var result = await _service.SendHttpDeleteRequest(expectedUri, expectedToken, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.ShouldNotBeNull(); + capturedRequest.Headers.Authorization.ShouldNotBeNull(); + capturedRequest.Headers.Authorization.Scheme.ShouldBe("Bearer"); + capturedRequest.Headers.Authorization.Parameter.ShouldBe(expectedToken); + } + + [Fact] + public async Task SendHttpDeleteRequest_ShouldAddAllAdditionalHeaders_ToRequest() + { + const string expectedUri = "https://api/delete-headers"; + var headers = new List<(string, string)> + { + ("X-Test-Delete", "Del1"), + ("X-Delete-Custom", "XYZ") + }; + + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Delete, expectedUri) + .WithHeaders("X-Test-Delete", "Del1") + .WithHeaders("X-Delete-Custom", "XYZ") + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { deleted = true }), System.Text.Encoding.UTF8, "application/json") + }; + }); + + var result = await _service.SendHttpDeleteRequest(expectedUri, headers, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + _mockHttp.VerifyNoOutstandingExpectation(); + + capturedRequest.ShouldNotBeNull(); + capturedRequest.Headers.Contains("X-Test-Delete").ShouldBeTrue(); + capturedRequest.Headers.GetValues("X-Test-Delete").ShouldContain("Del1"); + capturedRequest.Headers.Contains("X-Delete-Custom").ShouldBeTrue(); + capturedRequest.Headers.GetValues("X-Delete-Custom").ShouldContain("XYZ"); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest, ResultStatus.Invalid)] + [InlineData(HttpStatusCode.Forbidden, ResultStatus.Forbidden)] + [InlineData(HttpStatusCode.Unauthorized, ResultStatus.Unauthorized)] + [InlineData(HttpStatusCode.NotFound, ResultStatus.NotFound)] + [InlineData(HttpStatusCode.Conflict, ResultStatus.Conflict)] + [InlineData(HttpStatusCode.InternalServerError, ResultStatus.CriticalError)] + [InlineData(HttpStatusCode.GatewayTimeout, ResultStatus.Failure)] + public async Task SendHttpDeleteRequest_FailureBranches_ShouldReturnFailure(HttpStatusCode code, ResultStatus expectedStatus) + { + _mockHttp.When(HttpMethod.Delete, "https://api/delete-fail") + .Respond(code, "application/json", JsonConvert.SerializeObject(new { Text = "Error" })); + + var result = await _service.SendHttpDeleteRequest("https://api/delete-fail", CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(expectedStatus); + } + + [Fact] + public async Task SendHttpDeleteRequest_AllDeleteOverloads_ShouldCallBaseMethod() + { + const string uri = "https://api/delete-overload"; + _mockHttp.When(HttpMethod.Delete, uri) + .Respond(HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { ok = true })); + + var r1 = await _service.SendHttpDeleteRequest(uri, "token", CancellationToken.None); + var r2 = await _service.SendHttpDeleteRequest(uri, new List<(string, string)>(), CancellationToken.None); + var r3 = await _service.SendHttpDeleteRequest(uri, CancellationToken.None); + + r1.IsSuccess.ShouldBeTrue(); + r2.IsSuccess.ShouldBeTrue(); + r3.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public async Task SendHttpPutRequest_ShouldSendJsonBody_WhenRequestIsObject() + { + var uri = "https://api/put-json"; + var requestObject = new SampleRequest { Name = "Test", Id = 123 }; + + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Put, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { message = "ok" }), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPutRequest(uri, requestObject, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + + capturedRequest.ShouldNotBeNull(); + capturedRequest.Content.ShouldNotBeNull(); + var body = await capturedRequest.Content.ReadAsStringAsync(); + body.ShouldContain("\"Name\":\"Test\""); + body.ShouldContain("\"Id\":123"); + capturedRequest.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); + } + + [Fact] + public async Task SendHttpPutRequest_ShouldUseFormUrlEncodedContent_WhenRequestIsFormContent() + { + var uri = "https://api/put-form"; + var formContent = new FormUrlEncodedContent(new[] + { + new KeyValuePair("username", "admin"), + new KeyValuePair("password", "1234") + }); + + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Put, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { ok = true }), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPutRequest(uri, formContent, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.ShouldNotBeNull(); + capturedRequest.Content.ShouldBeOfType(); + } + + [Fact] + public async Task SendHttpPutRequest_ShouldAddBearerHeader_WhenAccessTokenPresent() + { + const string expectedToken = "put-token"; + const string uri = "https://api/put-auth"; + var payload = new SampleRequest { Name = "Authorized", Id = 1 }; + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Put, uri) + .WithHeaders("Authorization", $"Bearer {expectedToken}") + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { ok = true }), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPutRequest(uri, payload, expectedToken, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.Headers.Authorization.ShouldNotBeNull(); + capturedRequest.Headers.Authorization.Scheme.ShouldBe("Bearer"); + capturedRequest.Headers.Authorization.Parameter.ShouldBe(expectedToken); + } + + [Fact] + public async Task SendHttpPutRequest_ShouldAddAllAdditionalHeaders_ToRequest() + { + const string uri = "https://api/put-headers"; + var headers = new List<(string, string)> + { + ("X-Put-Test", "1"), + ("X-Custom-Header", "ABC") + }; + + var payload = new SampleRequest { Name = "HeaderCheck", Id = 55 }; + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Put, uri) + .WithHeaders("X-Put-Test", "1") + .WithHeaders("X-Custom-Header", "ABC") + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { ok = true }), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPutRequest(uri, payload, headers, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + _mockHttp.VerifyNoOutstandingExpectation(); + + capturedRequest.Headers.Contains("X-Put-Test").ShouldBeTrue(); + capturedRequest.Headers.GetValues("X-Custom-Header").ShouldContain("ABC"); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest, ResultStatus.Invalid)] + [InlineData(HttpStatusCode.Forbidden, ResultStatus.Forbidden)] + [InlineData(HttpStatusCode.Unauthorized, ResultStatus.Unauthorized)] + [InlineData(HttpStatusCode.NotFound, ResultStatus.NotFound)] + [InlineData(HttpStatusCode.Conflict, ResultStatus.Conflict)] + [InlineData(HttpStatusCode.InternalServerError, ResultStatus.CriticalError)] + [InlineData(HttpStatusCode.GatewayTimeout, ResultStatus.Failure)] + public async Task SendHttpPutRequest_FailureBranches_ShouldReturnFailure(HttpStatusCode code, ResultStatus expectedStatus) + { + const string uri = "https://api/put-fail"; + var payload = new SampleRequest { Name = "Fail", Id = 99 }; + + _mockHttp.When(HttpMethod.Put, uri) + .Respond(code, "application/json", JsonConvert.SerializeObject(new { Text = "Error" })); + + var result = await _service.SendHttpPutRequest(uri, payload, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(expectedStatus); + } + + [Fact] + public async Task SendHttpPutRequest_AllOverloads_ShouldCallBaseMethod() + { + const string uri = "https://api/put-overload"; + var payload = new SampleRequest { Name = "Overload", Id = 1 }; + + _mockHttp.When(HttpMethod.Put, uri) + .Respond(HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { ok = true })); + + var r1 = await _service.SendHttpPutRequest(uri, payload, "token", CancellationToken.None); + var r2 = await _service.SendHttpPutRequest(uri, payload, new List<(string, string)>(), CancellationToken.None); + var r3 = await _service.SendHttpPutRequest(uri, payload, CancellationToken.None); + + r1.IsSuccess.ShouldBeTrue(); + r2.IsSuccess.ShouldBeTrue(); + r3.IsSuccess.ShouldBeTrue(); + } + + [Fact] + public async Task SendHttpPatchRequest_ShouldSendJsonBody_WhenRequestIsObject() + { + var uri = "https://api/patch-json"; + var requestObject = new SampleRequest { Name = "PatchTest", Id = 101 }; + + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Patch, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { message = "ok" }), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPatchRequest(uri, requestObject, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + + capturedRequest.ShouldNotBeNull(); + capturedRequest.Content.ShouldNotBeNull(); + var body = await capturedRequest.Content.ReadAsStringAsync(); + body.ShouldContain("\"Name\":\"PatchTest\""); + body.ShouldContain("\"Id\":101"); + capturedRequest.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); + } + + [Fact] + public async Task SendHttpPatchRequest_ShouldUseFormUrlEncodedContent_WhenRequestIsFormContent() + { + var uri = "https://api/patch-form"; + var formContent = new FormUrlEncodedContent(new[] + { + new KeyValuePair("username", "patcher"), + new KeyValuePair("password", "secret") + }); + + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Patch, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { ok = true }), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPatchRequest(uri, formContent, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.ShouldNotBeNull(); + capturedRequest.Content.ShouldBeOfType(); + } + + [Fact] + public async Task SendHttpPatchRequest_ShouldAddBearerHeader_WhenAccessTokenPresent() + { + const string expectedToken = "patch-token"; + const string uri = "https://api/patch-auth"; + var payload = new SampleRequest { Name = "AuthorizedPatch", Id = 999 }; + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Patch, uri) + .WithHeaders("Authorization", $"Bearer {expectedToken}") + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { ok = true }), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPatchRequest(uri, payload, expectedToken, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.Headers.Authorization.ShouldNotBeNull(); + capturedRequest.Headers.Authorization.Scheme.ShouldBe("Bearer"); + capturedRequest.Headers.Authorization.Parameter.ShouldBe(expectedToken); + } + + [Fact] + public async Task SendHttpPatchRequest_ShouldAddAllAdditionalHeaders_ToRequest() + { + const string uri = "https://api/patch-headers"; + var headers = new List<(string, string)> + { + ("X-Patch-Test", "PATCH1"), + ("X-Custom-Header", "XYZ") + }; + + var payload = new SampleRequest { Name = "HeaderPatch", Id = 45 }; + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Patch, uri) + .WithHeaders("X-Patch-Test", "PATCH1") + .WithHeaders("X-Custom-Header", "XYZ") + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(new { ok = true }), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPatchRequest(uri, payload, headers, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + _mockHttp.VerifyNoOutstandingExpectation(); + + capturedRequest.Headers.Contains("X-Patch-Test").ShouldBeTrue(); + capturedRequest.Headers.GetValues("X-Custom-Header").ShouldContain("XYZ"); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest, ResultStatus.Invalid)] + [InlineData(HttpStatusCode.Forbidden, ResultStatus.Forbidden)] + [InlineData(HttpStatusCode.Unauthorized, ResultStatus.Unauthorized)] + [InlineData(HttpStatusCode.NotFound, ResultStatus.NotFound)] + [InlineData(HttpStatusCode.Conflict, ResultStatus.Conflict)] + [InlineData(HttpStatusCode.InternalServerError, ResultStatus.CriticalError)] + [InlineData(HttpStatusCode.GatewayTimeout, ResultStatus.Failure)] + public async Task SendHttpPatchRequest_FailureBranches_ShouldReturnFailure(HttpStatusCode code, ResultStatus expectedStatus) + { + const string uri = "https://api/patch-fail"; + var payload = new SampleRequest { Name = "FailPatch", Id = 88 }; + + _mockHttp.When(HttpMethod.Patch, uri) + .Respond(code, "application/json", JsonConvert.SerializeObject(new { Text = "Error" })); + + var result = await _service.SendHttpPatchRequest(uri, payload, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(expectedStatus); + } + + [Fact] + public async Task SendHttpPatchRequest_AllOverloads_ShouldCallBaseMethod() + { + const string uri = "https://api/patch-overload"; + var payload = new SampleRequest { Name = "OverloadPatch", Id = 1 }; + + _mockHttp.When(HttpMethod.Patch, uri) + .Respond(HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { ok = true })); + + var r1 = await _service.SendHttpPatchRequest(uri, payload, "token", CancellationToken.None); + var r2 = await _service.SendHttpPatchRequest(uri, payload, new List<(string, string)>(), CancellationToken.None); + var r3 = await _service.SendHttpPatchRequest(uri, payload, CancellationToken.None); + + r1.IsSuccess.ShouldBeTrue(); + r2.IsSuccess.ShouldBeTrue(); + r3.IsSuccess.ShouldBeTrue(); + } + + // ----------------------------- + // POST tests + // ----------------------------- + [Fact] + public async Task SendHttpPostRequest_TResponse_ShouldReturnDeserializedResponse() + { + const string uri = "https://api/post-generic-response"; + + var expected = new SampleResponse { Message = "Success", Value = 200 }; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(HttpStatusCode.OK, + "application/json", + JsonConvert.SerializeObject(expected)); + + var result = await _service.SendHttpPostRequest(uri, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.Message.ShouldBe("Success"); + result.Data.Value.ShouldBe(200); + } + + [Fact] + public async Task SendHttpPostRequest_TResponse_ShouldIncludeBearerHeader_WhenAccessTokenProvided() + { + const string uri = "https://api/post-bearer"; + const string token = "post-token"; + HttpRequestMessage capturedRequest = null; + + var expected = new SampleResponse { Message = "OK", Value = 123 }; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(expected), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPostRequest(uri, token, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.Headers.Authorization.ShouldNotBeNull(); + capturedRequest.Headers.Authorization.Scheme.ShouldBe("Bearer"); + capturedRequest.Headers.Authorization.Parameter.ShouldBe(token); + } + + [Fact] + public async Task SendHttpPostRequest_TResponse_ShouldIncludeAdditionalHeaders() + { + const string uri = "https://api/post-headers"; + var headers = new List<(string, string)> { ("X-Extra", "123"), ("X-Test", "ABC") }; + + var expected = new SampleResponse { Message = "OK", Value = 123 }; + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(expected), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPostRequest(uri, headers, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.Headers.Contains("X-Extra").ShouldBeTrue(); + capturedRequest.Headers.Contains("X-Test").ShouldBeTrue(); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest, ResultStatus.Invalid)] + [InlineData(HttpStatusCode.Forbidden, ResultStatus.Forbidden)] + [InlineData(HttpStatusCode.Unauthorized, ResultStatus.Unauthorized)] + [InlineData(HttpStatusCode.NotFound, ResultStatus.NotFound)] + [InlineData(HttpStatusCode.Conflict, ResultStatus.Conflict)] + [InlineData(HttpStatusCode.InternalServerError, ResultStatus.CriticalError)] + [InlineData(HttpStatusCode.GatewayTimeout, ResultStatus.Failure)] + public async Task SendHttpPostRequest_TResponse_FailureBranches_ShouldReturnFailure(HttpStatusCode code, ResultStatus expectedStatus) + { + const string uri = "https://api/post-fail"; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(code, "application/json", JsonConvert.SerializeObject(new { Text = "Error" })); + + var result = await _service.SendHttpPostRequest(uri, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(expectedStatus); + } + + // ----------------------------- + // POST tests + // ----------------------------- + [Fact] + public async Task SendHttpPostRequest_TRequestTResponse_ShouldSendJsonBody_AndReturnResponse() + { + const string uri = "https://api/post-json-body"; + var request = new SampleRequest { Name = "John", Id = 42 }; + var expected = new SampleResponse { Message = "Created", Value = 201 }; + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(expected), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPostRequest(uri, request, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.Message.ShouldBe("Created"); + result.Data.Value.ShouldBe(201); + + var body = await capturedRequest.Content.ReadAsStringAsync(); + body.ShouldContain("\"Name\":\"John\""); + body.ShouldContain("\"Id\":42"); + capturedRequest.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); + } + + [Fact] + public async Task SendHttpPostRequest_TRequestTResponse_ShouldUseFormUrlEncodedContent() + { + const string uri = "https://api/post-form-body"; + var formContent = new FormUrlEncodedContent(new[] + { + new KeyValuePair("field1", "value1"), + new KeyValuePair("field2", "value2") + }); + + var expected = new SampleResponse { Message = "Accepted", Value = 202 }; + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(expected), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPostRequest(uri, formContent, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.Content.ShouldBeOfType(); + } + + [Fact] + public async Task SendHttpPostRequest_TRequestTResponse_ShouldIncludeBearerHeader_WhenAccessTokenProvided() + { + const string uri = "https://api/post-body-auth"; + const string token = "auth-token"; + var request = new SampleRequest { Name = "Auth", Id = 7 }; + var expected = new SampleResponse { Message = "OK", Value = 200 }; + HttpRequestMessage capturedRequest = null; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(req => + { + capturedRequest = req; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(expected), + Encoding.UTF8, + "application/json") + }; + }); + + var result = await _service.SendHttpPostRequest(uri, request, token, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.Headers.Authorization.ShouldNotBeNull(); + capturedRequest.Headers.Authorization.Parameter.ShouldBe(token); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest, ResultStatus.Invalid)] + [InlineData(HttpStatusCode.Forbidden, ResultStatus.Forbidden)] + [InlineData(HttpStatusCode.Unauthorized, ResultStatus.Unauthorized)] + [InlineData(HttpStatusCode.NotFound, ResultStatus.NotFound)] + [InlineData(HttpStatusCode.Conflict, ResultStatus.Conflict)] + [InlineData(HttpStatusCode.InternalServerError, ResultStatus.CriticalError)] + [InlineData(HttpStatusCode.GatewayTimeout, ResultStatus.Failure)] + public async Task SendHttpPostRequest_TRequestTResponse_FailureBranches_ShouldReturnFailure(HttpStatusCode code, ResultStatus expectedStatus) + { + const string uri = "https://api/post-body-fail"; + var request = new SampleRequest { Name = "Fail", Id = 123 }; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(code, "application/json", JsonConvert.SerializeObject(new { Text = "Error" })); + + var result = await _service.SendHttpPostRequest(uri, request, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(expectedStatus); + } + + // ----------------------------- + // POST (no generic) tests + // ----------------------------- + [Fact] + public async Task SendHttpPostRequest_NoGenerics_ShouldReturnSuccess() + { + const string uri = "https://api/post-void"; + _mockHttp.When(HttpMethod.Post, uri) + .Respond(HttpStatusCode.OK, "application/json", "\"Done\""); + + var result = await _service.SendHttpPostRequest(uri, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest, ResultStatus.Invalid)] + [InlineData(HttpStatusCode.Forbidden, ResultStatus.Forbidden)] + [InlineData(HttpStatusCode.Unauthorized, ResultStatus.Unauthorized)] + [InlineData(HttpStatusCode.NotFound, ResultStatus.NotFound)] + [InlineData(HttpStatusCode.Conflict, ResultStatus.Conflict)] + [InlineData(HttpStatusCode.InternalServerError, ResultStatus.CriticalError)] + [InlineData(HttpStatusCode.GatewayTimeout, ResultStatus.Failure)] + public async Task SendHttpPostRequest_NoGenerics_FailureBranches_ShouldReturnFailure(HttpStatusCode code, ResultStatus expectedStatus) + { + const string uri = "https://api/post-void-fail"; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(code, "application/json", JsonConvert.SerializeObject(new { Text = "Error" })); + + var result = await _service.SendHttpPostRequest(uri, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(expectedStatus); + } + + // ----------------------------- + // Overload verification + // ----------------------------- + [Fact] + public async Task SendHttpPostRequest_AllOverloads_ShouldCallBaseMethodsSuccessfully() + { + const string uri = "https://api/post-overload"; + var request = new SampleRequest { Name = "Multi", Id = 9 }; + var expected = new SampleResponse { Message = "OK", Value = 200 }; + + _mockHttp.When(HttpMethod.Post, uri) + .Respond(HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(expected)); + + var r1 = await _service.SendHttpPostRequest(uri, "token", CancellationToken.None); + var r2 = await _service.SendHttpPostRequest(uri, new List<(string, string)>(), CancellationToken.None); + var r3 = await _service.SendHttpPostRequest(uri, CancellationToken.None); + var r4 = await _service.SendHttpPostRequest(uri, request, "token", CancellationToken.None); + var r5 = await _service.SendHttpPostRequest(uri, request, new List<(string, string)>(), CancellationToken.None); + var r6 = await _service.SendHttpPostRequest(uri, request, CancellationToken.None); + var r7 = await _service.SendHttpPostRequest(uri, "token", CancellationToken.None); + var r8 = await _service.SendHttpPostRequest(uri, new List<(string, string)>(), CancellationToken.None); + var r9 = await _service.SendHttpPostRequest(uri, CancellationToken.None); + + r1.IsSuccess.ShouldBeTrue(); + r2.IsSuccess.ShouldBeTrue(); + r3.IsSuccess.ShouldBeTrue(); + r4.IsSuccess.ShouldBeTrue(); + r5.IsSuccess.ShouldBeTrue(); + r6.IsSuccess.ShouldBeTrue(); + r7.IsSuccess.ShouldBeTrue(); + r8.IsSuccess.ShouldBeTrue(); + r9.IsSuccess.ShouldBeTrue(); + } + } +}