// 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.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; using Microsoft.AspNetCore.Sockets.Client.Http; using Microsoft.AspNetCore.Sockets.Client.Tests; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Microsoft.AspNetCore.SignalR.Client.Tests { public partial class HttpConnectionTests { // Nested class for grouping public class AbortAsync { [Fact] public async Task AbortAsyncTriggersClosedEventWithException() { var connection = CreateConnection(out var closedTask); try { // Start the connection await connection.StartAsync().OrTimeout(); // Abort with an error var expected = new Exception("Ruh roh!"); await connection.AbortAsync(expected).OrTimeout(); // Verify that it is thrown var actual = await Assert.ThrowsAsync(async () => await closedTask.OrTimeout()); Assert.Same(expected, actual); } finally { // Dispose should be clean and exception free. await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task AbortAsyncWhileStoppingTriggersClosedEventWithException() { var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(2, out var syncPoints)); try { // Start the connection await connection.StartAsync().OrTimeout(); // Stop normally var stopTask = connection.StopAsync().OrTimeout(); // Wait to reach the first sync point await syncPoints[0].WaitForSyncPoint().OrTimeout(); // Abort with an error var expected = new Exception("Ruh roh!"); var abortTask = connection.AbortAsync(expected).OrTimeout(); // Wait for the sync point to hit again await syncPoints[1].WaitForSyncPoint().OrTimeout(); // Release sync point 0 syncPoints[0].Continue(); // We should close with the error from Abort (because it was set by the call to Abort even though Stop triggered the close) var actual = await Assert.ThrowsAsync(async () => await closedTask.OrTimeout()); Assert.Same(expected, actual); // Clean-up syncPoints[1].Continue(); await Task.WhenAll(stopTask, abortTask).OrTimeout(); } finally { // Dispose should be clean and exception free. await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task StopAsyncWhileAbortingTriggersClosedEventWithoutException() { var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(2, out var syncPoints)); try { // Start the connection await connection.StartAsync().OrTimeout(); // Abort with an error var expected = new Exception("Ruh roh!"); var abortTask = connection.AbortAsync(expected).OrTimeout(); // Wait to reach the first sync point await syncPoints[0].WaitForSyncPoint().OrTimeout(); // Stop normally, without a sync point. // This should clear the exception, meaning Closed will not "throw" syncPoints[1].Continue(); await connection.StopAsync(); await closedTask.OrTimeout(); // Clean-up syncPoints[0].Continue(); await abortTask.OrTimeout(); } finally { // Dispose should be clean and exception free. await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task StartAsyncCannotBeCalledWhileAbortAsyncInProgress() { var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(out var syncPoint)); try { // Start the connection await connection.StartAsync().OrTimeout(); // Abort with an error var expected = new Exception("Ruh roh!"); var abortTask = connection.AbortAsync(expected).OrTimeout(); // Wait to reach the first sync point await syncPoint.WaitForSyncPoint().OrTimeout(); var ex = await Assert.ThrowsAsync(() => connection.StartAsync().OrTimeout()); Assert.Equal("Cannot start a connection that is not in the Disconnected state.", ex.Message); // Release the sync point and wait for close to complete // (it will throw the abort exception) syncPoint.Continue(); await abortTask.OrTimeout(); var actual = await Assert.ThrowsAsync(() => closedTask.OrTimeout()); Assert.Same(expected, actual); // We can start now await connection.StartAsync().OrTimeout(); // And we can stop without getting the abort exception. await connection.StopAsync().OrTimeout(); } finally { // Dispose should be clean and exception free. await connection.DisposeAsync().OrTimeout(); } } private HttpConnection CreateConnection(out Task closedTask, Func stopHandler = null) { var httpHandler = new TestHttpMessageHandler(); var transportFactory = new TestTransportFactory(new TestTransport(stopHandler)); var connection = new HttpConnection( new Uri("http://fakeuri.org/"), transportFactory, NullLoggerFactory.Instance, new HttpOptions() { HttpMessageHandler = httpHandler, }); var closedTcs = new TaskCompletionSource(); connection.Closed += ex => { if (ex != null) { closedTcs.SetException(ex); } else { closedTcs.SetResult(null); } }; closedTask = closedTcs.Task; return connection; } private class TestTransport : ITransport { private Channel _application; private readonly Func _stopHandler; public TransferMode? Mode => TransferMode.Text; public TestTransport(Func stopHandler) { _stopHandler = stopHandler ?? new Func(() => Task.CompletedTask); } public Task StartAsync(Uri url, Channel application, TransferMode requestedTransferMode, string connectionId, IConnection connection) { _application = application; return Task.CompletedTask; } public async Task StopAsync() { await _stopHandler(); _application.Writer.TryComplete(); } } // Possibly useful as a general-purpose async testing helper? private class SyncPoint { private TaskCompletionSource _atSyncPoint = new TaskCompletionSource(); private TaskCompletionSource _continueFromSyncPoint = new TaskCompletionSource(); // Used by the test code to wait and continue public Task WaitForSyncPoint() => _atSyncPoint.Task; public void Continue() => _continueFromSyncPoint.TrySetResult(null); // Used by the code under test to wait for the test code to release it. public Task WaitToContinue() { _atSyncPoint.TrySetResult(null); return _continueFromSyncPoint.Task; } public static Func Create(out SyncPoint syncPoint) { var handler = Create(1, out var syncPoints); syncPoint = syncPoints[0]; return handler; } /// /// Creates a re-entrant function that waits for sync points in sequence. /// /// The number of sync points to expect /// The objects that can be used to coordinate the sync point /// public static Func Create(int count, out SyncPoint[] syncPoints) { // Need to use a local so the closure can capture it. You can't use out vars in a closure. var localSyncPoints = new SyncPoint[count]; for (var i = 0; i < count; i += 1) { localSyncPoints[i] = new SyncPoint(); } syncPoints = localSyncPoints; var counter = 0; return () => { if (counter >= localSyncPoints.Length) { return Task.CompletedTask; } else { var syncPoint = localSyncPoints[counter]; counter += 1; return syncPoint.WaitToContinue(); } }; } } } } }