// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Buffers; using System.Collections.Generic; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Connections.Client; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.AspNetCore.SignalR.Tests; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { public class HubProtocolVersionTestsCollection : ICollectionFixture> { public const string Name = nameof(HubProtocolVersionTestsCollection); } [Collection(HubProtocolVersionTestsCollection.Name)] public class HubProtocolVersionTests : FunctionalTestBase { public HubProtocolVersionTests(ITestOutputHelper output) : base(output) { } [Theory] [MemberData(nameof(TransportTypes))] public async Task ClientUsingOldCallWithOriginalProtocol(HttpTransportType transportType) { using (StartServer(out var loggerFactory, out var server, $"{nameof(ClientUsingOldCallWithOriginalProtocol)}_{transportType}")) { var connectionBuilder = new HubConnectionBuilder() .WithLoggerFactory(loggerFactory) .WithUrl(server.Url + "/version", transportType); var connection = connectionBuilder.Build(); try { await connection.StartAsync().OrTimeout(); var result = await connection.InvokeAsync(nameof(VersionHub.Echo), "Hello World!").OrTimeout(); Assert.Equal("Hello World!", result); } catch (Exception ex) { loggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); throw; } finally { await connection.DisposeAsync().OrTimeout(); } } } [Theory] [MemberData(nameof(TransportTypes))] public async Task ClientUsingOldCallWithNewProtocol(HttpTransportType transportType) { using (StartServer(out var loggerFactory, out var server, $"{nameof(ClientUsingOldCallWithNewProtocol)}_{transportType}")) { var connectionBuilder = new HubConnectionBuilder() .WithLoggerFactory(loggerFactory) .WithUrl(server.Url + "/version", transportType); connectionBuilder.Services.AddSingleton(new VersionedJsonHubProtocol(1000)); var connection = connectionBuilder.Build(); try { await connection.StartAsync().OrTimeout(); var result = await connection.InvokeAsync(nameof(VersionHub.Echo), "Hello World!").OrTimeout(); Assert.Equal("Hello World!", result); } catch (Exception ex) { loggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); throw; } finally { await connection.DisposeAsync().OrTimeout(); } } } [Theory] [MemberData(nameof(TransportTypes))] public async Task ClientUsingNewCallWithNewProtocol(HttpTransportType transportType) { using (StartServer(out var loggerFactory, out var server, $"{nameof(ClientUsingNewCallWithNewProtocol)}_{transportType}")) { var httpConnectionFactory = new HttpConnectionFactory(Options.Create(new HttpConnectionOptions { Url = new Uri(server.Url + "/version"), Transports = transportType }), loggerFactory); var tcs = new TaskCompletionSource(); var proxyConnectionFactory = new ProxyConnectionFactory(httpConnectionFactory); var connectionBuilder = new HubConnectionBuilder() .WithLoggerFactory(loggerFactory); connectionBuilder.Services.AddSingleton(new VersionedJsonHubProtocol(1000)); connectionBuilder.Services.AddSingleton(proxyConnectionFactory); var connection = connectionBuilder.Build(); connection.On("NewProtocolMethodClient", () => { tcs.SetResult(null); }); try { await connection.StartAsync().OrTimeout(); // Task should already have been awaited in StartAsync var connectionContext = await proxyConnectionFactory.ConnectTask.OrTimeout(); // Simulate a new call from the client var messageToken = new JObject { ["type"] = int.MaxValue }; connectionContext.Transport.Output.Write(Encoding.UTF8.GetBytes(messageToken.ToString())); connectionContext.Transport.Output.Write(new[] { (byte)0x1e }); await connectionContext.Transport.Output.FlushAsync().OrTimeout(); await tcs.Task.OrTimeout(); } catch (Exception ex) { loggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); throw; } finally { await connection.DisposeAsync().OrTimeout(); } } } [Theory] [MemberData(nameof(TransportTypes))] public async Task ClientWithUnsupportedProtocolVersionDoesNotConnect(HttpTransportType transportType) { bool ExpectedErrors(WriteContext writeContext) { return writeContext.LoggerName == typeof(HubConnection).FullName; } using (StartServer(out var loggerFactory, out var server, LogLevel.Trace, $"{nameof(ClientWithUnsupportedProtocolVersionDoesNotConnect)}_{transportType}", expectedErrorsFilter: ExpectedErrors)) { var connectionBuilder = new HubConnectionBuilder() .WithLoggerFactory(loggerFactory) .WithUrl(server.Url + "/version", transportType); connectionBuilder.Services.AddSingleton(new VersionedJsonHubProtocol(int.MaxValue)); var connection = connectionBuilder.Build(); try { await ExceptionAssert.ThrowsAsync( () => connection.StartAsync(), "Unable to complete handshake with the server due to an error: The server does not support version 2147483647 of the 'json' protocol.").OrTimeout(); } catch (Exception ex) { loggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); throw; } finally { await connection.DisposeAsync().OrTimeout(); } } } private class ProxyConnectionFactory : IConnectionFactory { private readonly IConnectionFactory _innerFactory; public Task ConnectTask { get; private set; } public ProxyConnectionFactory(IConnectionFactory innerFactory) { _innerFactory = innerFactory; } public Task ConnectAsync(TransferFormat transferFormat, CancellationToken cancellationToken = default) { ConnectTask = _innerFactory.ConnectAsync(transferFormat, cancellationToken); return ConnectTask; } public Task DisposeAsync(ConnectionContext connection) { return _innerFactory.DisposeAsync(connection); } } public static IEnumerable TransportTypes() { if (TestHelpers.IsWebSocketsSupported()) { yield return new object[] { HttpTransportType.WebSockets }; } yield return new object[] { HttpTransportType.ServerSentEvents }; yield return new object[] { HttpTransportType.LongPolling }; } } }