Skip to content

Commit

Permalink
feat: enable custom timeout on HttpClient (#534)
Browse files Browse the repository at this point in the history
* Add RequestTimeout property on Configuration

* Add RequestTimeout to HttpClient if filled

* Support request timeout on VonageHttpClient

* Refactor RequestTimeout tests
  • Loading branch information
Tr00d authored Oct 12, 2023
1 parent a550a7b commit e9f75d8
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 181 deletions.
34 changes: 34 additions & 0 deletions Vonage.Common.Test/Client/VonageHttpClientTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Net;
using System.Net.Http.Headers;
using AutoFixture;
using FluentAssertions;
using FsCheck;
using FsCheck.Xunit;
using Vonage.Common.Client;
Expand All @@ -26,6 +27,10 @@ public VonageHttpClientTest()
this.request = BuildRequest();
}

[Fact]
public async Task SendAsync_ShouldThrowException_GivenOperationExceedsTimeout() =>
await this.VerifyReturnsFailureGivenOperationExceedsTimeout(client => client.SendAsync(this.request));

[Property]
public Property SendAsync_VerifyReturnsFailureGivenApiResponseIsError() =>
this.VerifyReturnsFailureGivenApiResponseIsError(BuildExpectedRequest(),
Expand All @@ -51,6 +56,11 @@ public async Task SendAsync_VerifyReturnsUnitGivenApiResponseIsSuccess() =>
await this.VerifyReturnsExpectedValueGivenApiResponseIsSuccess(BuildExpectedRequest(),
configuration => new VonageHttpClient(configuration, this.serializer).SendAsync(this.request));

[Fact]
public async Task SendWithoutHeaderAsync_ShouldThrowException_GivenOperationExceedsTimeout() =>
await this.VerifyReturnsFailureGivenOperationExceedsTimeout(client =>
client.SendWithoutHeadersAsync(this.request));

[Property]
public Property SendWithoutHeaderAsync_VerifyReturnsFailureGivenApiResponseIsError() =>
this.VerifyReturnsFailureGivenApiResponseIsError(BuildExpectedRequest(),
Expand All @@ -73,6 +83,11 @@ await this.VerifyReturnsExpectedValueGivenApiResponseIsSuccess(BuildExpectedRequ
configuration =>
new VonageHttpClient(configuration, this.serializer).SendWithoutHeadersAsync(this.request));

[Fact]
public async Task SendWithRawResponseAsync_ShouldThrowException_GivenOperationExceedsTimeout() =>
await this.VerifyReturnsFailureGivenOperationExceedsTimeout(client =>
client.SendWithRawResponseAsync(this.request));

[Fact]
public async Task SendWithRawResponseAsync_VerifyReturnsExpectedValueGivenApiResponseIsSuccess() =>
await this.VerifyReturnsRawContentGivenApiResponseIsSuccess(BuildExpectedRequest(),
Expand Down Expand Up @@ -102,6 +117,11 @@ await this.VerifyReturnsFailureGivenTokenGenerationFails(configuration =>
new VonageHttpClient(configuration, this.serializer).SendWithRawResponseAsync(
this.request));

[Fact]
public async Task SendWithResponseAsync_ShouldThrowException_GivenOperationExceedsTimeout() =>
await this.VerifyReturnsFailureGivenOperationExceedsTimeout(client =>
client.SendWithResponseAsync<FakeRequest, FakeResponse>(this.request));

[Fact]
public async Task SendWithResponseAsync_VerifyReturnsExpectedValueGivenApiResponseIsSuccess() =>
await this.VerifyReturnsExpectedValueGivenApiResponseIsSuccess(BuildExpectedRequest(),
Expand Down Expand Up @@ -220,6 +240,20 @@ private Property VerifyReturnsFailureGivenErrorCannotBeParsed<TResponse>(
jsonError));
});

private async Task VerifyReturnsFailureGivenOperationExceedsTimeout<TResponse>(
Func<VonageHttpClient, Task<Result<TResponse>>> operation)
{
var httpClient = FakeHttpRequestHandler.Build(HttpStatusCode.OK).WithDelay(TimeSpan.FromMilliseconds(500))
.ToHttpClient();
httpClient.Timeout = TimeSpan.FromMilliseconds(250);
var client =
new VonageHttpClient(
new VonageHttpClientConfiguration(httpClient, new AuthenticationHeaderValue("Anonymous"),
this.fixture.Create<string>()), this.serializer);
var act = () => operation(client);
await act.Should().ThrowAsync<Exception>();
}

private async Task VerifyReturnsFailureGivenRequestIsFailure<TRes>(
Func<VonageHttpClientConfiguration, Result<FakeRequest>, Task<Result<TRes>>> operation)
{
Expand Down
8 changes: 8 additions & 0 deletions Vonage.Common.Test/TestHelpers/FakeHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class FakeHttpRequestHandler : HttpMessageHandler
private readonly HttpStatusCode statusCode;
private Maybe<ExpectedRequest> expectedRequest = Maybe<ExpectedRequest>.None;
private Maybe<string> responseContent = Maybe<string>.None;
private Maybe<TimeSpan> responseDelay;
private readonly Uri baseUri = new("http://fake-host/api");

private FakeHttpRequestHandler(HttpStatusCode code) => this.statusCode = code;
Expand All @@ -22,6 +23,12 @@ public class FakeHttpRequestHandler : HttpMessageHandler
BaseAddress = this.baseUri,
};

public FakeHttpRequestHandler WithDelay(TimeSpan delay)
{
this.responseDelay = delay;
return this;
}

public FakeHttpRequestHandler WithExpectedRequest(ExpectedRequest expected)
{
this.expectedRequest = expected;
Expand Down Expand Up @@ -72,6 +79,7 @@ private static async Task<ReceivedRequest> ParseIncomingRequest(HttpRequestMessa
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
await Task.Delay(this.responseDelay.IfNone(TimeSpan.Zero), cancellationToken);
var incomingRequest = await ParseIncomingRequest(request);
this.expectedRequest.IfSome(expected => this.CompareRequests(incomingRequest, expected));
return this.CreateResponseMessage();
Expand Down
247 changes: 129 additions & 118 deletions Vonage.Test.Unit/ConfigurationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,139 +2,150 @@
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Vonage.Common.Test.Extensions;
using Vonage.Cryptography;
using Xunit;

namespace Vonage.Test.Unit
{
public class ConfigurationTest
{
[Fact]
public void BuildCredentials_ShouldCreateEmptyCredentials_GivenConfigurationContainsNoElement()
{
var credentials = Configuration.FromConfiguration(new ConfigurationBuilder().Build()).BuildCredentials();
credentials.ApiKey.Should().BeEmpty();
credentials.ApiSecret.Should().BeEmpty();
credentials.ApplicationId.Should().BeEmpty();
credentials.ApplicationKey.Should().BeEmpty();
credentials.SecuritySecret.Should().BeEmpty();
credentials.Method.Should().Be(SmsSignatureGenerator.Method.md5hash);
}
public class ConfigurationTest
{
[Fact]
public void BuildCredentials_ShouldCreateEmptyCredentials_GivenConfigurationContainsNoElement()
{
var credentials = Configuration.FromConfiguration(new ConfigurationBuilder().Build()).BuildCredentials();
credentials.ApiKey.Should().BeEmpty();
credentials.ApiSecret.Should().BeEmpty();
credentials.ApplicationId.Should().BeEmpty();
credentials.ApplicationKey.Should().BeEmpty();
credentials.SecuritySecret.Should().BeEmpty();
credentials.Method.Should().Be(SmsSignatureGenerator.Method.md5hash);
}

[Fact]
public void FromConfiguration_ShouldCreateEmptyConfiguration_GivenConfigurationContainsNoElement()
{
var configuration = Configuration.FromConfiguration(new ConfigurationBuilder().Build());
configuration.ApiKey.Should().BeEmpty();
configuration.ApiSecret.Should().BeEmpty();
configuration.ApplicationId.Should().BeEmpty();
configuration.ApplicationKey.Should().BeEmpty();
configuration.SecuritySecret.Should().BeEmpty();
configuration.SigningMethod.Should().BeEmpty();
configuration.UserAgent.Should().BeEmpty();
configuration.EuropeApiUrl.Should().Be(new Uri("https://api-eu.vonage.com"));
configuration.NexmoApiUrl.Should().Be(new Uri("https://api.nexmo.com"));
configuration.RestApiUrl.Should().Be(new Uri("https://rest.nexmo.com"));
configuration.VideoApiUrl.Should().Be(new Uri("https://video.api.vonage.com"));
}
[Fact]
public void FromConfiguration_ShouldCreateEmptyConfiguration_GivenConfigurationContainsNoElement()
{
var configuration = Configuration.FromConfiguration(new ConfigurationBuilder().Build());
configuration.ApiKey.Should().BeEmpty();
configuration.ApiSecret.Should().BeEmpty();
configuration.ApplicationId.Should().BeEmpty();
configuration.ApplicationKey.Should().BeEmpty();
configuration.SecuritySecret.Should().BeEmpty();
configuration.SigningMethod.Should().BeEmpty();
configuration.UserAgent.Should().BeEmpty();
configuration.EuropeApiUrl.Should().Be(new Uri("https://api-eu.vonage.com"));
configuration.NexmoApiUrl.Should().Be(new Uri("https://api.nexmo.com"));
configuration.RestApiUrl.Should().Be(new Uri("https://rest.nexmo.com"));
configuration.VideoApiUrl.Should().Be(new Uri("https://video.api.vonage.com"));
configuration.RequestTimeout.Should().BeNone();
}

[Fact]
public void FromConfiguration_ShouldSetApiKey_GivenConfigurationContainsApiKey() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage_key", "RandomValue"},
})
.Build()).ApiKey.Should().Be("RandomValue");
[Fact]
public void FromConfiguration_ShouldSetApiKey_GivenConfigurationContainsApiKey() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage_key", "RandomValue"},
})
.Build()).ApiKey.Should().Be("RandomValue");

[Fact]
public void FromConfiguration_ShouldSetApiKSecret_GivenConfigurationContainsApiSecret() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage_secret", "RandomValue"},
})
.Build()).ApiSecret.Should().Be("RandomValue");
[Fact]
public void FromConfiguration_ShouldSetApiKSecret_GivenConfigurationContainsApiSecret() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage_secret", "RandomValue"},
})
.Build()).ApiSecret.Should().Be("RandomValue");

[Fact]
public void FromConfiguration_ShouldSetApplicationId_GivenConfigurationContainsApplicationId() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Application.Id", "RandomValue"},
})
.Build()).ApplicationId.Should().Be("RandomValue");
[Fact]
public void FromConfiguration_ShouldSetApplicationId_GivenConfigurationContainsApplicationId() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Application.Id", "RandomValue"},
})
.Build()).ApplicationId.Should().Be("RandomValue");

[Fact]
public void FromConfiguration_ShouldSetApplicationKey_GivenConfigurationContainsApplicationKey() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Application.Key", "RandomValue"},
})
.Build()).ApplicationKey.Should().Be("RandomValue");
[Fact]
public void FromConfiguration_ShouldSetApplicationKey_GivenConfigurationContainsApplicationKey() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Application.Key", "RandomValue"},
})
.Build()).ApplicationKey.Should().Be("RandomValue");

[Fact]
public void FromConfiguration_ShouldSetEuropeApiUrl_GivenConfigurationContainsEuropeApiUrl() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Url.Api.Europe", "https://api.vonage.com"},
})
.Build()).EuropeApiUrl.Should().Be(new Uri("https://api.vonage.com"));
[Fact]
public void FromConfiguration_ShouldSetEuropeApiUrl_GivenConfigurationContainsEuropeApiUrl() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Url.Api.Europe", "https://api.vonage.com"},
})
.Build()).EuropeApiUrl.Should().Be(new Uri("https://api.vonage.com"));

[Fact]
public void FromConfiguration_ShouldSetNexmoApiUrl_GivenConfigurationContainsNexmoApiUrl() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Url.Api", "https://api.vonage.com"},
})
.Build()).NexmoApiUrl.Should().Be(new Uri("https://api.vonage.com"));
[Fact]
public void FromConfiguration_ShouldSetNexmoApiUrl_GivenConfigurationContainsNexmoApiUrl() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Url.Api", "https://api.vonage.com"},
})
.Build()).NexmoApiUrl.Should().Be(new Uri("https://api.vonage.com"));

[Fact]
public void FromConfiguration_ShouldSetRestApiUrl_GivenConfigurationContainsRestApiUrl() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Url.Rest", "https://api.vonage.com"},
})
.Build()).RestApiUrl.Should().Be(new Uri("https://api.vonage.com"));
[Fact]
public void FromConfiguration_ShouldSetRequestTimeout_GivenConfigurationContainsRequestTimeout() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.RequestsTimeout", "100"},
})
.Build()).RequestTimeout.Should().BeSome(TimeSpan.FromSeconds(100));

[Fact]
public void FromConfiguration_ShouldSetSecuritySecret_GivenConfigurationContainsSecuritySecret() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.security_secret", "RandomValue"},
})
.Build()).SecuritySecret.Should().Be("RandomValue");
[Fact]
public void FromConfiguration_ShouldSetRestApiUrl_GivenConfigurationContainsRestApiUrl() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Url.Rest", "https://api.vonage.com"},
})
.Build()).RestApiUrl.Should().Be(new Uri("https://api.vonage.com"));

[Fact]
public void FromConfiguration_ShouldSetSigningMethod_GivenConfigurationContainsSigningMethod() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.signing_method", "sha512"},
})
.Build()).SigningMethod.Should().Be("sha512");
[Fact]
public void FromConfiguration_ShouldSetSecuritySecret_GivenConfigurationContainsSecuritySecret() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.security_secret", "RandomValue"},
})
.Build()).SecuritySecret.Should().Be("RandomValue");

[Fact]
public void FromConfiguration_ShouldSetUserAgent_GivenConfigurationContainsUserAgent() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.UserAgent", "RandomValue"},
})
.Build()).UserAgent.Should().Be("RandomValue");
[Fact]
public void FromConfiguration_ShouldSetSigningMethod_GivenConfigurationContainsSigningMethod() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.signing_method", "sha512"},
})
.Build()).SigningMethod.Should().Be("sha512");

[Fact]
public void FromConfiguration_ShouldSetVideoApiUrl_GivenConfigurationContainsVideoApiUrl() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Url.Api.Video", "https://api.vonage.com"},
})
.Build()).VideoApiUrl.Should().Be(new Uri("https://api.vonage.com"));
}
[Fact]
public void FromConfiguration_ShouldSetUserAgent_GivenConfigurationContainsUserAgent() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.UserAgent", "RandomValue"},
})
.Build()).UserAgent.Should().Be("RandomValue");

[Fact]
public void FromConfiguration_ShouldSetVideoApiUrl_GivenConfigurationContainsVideoApiUrl() =>
Configuration.FromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{"appSettings:Vonage.Url.Api.Video", "https://api.vonage.com"},
})
.Build()).VideoApiUrl.Should().Be(new Uri("https://api.vonage.com"));
}
}
Loading

0 comments on commit e9f75d8

Please # to comment.