aspnetcore/test/Microsoft.AspNetCore.Signal.../HttpConnectionTests.Connect...

386 lines
17 KiB
C#

// 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.IO.Pipelines;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Http.Connections.Client;
using Microsoft.AspNetCore.Http.Connections.Client.Internal;
using Microsoft.AspNetCore.SignalR.Tests;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.SignalR.Client.Tests
{
public partial class HttpConnectionTests
{
public class ConnectionLifecycle : VerifiableLoggedTest
{
public ConnectionLifecycle(ITestOutputHelper output) : base(output)
{
}
[Fact]
public async Task CanStartStartedConnection()
{
using (StartVerifiableLog(out var loggerFactory))
{
await WithConnectionAsync(CreateConnection(loggerFactory: loggerFactory), async (connection) =>
{
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.StartAsync(TransferFormat.Text).OrTimeout();
});
}
}
[Fact]
public async Task CanStartStartingConnection()
{
using (StartVerifiableLog(out var loggerFactory))
{
await WithConnectionAsync(
CreateConnection(loggerFactory: loggerFactory, transport: new TestTransport(onTransportStart: SyncPoint.Create(out var syncPoint))),
async (connection) =>
{
var firstStart = connection.StartAsync(TransferFormat.Text).OrTimeout();
await syncPoint.WaitForSyncPoint();
var secondStart = connection.StartAsync(TransferFormat.Text).OrTimeout();
syncPoint.Continue();
await firstStart;
await secondStart;
});
}
}
[Fact]
public async Task CannotStartConnectionOnceDisposed()
{
using (StartVerifiableLog(out var loggerFactory))
{
await WithConnectionAsync(
CreateConnection(loggerFactory: loggerFactory),
async (connection) =>
{
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync();
var exception =
await Assert.ThrowsAsync<ObjectDisposedException>(
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
});
}
}
[Theory]
[InlineData(2)]
[InlineData(3)]
public async Task TransportThatFailsToStartFallsBack(int passThreshold)
{
bool ExpectedErrors(WriteContext writeContext)
{
return writeContext.LoggerName == typeof(HttpConnection).FullName &&
writeContext.EventId.Name == "ErrorStartingTransport";
}
using (StartVerifiableLog(out var loggerFactory, expectedErrorsFilter: ExpectedErrors))
{
var startCounter = 0;
var expected = new Exception("Transport failed to start");
// We have 4 cases here. Falling back once, falling back twice and each of these
// with WebSockets available and not. If Websockets aren't available and
// we can't to test the fallback once scenario we don't decrement the passthreshold
// because we still try to start twice (SSE and LP).
if (!TestHelpers.IsWebSocketsSupported() && passThreshold > 2)
{
passThreshold -= 1;
}
Task OnTransportStart()
{
startCounter++;
if (startCounter < passThreshold)
{
// Succeed next time
return Task.FromException(expected);
}
else
{
return Task.CompletedTask;
}
}
await WithConnectionAsync(
CreateConnection(
loggerFactory: loggerFactory,
transportType: HttpTransports.All,
transport: new TestTransport(onTransportStart: OnTransportStart)),
async (connection) =>
{
Assert.Equal(0, startCounter);
await connection.StartAsync(TransferFormat.Text);
Assert.Equal(passThreshold, startCounter);
});
}
}
[Fact]
public async Task StartThrowsAfterAllTransportsFail()
{
bool ExpectedErrors(WriteContext writeContext)
{
return writeContext.LoggerName == typeof(HttpConnection).FullName &&
writeContext.EventId.Name == "ErrorStartingTransport";
}
using (StartVerifiableLog(out var loggerFactory, expectedErrorsFilter: ExpectedErrors))
{
var startCounter = 0;
var availableTransports = 3;
var expected = new Exception("Transport failed to start");
Task OnTransportStart()
{
startCounter++;
return Task.FromException(expected);
}
await WithConnectionAsync(
CreateConnection(
loggerFactory: loggerFactory,
transportType: HttpTransports.All,
transport: new TestTransport(onTransportStart: OnTransportStart)),
async (connection) =>
{
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text));
Assert.Equal("Unable to connect to the server with any of the available transports.", ex.Message);
// If websockets aren't supported then we expect one less attmept to start.
if (!TestHelpers.IsWebSocketsSupported())
{
availableTransports -= 1;
}
Assert.Equal(availableTransports, startCounter);
});
}
}
[Fact]
public async Task CanDisposeUnstartedConnection()
{
using (StartVerifiableLog(out var loggerFactory))
{
await WithConnectionAsync(
CreateConnection(loggerFactory: loggerFactory),
async (connection) =>
{
await connection.DisposeAsync();
});
}
}
[Fact]
public async Task CanDisposeStartingConnection()
{
using (StartVerifiableLog(out var loggerFactory))
{
await WithConnectionAsync(
CreateConnection(
loggerFactory: loggerFactory,
transport: new TestTransport(
onTransportStart: SyncPoint.Create(out var transportStart),
onTransportStop: SyncPoint.Create(out var transportStop))),
async (connection) =>
{
// Start the connection and wait for the transport to start up.
var startTask = connection.StartAsync(TransferFormat.Text);
await transportStart.WaitForSyncPoint().OrTimeout();
// While the transport is starting, dispose the connection
var disposeTask = connection.DisposeAsync().OrTimeout();
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.
await startTask.OrTimeout();
// Then release DisposeAsync (via the transport StopAsync call)
await transportStop.WaitForSyncPoint().OrTimeout();
transportStop.Continue();
// Dispose should finish
await disposeTask;
});
}
}
[Fact]
public async Task CanDisposeDisposingConnection()
{
using (StartVerifiableLog(out var loggerFactory))
{
await WithConnectionAsync(
CreateConnection(
loggerFactory: loggerFactory,
transport: new TestTransport(onTransportStop: SyncPoint.Create(out var transportStop))),
async (connection) =>
{
// Start the connection
await connection.StartAsync(TransferFormat.Text).OrTimeout();
// Dispose the connection
var stopTask = connection.DisposeAsync().OrTimeout();
// Once the transport starts shutting down
await transportStop.WaitForSyncPoint();
Assert.False(stopTask.IsCompleted);
// Start disposing again, and then let the first dispose continue
var disposeTask = connection.DisposeAsync().OrTimeout();
transportStop.Continue();
// Wait for the tasks to complete
await stopTask.OrTimeout();
await disposeTask.OrTimeout();
// We should be disposed and thus unable to restart.
await AssertDisposedAsync(connection);
});
}
}
[Fact]
public async Task TransportIsStoppedWhenConnectionIsDisposed()
{
var testHttpHandler = new TestHttpMessageHandler();
using (var httpClient = new HttpClient(testHttpHandler))
{
var testTransport = new TestTransport();
await WithConnectionAsync(
CreateConnection(transport: testTransport),
async (connection) =>
{
// Start the transport
await connection.StartAsync(TransferFormat.Text).OrTimeout();
Assert.NotNull(testTransport.Receiving);
Assert.False(testTransport.Receiving.IsCompleted);
// Stop the connection, and we should stop the transport
await connection.DisposeAsync().OrTimeout();
await testTransport.Receiving.OrTimeout();
});
}
}
[Fact]
public async Task TransportPipeIsCompletedWhenErrorOccursInTransport()
{
bool ExpectedErrors(WriteContext writeContext)
{
return writeContext.LoggerName == typeof(LongPollingTransport).FullName &&
writeContext.EventId.Name == "ErrorSending";
}
using (StartVerifiableLog(out var loggerFactory, expectedErrorsFilter: ExpectedErrors))
{
var httpHandler = new TestHttpMessageHandler();
var longPollResult = new TaskCompletionSource<HttpResponseMessage>();
httpHandler.OnLongPoll(cancellationToken =>
{
cancellationToken.Register(() =>
{
longPollResult.TrySetResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
});
return longPollResult.Task;
});
httpHandler.OnLongPollDelete(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
httpHandler.OnSocketSend((data, _) =>
{
Assert.Collection(data, i => Assert.Equal(0x42, i));
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError));
});
await WithConnectionAsync(
CreateConnection(httpHandler, loggerFactory),
async (connection) =>
{
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.Transport.Output.WriteAsync(new byte[] { 0x42 }).OrTimeout();
// We should get the exception in the transport input completion.
await Assert.ThrowsAsync<HttpRequestException>(() => connection.Transport.Input.WaitForWriterToComplete());
});
}
}
[Fact]
public async Task SSEWontStartIfSuccessfulConnectionIsNotEstablished()
{
// TODO: Add logging https://github.com/aspnet/SignalR/issues/2879
var httpHandler = new TestHttpMessageHandler();
httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", (_, __) =>
{
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError));
});
var sse = new ServerSentEventsTransport(new HttpClient(httpHandler));
await WithConnectionAsync(
CreateConnection(httpHandler, transport: sse),
async (connection) =>
{
await Assert.ThrowsAsync<InvalidOperationException>(
() => connection.StartAsync(TransferFormat.Text).OrTimeout());
});
}
[Fact]
public async Task SSEWaitsForResponseToStart()
{
using (StartVerifiableLog(out var loggerFactory))
{
var httpHandler = new TestHttpMessageHandler();
var connectResponseTcs = new TaskCompletionSource<object>();
httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", async (_, __) =>
{
await connectResponseTcs.Task;
return ResponseUtils.CreateResponse(HttpStatusCode.Accepted);
});
var sse = new ServerSentEventsTransport(new HttpClient(httpHandler));
await WithConnectionAsync(
CreateConnection(httpHandler, loggerFactory: loggerFactory, transport: sse),
async (connection) =>
{
var startTask = connection.StartAsync(TransferFormat.Text).OrTimeout();
Assert.False(connectResponseTcs.Task.IsCompleted);
Assert.False(startTask.IsCompleted);
connectResponseTcs.TrySetResult(null);
await startTask;
});
}
}
private static async Task AssertDisposedAsync(HttpConnection connection)
{
var exception =
await Assert.ThrowsAsync<ObjectDisposedException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
}
}
}
}