diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln index ab95f2ec..d6faf815 100644 --- a/GraphQL.Client.sln +++ b/GraphQL.Client.sln @@ -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 @@ -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 @@ -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} diff --git a/src/GraphQL.Client.TestHost/GraphQL.Client.TestHost.csproj b/src/GraphQL.Client.TestHost/GraphQL.Client.TestHost.csproj new file mode 100644 index 00000000..c8658076 --- /dev/null +++ b/src/GraphQL.Client.TestHost/GraphQL.Client.TestHost.csproj @@ -0,0 +1,15 @@ + + + + net461;netstandard2.1 + + + + + + + + + + + diff --git a/src/GraphQL.Client.TestHost/TestServerExtensions.cs b/src/GraphQL.Client.TestHost/TestServerExtensions.cs new file mode 100644 index 00000000..10054f1b --- /dev/null +++ b/src/GraphQL.Client.TestHost/TestServerExtensions.cs @@ -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)); + } + } +} diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 9d2aac47..ada1e7f8 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -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 @@ -63,6 +66,11 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson _disposeHttpClient = true; } + internal GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient, Func> connectedWebSocketFactory):this(options,serializer,httpClient) + { + _lazyHttpWebSocket = new Lazy(()=>CreateGraphQLHttpWebSocket(connectedWebSocketFactory)); + } + public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient) { Options = options ?? throw new ArgumentNullException(nameof(options)); @@ -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(CreateGraphQLHttpWebSocket); + _lazyHttpWebSocket = new Lazy(()=>CreateGraphQLHttpWebSocket()); } #endregion @@ -162,7 +170,7 @@ private async Task> SendHttpRequestAsync>? connectedSocketFactory=null) { if(Options.WebSocketEndPoint is null && Options.EndPoint is null) throw new InvalidOperationException("no endpoint configured"); @@ -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 diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index ec4ac4b7..47d8cb48 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -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 @@ -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>? connectedWebSocketFactory):this(webSocketUri, client) + { + if(connectedWebSocketFactory!=null) + _connectedWebSocketFactory = connectedWebSocketFactory; } #region Send requests @@ -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); @@ -608,8 +629,6 @@ private async Task 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."); @@ -670,6 +689,7 @@ public void Complete() public Task? Completion { get; private set; } private readonly object _completedLocker = new object(); + private readonly Func> _connectedWebSocketFactory; private async Task CompleteAsync() { Debug.WriteLine("disposing GraphQLHttpWebSocket..."); diff --git a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj index 6aca8a5b..facb750c 100644 --- a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj +++ b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj @@ -7,6 +7,7 @@ + @@ -14,6 +15,7 @@ + diff --git a/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs index 25773b47..f13c67cc 100644 --- a/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs +++ b/tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs @@ -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; } @@ -23,7 +23,7 @@ public IntegrationServerTestFixture() Port = NetworkHelpers.GetFreeTcpPortNumber(); } - public async Task CreateServer() + public virtual async Task CreateServer() { if (Server != null) return; @@ -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(); - } } diff --git a/tests/GraphQL.Integration.Tests/Helpers/NewtonsoftIntegrationServerTestFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/NewtonsoftIntegrationServerTestFixture.cs new file mode 100644 index 00000000..317a295f --- /dev/null +++ b/tests/GraphQL.Integration.Tests/Helpers/NewtonsoftIntegrationServerTestFixture.cs @@ -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(); + } +} \ No newline at end of file diff --git a/tests/GraphQL.Integration.Tests/Helpers/SystemTextJsonIntegrationServerTestFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/SystemTextJsonIntegrationServerTestFixture.cs new file mode 100644 index 00000000..a971825c --- /dev/null +++ b/tests/GraphQL.Integration.Tests/Helpers/SystemTextJsonIntegrationServerTestFixture.cs @@ -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(); + } +} \ No newline at end of file diff --git a/tests/GraphQL.Integration.Tests/Helpers/TestServerTestFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/TestServerTestFixture.cs new file mode 100644 index 00000000..dac5b37f --- /dev/null +++ b/tests/GraphQL.Integration.Tests/Helpers/TestServerTestFixture.cs @@ -0,0 +1,48 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using GraphQL.Client.Http; +using GraphQL.Client.TestHost; +using IntegrationTestServer; +using MartinCostello.Logging.XUnit; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace GraphQL.Integration.Tests.Helpers +{ + public abstract class TestServerTestFixture : IntegrationServerTestFixture + { + private TestServer _testServer; + public ITestOutputHelper Output { get; set; } + public override async Task CreateServer() + { + var host = + new WebHostBuilder() + .UseStartup() + .ConfigureLogging((ctx, logging) => + { + logging.AddProvider(new XUnitLoggerProvider(Output, new XUnitLoggerOptions())); + logging.SetMinimumLevel(LogLevel.Trace); + }); + + _testServer = new TestServer(host); + Server = _testServer.Host; + await _testServer.Host.StartAsync(); + } + + protected override GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false) + { + if (Serializer == null) + throw new InvalidOperationException("JSON serializer not configured"); + + return _testServer.CreateGraphQLHttpClient(new GraphQLHttpClientOptions + { + EndPoint = new Uri($"http://localhost:{Port}{endpoint}"), + UseWebSocketForQueriesAndMutations = requestsViaWebsocket + }, + Serializer); + } + } +} diff --git a/tests/GraphQL.Integration.Tests/Helpers/TestServerTestNewtonsoftFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/TestServerTestNewtonsoftFixture.cs new file mode 100644 index 00000000..aee9df82 --- /dev/null +++ b/tests/GraphQL.Integration.Tests/Helpers/TestServerTestNewtonsoftFixture.cs @@ -0,0 +1,10 @@ +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Serializer.Newtonsoft; + +namespace GraphQL.Integration.Tests.Helpers +{ + public class TestServerTestNewtonsoftFixture : TestServerTestFixture + { + public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer(); + } +} \ No newline at end of file diff --git a/tests/GraphQL.Integration.Tests/Helpers/TestServerTestSystemTextFixture.cs b/tests/GraphQL.Integration.Tests/Helpers/TestServerTestSystemTextFixture.cs new file mode 100644 index 00000000..6a208b9c --- /dev/null +++ b/tests/GraphQL.Integration.Tests/Helpers/TestServerTestSystemTextFixture.cs @@ -0,0 +1,10 @@ +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Serializer.SystemTextJson; + +namespace GraphQL.Integration.Tests.Helpers +{ + public class TestServerTestSystemTextFixture : TestServerTestFixture + { + public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer(); + } +} \ No newline at end of file diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs index 4bedde89..71cc610a 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs @@ -1,16 +1,12 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Execution; -using FluentAssertions.Extensions; using GraphQL.Client.Abstractions; -using GraphQL.Client.Abstractions.Websocket; using GraphQL.Client.Http; using GraphQL.Client.Tests.Common.Chat; using GraphQL.Client.Tests.Common.Chat.Schema; @@ -81,7 +77,7 @@ public async void CanUseWebSocketScheme() response.Errors.Should().BeNullOrEmpty(); response.Data.AddMessage.Content.Should().Be(message); } - + [Fact] public async void CanUseDedicatedWebSocketEndpoint() { @@ -94,7 +90,7 @@ public async void CanUseDedicatedWebSocketEndpoint() response.Errors.Should().BeNullOrEmpty(); response.Data.AddMessage.Content.Should().Be(message); } - + [Fact] public async void CanUseDedicatedWebSocketEndpointWithoutHttpEndpoint() { @@ -161,7 +157,7 @@ public async void CanHandleRequestErrorViaWebsocket() } }"; - private readonly GraphQLRequest _subscriptionRequest = new GraphQLRequest(SUBSCRIPTION_QUERY); + protected readonly GraphQLRequest _subscriptionRequest = new GraphQLRequest(SUBSCRIPTION_QUERY); [Fact] @@ -360,79 +356,6 @@ public async void CanConnectTwoSubscriptionsSimultaneously() } - [Fact] - public async void CanHandleConnectionTimeout() - { - var errorMonitor = new CallbackMonitor(); - var reconnectBlocker = new ManualResetEventSlim(false); - - var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); - // configure back-off strategy to allow it to be controlled from within the unit test - ChatClient.Options.BackOffStrategy = i => - { - Debug.WriteLine("back-off strategy: waiting on reconnect blocker"); - reconnectBlocker.Wait(); - Debug.WriteLine("back-off strategy: reconnecting..."); - return TimeSpan.Zero; - }; - - var websocketStates = new ConcurrentQueue(); - - using (ChatClient.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) - { - websocketStates.Should().ContainSingle(state => state == GraphQLWebsocketConnectionState.Disconnected); - - Debug.WriteLine($"Test method thread id: {Thread.CurrentThread.ManagedThreadId}"); - Debug.WriteLine("creating subscription stream"); - var observable = ChatClient.CreateSubscriptionStream(_subscriptionRequest, errorMonitor.Invoke); - - Debug.WriteLine("subscribing..."); - var observer = observable.Observe(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(); - - websocketStates.Should().ContainInOrder( - GraphQLWebsocketConnectionState.Disconnected, - GraphQLWebsocketConnectionState.Connecting, - GraphQLWebsocketConnectionState.Connected); - // clear the collection so the next tests on the collection work as expected - websocketStates.Clear(); - - await observer.Should().PushAsync(1); - observer.RecordedMessages.Last().Data.MessageAdded.Content.Should().Be(InitialMessage.Content); - - const string message1 = "Hello World"; - var response = await ChatClient.AddMessageAsync(message1); - response.Data.AddMessage.Content.Should().Be(message1); - await observer.Should().PushAsync(2); - observer.RecordedMessages.Last().Data.MessageAdded.Content.Should().Be(message1); - - Debug.WriteLine("stopping web host..."); - await Fixture.ShutdownServer(); - Debug.WriteLine("web host stopped"); - - errorMonitor.Should().HaveBeenInvokedWithPayload(10.Seconds()) - .Which.Should().BeOfType(); - websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); - - Debug.WriteLine("restarting web host..."); - await InitializeAsync(); - Debug.WriteLine("web host started"); - reconnectBlocker.Set(); - callbackMonitor.Should().HaveBeenInvokedWithPayload(3.Seconds()); - await observer.Should().PushAsync(3); - observer.RecordedMessages.Last().Data.MessageAdded.Content.Should().Be(InitialMessage.Content); - - websocketStates.Should().ContainInOrder( - GraphQLWebsocketConnectionState.Disconnected, - GraphQLWebsocketConnectionState.Connecting, - GraphQLWebsocketConnectionState.Connected); - - // disposing the client should complete the subscription - ChatClient.Dispose(); - await observer.Should().CompleteAsync(5.Seconds()); - } - } - [Fact] public async void CanHandleSubscriptionError() { diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/BaseWithTimeout.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/BaseWithTimeout.cs new file mode 100644 index 00000000..37b70d0d --- /dev/null +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/BaseWithTimeout.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Net.WebSockets; +using System.Threading; +using FluentAssertions; +using FluentAssertions.Extensions; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Tests.Common.Chat; +using GraphQL.Client.Tests.Common.FluentAssertions.Reactive; +using GraphQL.Client.Tests.Common.Helpers; +using GraphQL.Integration.Tests.Helpers; +using Xunit; +using Xunit.Abstractions; + +namespace GraphQL.Integration.Tests.WebsocketTests +{ + public abstract class BaseWithTimeout:Base + { + + protected BaseWithTimeout(ITestOutputHelper output, IntegrationServerTestFixture fixture) : base(output, fixture) + { + } + + [Fact] + public async void CanHandleConnectionTimeout() + { + var errorMonitor = new CallbackMonitor(); + var reconnectBlocker = new ManualResetEventSlim(false); + + var callbackMonitor = ChatClient.ConfigureMonitorForOnWebsocketConnected(); + // configure back-off strategy to allow it to be controlled from within the unit test + ChatClient.Options.BackOffStrategy = i => + { + Debug.WriteLine("back-off strategy: waiting on reconnect blocker"); + reconnectBlocker.Wait(); + Debug.WriteLine("back-off strategy: reconnecting..."); + return TimeSpan.Zero; + }; + + var websocketStates = new ConcurrentQueue(); + + using (ChatClient.WebsocketConnectionState.Subscribe(websocketStates.Enqueue)) + { + websocketStates.Should().ContainSingle(state => state == GraphQLWebsocketConnectionState.Disconnected); + + Debug.WriteLine($"Test method thread id: {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine("creating subscription stream"); + var observable = ChatClient.CreateSubscriptionStream(_subscriptionRequest, errorMonitor.Invoke); + + Debug.WriteLine("subscribing..."); + var observer = observable.Observe(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(); + + websocketStates.Should().ContainInOrder( + GraphQLWebsocketConnectionState.Disconnected, + GraphQLWebsocketConnectionState.Connecting, + GraphQLWebsocketConnectionState.Connected); + // clear the collection so the next tests on the collection work as expected + websocketStates.Clear(); + + await observer.Should().PushAsync(1); + observer.RecordedMessages.Last().Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + + const string message1 = "Hello World"; + var response = await ChatClient.AddMessageAsync(message1); + response.Data.AddMessage.Content.Should().Be(message1); + await observer.Should().PushAsync(2); + observer.RecordedMessages.Last().Data.MessageAdded.Content.Should().Be(message1); + + Debug.WriteLine("stopping web host..."); + await Fixture.ShutdownServer(); + Debug.WriteLine("web host stopped"); + + errorMonitor.Should().HaveBeenInvokedWithPayload(100.Seconds()) + .Which.Should().BeOfType(); + websocketStates.Should().Contain(GraphQLWebsocketConnectionState.Disconnected); + + Debug.WriteLine("restarting web host..."); + await InitializeAsync(); + Debug.WriteLine("web host started"); + reconnectBlocker.Set(); + callbackMonitor.Should().HaveBeenInvokedWithPayload(3.Seconds()); + await observer.Should().PushAsync(3); + observer.RecordedMessages.Last().Data.MessageAdded.Content.Should().Be(InitialMessage.Content); + + websocketStates.Should().ContainInOrder( + GraphQLWebsocketConnectionState.Disconnected, + GraphQLWebsocketConnectionState.Connecting, + GraphQLWebsocketConnectionState.Connected); + + // disposing the client should complete the subscription + ChatClient.Dispose(); + await observer.Should().CompleteAsync(5.Seconds()); + } + } + } +} diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs index d77ca14c..9fb7a5f8 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/Newtonsoft.cs @@ -4,7 +4,7 @@ namespace GraphQL.Integration.Tests.WebsocketTests { - public class Newtonsoft : Base, IClassFixture + public class Newtonsoft : BaseWithTimeout, IClassFixture { public Newtonsoft(ITestOutputHelper output, NewtonsoftIntegrationServerTestFixture fixture) : base(output, fixture) { diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs index ab4659a4..1cf9a44d 100644 --- a/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJson.cs @@ -4,7 +4,7 @@ namespace GraphQL.Integration.Tests.WebsocketTests { - public class SystemTextJson : Base, IClassFixture + public class SystemTextJson : BaseWithTimeout, IClassFixture { public SystemTextJson(ITestOutputHelper output, SystemTextJsonIntegrationServerTestFixture fixture) : base(output, fixture) { diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/TestServerNewtonsoft.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/TestServerNewtonsoft.cs new file mode 100644 index 00000000..c694d559 --- /dev/null +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/TestServerNewtonsoft.cs @@ -0,0 +1,14 @@ +using GraphQL.Integration.Tests.Helpers; +using Xunit; +using Xunit.Abstractions; + +namespace GraphQL.Integration.Tests.WebsocketTests +{ + public class TestServerNewtonsoft : Base, IClassFixture + { + public TestServerNewtonsoft(ITestOutputHelper output, TestServerTestNewtonsoftFixture fixture) : base(output, fixture) + { + fixture.Output = output; + } + } +} diff --git a/tests/GraphQL.Integration.Tests/WebsocketTests/TestServerSystemTextJson.cs b/tests/GraphQL.Integration.Tests/WebsocketTests/TestServerSystemTextJson.cs new file mode 100644 index 00000000..b6cd8395 --- /dev/null +++ b/tests/GraphQL.Integration.Tests/WebsocketTests/TestServerSystemTextJson.cs @@ -0,0 +1,14 @@ +using GraphQL.Integration.Tests.Helpers; +using Xunit; +using Xunit.Abstractions; + +namespace GraphQL.Integration.Tests.WebsocketTests +{ + public class TestServerSystemTextJson : Base, IClassFixture + { + public TestServerSystemTextJson(ITestOutputHelper output, TestServerTestSystemTextFixture fixture) : base(output, fixture) + { + fixture.Output = output; + } + } +}