Skip to content

Commit

Permalink
Add TestServer support for GraphQLHttpClient
Browse files Browse the repository at this point in the history
  • Loading branch information
andreyleskov authored and rose-a committed Sep 14, 2021
1 parent fe110b6 commit 73060d6
Show file tree
Hide file tree
Showing 18 changed files with 360 additions and 159 deletions.
7 changes: 7 additions & 0 deletions GraphQL.Client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{89
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Example", "examples\GraphQL.Client.Example\GraphQL.Client.Example.csproj", "{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.TestHost", "src\GraphQL.Client.TestHost\GraphQL.Client.TestHost.csproj", "{01AE8466-3E48-4988-81F1-7F93F1531302}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -127,6 +129,10 @@ Global
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.Build.0 = Release|Any CPU
{01AE8466-3E48-4988-81F1-7F93F1531302}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01AE8466-3E48-4988-81F1-7F93F1531302}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01AE8466-3E48-4988-81F1-7F93F1531302}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01AE8466-3E48-4988-81F1-7F93F1531302}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -146,6 +152,7 @@ Global
{0D307BAD-27AE-4A5D-8764-4AA2620B01E9} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
{7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD} = {89AD33AB-64F6-4F82-822F-21DF7A10CEC0}
{01AE8466-3E48-4988-81F1-7F93F1531302} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4}
Expand Down
15 changes: 15 additions & 0 deletions src/GraphQL.Client.TestHost/GraphQL.Client.TestHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net461;netstandard2.1</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\GraphQL.Client\GraphQL.Client.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.2.0" />
</ItemGroup>

</Project>
21 changes: 21 additions & 0 deletions src/GraphQL.Client.TestHost/TestServerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Http;
using Microsoft.AspNetCore.TestHost;

namespace GraphQL.Client.TestHost
{
public static class TestServerExtensions
{
public static GraphQLHttpClient CreateGraphQLHttpClient(this TestServer server, GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer)
{
var testWebSocketClient = server.CreateWebSocketClient();
testWebSocketClient.ConfigureRequest = r =>
{
r.Headers["Sec-WebSocket-Protocol"] = "graphql-ws";
};

return new GraphQLHttpClient(options, serializer, server.CreateClient(), (uri, token) => testWebSocketClient.ConnectAsync(uri, token));
}
}
}
16 changes: 12 additions & 4 deletions src/GraphQL.Client/GraphQLHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Http.Websocket;

[assembly:InternalsVisibleTo("GraphQL.Client.TestHost")]
[assembly:InternalsVisibleTo("GraphQL.Integration.Tests")]
namespace GraphQL.Client.Http
{
public class GraphQLHttpClient : IGraphQLClient
Expand Down Expand Up @@ -63,6 +66,11 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson
_disposeHttpClient = true;
}

internal GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient, Func<Uri, CancellationToken, Task<WebSocket>> connectedWebSocketFactory):this(options,serializer,httpClient)
{
_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(()=>CreateGraphQLHttpWebSocket(connectedWebSocketFactory));
}

public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
Expand All @@ -72,7 +80,7 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson
if (!HttpClient.DefaultRequestHeaders.UserAgent.Any())
HttpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GetType().Assembly.GetName().Name, GetType().Assembly.GetName().Version.ToString()));

_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(CreateGraphQLHttpWebSocket);
_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(()=>CreateGraphQLHttpWebSocket());
}

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

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

return new GraphQLHttpWebSocket(webSocketEndpoint, this);
return new GraphQLHttpWebSocket(webSocketEndpoint, this,connectedSocketFactory);
}

#endregion
Expand Down
140 changes: 80 additions & 60 deletions src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ internal class GraphQLHttpWebSocket : IDisposable
private Task _initializeWebSocketTask = Task.CompletedTask;
private readonly object _initializeLock = new object();

#if NETFRAMEWORK
private WebSocket _clientWebSocket = null;
#else
private ClientWebSocket _clientWebSocket = null;
#endif
private WebSocket _clientWebSocket = null;

#endregion

Expand Down Expand Up @@ -83,6 +79,25 @@ public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client)
.Select(request => Observable.FromAsync(() => SendWebSocketRequestAsync(request)))
.Concat()
.Subscribe();

_connectedWebSocketFactory = async (uri, token) =>
{
#if NETFRAMEWORK
var socket = InitializeNetClientWebSocket();
#else
var socket = InitializeNetCoreClientWebSocket();
#endif
Debug.WriteLine($"opening websocket {socket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})");
await socket.ConnectAsync(uri, token);
return socket;
};

}
internal GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client, Func<Uri, CancellationToken,Task<WebSocket>>? connectedWebSocketFactory):this(webSocketUri, client)
{
if(connectedWebSocketFactory!=null)
_connectedWebSocketFactory = connectedWebSocketFactory;
}

#region Send requests
Expand Down Expand Up @@ -379,71 +394,77 @@ public Task InitializeWebSocket()

// else (re-)create websocket and connect
_clientWebSocket?.Dispose();

return _initializeWebSocketTask = ConnectAsync(_internalCancellationToken);
}
}
#if NETFRAMEWORK
// fix websocket not supported on win 7 using
// https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed
_clientWebSocket = SystemClientWebSocket.CreateClientWebSocket();
switch (_clientWebSocket) {
case ClientWebSocket nativeWebSocket:
nativeWebSocket.Options.AddSubProtocol("graphql-ws");
nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
Options.ConfigureWebsocketOptions(nativeWebSocket.Options);
break;
case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket:
managedWebSocket.Options.AddSubProtocol("graphql-ws");
managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
break;
default:
throw new NotSupportedException($"unknown websocket type {_clientWebSocket.GetType().Name}");
}
private WebSocket InitializeNetClientWebSocket()
{
// fix websocket not supported on win 7 using
// https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed
var socket = SystemClientWebSocket.CreateClientWebSocket();
switch (socket) {
case ClientWebSocket nativeWebSocket:
nativeWebSocket.Options.AddSubProtocol("graphql-ws");
nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
Options.ConfigureWebsocketOptions(nativeWebSocket.Options);
break;
case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket:
managedWebSocket.Options.AddSubProtocol("graphql-ws");
managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
break;
default:
throw new NotSupportedException($"unknown websocket type {socket.GetType().Name}");
}
return socket;
}
#else
_clientWebSocket = new ClientWebSocket();
_clientWebSocket.Options.AddSubProtocol("graphql-ws");

// the following properties are not supported in Blazor WebAssembly and throw a PlatformNotSupportedException error when accessed
try
{
_clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
}
catch (NotImplementedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not implemented by current platform");
}
catch (PlatformNotSupportedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not supported by current platform");
}
private ClientWebSocket InitializeNetCoreClientWebSocket()
{
var webSocket = new ClientWebSocket();
webSocket.Options.AddSubProtocol("graphql-ws");

try
{
_clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
}
catch (NotImplementedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.UseDefaultCredentials' not implemented by current platform");
}
catch (PlatformNotSupportedException)
{
Debug.WriteLine("Property 'ClientWebSocketOptions.UseDefaultCredentials' not supported by current platform");
}
// the following properties are not supported in Blazor WebAssembly and throw a PlatformNotSupportedException error when accessed
try
{
webSocket.Options.ClientCertificates = ((HttpClientHandler) Options.HttpMessageHandler).ClientCertificates;
}
catch (NotImplementedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not implemented by current platform");
}
catch (PlatformNotSupportedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not supported by current platform");
}

Options.ConfigureWebsocketOptions(_clientWebSocket.Options);
#endif
return _initializeWebSocketTask = ConnectAsync(_internalCancellationToken);
try
{
webSocket.Options.UseDefaultCredentials =
((HttpClientHandler) Options.HttpMessageHandler).UseDefaultCredentials;
}
catch (NotImplementedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.UseDefaultCredentials' not implemented by current platform");
}
catch (PlatformNotSupportedException)
{
Debug.WriteLine("Property 'ClientWebSocketOptions.UseDefaultCredentials' not supported by current platform");
}
}

Options.ConfigureWebsocketOptions(webSocket.Options);
return webSocket;
}
#endif
private async Task ConnectAsync(CancellationToken token)
{
try
{
await BackOff();
_stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting);
Debug.WriteLine($"opening websocket {_clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})");
await _clientWebSocket.ConnectAsync(_webSocketUri, token);
_clientWebSocket = await _connectedWebSocketFactory(_webSocketUri, token);
_stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected);
Debug.WriteLine($"connection established on websocket {_clientWebSocket.GetHashCode()}, invoking Options.OnWebsocketConnected()");
await (Options.OnWebsocketConnected?.Invoke(_client) ?? Task.CompletedTask);
Expand Down Expand Up @@ -608,8 +629,6 @@ private async Task<WebsocketMessageWrapper> ReceiveWebsocketMessagesAsync()
return response;

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

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

private readonly object _completedLocker = new object();
private readonly Func<Uri, CancellationToken,Task<WebSocket>> _connectedWebSocketFactory;
private async Task CompleteAsync()
{
Debug.WriteLine("disposing GraphQLHttpWebSocket...");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.1" />
<PackageReference Include="Microsoft.Reactive.Testing" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\GraphQL.Client.Serializer.Newtonsoft\GraphQL.Client.Serializer.Newtonsoft.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client.Serializer.SystemTextJson\GraphQL.Client.Serializer.SystemTextJson.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client.TestHost\GraphQL.Client.TestHost.csproj" />
<ProjectReference Include="..\..\src\GraphQL.Client\GraphQL.Client.csproj" />
<ProjectReference Include="..\IntegrationTestServer\IntegrationTestServer.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public abstract class IntegrationServerTestFixture
{
public int Port { get; private set; }

public IWebHost Server { get; private set; }
public IWebHost Server { get; protected set; }

public abstract IGraphQLWebsocketJsonSerializer Serializer { get; }

Expand All @@ -23,7 +23,7 @@ public IntegrationServerTestFixture()
Port = NetworkHelpers.GetFreeTcpPortNumber();
}

public async Task CreateServer()
public virtual async Task CreateServer()
{
if (Server != null)
return;
Expand All @@ -46,21 +46,11 @@ public GraphQLHttpClient GetStarWarsClient(bool requestsViaWebsocket = false)
public GraphQLHttpClient GetChatClient(bool requestsViaWebsocket = false)
=> GetGraphQLClient(Common.CHAT_ENDPOINT, requestsViaWebsocket);

private GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false)
protected virtual GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false)
{
if (Serializer == null)
throw new InvalidOperationException("JSON serializer not configured");
return WebHostHelpers.GetGraphQLClient(Port, endpoint, requestsViaWebsocket, Serializer);
}
}

public class NewtonsoftIntegrationServerTestFixture : IntegrationServerTestFixture
{
public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer();
}

public class SystemTextJsonIntegrationServerTestFixture : IntegrationServerTestFixture
{
public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Serializer.Newtonsoft;

namespace GraphQL.Integration.Tests.Helpers
{
public class NewtonsoftIntegrationServerTestFixture : IntegrationServerTestFixture
{
public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Serializer.SystemTextJson;

namespace GraphQL.Integration.Tests.Helpers
{
public class SystemTextJsonIntegrationServerTestFixture : IntegrationServerTestFixture
{
public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer();
}
}
Loading

0 comments on commit 73060d6

Please sign in to comment.