Skip to content

Commit 73060d6

Browse files
andreyleskovrose-a
authored andcommitted
Add TestServer support for GraphQLHttpClient
1 parent fe110b6 commit 73060d6

18 files changed

+360
-159
lines changed

GraphQL.Client.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{89
6565
EndProject
6666
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Example", "examples\GraphQL.Client.Example\GraphQL.Client.Example.csproj", "{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}"
6767
EndProject
68+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.TestHost", "src\GraphQL.Client.TestHost\GraphQL.Client.TestHost.csproj", "{01AE8466-3E48-4988-81F1-7F93F1531302}"
69+
EndProject
6870
Global
6971
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7072
Debug|Any CPU = Debug|Any CPU
@@ -127,6 +129,10 @@ Global
127129
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
128130
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
129131
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.Build.0 = Release|Any CPU
132+
{01AE8466-3E48-4988-81F1-7F93F1531302}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
133+
{01AE8466-3E48-4988-81F1-7F93F1531302}.Debug|Any CPU.Build.0 = Debug|Any CPU
134+
{01AE8466-3E48-4988-81F1-7F93F1531302}.Release|Any CPU.ActiveCfg = Release|Any CPU
135+
{01AE8466-3E48-4988-81F1-7F93F1531302}.Release|Any CPU.Build.0 = Release|Any CPU
130136
EndGlobalSection
131137
GlobalSection(SolutionProperties) = preSolution
132138
HideSolutionNode = FALSE
@@ -146,6 +152,7 @@ Global
146152
{0D307BAD-27AE-4A5D-8764-4AA2620B01E9} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
147153
{7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
148154
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD} = {89AD33AB-64F6-4F82-822F-21DF7A10CEC0}
155+
{01AE8466-3E48-4988-81F1-7F93F1531302} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
149156
EndGlobalSection
150157
GlobalSection(ExtensibilityGlobals) = postSolution
151158
SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net461;netstandard2.1</TargetFrameworks>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<ProjectReference Include="..\GraphQL.Client\GraphQL.Client.csproj" />
9+
</ItemGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.2.0" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using GraphQL.Client.Abstractions.Websocket;
3+
using GraphQL.Client.Http;
4+
using Microsoft.AspNetCore.TestHost;
5+
6+
namespace GraphQL.Client.TestHost
7+
{
8+
public static class TestServerExtensions
9+
{
10+
public static GraphQLHttpClient CreateGraphQLHttpClient(this TestServer server, GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer)
11+
{
12+
var testWebSocketClient = server.CreateWebSocketClient();
13+
testWebSocketClient.ConfigureRequest = r =>
14+
{
15+
r.Headers["Sec-WebSocket-Protocol"] = "graphql-ws";
16+
};
17+
18+
return new GraphQLHttpClient(options, serializer, server.CreateClient(), (uri, token) => testWebSocketClient.ConnectAsync(uri, token));
19+
}
20+
}
21+
}

src/GraphQL.Client/GraphQLHttpClient.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
using System.Linq;
66
using System.Net.Http;
77
using System.Net.Http.Headers;
8+
using System.Net.WebSockets;
9+
using System.Runtime.CompilerServices;
810
using System.Threading;
911
using System.Threading.Tasks;
1012
using GraphQL.Client.Abstractions;
1113
using GraphQL.Client.Abstractions.Websocket;
1214
using GraphQL.Client.Http.Websocket;
13-
15+
[assembly:InternalsVisibleTo("GraphQL.Client.TestHost")]
16+
[assembly:InternalsVisibleTo("GraphQL.Integration.Tests")]
1417
namespace GraphQL.Client.Http
1518
{
1619
public class GraphQLHttpClient : IGraphQLClient
@@ -63,6 +66,11 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson
6366
_disposeHttpClient = true;
6467
}
6568

69+
internal GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient, Func<Uri, CancellationToken, Task<WebSocket>> connectedWebSocketFactory):this(options,serializer,httpClient)
70+
{
71+
_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(()=>CreateGraphQLHttpWebSocket(connectedWebSocketFactory));
72+
}
73+
6674
public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient)
6775
{
6876
Options = options ?? throw new ArgumentNullException(nameof(options));
@@ -72,7 +80,7 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson
7280
if (!HttpClient.DefaultRequestHeaders.UserAgent.Any())
7381
HttpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GetType().Assembly.GetName().Name, GetType().Assembly.GetName().Version.ToString()));
7482

75-
_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(CreateGraphQLHttpWebSocket);
83+
_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(()=>CreateGraphQLHttpWebSocket());
7684
}
7785

7886
#endregion
@@ -162,7 +170,7 @@ private async Task<GraphQLHttpResponse<TResponse>> SendHttpRequestAsync<TRespons
162170
throw new GraphQLHttpRequestException(httpResponseMessage.StatusCode, httpResponseMessage.Headers, content);
163171
}
164172

165-
private GraphQLHttpWebSocket CreateGraphQLHttpWebSocket()
173+
private GraphQLHttpWebSocket CreateGraphQLHttpWebSocket(Func<Uri, CancellationToken, Task<WebSocket>>? connectedSocketFactory=null)
166174
{
167175
if(Options.WebSocketEndPoint is null && Options.EndPoint is null)
168176
throw new InvalidOperationException("no endpoint configured");
@@ -171,7 +179,7 @@ private GraphQLHttpWebSocket CreateGraphQLHttpWebSocket()
171179
if (!webSocketEndpoint.HasWebSocketScheme())
172180
throw new InvalidOperationException($"uri \"{webSocketEndpoint}\" is not a websocket endpoint");
173181

174-
return new GraphQLHttpWebSocket(webSocketEndpoint, this);
182+
return new GraphQLHttpWebSocket(webSocketEndpoint, this,connectedSocketFactory);
175183
}
176184

177185
#endregion

src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs

Lines changed: 80 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,7 @@ internal class GraphQLHttpWebSocket : IDisposable
3939
private Task _initializeWebSocketTask = Task.CompletedTask;
4040
private readonly object _initializeLock = new object();
4141

42-
#if NETFRAMEWORK
43-
private WebSocket _clientWebSocket = null;
44-
#else
45-
private ClientWebSocket _clientWebSocket = null;
46-
#endif
42+
private WebSocket _clientWebSocket = null;
4743

4844
#endregion
4945

@@ -83,6 +79,25 @@ public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client)
8379
.Select(request => Observable.FromAsync(() => SendWebSocketRequestAsync(request)))
8480
.Concat()
8581
.Subscribe();
82+
83+
_connectedWebSocketFactory = async (uri, token) =>
84+
{
85+
#if NETFRAMEWORK
86+
var socket = InitializeNetClientWebSocket();
87+
#else
88+
var socket = InitializeNetCoreClientWebSocket();
89+
#endif
90+
Debug.WriteLine($"opening websocket {socket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})");
91+
await socket.ConnectAsync(uri, token);
92+
return socket;
93+
94+
};
95+
96+
}
97+
internal GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client, Func<Uri, CancellationToken,Task<WebSocket>>? connectedWebSocketFactory):this(webSocketUri, client)
98+
{
99+
if(connectedWebSocketFactory!=null)
100+
_connectedWebSocketFactory = connectedWebSocketFactory;
86101
}
87102

88103
#region Send requests
@@ -379,71 +394,77 @@ public Task InitializeWebSocket()
379394

380395
// else (re-)create websocket and connect
381396
_clientWebSocket?.Dispose();
382-
397+
return _initializeWebSocketTask = ConnectAsync(_internalCancellationToken);
398+
}
399+
}
383400
#if NETFRAMEWORK
384-
// fix websocket not supported on win 7 using
385-
// https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed
386-
_clientWebSocket = SystemClientWebSocket.CreateClientWebSocket();
387-
switch (_clientWebSocket) {
388-
case ClientWebSocket nativeWebSocket:
389-
nativeWebSocket.Options.AddSubProtocol("graphql-ws");
390-
nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
391-
nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
392-
Options.ConfigureWebsocketOptions(nativeWebSocket.Options);
393-
break;
394-
case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket:
395-
managedWebSocket.Options.AddSubProtocol("graphql-ws");
396-
managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
397-
managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
398-
break;
399-
default:
400-
throw new NotSupportedException($"unknown websocket type {_clientWebSocket.GetType().Name}");
401-
}
401+
private WebSocket InitializeNetClientWebSocket()
402+
{
403+
// fix websocket not supported on win 7 using
404+
// https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed
405+
var socket = SystemClientWebSocket.CreateClientWebSocket();
406+
switch (socket) {
407+
case ClientWebSocket nativeWebSocket:
408+
nativeWebSocket.Options.AddSubProtocol("graphql-ws");
409+
nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
410+
nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
411+
Options.ConfigureWebsocketOptions(nativeWebSocket.Options);
412+
break;
413+
case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket:
414+
managedWebSocket.Options.AddSubProtocol("graphql-ws");
415+
managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
416+
managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
417+
break;
418+
default:
419+
throw new NotSupportedException($"unknown websocket type {socket.GetType().Name}");
420+
}
421+
return socket;
422+
}
402423
#else
403-
_clientWebSocket = new ClientWebSocket();
404-
_clientWebSocket.Options.AddSubProtocol("graphql-ws");
405-
406-
// the following properties are not supported in Blazor WebAssembly and throw a PlatformNotSupportedException error when accessed
407-
try
408-
{
409-
_clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
410-
}
411-
catch (NotImplementedException)
412-
{
413-
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not implemented by current platform");
414-
}
415-
catch (PlatformNotSupportedException)
416-
{
417-
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not supported by current platform");
418-
}
424+
private ClientWebSocket InitializeNetCoreClientWebSocket()
425+
{
426+
var webSocket = new ClientWebSocket();
427+
webSocket.Options.AddSubProtocol("graphql-ws");
419428

420-
try
421-
{
422-
_clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
423-
}
424-
catch (NotImplementedException)
425-
{
426-
Debug.WriteLine("property 'ClientWebSocketOptions.UseDefaultCredentials' not implemented by current platform");
427-
}
428-
catch (PlatformNotSupportedException)
429-
{
430-
Debug.WriteLine("Property 'ClientWebSocketOptions.UseDefaultCredentials' not supported by current platform");
431-
}
429+
// the following properties are not supported in Blazor WebAssembly and throw a PlatformNotSupportedException error when accessed
430+
try
431+
{
432+
webSocket.Options.ClientCertificates = ((HttpClientHandler) Options.HttpMessageHandler).ClientCertificates;
433+
}
434+
catch (NotImplementedException)
435+
{
436+
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not implemented by current platform");
437+
}
438+
catch (PlatformNotSupportedException)
439+
{
440+
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not supported by current platform");
441+
}
432442

433-
Options.ConfigureWebsocketOptions(_clientWebSocket.Options);
434-
#endif
435-
return _initializeWebSocketTask = ConnectAsync(_internalCancellationToken);
443+
try
444+
{
445+
webSocket.Options.UseDefaultCredentials =
446+
((HttpClientHandler) Options.HttpMessageHandler).UseDefaultCredentials;
447+
}
448+
catch (NotImplementedException)
449+
{
450+
Debug.WriteLine("property 'ClientWebSocketOptions.UseDefaultCredentials' not implemented by current platform");
451+
}
452+
catch (PlatformNotSupportedException)
453+
{
454+
Debug.WriteLine("Property 'ClientWebSocketOptions.UseDefaultCredentials' not supported by current platform");
436455
}
437-
}
438456

457+
Options.ConfigureWebsocketOptions(webSocket.Options);
458+
return webSocket;
459+
}
460+
#endif
439461
private async Task ConnectAsync(CancellationToken token)
440462
{
441463
try
442464
{
443465
await BackOff();
444466
_stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting);
445-
Debug.WriteLine($"opening websocket {_clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})");
446-
await _clientWebSocket.ConnectAsync(_webSocketUri, token);
467+
_clientWebSocket = await _connectedWebSocketFactory(_webSocketUri, token);
447468
_stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected);
448469
Debug.WriteLine($"connection established on websocket {_clientWebSocket.GetHashCode()}, invoking Options.OnWebsocketConnected()");
449470
await (Options.OnWebsocketConnected?.Invoke(_client) ?? Task.CompletedTask);
@@ -608,8 +629,6 @@ private async Task<WebsocketMessageWrapper> ReceiveWebsocketMessagesAsync()
608629
return response;
609630

610631
case WebSocketMessageType.Close:
611-
var closeResponse = await _client.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms);
612-
closeResponse.MessageBytes = ms.ToArray();
613632
Debug.WriteLine($"Connection closed by the server.");
614633
throw new Exception("Connection closed by the server.");
615634

@@ -670,6 +689,7 @@ public void Complete()
670689
public Task? Completion { get; private set; }
671690

672691
private readonly object _completedLocker = new object();
692+
private readonly Func<Uri, CancellationToken,Task<WebSocket>> _connectedWebSocketFactory;
673693
private async Task CompleteAsync()
674694
{
675695
Debug.WriteLine("disposing GraphQLHttpWebSocket...");

tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.1.1" />
1011
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
1112
<PackageReference Include="Microsoft.Reactive.Testing" Version="5.0.0" />
1213
</ItemGroup>
1314

1415
<ItemGroup>
1516
<ProjectReference Include="..\..\src\GraphQL.Client.Serializer.Newtonsoft\GraphQL.Client.Serializer.Newtonsoft.csproj" />
1617
<ProjectReference Include="..\..\src\GraphQL.Client.Serializer.SystemTextJson\GraphQL.Client.Serializer.SystemTextJson.csproj" />
18+
<ProjectReference Include="..\..\src\GraphQL.Client.TestHost\GraphQL.Client.TestHost.csproj" />
1719
<ProjectReference Include="..\..\src\GraphQL.Client\GraphQL.Client.csproj" />
1820
<ProjectReference Include="..\IntegrationTestServer\IntegrationTestServer.csproj" />
1921
</ItemGroup>

tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public abstract class IntegrationServerTestFixture
1414
{
1515
public int Port { get; private set; }
1616

17-
public IWebHost Server { get; private set; }
17+
public IWebHost Server { get; protected set; }
1818

1919
public abstract IGraphQLWebsocketJsonSerializer Serializer { get; }
2020

@@ -23,7 +23,7 @@ public IntegrationServerTestFixture()
2323
Port = NetworkHelpers.GetFreeTcpPortNumber();
2424
}
2525

26-
public async Task CreateServer()
26+
public virtual async Task CreateServer()
2727
{
2828
if (Server != null)
2929
return;
@@ -46,21 +46,11 @@ public GraphQLHttpClient GetStarWarsClient(bool requestsViaWebsocket = false)
4646
public GraphQLHttpClient GetChatClient(bool requestsViaWebsocket = false)
4747
=> GetGraphQLClient(Common.CHAT_ENDPOINT, requestsViaWebsocket);
4848

49-
private GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false)
49+
protected virtual GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false)
5050
{
5151
if (Serializer == null)
5252
throw new InvalidOperationException("JSON serializer not configured");
5353
return WebHostHelpers.GetGraphQLClient(Port, endpoint, requestsViaWebsocket, Serializer);
5454
}
5555
}
56-
57-
public class NewtonsoftIntegrationServerTestFixture : IntegrationServerTestFixture
58-
{
59-
public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer();
60-
}
61-
62-
public class SystemTextJsonIntegrationServerTestFixture : IntegrationServerTestFixture
63-
{
64-
public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer();
65-
}
6656
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using GraphQL.Client.Abstractions.Websocket;
2+
using GraphQL.Client.Serializer.Newtonsoft;
3+
4+
namespace GraphQL.Integration.Tests.Helpers
5+
{
6+
public class NewtonsoftIntegrationServerTestFixture : IntegrationServerTestFixture
7+
{
8+
public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer();
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using GraphQL.Client.Abstractions.Websocket;
2+
using GraphQL.Client.Serializer.SystemTextJson;
3+
4+
namespace GraphQL.Integration.Tests.Helpers
5+
{
6+
public class SystemTextJsonIntegrationServerTestFixture : IntegrationServerTestFixture
7+
{
8+
public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer();
9+
}
10+
}

0 commit comments

Comments
 (0)