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.Abstractions.Websocket/IWebSocketFactory.cs b/src/GraphQL.Client.Abstractions.Websocket/IWebSocketFactory.cs new file mode 100644 index 00000000..8fd9273e --- /dev/null +++ b/src/GraphQL.Client.Abstractions.Websocket/IWebSocketFactory.cs @@ -0,0 +1,15 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace GraphQL.Client.Abstractions.Websocket +{ + /// + /// creates and returns a configured and connected instance + /// + public interface IWebSocketFactory + { + Task ConnectAsync(Uri webSocketUri, CancellationToken cancellationToken); + } +} 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..5bc17379 --- /dev/null +++ b/src/GraphQL.Client.TestHost/TestServerExtensions.cs @@ -0,0 +1,13 @@ +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 testServer, GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer) + => new GraphQLHttpClient(options, serializer, testServer.CreateClient(), new TestServerWebSocketFactory(testServer)); + } +} diff --git a/src/GraphQL.Client.TestHost/TestServerWebSocketFactory.cs b/src/GraphQL.Client.TestHost/TestServerWebSocketFactory.cs new file mode 100644 index 00000000..a6ba4d52 --- /dev/null +++ b/src/GraphQL.Client.TestHost/TestServerWebSocketFactory.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Http.Websocket; +using Microsoft.AspNetCore.TestHost; + +namespace GraphQL.Client.TestHost +{ + public class TestServerWebSocketFactory: IWebSocketFactory + { + private readonly WebSocketClient _webSocketClient; + + public TestServerWebSocketFactory(TestServer testServer) + { + _webSocketClient = testServer.CreateWebSocketClient(); + _webSocketClient.ConfigureRequest = r => + { + r.Headers["Sec-WebSocket-Protocol"] = "graphql-ws"; + }; + } + + public Task ConnectAsync(Uri webSocketUri, CancellationToken cancellationToken) + => _webSocketClient.ConnectAsync(webSocketUri, cancellationToken); + } +} diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 9d2aac47..a525246c 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -5,16 +5,20 @@ 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 { + private readonly IWebSocketFactory _webSocketFactory; private readonly Lazy _lazyHttpWebSocket; private GraphQLHttpWebSocket GraphQlHttpWebSocket => _lazyHttpWebSocket.Value; @@ -63,16 +67,17 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson _disposeHttpClient = true; } - public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient) + public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient, IWebSocketFactory? webSocketFactory = null) { Options = options ?? throw new ArgumentNullException(nameof(options)); JsonSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer), "please configure the JSON serializer you want to use"); HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _webSocketFactory = webSocketFactory ?? WebSocketFactoryHelper.GetDefaultWebSocketFactory(options); 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 @@ -171,7 +176,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, _webSocketFactory); } #endregion diff --git a/src/GraphQL.Client/Websocket/ClientWebSocketFactory.cs b/src/GraphQL.Client/Websocket/ClientWebSocketFactory.cs new file mode 100644 index 00000000..d2222644 --- /dev/null +++ b/src/GraphQL.Client/Websocket/ClientWebSocketFactory.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions.Websocket; + +namespace GraphQL.Client.Http.Websocket +{ +#if NETSTANDARD + /// + /// Default web socket factory for netstandard2.0 + /// + public class ClientWebSocketFactory : IWebSocketFactory + { + private readonly GraphQLHttpClientOptions _options; + + public ClientWebSocketFactory(GraphQLHttpClientOptions options) + { + _options = options; + } + + public async Task ConnectAsync(Uri webSocketUri, CancellationToken cancellationToken) + { + var webSocket = new ClientWebSocket(); + webSocket.Options.AddSubProtocol("graphql-ws"); + + // 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"); + } + + 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); + + Debug.WriteLine($"opening websocket {webSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})"); + await webSocket.ConnectAsync(webSocketUri, cancellationToken); + return webSocket; + } + } +#endif +} diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs index ec4ac4b7..7eaaadd8 100644 --- a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs +++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs @@ -30,6 +30,7 @@ internal class GraphQLHttpWebSocket : IDisposable private readonly BehaviorSubject _stateSubject = new BehaviorSubject(GraphQLWebsocketConnectionState.Disconnected); private readonly IDisposable _requestSubscription; + private readonly IWebSocketFactory _webSocketFactory; private int _connectionAttempt = 0; private IConnectableObservable _incomingMessages; @@ -39,11 +40,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 @@ -71,11 +68,12 @@ internal class GraphQLHttpWebSocket : IDisposable #endregion - public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client) + public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client, IWebSocketFactory webSocketFactory) { _internalCancellationToken = _internalCancellationTokenSource.Token; _webSocketUri = webSocketUri; _client = client; + _webSocketFactory = webSocketFactory; _buffer = new ArraySegment(new byte[8192]); IncomingMessageStream = GetMessageStream(); @@ -182,7 +180,7 @@ public IObservable> CreateSubscriptionStream 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."); diff --git a/src/GraphQL.Client/Websocket/NetFrameworkWebSocketFactory.cs b/src/GraphQL.Client/Websocket/NetFrameworkWebSocketFactory.cs new file mode 100644 index 00000000..a867a1fd --- /dev/null +++ b/src/GraphQL.Client/Websocket/NetFrameworkWebSocketFactory.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions.Websocket; + +namespace GraphQL.Client.Http.Websocket +{ + +#if NETFRAMEWORK + /// + /// Default web socket factory for net4.6.1 (including support for Windows 7 and Windows Server 2008) + /// + public class NetFrameworkWebSocketFactory : IWebSocketFactory + { + private readonly GraphQLHttpClientOptions _options; + + public NetFrameworkWebSocketFactory(GraphQLHttpClientOptions options) + { + _options = options; + } + + public async Task ConnectAsync(Uri webSocketUri, CancellationToken cancellationToken) + { + // fix websocket not supported on win 7 using + // https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed + var webSocket = SystemClientWebSocket.CreateClientWebSocket(); + switch (webSocket) { + 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 {webSocket.GetType().Name}"); + } + + Debug.WriteLine($"opening websocket {webSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})"); + await webSocket.ConnectAsync(webSocketUri, cancellationToken); + return webSocket; + } + } +#endif +} diff --git a/src/GraphQL.Client/Websocket/WebSocketFactoryHelper.cs b/src/GraphQL.Client/Websocket/WebSocketFactoryHelper.cs new file mode 100644 index 00000000..c59d3fd8 --- /dev/null +++ b/src/GraphQL.Client/Websocket/WebSocketFactoryHelper.cs @@ -0,0 +1,16 @@ +using GraphQL.Client.Abstractions.Websocket; + +namespace GraphQL.Client.Http.Websocket +{ + public static class WebSocketFactoryHelper + { + public static IWebSocketFactory GetDefaultWebSocketFactory(GraphQLHttpClientOptions options) + { +#if NETFRAMEWORK + return new NetFrameworkWebSocketFactory(options); +#else + return new ClientWebSocketFactory(options); +#endif + } + } +} 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..c135caec --- /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 UriBuilder(_testServer.BaseAddress) { Path = endpoint }.Uri, + 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; + } + } +}