Skip to content

Commit 94fc926

Browse files
authored
Fix pattern for tests that use HttpClient (#589)
This change introduces a better API for us to test the DaprClient or other HttpClient based APIs. This will resolve the flakiness problems that we're seeing with some of the actors tests. Fixes: #573 Fixes: #588 Additionally fixed an issue where DaprHttpInteractor was misuing HttpClientHandler. This would result in a new handler being created when it isn't needed.
1 parent 847cd31 commit 94fc926

26 files changed

+1630
-1314
lines changed

properties/IsExternalInit.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel;
5+
6+
namespace System.Runtime.CompilerServices
7+
{
8+
/// <summary>
9+
/// Reserved to be used by the compiler for tracking metadata.
10+
/// This class should not be used by developers in source code.
11+
/// </summary>
12+
[EditorBrowsable(EditorBrowsableState.Never)]
13+
internal static class IsExternalInit
14+
{
15+
}
16+
17+
// This is a polyfill for init only properties in netcoreapp3.1
18+
}

properties/dapr_managed_netcore.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
<Import Project="dapr_common.props" />
44
<PropertyGroup>
55
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
6+
<LangVersion>9.0</LangVersion>
67
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
78
<WarningLevel>4</WarningLevel>
89
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
910
</PropertyGroup>
1011

12+
<ItemGroup>
13+
<Compile Include="$(MSBuildThisFileDirectory)\IsExternalInit.cs" />
14+
</ItemGroup>
15+
1116
<!-- Cls Compliant -->
1217
<PropertyGroup>
1318
<AssemblyClsCompliant>true</AssemblyClsCompliant>

src/Dapr.Actors/Client/ActorProxyFactory.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Dapr.Actors.Client
1616
public class ActorProxyFactory : IActorProxyFactory
1717
{
1818
private ActorProxyOptions defaultOptions;
19-
private readonly HttpClientHandler handler;
19+
private readonly HttpMessageHandler handler;
2020

2121
/// <inheritdoc/>
2222
public ActorProxyOptions DefaultOptions
@@ -32,7 +32,16 @@ public ActorProxyOptions DefaultOptions
3232
/// <summary>
3333
/// Initializes a new instance of the <see cref="ActorProxyFactory"/> class.
3434
/// </summary>
35-
public ActorProxyFactory(ActorProxyOptions options = null, HttpClientHandler handler = null)
35+
[Obsolete("Use the constructor that accepts HttpMessageHandler. This will be removed in the future.")]
36+
public ActorProxyFactory(ActorProxyOptions options, HttpClientHandler handler)
37+
: this(options, (HttpMessageHandler)handler)
38+
{
39+
}
40+
41+
/// <summary>
42+
/// Initializes a new instance of the <see cref="ActorProxyFactory"/> class.
43+
/// </summary>
44+
public ActorProxyFactory(ActorProxyOptions options = null, HttpMessageHandler handler = null)
3645
{
3746
this.defaultOptions = options ?? new ActorProxyOptions();
3847
this.handler = handler;

src/Dapr.Actors/DaprHttpInteractor.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,20 @@ internal class DaprHttpInteractor : IDaprInteractor
2828
private readonly JsonSerializerOptions jsonSerializerOptions = JsonSerializerDefaults.Web;
2929
private const string DaprEndpoint = Constants.DaprDefaultEndpoint;
3030
private readonly string daprPort;
31-
private static HttpClientHandler innerHandler = new HttpClientHandler();
32-
private HttpClient httpClient = null;
33-
private bool disposed = false;
31+
private readonly static HttpMessageHandler defaultHandler = new HttpClientHandler();
32+
private readonly HttpMessageHandler handler;
33+
private HttpClient httpClient;
34+
private bool disposed;
3435
private string daprApiToken;
3536

3637
public DaprHttpInteractor(
37-
HttpClientHandler clientHandler = null,
38+
HttpMessageHandler clientHandler = null,
3839
string apiToken = null)
3940
{
4041
// Get Dapr port from Environment Variable if it has been overridden.
4142
this.daprPort = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT") ?? Constants.DaprDefaultPort;
4243

43-
innerHandler = clientHandler ?? new HttpClientHandler();
44+
this.handler = clientHandler ?? defaultHandler;
4445
this.daprApiToken = apiToken;
4546
this.httpClient = this.CreateHttpClient();
4647
}
@@ -433,7 +434,7 @@ private async Task<HttpResponseMessage> SendAsyncHandleSecurityExceptions(
433434

434435
private HttpClient CreateHttpClient()
435436
{
436-
return new HttpClient(innerHandler, false);
437+
return new HttpClient(this.handler, false);
437438
}
438439

439440
private void AddDaprApiTokenHeader(HttpRequestMessage request)

src/Dapr.Client/DaprClientBuilder.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public DaprClientBuilder()
4848
// property exposed for testing purposes
4949
internal string HttpEndpoint { get; private set; }
5050

51+
private Func<HttpClient> HttpClientFactory { get; set; }
52+
5153
// property exposed for testing purposes
5254
internal JsonSerializerOptions JsonSerializerOptions { get; private set; }
5355

@@ -71,6 +73,13 @@ public DaprClientBuilder UseHttpEndpoint(string httpEndpoint)
7173
return this;
7274
}
7375

76+
// Internal for testing of DaprClient
77+
internal DaprClientBuilder UseHttpClientFactory(Func<HttpClient> factory)
78+
{
79+
this.HttpClientFactory = factory;
80+
return this;
81+
}
82+
7483
/// <summary>
7584
/// Overrides the gRPC endpoint used by <see cref="DaprClient" /> for communicating with the Dapr runtime.
7685
/// </summary>
@@ -153,7 +162,8 @@ public DaprClient Build()
153162
var client = new Autogenerated.Dapr.DaprClient(channel);
154163

155164
var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken);
156-
return new DaprClientGrpc(channel, client, new HttpClient(), httpEndpoint, this.JsonSerializerOptions, apiTokenHeader);
165+
var httpClient = HttpClientFactory is object ? HttpClientFactory() : new HttpClient();
166+
return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader);
157167
}
158168
}
159169
}

src/Dapr.Client/properties/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Licensed under the MIT License.
44
// ------------------------------------------------------------
55

6+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
67
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
78
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
89
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]

test/Dapr.Actors.Test/ApiTokenTests.cs

Lines changed: 59 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
// ------------------------------------------------------------
55

66
using System;
7-
using System.Collections.Concurrent;
8-
using System.Net.Http;
97
using System.Threading;
108
using System.Threading.Tasks;
119
using Dapr.Actors.Client;
@@ -16,107 +14,96 @@ namespace Dapr.Actors.Test
1614
{
1715
public class ApiTokenTests
1816
{
19-
[Fact(Skip = "Failing due to #573")]
20-
public void CreateProxyWithRemoting_WithApiToken()
17+
[Fact]
18+
public async Task CreateProxyWithRemoting_WithApiToken()
2119
{
20+
await using var client = TestClient.CreateForMessageHandler();
21+
2222
var actorId = new ActorId("abc");
23-
var handler = new TestHttpClientHandler();
2423
var options = new ActorProxyOptions
2524
{
2625
DaprApiToken = "test_token",
2726
};
28-
var factory = new ActorProxyFactory(options, handler);
29-
var proxy = factory.CreateActorProxy<ITestActor>(actorId, "TestActor");
30-
var task = proxy.SetCountAsync(1, new CancellationToken());
3127

32-
handler.Requests.TryDequeue(out var entry).Should().BeTrue();
33-
var headerValues = entry.Request.Headers.GetValues("dapr-api-token");
28+
var request = await client.CaptureHttpRequestAsync(async handler =>
29+
{
30+
var factory = new ActorProxyFactory(options, handler);
31+
var proxy = factory.CreateActorProxy<ITestActor>(actorId, "TestActor");
32+
await proxy.SetCountAsync(1, new CancellationToken());
33+
});
34+
35+
request.Dismiss();
36+
37+
var headerValues = request.Request.Headers.GetValues("dapr-api-token");
3438
headerValues.Should().Contain("test_token");
3539
}
3640

37-
[Fact(Skip = "Failing due to #573")]
38-
public void CreateProxyWithRemoting_WithNoApiToken()
41+
[Fact]
42+
public async Task CreateProxyWithRemoting_WithNoApiToken()
3943
{
44+
await using var client = TestClient.CreateForMessageHandler();
45+
4046
var actorId = new ActorId("abc");
41-
var handler = new TestHttpClientHandler();
42-
var factory = new ActorProxyFactory(null, handler);
43-
var proxy = factory.CreateActorProxy<ITestActor>(actorId, "TestActor");
44-
var task = proxy.SetCountAsync(1, new CancellationToken());
45-
46-
handler.Requests.TryDequeue(out var entry).Should().BeTrue();
47-
Action action = () => entry.Request.Headers.GetValues("dapr-api-token");
48-
action.Should().Throw<InvalidOperationException>();
47+
48+
var request = await client.CaptureHttpRequestAsync(async handler =>
49+
{
50+
var factory = new ActorProxyFactory(null, handler);
51+
var proxy = factory.CreateActorProxy<ITestActor>(actorId, "TestActor");
52+
await proxy.SetCountAsync(1, new CancellationToken());
53+
});
54+
55+
request.Dismiss();
56+
57+
Assert.Throws<InvalidOperationException>(() =>
58+
{
59+
request.Request.Headers.GetValues("dapr-api-token");
60+
});
4961
}
5062

51-
[Fact(Skip = "Failing due to #573")]
52-
public void CreateProxyWithNoRemoting_WithApiToken()
63+
[Fact]
64+
public async Task CreateProxyWithNoRemoting_WithApiToken()
5365
{
66+
await using var client = TestClient.CreateForMessageHandler();
67+
5468
var actorId = new ActorId("abc");
55-
var handler = new TestHttpClientHandler();
5669
var options = new ActorProxyOptions
5770
{
5871
DaprApiToken = "test_token",
5972
};
60-
var factory = new ActorProxyFactory(options, handler);
61-
var proxy = factory.Create(actorId, "TestActor");
62-
var task = proxy.InvokeMethodAsync("SetCountAsync", 1, new CancellationToken());
63-
64-
handler.Requests.TryDequeue(out var entry).Should().BeTrue();
65-
var headerValues = entry.Request.Headers.GetValues("dapr-api-token");
66-
headerValues.Should().Contain("test_token");
67-
}
6873

69-
[Fact(Skip = "Failing due to #573")]
70-
public void CreateProxyWithNoRemoting_WithNoApiToken()
71-
{
72-
var actorId = new ActorId("abc");
73-
var handler = new TestHttpClientHandler();
74-
var factory = new ActorProxyFactory(null, handler);
75-
var proxy = factory.Create(actorId, "TestActor");
76-
var task = proxy.InvokeMethodAsync("SetCountAsync", 1, new CancellationToken());
77-
78-
handler.Requests.TryDequeue(out var entry).Should().BeTrue();
79-
Action action = () => entry.Request.Headers.GetValues("dapr-api-token");
80-
action.Should().Throw<InvalidOperationException>();
81-
}
82-
83-
84-
public class Entry
85-
{
86-
public Entry(HttpRequestMessage request)
74+
var request = await client.CaptureHttpRequestAsync(async handler =>
8775
{
88-
this.Request = request;
76+
var factory = new ActorProxyFactory(options, handler);
77+
var proxy = factory.Create(actorId, "TestActor");
78+
await proxy.InvokeMethodAsync("SetCountAsync", 1, new CancellationToken());
79+
});
8980

90-
this.Completion = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
91-
}
81+
request.Dismiss();
9282

93-
public TaskCompletionSource<HttpResponseMessage> Completion { get; }
94-
95-
public HttpRequestMessage Request { get; }
83+
var headerValues = request.Request.Headers.GetValues("dapr-api-token");
84+
headerValues.Should().Contain("test_token");
9685
}
9786

98-
private class TestHttpClientHandler : HttpClientHandler
87+
[Fact]
88+
public async Task CreateProxyWithNoRemoting_WithNoApiToken()
9989
{
100-
public TestHttpClientHandler()
101-
{
102-
this.Requests = new ConcurrentQueue<Entry>();
103-
}
90+
await using var client = TestClient.CreateForMessageHandler();
10491

105-
public ConcurrentQueue<Entry> Requests { get; }
92+
var actorId = new ActorId("abc");
93+
94+
var request = await client.CaptureHttpRequestAsync(async handler =>
95+
{
96+
var factory = new ActorProxyFactory(null, handler);
97+
var proxy = factory.Create(actorId, "TestActor");
98+
await proxy.InvokeMethodAsync("SetCountAsync", 1, new CancellationToken());
99+
});
106100

107-
public Action<Entry> Handler { get; set; }
101+
request.Dismiss();
108102

109-
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
103+
Assert.Throws<InvalidOperationException>(() =>
110104
{
111-
var entry = new Entry(request);
112-
this.Handler?.Invoke(entry);
113-
this.Requests.Enqueue(entry);
114-
115-
using (cancellationToken.Register(() => entry.Completion.TrySetCanceled()))
116-
{
117-
return await entry.Completion.Task.ConfigureAwait(false);
118-
}
119-
}
105+
request.Request.Headers.GetValues("dapr-api-token");
106+
});
120107
}
121108
}
122109
}

test/Dapr.Actors.Test/Dapr.Actors.Test.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<PropertyGroup>
33
<TargetFrameworks>netcoreapp3.1;net5</TargetFrameworks>
44
<RootNamespace>Dapr.Actors</RootNamespace>
5+
<DefineConstants>$(DefineConstants);ACTORS</DefineConstants>
56
</PropertyGroup>
67

78
<ItemGroup>
@@ -20,6 +21,11 @@
2021
</PackageReference>
2122
</ItemGroup>
2223

24+
<ItemGroup>
25+
<Compile Include="..\Shared\GrpcUtils.cs" />
26+
<Compile Include="..\Shared\TestClient.cs" />
27+
</ItemGroup>
28+
2329
<ItemGroup>
2430
<ProjectReference Include="..\..\src\Dapr.Actors\Dapr.Actors.csproj" />
2531
</ItemGroup>

0 commit comments

Comments
 (0)