Log clearer handshake failures in SignalR .NET client (#10433)
This commit is contained in:
parent
54fff2b168
commit
49c01eefec
|
|
@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(34, "ErrorInvokingClientSideMethod"), "Invoking client side method '{MethodName}' failed.");
|
LoggerMessage.Define<string>(LogLevel.Error, new EventId(34, "ErrorInvokingClientSideMethod"), "Invoking client side method '{MethodName}' failed.");
|
||||||
|
|
||||||
private static readonly Action<ILogger, Exception> _errorProcessingHandshakeResponse =
|
private static readonly Action<ILogger, Exception> _errorProcessingHandshakeResponse =
|
||||||
LoggerMessage.Define(LogLevel.Error, new EventId(35, "ErrorReceivingHandshakeResponse"), "Error processing the handshake response.");
|
LoggerMessage.Define(LogLevel.Error, new EventId(35, "ErrorReceivingHandshakeResponse"), "The underlying connection closed while processing the handshake response. See exception for details.");
|
||||||
|
|
||||||
private static readonly Action<ILogger, string, Exception> _handshakeServerError =
|
private static readonly Action<ILogger, string, Exception> _handshakeServerError =
|
||||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(36, "HandshakeServerError"), "Server returned handshake error: {Error}");
|
LoggerMessage.Define<string>(LogLevel.Error, new EventId(36, "HandshakeServerError"), "Server returned handshake error: {Error}");
|
||||||
|
|
@ -240,6 +240,15 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
private static readonly Action<ILogger, HubConnectionState, HubConnectionState, Exception> _attemptingStateTransition =
|
private static readonly Action<ILogger, HubConnectionState, HubConnectionState, Exception> _attemptingStateTransition =
|
||||||
LoggerMessage.Define<HubConnectionState, HubConnectionState>(LogLevel.Trace, new EventId(80, "AttemptingStateTransition"), "The HubConnection is attempting to transition from the {ExpectedState} state to the {NewState} state.");
|
LoggerMessage.Define<HubConnectionState, HubConnectionState>(LogLevel.Trace, new EventId(80, "AttemptingStateTransition"), "The HubConnection is attempting to transition from the {ExpectedState} state to the {NewState} state.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, Exception> _errorInvalidHandshakeResponse =
|
||||||
|
LoggerMessage.Define(LogLevel.Error, new EventId(81, "ErrorInvalidHandshakeResponse"), "Received an invalid handshake response.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, double, Exception> _errorHandshakeTimedOut =
|
||||||
|
LoggerMessage.Define<double>(LogLevel.Error, new EventId(82, "ErrorHandshakeTimedOut"), "The handshake timed out after {HandshakeTimeoutSeconds} seconds.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, Exception> _errorHandshakeCanceled =
|
||||||
|
LoggerMessage.Define(LogLevel.Error, new EventId(83, "ErrorHandshakeCanceled"), "The handshake was canceled by the client.");
|
||||||
|
|
||||||
public static void PreparingNonBlockingInvocation(ILogger logger, string target, int count)
|
public static void PreparingNonBlockingInvocation(ILogger logger, string target, int count)
|
||||||
{
|
{
|
||||||
_preparingNonBlockingInvocation(logger, target, count, null);
|
_preparingNonBlockingInvocation(logger, target, count, null);
|
||||||
|
|
@ -640,6 +649,21 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
{
|
{
|
||||||
_attemptingStateTransition(logger, expectedState, newState, null);
|
_attemptingStateTransition(logger, expectedState, newState, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ErrorInvalidHandshakeResponse(ILogger logger, Exception exception)
|
||||||
|
{
|
||||||
|
_errorInvalidHandshakeResponse(logger, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ErrorHandshakeTimedOut(ILogger logger, TimeSpan handshakeTimeout, Exception exception)
|
||||||
|
{
|
||||||
|
_errorHandshakeTimedOut(logger, handshakeTimeout.TotalSeconds, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ErrorHandshakeCanceled(ILogger logger, Exception exception)
|
||||||
|
{
|
||||||
|
_errorHandshakeCanceled(logger, exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -253,9 +253,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
throw new InvalidOperationException($"The {nameof(HubConnection)} cannot be started while {nameof(StopAsync)} is running.");
|
throw new InvalidOperationException($"The {nameof(HubConnection)} cannot be started while {nameof(StopAsync)} is running.");
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _state.StopCts.Token))
|
using (CreateLinkedToken(cancellationToken, _state.StopCts.Token, out var linkedToken))
|
||||||
{
|
{
|
||||||
await StartAsyncCore(cancellationToken);
|
await StartAsyncCore(linkedToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
_state.ChangeState(HubConnectionState.Connecting, HubConnectionState.Connected);
|
_state.ChangeState(HubConnectionState.Connecting, HubConnectionState.Connected);
|
||||||
|
|
@ -1018,20 +1018,23 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
if (sendHandshakeResult.IsCompleted)
|
if (sendHandshakeResult.IsCompleted)
|
||||||
{
|
{
|
||||||
// The other side disconnected
|
// The other side disconnected
|
||||||
throw new InvalidOperationException("The server disconnected before the handshake was completed");
|
var ex = new IOException("The server disconnected before the handshake could be started.");
|
||||||
|
Log.ErrorReceivingHandshakeResponse(_logger, ex);
|
||||||
|
throw ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
var input = startingConnectionState.Connection.Transport.Input;
|
var input = startingConnectionState.Connection.Transport.Input;
|
||||||
|
|
||||||
|
using var handshakeCts = new CancellationTokenSource(HandshakeTimeout);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var handshakeCts = new CancellationTokenSource(HandshakeTimeout))
|
|
||||||
// cancellationToken already contains _state.StopCts.Token, so we don't have to link it again
|
// cancellationToken already contains _state.StopCts.Token, so we don't have to link it again
|
||||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, handshakeCts.Token))
|
using (CreateLinkedToken(cancellationToken, handshakeCts.Token, out var linkedToken))
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var result = await input.ReadAsync(cts.Token);
|
var result = await input.ReadAsync(linkedToken);
|
||||||
|
|
||||||
var buffer = result.Buffer;
|
var buffer = result.Buffer;
|
||||||
var consumed = buffer.Start;
|
var consumed = buffer.Start;
|
||||||
|
|
@ -1057,6 +1060,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
$"Unable to complete handshake with the server due to an error: {message.Error}");
|
$"Unable to complete handshake with the server due to an error: {message.Error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.HandshakeComplete(_logger);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1075,17 +1079,34 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (HubException)
|
||||||
|
{
|
||||||
|
// This was already logged as a HandshakeServerError
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (InvalidDataException ex)
|
||||||
|
{
|
||||||
|
Log.ErrorInvalidHandshakeResponse(_logger, ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
if (handshakeCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Log.ErrorHandshakeTimedOut(_logger, HandshakeTimeout, ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.ErrorHandshakeCanceled(_logger, ex);
|
||||||
|
}
|
||||||
|
|
||||||
// shutdown if we're unable to read handshake
|
throw;
|
||||||
// Ignore HubException because we throw it when we receive a handshake response with an error
|
}
|
||||||
// And because we already have the error, we don't need to log that the handshake failed
|
catch (Exception ex)
|
||||||
catch (Exception ex) when (!(ex is HubException))
|
|
||||||
{
|
{
|
||||||
Log.ErrorReceivingHandshakeResponse(_logger, ex);
|
Log.ErrorReceivingHandshakeResponse(_logger, ex);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.HandshakeComplete(_logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReceiveLoop(ConnectionState connectionState)
|
private async Task ReceiveLoop(ConnectionState connectionState)
|
||||||
|
|
@ -1485,6 +1506,26 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IDisposable CreateLinkedToken(CancellationToken token1, CancellationToken token2, out CancellationToken linkedToken)
|
||||||
|
{
|
||||||
|
if (!token1.CanBeCanceled)
|
||||||
|
{
|
||||||
|
linkedToken = token2;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (!token2.CanBeCanceled)
|
||||||
|
{
|
||||||
|
linkedToken = token1;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
|
||||||
|
linkedToken = cts.Token;
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Debug.Assert plays havoc with Unit Tests. But I want something that I can "assert" only in Debug builds.
|
// Debug.Assert plays havoc with Unit Tests. But I want something that I can "assert" only in Debug builds.
|
||||||
[Conditional("DEBUG")]
|
[Conditional("DEBUG")]
|
||||||
private static void SafeAssert(bool condition, string message, [CallerMemberName] string memberName = null, [CallerFilePath] string fileName = null, [CallerLineNumber] int lineNumber = 0)
|
private static void SafeAssert(bool condition, string message, [CallerMemberName] string memberName = null, [CallerFilePath] string fileName = null, [CallerLineNumber] int lineNumber = 0)
|
||||||
|
|
@ -1495,7 +1536,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class Subscription : IDisposable
|
private class Subscription : IDisposable
|
||||||
{
|
{
|
||||||
private readonly InvocationHandler _handler;
|
private readonly InvocationHandler _handler;
|
||||||
|
|
|
||||||
|
|
@ -43,13 +43,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
CreateConnection(loggerFactory: LoggerFactory, transport: new TestTransport(onTransportStart: SyncPoint.Create(out var syncPoint))),
|
CreateConnection(loggerFactory: LoggerFactory, transport: new TestTransport(onTransportStart: SyncPoint.Create(out var syncPoint))),
|
||||||
async (connection) =>
|
async (connection) =>
|
||||||
{
|
{
|
||||||
var firstStart = connection.StartAsync(TransferFormat.Text).OrTimeout();
|
var firstStart = connection.StartAsync(TransferFormat.Text);
|
||||||
await syncPoint.WaitForSyncPoint();
|
await syncPoint.WaitForSyncPoint().OrTimeout();
|
||||||
var secondStart = connection.StartAsync(TransferFormat.Text).OrTimeout();
|
var secondStart = connection.StartAsync(TransferFormat.Text);
|
||||||
syncPoint.Continue();
|
syncPoint.Continue();
|
||||||
|
|
||||||
await firstStart;
|
await firstStart.OrTimeout();
|
||||||
await secondStart;
|
await secondStart.OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,10 +64,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
async (connection) =>
|
async (connection) =>
|
||||||
{
|
{
|
||||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||||
await connection.DisposeAsync();
|
await connection.DisposeAsync().OrTimeout();
|
||||||
var exception =
|
var exception =
|
||||||
await Assert.ThrowsAsync<ObjectDisposedException>(
|
await Assert.ThrowsAsync<ObjectDisposedException>(
|
||||||
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
|
async () => await connection.StartAsync(TransferFormat.Text)).OrTimeout();
|
||||||
|
|
||||||
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
|
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
|
||||||
});
|
});
|
||||||
|
|
@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
async (connection) =>
|
async (connection) =>
|
||||||
{
|
{
|
||||||
Assert.Equal(0, startCounter);
|
Assert.Equal(0, startCounter);
|
||||||
await connection.StartAsync(TransferFormat.Text);
|
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||||
Assert.Equal(passThreshold, startCounter);
|
Assert.Equal(passThreshold, startCounter);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
transport: new TestTransport(onTransportStart: OnTransportStart)),
|
transport: new TestTransport(onTransportStart: OnTransportStart)),
|
||||||
async (connection) =>
|
async (connection) =>
|
||||||
{
|
{
|
||||||
var ex = await Assert.ThrowsAsync<AggregateException>(() => connection.StartAsync(TransferFormat.Text));
|
var ex = await Assert.ThrowsAsync<AggregateException>(() => connection.StartAsync(TransferFormat.Text)).OrTimeout();
|
||||||
Assert.Equal("Unable to connect to the server with any of the available transports. " +
|
Assert.Equal("Unable to connect to the server with any of the available transports. " +
|
||||||
"(WebSockets failed: Transport failed to start) (ServerSentEvents failed: Transport failed to start) (LongPolling failed: Transport failed to start)",
|
"(WebSockets failed: Transport failed to start) (ServerSentEvents failed: Transport failed to start) (LongPolling failed: Transport failed to start)",
|
||||||
ex.Message);
|
ex.Message);
|
||||||
|
|
@ -179,8 +179,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
CreateConnection(loggerFactory: LoggerFactory),
|
CreateConnection(loggerFactory: LoggerFactory),
|
||||||
async (connection) =>
|
async (connection) =>
|
||||||
{
|
{
|
||||||
await connection.DisposeAsync();
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +202,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await transportStart.WaitForSyncPoint().OrTimeout();
|
await transportStart.WaitForSyncPoint().OrTimeout();
|
||||||
|
|
||||||
// While the transport is starting, dispose the connection
|
// While the transport is starting, dispose the connection
|
||||||
var disposeTask = connection.DisposeAsync().OrTimeout();
|
var disposeTask = connection.DisposeAsync();
|
||||||
transportStart.Continue(); // We need to release StartAsync, because Dispose waits for it.
|
transportStart.Continue(); // We need to release StartAsync, because Dispose waits for it.
|
||||||
|
|
||||||
// Wait for start to finish, as that has to finish before the transport will be stopped.
|
// Wait for start to finish, as that has to finish before the transport will be stopped.
|
||||||
|
|
@ -214,7 +213,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
transportStop.Continue();
|
transportStop.Continue();
|
||||||
|
|
||||||
// Dispose should finish
|
// Dispose should finish
|
||||||
await disposeTask;
|
await disposeTask.OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,14 +233,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||||
|
|
||||||
// Dispose the connection
|
// Dispose the connection
|
||||||
var stopTask = connection.DisposeAsync().OrTimeout();
|
var stopTask = connection.DisposeAsync();
|
||||||
|
|
||||||
// Once the transport starts shutting down
|
// Once the transport starts shutting down
|
||||||
await transportStop.WaitForSyncPoint();
|
await transportStop.WaitForSyncPoint().OrTimeout();
|
||||||
Assert.False(stopTask.IsCompleted);
|
Assert.False(stopTask.IsCompleted);
|
||||||
|
|
||||||
// Start disposing again, and then let the first dispose continue
|
// Start disposing again, and then let the first dispose continue
|
||||||
var disposeTask = connection.DisposeAsync().OrTimeout();
|
var disposeTask = connection.DisposeAsync();
|
||||||
transportStop.Continue();
|
transportStop.Continue();
|
||||||
|
|
||||||
// Wait for the tasks to complete
|
// Wait for the tasks to complete
|
||||||
|
|
@ -249,7 +248,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await disposeTask.OrTimeout();
|
await disposeTask.OrTimeout();
|
||||||
|
|
||||||
// We should be disposed and thus unable to restart.
|
// We should be disposed and thus unable to restart.
|
||||||
await AssertDisposedAsync(connection);
|
await AssertDisposedAsync(connection).OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +315,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await connection.Transport.Output.WriteAsync(new byte[] { 0x42 }).OrTimeout();
|
await connection.Transport.Output.WriteAsync(new byte[] { 0x42 }).OrTimeout();
|
||||||
|
|
||||||
// We should get the exception in the transport input completion.
|
// We should get the exception in the transport input completion.
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(() => connection.Transport.Input.WaitForWriterToComplete());
|
await Assert.ThrowsAsync<HttpRequestException>(() => connection.Transport.Input.WaitForWriterToComplete()).OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -371,11 +370,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
CreateConnection(httpHandler, loggerFactory: LoggerFactory, transport: sse),
|
CreateConnection(httpHandler, loggerFactory: LoggerFactory, transport: sse),
|
||||||
async (connection) =>
|
async (connection) =>
|
||||||
{
|
{
|
||||||
var startTask = connection.StartAsync(TransferFormat.Text).OrTimeout();
|
var startTask = connection.StartAsync(TransferFormat.Text);
|
||||||
Assert.False(connectResponseTcs.Task.IsCompleted);
|
Assert.False(connectResponseTcs.Task.IsCompleted);
|
||||||
Assert.False(startTask.IsCompleted);
|
Assert.False(startTask.IsCompleted);
|
||||||
connectResponseTcs.TrySetResult(null);
|
connectResponseTcs.TrySetResult(null);
|
||||||
await startTask;
|
await startTask.OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -383,7 +382,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
private static async Task AssertDisposedAsync(HttpConnection connection)
|
private static async Task AssertDisposedAsync(HttpConnection connection)
|
||||||
{
|
{
|
||||||
var exception =
|
var exception =
|
||||||
await Assert.ThrowsAsync<ObjectDisposedException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
await Assert.ThrowsAsync<ObjectDisposedException>(() => connection.StartAsync(TransferFormat.Text));
|
||||||
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
|
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
using Microsoft.AspNetCore.SignalR.Tests;
|
using Microsoft.AspNetCore.SignalR.Tests;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
public partial class HubConnectionTests
|
public partial class HubConnectionTests
|
||||||
{
|
{
|
||||||
public class ConnectionLifecycle
|
public class ConnectionLifecycle : VerifiableLoggedTest
|
||||||
{
|
{
|
||||||
// This tactic (using names and a dictionary) allows non-serializable data (like a Func) to be used in a theory AND get it to show in the new hierarchical view in Test Explorer as separate tests you can run individually.
|
// This tactic (using names and a dictionary) allows non-serializable data (like a Func) to be used in a theory AND get it to show in the new hierarchical view in Test Explorer as separate tests you can run individually.
|
||||||
private static readonly IDictionary<string, Func<HubConnection, Task>> MethodsThatRequireActiveConnection = new Dictionary<string, Func<HubConnection, Task>>()
|
private static readonly IDictionary<string, Func<HubConnection, Task>> MethodsThatRequireActiveConnection = new Dictionary<string, Func<HubConnection, Task>>()
|
||||||
|
|
@ -30,27 +31,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
public static IEnumerable<object[]> MethodsNamesThatRequireActiveConnection => MethodsThatRequireActiveConnection.Keys.Select(k => new object[] { k });
|
public static IEnumerable<object[]> MethodsNamesThatRequireActiveConnection => MethodsThatRequireActiveConnection.Keys.Select(k => new object[] { k });
|
||||||
|
|
||||||
private HubConnection CreateHubConnection(TestConnection testConnection)
|
|
||||||
{
|
|
||||||
var builder = new HubConnectionBuilder();
|
|
||||||
|
|
||||||
var delegateConnectionFactory = new DelegateConnectionFactory(
|
|
||||||
testConnection.StartAsync,
|
|
||||||
connection => ((TestConnection)connection).DisposeAsync());
|
|
||||||
builder.Services.AddSingleton<IConnectionFactory>(delegateConnectionFactory);
|
|
||||||
|
|
||||||
return builder.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private HubConnection CreateHubConnection(Func<TransferFormat, Task<ConnectionContext>> connectDelegate, Func<ConnectionContext, Task> disposeDelegate)
|
|
||||||
{
|
|
||||||
var builder = new HubConnectionBuilder();
|
|
||||||
|
|
||||||
var delegateConnectionFactory = new DelegateConnectionFactory(connectDelegate, disposeDelegate);
|
|
||||||
builder.Services.AddSingleton<IConnectionFactory>(delegateConnectionFactory);
|
|
||||||
|
|
||||||
return builder.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StartAsyncStartsTheUnderlyingConnection()
|
public async Task StartAsyncStartsTheUnderlyingConnection()
|
||||||
|
|
@ -60,7 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
Assert.Equal(HubConnectionState.Disconnected, connection.State);
|
Assert.Equal(HubConnectionState.Disconnected, connection.State);
|
||||||
|
|
||||||
await connection.StartAsync();
|
await connection.StartAsync().OrTimeout();
|
||||||
Assert.True(testConnection.Started.IsCompleted);
|
Assert.True(testConnection.Started.IsCompleted);
|
||||||
Assert.Equal(HubConnectionState.Connected, connection.State);
|
Assert.Equal(HubConnectionState.Connected, connection.State);
|
||||||
});
|
});
|
||||||
|
|
@ -73,22 +53,22 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
var testConnection = new TestConnection(onStart: SyncPoint.Create(out var syncPoint));
|
var testConnection = new TestConnection(onStart: SyncPoint.Create(out var syncPoint));
|
||||||
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
||||||
{
|
{
|
||||||
var firstStart = connection.StartAsync().OrTimeout();
|
var firstStart = connection.StartAsync();
|
||||||
Assert.False(firstStart.IsCompleted);
|
Assert.False(firstStart.IsCompleted);
|
||||||
|
|
||||||
// Wait for us to be in IConnectionFactory.ConnectAsync
|
// Wait for us to be in IConnectionFactory.ConnectAsync
|
||||||
await syncPoint.WaitForSyncPoint();
|
await syncPoint.WaitForSyncPoint().OrTimeout();
|
||||||
|
|
||||||
// Try starting again
|
// Try starting again
|
||||||
var secondStart = connection.StartAsync().OrTimeout();
|
var secondStart = connection.StartAsync();
|
||||||
Assert.False(secondStart.IsCompleted);
|
Assert.False(secondStart.IsCompleted);
|
||||||
|
|
||||||
// Release the sync point
|
// Release the sync point
|
||||||
syncPoint.Continue();
|
syncPoint.Continue();
|
||||||
|
|
||||||
// The first start should finish fine, but the second throws an InvalidOperationException.
|
// The first start should finish fine, but the second throws an InvalidOperationException.
|
||||||
await firstStart;
|
await firstStart.OrTimeout();
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() => secondStart);
|
await Assert.ThrowsAsync<InvalidOperationException>(() => secondStart).OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +88,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
return ((TestConnection)connection).DisposeAsync();
|
return ((TestConnection)connection).DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
await AsyncUsing(CreateHubConnection(ConnectionFactory, DisposeAsync), async connection =>
|
var builder = new HubConnectionBuilder();
|
||||||
|
var delegateConnectionFactory = new DelegateConnectionFactory(ConnectionFactory, DisposeAsync);
|
||||||
|
builder.Services.AddSingleton<IConnectionFactory>(delegateConnectionFactory);
|
||||||
|
|
||||||
|
await AsyncUsing(builder.Build(), async connection =>
|
||||||
{
|
{
|
||||||
Assert.Equal(HubConnectionState.Disconnected, connection.State);
|
Assert.Equal(HubConnectionState.Disconnected, connection.State);
|
||||||
|
|
||||||
|
|
@ -139,47 +123,74 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
Task DisposeAsync(ConnectionContext connection) => ((TestConnection)connection).DisposeAsync();
|
Task DisposeAsync(ConnectionContext connection) => ((TestConnection)connection).DisposeAsync();
|
||||||
|
|
||||||
await AsyncUsing(CreateHubConnection(ConnectionFactory, DisposeAsync), async connection =>
|
var builder = new HubConnectionBuilder();
|
||||||
|
var delegateConnectionFactory = new DelegateConnectionFactory(ConnectionFactory, DisposeAsync);
|
||||||
|
builder.Services.AddSingleton<IConnectionFactory>(delegateConnectionFactory);
|
||||||
|
|
||||||
|
await AsyncUsing(builder.Build(), async connection =>
|
||||||
{
|
{
|
||||||
await connection.StartAsync().OrTimeout();
|
await connection.StartAsync().OrTimeout();
|
||||||
Assert.Equal(1, createCount);
|
Assert.Equal(1, createCount);
|
||||||
|
|
||||||
var stopTask = connection.StopAsync().OrTimeout();
|
var stopTask = connection.StopAsync();
|
||||||
|
|
||||||
// Wait to hit DisposeAsync on TestConnection (which should be after StopAsync has cleared the connection state)
|
// Wait to hit DisposeAsync on TestConnection (which should be after StopAsync has cleared the connection state)
|
||||||
await syncPoint.WaitForSyncPoint().OrTimeout();
|
await syncPoint.WaitForSyncPoint().OrTimeout();
|
||||||
|
|
||||||
// We should not yet be able to start now because StopAsync hasn't completed
|
// We should not yet be able to start now because StopAsync hasn't completed
|
||||||
Assert.False(stopTask.IsCompleted);
|
Assert.False(stopTask.IsCompleted);
|
||||||
var startTask = connection.StartAsync().OrTimeout();
|
var startTask = connection.StartAsync();
|
||||||
Assert.False(stopTask.IsCompleted);
|
Assert.False(stopTask.IsCompleted);
|
||||||
|
|
||||||
// When we release the sync point, the StopAsync task will finish
|
// When we release the sync point, the StopAsync task will finish
|
||||||
syncPoint.Continue();
|
syncPoint.Continue();
|
||||||
await stopTask;
|
await stopTask.OrTimeout();
|
||||||
|
|
||||||
// Which will then allow StartAsync to finish.
|
// Which will then allow StartAsync to finish.
|
||||||
await startTask;
|
await startTask.OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StartAsyncWithFailedHandshakeCanBeStopped()
|
public async Task StartAsyncWithFailedHandshakeCanBeStopped()
|
||||||
{
|
{
|
||||||
var testConnection = new TestConnection(autoHandshake: false);
|
var handshakeConnectionErrorLogged = false;
|
||||||
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
|
||||||
{
|
|
||||||
testConnection.Transport.Input.Complete();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await connection.StartAsync();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{ }
|
|
||||||
|
|
||||||
await connection.StopAsync();
|
bool ExpectedErrors(WriteContext writeContext)
|
||||||
Assert.True(testConnection.Started.IsCompleted);
|
{
|
||||||
});
|
if (writeContext.LoggerName == typeof(HubConnection).FullName)
|
||||||
|
{
|
||||||
|
if (writeContext.EventId.Name == "ErrorReceivingHandshakeResponse")
|
||||||
|
{
|
||||||
|
handshakeConnectionErrorLogged = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeContext.EventId.Name == "ErrorStartingConnection";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (StartVerifiableLog(ExpectedErrors))
|
||||||
|
{
|
||||||
|
var testConnection = new TestConnection(autoHandshake: false);
|
||||||
|
await AsyncUsing(CreateHubConnection(testConnection, loggerFactory: LoggerFactory), async connection =>
|
||||||
|
{
|
||||||
|
testConnection.Transport.Input.Complete();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{ }
|
||||||
|
|
||||||
|
await connection.StopAsync().OrTimeout();
|
||||||
|
Assert.True(testConnection.Started.IsCompleted);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(handshakeConnectionErrorLogged, "The connnection error during the handshake wasn't logged.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -191,7 +202,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
var testConnection = new TestConnection();
|
var testConnection = new TestConnection();
|
||||||
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
||||||
{
|
{
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => method(connection));
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => method(connection)).OrTimeout();
|
||||||
Assert.Equal($"The '{name}' method cannot be called if the connection is not active", ex.Message);
|
Assert.Equal($"The '{name}' method cannot be called if the connection is not active", ex.Message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -207,27 +218,27 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
||||||
{
|
{
|
||||||
// Start, and wait for the sync point to be hit
|
// Start, and wait for the sync point to be hit
|
||||||
var startTask = connection.StartAsync().OrTimeout();
|
var startTask = connection.StartAsync();
|
||||||
Assert.False(startTask.IsCompleted);
|
Assert.False(startTask.IsCompleted);
|
||||||
await syncPoint.WaitForSyncPoint();
|
await syncPoint.WaitForSyncPoint().OrTimeout();
|
||||||
|
|
||||||
// Run the method, but it will be waiting for the lock
|
// Run the method, but it will be waiting for the lock
|
||||||
var targetTask = method(connection).OrTimeout();
|
var targetTask = method(connection);
|
||||||
|
|
||||||
// Release the SyncPoint
|
// Release the SyncPoint
|
||||||
syncPoint.Continue();
|
syncPoint.Continue();
|
||||||
|
|
||||||
// Wait for start to finish
|
// Wait for start to finish
|
||||||
await startTask;
|
await startTask.OrTimeout();
|
||||||
|
|
||||||
// We need some special logic to ensure InvokeAsync completes.
|
// We need some special logic to ensure InvokeAsync completes.
|
||||||
if (string.Equals(name, nameof(HubConnection.InvokeCoreAsync)))
|
if (string.Equals(name, nameof(HubConnection.InvokeCoreAsync)))
|
||||||
{
|
{
|
||||||
await ForceLastInvocationToComplete(testConnection);
|
await ForceLastInvocationToComplete(testConnection).OrTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the method to complete.
|
// Wait for the method to complete.
|
||||||
await targetTask;
|
await targetTask.OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,9 +250,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
await AsyncUsing(CreateHubConnection(testConnection), async connection =>
|
||||||
{
|
{
|
||||||
// Start, and wait for the sync point to be hit
|
// Start, and wait for the sync point to be hit
|
||||||
var startTask = connection.StartAsync().OrTimeout();
|
var startTask = connection.StartAsync();
|
||||||
Assert.False(startTask.IsCompleted);
|
Assert.False(startTask.IsCompleted);
|
||||||
await syncPoint.WaitForSyncPoint();
|
await syncPoint.WaitForSyncPoint().OrTimeout();
|
||||||
|
|
||||||
Assert.Equal(HubConnectionState.Connecting, connection.State);
|
Assert.Equal(HubConnectionState.Connecting, connection.State);
|
||||||
|
|
||||||
|
|
@ -249,7 +260,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
syncPoint.Continue();
|
syncPoint.Continue();
|
||||||
|
|
||||||
// Wait for start to finish
|
// Wait for start to finish
|
||||||
await startTask;
|
await startTask.OrTimeout();
|
||||||
|
|
||||||
Assert.Equal(HubConnectionState.Connected, connection.State);
|
Assert.Equal(HubConnectionState.Connected, connection.State);
|
||||||
});
|
});
|
||||||
|
|
@ -349,7 +360,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await closed.Task.OrTimeout();
|
await closed.Task.OrTimeout();
|
||||||
|
|
||||||
// We should be stopped now
|
// We should be stopped now
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo").OrTimeout());
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo")).OrTimeout();
|
||||||
Assert.Equal($"The '{nameof(HubConnection.SendCoreAsync)}' method cannot be called if the connection is not active", ex.Message);
|
Assert.Equal($"The '{nameof(HubConnection.SendCoreAsync)}' method cannot be called if the connection is not active", ex.Message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -374,20 +385,20 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
Assert.True(testConnection.Started.IsCompleted);
|
Assert.True(testConnection.Started.IsCompleted);
|
||||||
|
|
||||||
// Start shutting down and complete the transport side
|
// Start shutting down and complete the transport side
|
||||||
var stopTask = connection.StopAsync().OrTimeout();
|
var stopTask = connection.StopAsync();
|
||||||
testConnection.CompleteFromTransport();
|
testConnection.CompleteFromTransport();
|
||||||
|
|
||||||
// Wait for the connection to close.
|
// Wait for the connection to close.
|
||||||
await testConnectionClosed.Task.OrTimeout();
|
await testConnectionClosed.Task.OrTimeout();
|
||||||
|
|
||||||
// The stop should be completed.
|
// The stop should be completed.
|
||||||
await stopTask;
|
await stopTask.OrTimeout();
|
||||||
|
|
||||||
// The HubConnection should now be closed.
|
// The HubConnection should now be closed.
|
||||||
await connectionClosed.Task.OrTimeout();
|
await connectionClosed.Task.OrTimeout();
|
||||||
|
|
||||||
// We should be stopped now
|
// We should be stopped now
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo").OrTimeout());
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo")).OrTimeout();
|
||||||
Assert.Equal($"The '{nameof(HubConnection.SendCoreAsync)}' method cannot be called if the connection is not active", ex.Message);
|
Assert.Equal($"The '{nameof(HubConnection.SendCoreAsync)}' method cannot be called if the connection is not active", ex.Message);
|
||||||
|
|
||||||
await testConnection.Disposed.OrTimeout();
|
await testConnection.Disposed.OrTimeout();
|
||||||
|
|
@ -416,16 +427,16 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
testConnection.CompleteFromTransport();
|
testConnection.CompleteFromTransport();
|
||||||
|
|
||||||
// Start stopping manually (these can't be synchronized by a Sync Point because the transport is disposed outside the lock)
|
// Start stopping manually (these can't be synchronized by a Sync Point because the transport is disposed outside the lock)
|
||||||
var stopTask = connection.StopAsync().OrTimeout();
|
var stopTask = connection.StopAsync();
|
||||||
|
|
||||||
await testConnection.Disposed.OrTimeout();
|
await testConnection.Disposed.OrTimeout();
|
||||||
|
|
||||||
// Wait for the stop task to complete and the closed event to fire
|
// Wait for the stop task to complete and the closed event to fire
|
||||||
await stopTask;
|
await stopTask.OrTimeout();
|
||||||
await connectionClosed.Task.OrTimeout();
|
await connectionClosed.Task.OrTimeout();
|
||||||
|
|
||||||
// We should be stopped now
|
// We should be stopped now
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo").OrTimeout());
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo")).OrTimeout();
|
||||||
Assert.Equal($"The '{nameof(HubConnection.SendCoreAsync)}' method cannot be called if the connection is not active", ex.Message);
|
Assert.Equal($"The '{nameof(HubConnection.SendCoreAsync)}' method cannot be called if the connection is not active", ex.Message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -444,89 +455,141 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
// Stop and invoke the method. These two aren't synchronizable via a Sync Point any more because the transport is disposed
|
// Stop and invoke the method. These two aren't synchronizable via a Sync Point any more because the transport is disposed
|
||||||
// outside the lock :(
|
// outside the lock :(
|
||||||
var disposeTask = connection.StopAsync().OrTimeout();
|
var disposeTask = connection.StopAsync();
|
||||||
|
|
||||||
// Wait to hit DisposeAsync on TestConnection (which should be after StopAsync has cleared the connection state)
|
// Wait to hit DisposeAsync on TestConnection (which should be after StopAsync has cleared the connection state)
|
||||||
await syncPoint.WaitForSyncPoint().OrTimeout();
|
await syncPoint.WaitForSyncPoint().OrTimeout();
|
||||||
|
|
||||||
var targetTask = method(connection).OrTimeout();
|
var targetTask = method(connection);
|
||||||
|
|
||||||
// Release the sync point
|
// Release the sync point
|
||||||
syncPoint.Continue();
|
syncPoint.Continue();
|
||||||
|
|
||||||
// Wait for the method to complete, with an expected error.
|
// Wait for the method to complete, with an expected error.
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => targetTask);
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => targetTask).OrTimeout();
|
||||||
Assert.Equal($"The '{methodName}' method cannot be called if the connection is not active", ex.Message);
|
Assert.Equal($"The '{methodName}' method cannot be called if the connection is not active", ex.Message);
|
||||||
|
|
||||||
await disposeTask;
|
await disposeTask.OrTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ClientTimesoutWhenHandshakeResponseTakesTooLong()
|
public async Task ClientTimesoutWhenHandshakeResponseTakesTooLong()
|
||||||
{
|
{
|
||||||
var connection = new TestConnection(autoHandshake: false);
|
var handshakeTimeoutLogged = false;
|
||||||
var hubConnection = CreateHubConnection(connection);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
hubConnection.HandshakeTimeout = TimeSpan.FromMilliseconds(1);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<OperationCanceledException>(() => hubConnection.StartAsync().OrTimeout());
|
bool ExpectedErrors(WriteContext writeContext)
|
||||||
Assert.Equal(HubConnectionState.Disconnected, hubConnection.State);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
await hubConnection.DisposeAsync().OrTimeout();
|
if (writeContext.LoggerName == typeof(HubConnection).FullName)
|
||||||
await connection.DisposeAsync().OrTimeout();
|
{
|
||||||
|
if (writeContext.EventId.Name == "ErrorHandshakeTimedOut")
|
||||||
|
{
|
||||||
|
handshakeTimeoutLogged = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeContext.EventId.Name == "ErrorStartingConnection";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using (StartVerifiableLog(ExpectedErrors))
|
||||||
|
{
|
||||||
|
var connection = new TestConnection(autoHandshake: false);
|
||||||
|
var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hubConnection.HandshakeTimeout = TimeSpan.FromMilliseconds(1);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<OperationCanceledException>(() => hubConnection.StartAsync()).OrTimeout();
|
||||||
|
Assert.Equal(HubConnectionState.Disconnected, hubConnection.State);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await hubConnection.DisposeAsync().OrTimeout();
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(handshakeTimeoutLogged, "The handshake timeout wasn't logged.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StartAsyncWithTriggeredCancellationTokenIsCanceled()
|
public async Task StartAsyncWithTriggeredCancellationTokenIsCanceled()
|
||||||
{
|
{
|
||||||
var onStartCalled = false;
|
using (StartVerifiableLog())
|
||||||
var connection = new TestConnection(onStart: () =>
|
|
||||||
{
|
{
|
||||||
onStartCalled = true;
|
var onStartCalled = false;
|
||||||
return Task.CompletedTask;
|
var connection = new TestConnection(onStart: () =>
|
||||||
});
|
{
|
||||||
var hubConnection = CreateHubConnection(connection);
|
onStartCalled = true;
|
||||||
try
|
return Task.CompletedTask;
|
||||||
{
|
});
|
||||||
await Assert.ThrowsAsync<OperationCanceledException>(() => hubConnection.StartAsync(new CancellationToken(canceled: true)).OrTimeout());
|
var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory);
|
||||||
Assert.False(onStartCalled);
|
try
|
||||||
}
|
{
|
||||||
finally
|
await Assert.ThrowsAsync<OperationCanceledException>(() => hubConnection.StartAsync(new CancellationToken(canceled: true))).OrTimeout();
|
||||||
{
|
Assert.False(onStartCalled);
|
||||||
await hubConnection.DisposeAsync().OrTimeout();
|
}
|
||||||
await connection.DisposeAsync().OrTimeout();
|
finally
|
||||||
|
{
|
||||||
|
await hubConnection.DisposeAsync().OrTimeout();
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StartAsyncCanTriggerCancellationTokenToCancelHandshake()
|
public async Task StartAsyncCanTriggerCancellationTokenToCancelHandshake()
|
||||||
{
|
{
|
||||||
var cts = new CancellationTokenSource();
|
var handshakeCancellationLogged = false;
|
||||||
var connection = new TestConnection(onStart: () =>
|
|
||||||
{
|
|
||||||
cts.Cancel();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}, autoHandshake: false);
|
|
||||||
var hubConnection = CreateHubConnection(connection);
|
|
||||||
// We want to make sure the cancellation is because of the token passed to StartAsync
|
|
||||||
hubConnection.HandshakeTimeout = Timeout.InfiniteTimeSpan;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var startTask = hubConnection.StartAsync(cts.Token);
|
|
||||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => startTask.OrTimeout());
|
|
||||||
|
|
||||||
// We aren't worried about the exact message and it's localized so asserting it is non-trivial.
|
bool ExpectedErrors(WriteContext writeContext)
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
await hubConnection.DisposeAsync().OrTimeout();
|
if (writeContext.LoggerName == typeof(HubConnection).FullName)
|
||||||
await connection.DisposeAsync().OrTimeout();
|
{
|
||||||
|
if (writeContext.EventId.Name == "ErrorHandshakeCanceled")
|
||||||
|
{
|
||||||
|
handshakeCancellationLogged = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeContext.EventId.Name == "ErrorStartingConnection";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using (StartVerifiableLog(ExpectedErrors))
|
||||||
|
{
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
TestConnection connection = null;
|
||||||
|
|
||||||
|
connection = new TestConnection(autoHandshake: false);
|
||||||
|
|
||||||
|
var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory);
|
||||||
|
// We want to make sure the cancellation is because of the token passed to StartAsync
|
||||||
|
hubConnection.HandshakeTimeout = Timeout.InfiniteTimeSpan;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var startTask = hubConnection.StartAsync(cts.Token);
|
||||||
|
|
||||||
|
await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
// We aren't worried about the exact message and it's localized so asserting it is non-trivial.
|
||||||
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => startTask).OrTimeout();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await hubConnection.DisposeAsync().OrTimeout();
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(handshakeCancellationLogged, "The handshake cancellation wasn't logged.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -574,13 +637,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Using OrTimeout here will hide any timeout issues in the test :(.
|
||||||
await action(connection);
|
await action(connection);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Dispose isn't under test here, so fire and forget so that errors/timeouts here don't cause
|
// Dispose isn't under test here, so fire and forget so that errors/timeouts here don't cause
|
||||||
// test errors that mask the real errors.
|
// test errors that mask the real errors.
|
||||||
_ = connection.DisposeAsync();
|
_ = connection.DisposeAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// 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 Microsoft.AspNetCore.SignalR.Protocol;
|
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
using Microsoft.AspNetCore.SignalR.Tests;
|
using Microsoft.AspNetCore.SignalR.Tests;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -7,7 +10,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
public partial class HubConnectionTests
|
public partial class HubConnectionTests
|
||||||
{
|
{
|
||||||
private static HubConnection CreateHubConnection(TestConnection connection, IHubProtocol protocol = null, ILoggerFactory loggerFactory = null, IRetryPolicy reconnectPolicy = null)
|
private static HubConnection CreateHubConnection(TestConnection connection, IHubProtocol protocol = null, ILoggerFactory loggerFactory = null)
|
||||||
{
|
{
|
||||||
var builder = new HubConnectionBuilder();
|
var builder = new HubConnectionBuilder();
|
||||||
|
|
||||||
|
|
@ -27,11 +30,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
builder.Services.AddSingleton(protocol);
|
builder.Services.AddSingleton(protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reconnectPolicy != null)
|
|
||||||
{
|
|
||||||
builder.WithAutomaticReconnect(reconnectPolicy);
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.IO;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Tests;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
// don't cause problems.
|
// don't cause problems.
|
||||||
public partial class HubConnectionTests
|
public partial class HubConnectionTests
|
||||||
{
|
{
|
||||||
public class Protocol
|
public class Protocol : VerifiableLoggedTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendAsyncSendsANonBlockingInvocationMessage()
|
public async Task SendAsyncSendsANonBlockingInvocationMessage()
|
||||||
|
|
@ -31,6 +32,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
||||||
Assert.Equal("{\"type\":1,\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
Assert.Equal("{\"type\":1,\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
||||||
|
|
||||||
|
await invokeTask.OrTimeout();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -47,14 +50,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// We can't await StartAsync because it depends on the negotiate process!
|
// We can't await StartAsync because it depends on the negotiate process!
|
||||||
var startTask = hubConnection.StartAsync().OrTimeout();
|
var startTask = hubConnection.StartAsync();
|
||||||
|
|
||||||
var handshakeMessage = await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
var handshakeMessage = await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||||
|
|
||||||
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
||||||
Assert.Equal("{\"protocol\":\"json\",\"version\":1}", handshakeMessage);
|
Assert.Equal("{\"protocol\":\"json\",\"version\":1}", handshakeMessage);
|
||||||
|
|
||||||
await startTask;
|
await startTask.OrTimeout();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -63,6 +66,35 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvalidHandshakeResponseCausesStartToFail()
|
||||||
|
{
|
||||||
|
using (StartVerifiableLog())
|
||||||
|
{
|
||||||
|
var connection = new TestConnection(autoHandshake: false);
|
||||||
|
var hubConnection = CreateHubConnection(connection);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// We can't await StartAsync because it depends on the negotiate process!
|
||||||
|
var startTask = hubConnection.StartAsync();
|
||||||
|
|
||||||
|
await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||||
|
|
||||||
|
// The client expects the first message to be a handshake response, but a handshake response doesn't have a "type".
|
||||||
|
await connection.ReceiveJsonMessage(new { type = "foo" }).OrTimeout();
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidDataException>(() => startTask).OrTimeout();
|
||||||
|
|
||||||
|
Assert.Equal("Expected a handshake response from the server.", ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await hubConnection.DisposeAsync().OrTimeout();
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ClientIsOkayReceivingMinorVersionInHandshake()
|
public async Task ClientIsOkayReceivingMinorVersionInHandshake()
|
||||||
{
|
{
|
||||||
|
|
@ -74,9 +106,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var startTask = hubConnection.StartAsync();
|
var startTask = hubConnection.StartAsync();
|
||||||
var message = await connection.ReadHandshakeAndSendResponseAsync(56);
|
var message = await connection.ReadHandshakeAndSendResponseAsync(56).OrTimeout();
|
||||||
|
|
||||||
await startTask;
|
await startTask.OrTimeout();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -94,12 +126,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout();
|
var invokeTask = hubConnection.InvokeAsync("Foo");
|
||||||
|
|
||||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||||
|
|
||||||
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
||||||
Assert.Equal("{\"type\":1,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
Assert.Equal("{\"type\":1,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
||||||
|
|
||||||
|
Assert.Equal(TaskStatus.WaitingForActivation, invokeTask.Status);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -184,7 +218,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
// Complete the channel
|
// Complete the channel
|
||||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
||||||
await channel.Completion;
|
await channel.Completion.OrTimeout();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -202,7 +236,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout();
|
var invokeTask = hubConnection.InvokeAsync("Foo");
|
||||||
|
|
||||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
||||||
|
|
||||||
|
|
@ -246,7 +280,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo").OrTimeout();
|
var invokeTask = hubConnection.InvokeAsync<int>("Foo");
|
||||||
|
|
||||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, result = 42 }).OrTimeout();
|
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, result = 42 }).OrTimeout();
|
||||||
|
|
||||||
|
|
@ -268,7 +302,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo").OrTimeout();
|
var invokeTask = hubConnection.InvokeAsync<int>("Foo");
|
||||||
|
|
||||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, error = "An error occurred" }).OrTimeout();
|
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, error = "An error occurred" }).OrTimeout();
|
||||||
|
|
||||||
|
|
@ -295,7 +329,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, result = "Oops" }).OrTimeout();
|
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, result = "Oops" }).OrTimeout();
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await channel.ReadAndCollectAllAsync().OrTimeout());
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => channel.ReadAndCollectAllAsync()).OrTimeout();
|
||||||
Assert.Equal("Server provided a result in a completion response to a streamed invocation.", ex.Message);
|
Assert.Equal("Server provided a result in a completion response to a streamed invocation.", ex.Message);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -318,7 +352,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, error = "An error occurred" }).OrTimeout();
|
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, error = "An error occurred" }).OrTimeout();
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<HubException>(async () => await channel.ReadAndCollectAllAsync().OrTimeout());
|
var ex = await Assert.ThrowsAsync<HubException>(async () => await channel.ReadAndCollectAllAsync()).OrTimeout();
|
||||||
Assert.Equal("An error occurred", ex.Message);
|
Assert.Equal("An error occurred", ex.Message);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -337,7 +371,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo").OrTimeout();
|
var invokeTask = hubConnection.InvokeAsync<int>("Foo");
|
||||||
|
|
||||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = 42 }).OrTimeout();
|
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = 42 }).OrTimeout();
|
||||||
|
|
||||||
|
|
@ -479,7 +513,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
// Send an invocation
|
// Send an invocation
|
||||||
var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout();
|
var invokeTask = hubConnection.InvokeAsync("Foo");
|
||||||
|
|
||||||
// Receive the ping mid-invocation so we can see that the rest of the flow works fine
|
// Receive the ping mid-invocation so we can see that the rest of the flow works fine
|
||||||
await connection.ReceiveJsonMessage(new { type = 6 }).OrTimeout();
|
await connection.ReceiveJsonMessage(new { type = 6 }).OrTimeout();
|
||||||
|
|
@ -506,15 +540,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
var task = hubConnection.StartAsync();
|
var task = hubConnection.StartAsync();
|
||||||
|
|
||||||
await connection.ReceiveTextAsync("{");
|
await connection.ReceiveTextAsync("{").OrTimeout();
|
||||||
|
|
||||||
Assert.False(task.IsCompleted);
|
Assert.False(task.IsCompleted);
|
||||||
|
|
||||||
await connection.ReceiveTextAsync("}");
|
await connection.ReceiveTextAsync("}").OrTimeout();
|
||||||
|
|
||||||
Assert.False(task.IsCompleted);
|
Assert.False(task.IsCompleted);
|
||||||
|
|
||||||
await connection.ReceiveTextAsync("\u001e");
|
await connection.ReceiveTextAsync("\u001e").OrTimeout();
|
||||||
|
|
||||||
await task.OrTimeout();
|
await task.OrTimeout();
|
||||||
}
|
}
|
||||||
|
|
@ -539,9 +573,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
tcs.TrySetResult(data);
|
tcs.TrySetResult(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
await connection.ReceiveTextAsync(payload);
|
await connection.ReceiveTextAsync(payload).OrTimeout();
|
||||||
|
|
||||||
await hubConnection.StartAsync();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
var response = await tcs.Task.OrTimeout();
|
var response = await tcs.Task.OrTimeout();
|
||||||
Assert.Equal("hello", response);
|
Assert.Equal("hello", response);
|
||||||
|
|
@ -568,15 +602,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
await connection.ReceiveTextAsync("{\"type\":1, ");
|
await connection.ReceiveTextAsync("{\"type\":1, ").OrTimeout();
|
||||||
|
|
||||||
Assert.False(tcs.Task.IsCompleted);
|
Assert.False(tcs.Task.IsCompleted);
|
||||||
|
|
||||||
await connection.ReceiveTextAsync("\"target\": \"Echo\", \"arguments\"");
|
await connection.ReceiveTextAsync("\"target\": \"Echo\", \"arguments\"").OrTimeout();
|
||||||
|
|
||||||
Assert.False(tcs.Task.IsCompleted);
|
Assert.False(tcs.Task.IsCompleted);
|
||||||
|
|
||||||
await connection.ReceiveTextAsync(":[\"hello\"]}\u001e");
|
await connection.ReceiveTextAsync(":[\"hello\"]}\u001e").OrTimeout();
|
||||||
|
|
||||||
var response = await tcs.Task.OrTimeout();
|
var response = await tcs.Task.OrTimeout();
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue