Skip to content

Commit

Permalink
Add support for Timeout.InifiniteTimeSpan (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
evgenyvalavin authored Nov 15, 2022
1 parent 7955be3 commit 91408ed
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 67 deletions.
49 changes: 30 additions & 19 deletions src/Arbus.Network.UnitTests/Arbus.Network.UnitTests.csproj
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Arbus.Network\Arbus.Network.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Arbus.Network\Arbus.Network.csproj" />
</ItemGroup>

</Project>
<ItemGroup>
<Using Include="NUnit.Framework" />
<Using Include="Moq" />
</ItemGroup>

</Project>
17 changes: 11 additions & 6 deletions src/Arbus.Network.UnitTests/TestFixture.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
using Moq;

namespace Arbus.Network.UnitTests;
namespace Arbus.Network.UnitTests;

[TestFixture]
public abstract class TestFixture
{
private readonly MockRepository _mockRepository = new(default);

public Mock<T> CreateMock<T>(MockBehavior mockBehavior = MockBehavior.Strict) where T : class
=> _mockRepository.Create<T>(mockBehavior);
}
public Mock<T> CreateMock<T>(MockBehavior mockBehavior = MockBehavior.Strict, params object[] args) where T : class
=> _mockRepository.Create<T>(mockBehavior, args);

[TearDown]
public void AfterEachTest()
{
_mockRepository.VerifyAll();
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
using Arbus.Network.Abstractions;
using Moq;

namespace Arbus.Network.UnitTests;
namespace Arbus.Network.UnitTests.Tests;

public class HttpClientContextTests : TestFixture
{
[Test]
public async Task RunEndpointInternal_InvokesSendRequestWithCancellationTokenFromEndpoint()
{
using CancellationTokenSource cancellationTokenSource = new();
CancellationToken cancellationToken = cancellationTokenSource.Token;
using CancellationTokenSource cts = new();
CancellationToken cancellationToken = cts.Token;

var mockApiEndpoint = Mock.Of<ApiEndpoint>(x => x.CancellationToken == cancellationToken && x.Path == "http://localhost" && x.Method == HttpMethod.Get);
var mockDefaultHttpClient = CreateMock<INativeHttpClient>();
mockDefaultHttpClient.Setup(x => x.SendRequest(It.IsAny<HttpRequestMessage>(), cancellationToken)).ReturnsAsync(new HttpResponseMessage());
mockDefaultHttpClient.Setup(x => x.SendRequest(It.IsAny<HttpRequestMessage>(), cancellationToken, It.IsAny<HttpCompletionOption>())).ReturnsAsync(new HttpResponseMessage());

HttpClientContext httpClientContext = new(mockDefaultHttpClient.Object);

Expand All @@ -25,7 +24,7 @@ public async Task RunEndpointInternal_InvokesSendRequestWithDefaultCancellationT
{
var mockApiEndpoint = Mock.Of<ApiEndpoint>(x => x.Path == "http://localhost" && x.Method == HttpMethod.Get);
var mockDefaultHttpClient = CreateMock<INativeHttpClient>();
mockDefaultHttpClient.Setup(x => x.SendRequest(It.IsAny<HttpRequestMessage>(), default)).ReturnsAsync(new HttpResponseMessage());
mockDefaultHttpClient.Setup(x => x.SendRequest(It.IsAny<HttpRequestMessage>(), default, It.IsAny<HttpCompletionOption>())).ReturnsAsync(new HttpResponseMessage());

HttpClientContext httpClientContext = new(mockDefaultHttpClient.Object);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Arbus.Network.Abstractions;
using Arbus.Network.Exceptions;
using Arbus.Network.Extensions;

namespace Arbus.Network.UnitTests.Application;
namespace Arbus.Network.UnitTests.Tests;

public class NativeHttpClientTests : TestFixture
{
Expand Down Expand Up @@ -43,4 +44,39 @@ public void EnsureNetworkAvailable_NetworkAvailable_NoException()

Assert.DoesNotThrow(() => nativeHttpClient.EnsureNetworkAvailable());
}
}

[Test]
public void GetTimeoutCts_InfiniteTimeSpan_AssertNullCts()
{
using HttpRequestMessage timeout = new();
timeout.SetTimeout(Timeout.InfiniteTimeSpan);

var cts = NativeHttpClient.GetTimeoutCts(timeout, default);

Assert.IsNull(cts);
}

[Test]
public void GetTimeoutCts_NotInfiniteTimeSpan_AssertNotNUllCts()
{
using HttpRequestMessage timeout = new();
timeout.SetTimeout(TimeSpan.FromSeconds(1));

using var cts = NativeHttpClient.GetTimeoutCts(timeout, default);

Assert.NotNull(cts);
}

[Test]
public void GetTimeoutCts_CancellFirstToken_AssertSecondIsCancelled()
{
using HttpRequestMessage timeout = new();
timeout.SetTimeout(TimeSpan.FromSeconds(1));
using var cts1 = new CancellationTokenSource();
cts1.Cancel();

using var cts2 = NativeHttpClient.GetTimeoutCts(timeout, cts1.Token);

Assert.IsTrue(cts2?.Token.IsCancellationRequested);
}
}
1 change: 0 additions & 1 deletion src/Arbus.Network.UnitTests/Usings.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Arbus.Network/Abstractions/INativeHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
public interface INativeHttpClient
{
Task<HttpResponseMessage> SendRequest(HttpRequestMessage request, CancellationToken cancellationToken);
Task<HttpResponseMessage> SendRequest(HttpRequestMessage request, CancellationToken cancellationToken, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead);
Task<string> GetString(string url, TimeSpan? timeout = default);
Task<string> GetString(Uri uri, TimeSpan? timeout = default);
}
Expand Down
11 changes: 7 additions & 4 deletions src/Arbus.Network/Arbus.Network.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10</LangVersion>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<LangVersion>Latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand All @@ -13,6 +13,9 @@
<PackageProjectUrl>https://github.com/ArbusBiz/Arbus.Network</PackageProjectUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="System.Text.Json" Version="7.0.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' ">
<Compile Remove="IsExternalInit.cs"/>
</ItemGroup>
</Project>
26 changes: 19 additions & 7 deletions src/Arbus.Network/Extensions/HttpRequestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@

public static class HttpRequestExtensions
{
private const string _timeoutProperyKey = "Timeout";
private const int _defaultTimeoutInSeconds = 20;
private const string _key = "Timeout";
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(100);
#if NET6_0_OR_GREATER
private static HttpRequestOptionsKey<TimeSpan> _optionsKey = new(_key);
#endif

public static void SetTimeout(this HttpRequestMessage request, TimeSpan timeout)
=> request.Properties[_timeoutProperyKey] = timeout;
#if NETSTANDARD
=> request.Properties[_key] = timeout;
#else
=> request.Options.Set(_optionsKey, timeout);
#endif

public static TimeSpan GetTimeout(this HttpRequestMessage request)
{
return request.Properties.TryGetValue(_timeoutProperyKey, out object value)
? (TimeSpan)value
: TimeSpan.FromSeconds(_defaultTimeoutInSeconds);
#if NETSTANDARD
if (request.Properties.TryGetValue(_key, out object value))
return (TimeSpan)value;
return _defaultTimeout;
#else
return request.Options.TryGetValue(_optionsKey, out var value)
? value : _defaultTimeout;
#endif
}
}
}
1 change: 1 addition & 0 deletions src/Arbus.Network/Extensions/HttpResponseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public static class HttpResponseExtensions
{
[Obsolete("Do not use. Will be removed.")]
public static string ToReadableString(this HttpResponseMessage response)
=> $"\n{response.RequestMessage.Method} {response.RequestMessage.RequestUri} {response.StatusCode} {(int)response.StatusCode}";
}
2 changes: 1 addition & 1 deletion src/Arbus.Network/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel;

namespace System.Runtime.CompilerServices
namespace Arbus.Network
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
Expand Down
48 changes: 28 additions & 20 deletions src/Arbus.Network/NativeHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ namespace Arbus.Network;

public class NativeHttpClient : INativeHttpClient
{
private static readonly HttpClient _httpClient = new();
private static readonly HttpClient _httpClient = new()
{
Timeout = Timeout.InfiniteTimeSpan
};
private readonly INetworkManager _networkManager;

public NativeHttpClient(INetworkManager networkManager)
public NativeHttpClient(INetworkManager networkManager, ProductInfoHeaderValue userAgent) : this(networkManager)
{
_networkManager = networkManager;
_httpClient.DefaultRequestHeaders.UserAgent.Add(userAgent);
}
public NativeHttpClient(INetworkManager networkManager, ProductInfoHeaderValue userAgent)

public NativeHttpClient(INetworkManager networkManager)
{
_networkManager = networkManager;
_httpClient.DefaultRequestHeaders.UserAgent.Add(userAgent);

}

public Task<string> GetString(string uri, TimeSpan? timeout = null) => GetString(new Uri(uri), timeout);
Expand All @@ -34,24 +35,18 @@ public async Task<string> GetString(Uri uri, TimeSpan? timeout = null)
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}

public virtual Task<HttpResponseMessage> Send(HttpRequestMessage httpRequest, CancellationToken timeout, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead)
public virtual async Task<HttpResponseMessage> SendRequest(HttpRequestMessage request, CancellationToken cancellationToken, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead)
{
return _httpClient.SendAsync(httpRequest, httpCompletionOption, timeout);
}

public async Task<HttpResponseMessage> SendRequest(HttpRequestMessage request, CancellationToken cancellationToken)
{
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
using var timeoutCts = GetTimeoutCts(request, cancellationToken);
try
{
linkedTokenSource.CancelAfter(request.GetTimeout());
var response = await Send(request, linkedTokenSource.Token).ConfigureAwait(false);
var response = await _httpClient.SendAsync(request, httpCompletionOption, timeoutCts?.Token ?? cancellationToken).ConfigureAwait(false);
return await EnsureSuccessResponse(response).ConfigureAwait(false);
}
catch (Exception) when (cancellationToken.IsCancellationRequested is false)
{
EnsureNetworkAvailable();
EnsureNoTimeout(linkedTokenSource);
EnsureNoTimeout(timeoutCts);
throw;
}
finally
Expand All @@ -60,15 +55,28 @@ public async Task<HttpResponseMessage> SendRequest(HttpRequestMessage request, C
}
}

public static CancellationTokenSource? GetTimeoutCts(HttpRequestMessage request, CancellationToken cancellationToken)
{
var timeout = request.GetTimeout();
var hasTimeout = timeout != Timeout.InfiniteTimeSpan;
if (hasTimeout)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeout);
return cts;
}
return default;
}

public void EnsureNetworkAvailable()
{
if (_networkManager.IsNetworkAvailable is false)
throw new NoNetoworkConnectionException();
}

public static void EnsureNoTimeout(CancellationTokenSource linkedTokenSource)
public static void EnsureNoTimeout(CancellationTokenSource? cts)
{
if (linkedTokenSource.IsCancellationRequested)
if (cts != null && cts.IsCancellationRequested)
throw new HttpTimeoutException();
}

Expand All @@ -84,7 +92,7 @@ public virtual Task<HttpResponseMessage> HandleNotSuccessStatusCode(HttpResponse
return HandleAnyResponse(response);
}

public async Task<HttpResponseMessage> HandleProblemDetailsResponse(HttpResponseMessage response)
public static async Task<HttpResponseMessage> HandleProblemDetailsResponse(HttpResponseMessage response)
{
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var problemDetails = await DefaultJsonSerializer.DeserializeAsync<ProblemDetails>(responseStream).ConfigureAwait(false)
Expand Down

0 comments on commit 91408ed

Please sign in to comment.