Merge branch 'release/2.1' into dev

This commit is contained in:
David Fowler 2018-04-17 00:50:10 -07:00
commit 8d8a546a3f
14 changed files with 398 additions and 114 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@aspnet/signalr-protocol-msgpack", "name": "@aspnet/signalr-protocol-msgpack",
"version": "1.0.0-preview3-t000", "version": "1.0.0-rc1-t000",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@aspnet/signalr", "name": "@aspnet/signalr",
"version": "1.0.0-preview3-t000", "version": "1.0.0-rc1-t000",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -288,13 +288,73 @@ describe("HttpConnection", () => {
} }
}); });
it("sets inherentKeepAlive feature when using LongPolling", async (done) => { it("authorization header removed when token factory returns null and using LongPolling", async (done) => {
const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] }; const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] };
var httpClientGetCount = 0;
var accessTokenFactoryCount = 0;
const options: IHttpConnectionOptions = { const options: IHttpConnectionOptions = {
...commonOptions, ...commonOptions,
httpClient: new TestHttpClient() httpClient: new TestHttpClient()
.on("POST", (r) => ({ connectionId: "42", availableTransports: [availableTransport] })), .on("POST", (r) => ({ connectionId: "42", availableTransports: [availableTransport] }))
.on("GET", (r) => {
httpClientGetCount++;
const authorizationValue = r.headers["Authorization"];
if (httpClientGetCount == 1) {
if (authorizationValue) {
fail("First long poll request should have a authorization header.");
}
// First long polling request must succeed so start completes
return "";
} else {
// Check second long polling request has its header removed
if (authorizationValue) {
fail("Second long poll request should have no authorization header.");
}
throw new Error("fail");
}
}),
accessTokenFactory: () => {
accessTokenFactoryCount++;
if (accessTokenFactoryCount == 1) {
return "A token value";
} else {
// Return a null value after the first call to test the header being removed
return null;
}
},
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
expect(httpClientGetCount).toBeGreaterThanOrEqual(2);
expect(accessTokenFactoryCount).toBeGreaterThanOrEqual(2);
done();
} catch (e) {
fail(e);
done();
}
});
it("sets inherentKeepAlive feature when using LongPolling", async (done) => {
const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] };
var httpClientGetCount = 0;
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => ({ connectionId: "42", availableTransports: [availableTransport] }))
.on("GET", (r) => {
httpClientGetCount++;
if (httpClientGetCount == 1) {
// First long polling request must succeed so start completes
return "";
} else {
throw new Error("fail");
}
}),
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", options);

View File

@ -31,7 +31,7 @@ export class LongPollingTransport implements ITransport {
this.logMessageContent = logMessageContent; this.logMessageContent = logMessageContent;
} }
public connect(url: string, transferFormat: TransferFormat): Promise<void> { public async connect(url: string, transferFormat: TransferFormat): Promise<void> {
Arg.isRequired(url, "url"); Arg.isRequired(url, "url");
Arg.isRequired(transferFormat, "transferFormat"); Arg.isRequired(transferFormat, "transferFormat");
Arg.isIn(transferFormat, TransferFormat, "transferFormat"); Arg.isIn(transferFormat, TransferFormat, "transferFormat");
@ -45,13 +45,6 @@ export class LongPollingTransport implements ITransport {
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported."); throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
} }
this.poll(this.url, transferFormat);
return Promise.resolve();
}
private async poll(url: string, transferFormat: TransferFormat): Promise<void> {
this.running = true;
const pollOptions: HttpRequest = { const pollOptions: HttpRequest = {
abortSignal: this.pollAbort.signal, abortSignal: this.pollAbort.signal,
headers: {}, headers: {},
@ -62,15 +55,49 @@ export class LongPollingTransport implements ITransport {
pollOptions.responseType = "arraybuffer"; pollOptions.responseType = "arraybuffer";
} }
const token = await this.accessTokenFactory();
this.updateHeaderToken(pollOptions, token);
let closeError: Error; let closeError: Error;
// Make initial long polling request
// Server uses first long polling request to finish initializing connection and it returns without data
const pollUrl = `${url}&_=${Date.now()}`;
this.logger.log(LogLevel.Trace, `(LongPolling transport) polling: ${pollUrl}`);
const response = await this.httpClient.get(pollUrl, pollOptions);
if (response.statusCode !== 200) {
this.logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}`);
// Mark running as false so that the poll immediately ends and runs the close logic
closeError = new HttpError(response.statusText, response.statusCode);
this.running = false;
} else {
this.running = true;
}
this.poll(this.url, pollOptions, closeError);
return Promise.resolve();
}
private updateHeaderToken(request: HttpRequest, token: string) {
if (token) {
// tslint:disable-next-line:no-string-literal
request.headers["Authorization"] = `Bearer ${token}`;
return;
}
// tslint:disable-next-line:no-string-literal
if (request.headers["Authorization"]) {
// tslint:disable-next-line:no-string-literal
delete request.headers["Authorization"];
}
}
private async poll(url: string, pollOptions: HttpRequest, closeError: Error): Promise<void> {
try { try {
while (this.running) { while (this.running) {
// We have to get the access token on each poll, in case it changes // We have to get the access token on each poll, in case it changes
const token = await this.accessTokenFactory(); const token = await this.accessTokenFactory();
if (token) { this.updateHeaderToken(pollOptions, token);
// tslint:disable-next-line:no-string-literal
pollOptions.headers["Authorization"] = `Bearer ${token}`;
}
try { try {
const pollUrl = `${url}&_=${Date.now()}`; const pollUrl = `${url}&_=${Date.now()}`;
@ -142,14 +169,11 @@ export class LongPollingTransport implements ITransport {
this.running = false; this.running = false;
this.logger.log(LogLevel.Trace, `(LongPolling transport) sending DELETE request to ${this.url}.`); this.logger.log(LogLevel.Trace, `(LongPolling transport) sending DELETE request to ${this.url}.`);
const deleteOptions: HttpRequest = {}; const deleteOptions: HttpRequest = {
headers: {},
};
const token = await this.accessTokenFactory(); const token = await this.accessTokenFactory();
if (token) { this.updateHeaderToken(deleteOptions, token);
// tslint:disable-next-line:no-string-literal
deleteOptions.headers = {
["Authorization"]: `Bearer ${token}`,
};
}
const response = await this.httpClient.delete(this.url, deleteOptions); const response = await this.httpClient.delete(this.url, deleteOptions);
this.logger.log(LogLevel.Trace, "(LongPolling transport) DELETE request accepted."); this.logger.log(LogLevel.Trace, "(LongPolling transport) DELETE request accepted.");

View File

@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<LongPollingTransport>(); _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<LongPollingTransport>();
} }
public Task StartAsync(Uri url, TransferFormat transferFormat) public async Task StartAsync(Uri url, TransferFormat transferFormat)
{ {
if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text) if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text)
{ {
@ -49,6 +49,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
Log.StartTransport(_logger, transferFormat); Log.StartTransport(_logger, transferFormat);
// Make initial long polling request
// Server uses first long polling request to finish initializing connection and it returns without data
var request = new HttpRequestMessage(HttpMethod.Get, url);
using (var response = await _httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
}
// Create the pipe pair (Application's writer is connected to Transport's reader, and vice versa) // Create the pipe pair (Application's writer is connected to Transport's reader, and vice versa)
var options = ClientPipeOptions.DefaultOptions; var options = ClientPipeOptions.DefaultOptions;
var pair = DuplexPipe.CreateConnectionPair(options, options); var pair = DuplexPipe.CreateConnectionPair(options, options);
@ -57,8 +65,6 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
_application = pair.Application; _application = pair.Application;
Running = ProcessAsync(url); Running = ProcessAsync(url);
return Task.CompletedTask;
} }
private async Task ProcessAsync(Uri url) private async Task ProcessAsync(Uri url)
@ -105,6 +111,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
{ {
Log.TransportStopping(_logger); Log.TransportStopping(_logger);
if (_application == null)
{
// We never started
return;
}
_application.Input.CancelPendingRead(); _application.Input.CancelPendingRead();
try try

View File

@ -207,6 +207,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
{ {
Log.TransportStopping(_logger); Log.TransportStopping(_logger);
if (_application == null)
{
// We never started
return;
}
_transport.Output.Complete(); _transport.Output.Complete();
_transport.Input.Complete(); _transport.Input.Complete();

View File

@ -373,6 +373,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
{ {
Log.TransportStopping(_logger); Log.TransportStopping(_logger);
if (_application == null)
{
// We never started
return;
}
_transport.Output.Complete(); _transport.Output.Complete();
_transport.Input.Complete(); _transport.Input.Complete();

View File

@ -210,7 +210,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
using (connection.Cancellation) using (connection.Cancellation)
{ {
// Cancel the previous request // Cancel the previous request
connection.Cancellation.Cancel(); connection.Cancellation?.Cancel();
// Wait for the previous request to drain // Wait for the previous request to drain
await connection.TransportTask; await connection.TransportTask;
@ -228,29 +228,38 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
Log.EstablishedConnection(_logger); Log.EstablishedConnection(_logger);
connection.ApplicationTask = ExecuteApplication(connectionDelegate, connection); connection.ApplicationTask = ExecuteApplication(connectionDelegate, connection);
context.Response.ContentType = "application/octet-stream";
// This request has no content
context.Response.ContentLength = 0;
// On the first poll, we flush the response immediately to mark the poll as "initialized" so future
// requests can be made safely
connection.TransportTask = context.Response.Body.FlushAsync();
} }
else else
{ {
Log.ResumingConnection(_logger); Log.ResumingConnection(_logger);
// REVIEW: Performance of this isn't great as this does a bunch of per request allocations
connection.Cancellation = new CancellationTokenSource();
var timeoutSource = new CancellationTokenSource();
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(connection.Cancellation.Token, context.RequestAborted, timeoutSource.Token);
// Dispose these tokens when the request is over
context.Response.RegisterForDispose(timeoutSource);
context.Response.RegisterForDispose(tokenSource);
var longPolling = new LongPollingTransport(timeoutSource.Token, connection.Application.Input, _loggerFactory);
// Start the transport
connection.TransportTask = longPolling.ProcessRequestAsync(context, tokenSource.Token);
// Start the timeout after we return from creating the transport task
timeoutSource.CancelAfter(options.LongPolling.PollTimeout);
} }
// REVIEW: Performance of this isn't great as this does a bunch of per request allocations
connection.Cancellation = new CancellationTokenSource();
var timeoutSource = new CancellationTokenSource();
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(connection.Cancellation.Token, context.RequestAborted, timeoutSource.Token);
// Dispose these tokens when the request is over
context.Response.RegisterForDispose(timeoutSource);
context.Response.RegisterForDispose(tokenSource);
var longPolling = new LongPollingTransport(timeoutSource.Token, connection.Application.Input, _loggerFactory);
// Start the transport
connection.TransportTask = longPolling.ProcessRequestAsync(context, tokenSource.Token);
// Start the timeout after we return from creating the transport task
timeoutSource.CancelAfter(options.LongPolling.PollTimeout);
} }
finally finally
{ {
@ -302,7 +311,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
connection.Status = HttpConnectionStatus.Inactive; connection.Status = HttpConnectionStatus.Inactive;
// Dispose the cancellation token // Dispose the cancellation token
connection.Cancellation.Dispose(); connection.Cancellation?.Dispose();
connection.Cancellation = null; connection.Cancellation = null;
} }

View File

@ -636,6 +636,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
// Start a poll // Start a poll
var task = dispatcher.ExecuteAsync(context, new HttpConnectionDispatcherOptions(), app); var task = dispatcher.ExecuteAsync(context, new HttpConnectionDispatcherOptions(), app);
Assert.True(task.IsCompleted);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
task = dispatcher.ExecuteAsync(context, new HttpConnectionDispatcherOptions(), app);
// Send to the application // Send to the application
var buffer = Encoding.UTF8.GetBytes("Hello World"); var buffer = Encoding.UTF8.GetBytes("Hello World");
@ -745,7 +749,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
} }
[Theory] [Theory]
[InlineData(HttpTransportType.LongPolling, 204)] [InlineData(HttpTransportType.LongPolling, 200)]
[InlineData(HttpTransportType.WebSockets, 404)] [InlineData(HttpTransportType.WebSockets, 404)]
[InlineData(HttpTransportType.ServerSentEvents, 404)] [InlineData(HttpTransportType.ServerSentEvents, 404)]
public async Task EndPointThatOnlySupportsLongPollingRejectsOtherTransports(HttpTransportType transportType, int status) public async Task EndPointThatOnlySupportsLongPollingRejectsOtherTransports(HttpTransportType transportType, int status)
@ -869,6 +873,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var builder = new ConnectionBuilder(services.BuildServiceProvider()); var builder = new ConnectionBuilder(services.BuildServiceProvider());
builder.UseConnectionHandler<ImmediatelyCompleteConnectionHandler>(); builder.UseConnectionHandler<ImmediatelyCompleteConnectionHandler>();
var app = builder.Build(); var app = builder.Build();
// First poll will 200
await dispatcher.ExecuteAsync(context, new HttpConnectionDispatcherOptions(), app);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
await dispatcher.ExecuteAsync(context, new HttpConnectionDispatcherOptions(), app); await dispatcher.ExecuteAsync(context, new HttpConnectionDispatcherOptions(), app);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
@ -998,6 +1006,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var app = builder.Build(); var app = builder.Build();
var options = new HttpConnectionDispatcherOptions(); var options = new HttpConnectionDispatcherOptions();
var request1 = dispatcher.ExecuteAsync(context1, options, app); var request1 = dispatcher.ExecuteAsync(context1, options, app);
Assert.True(request1.IsCompleted);
request1 = dispatcher.ExecuteAsync(context1, options, app);
var request2 = dispatcher.ExecuteAsync(context2, options, app); var request2 = dispatcher.ExecuteAsync(context2, options, app);
await request1; await request1;
@ -1132,7 +1143,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
builder.UseConnectionHandler<BlockingConnectionHandler>(); builder.UseConnectionHandler<BlockingConnectionHandler>();
var app = builder.Build(); var app = builder.Build();
var options = new HttpConnectionDispatcherOptions(); var options = new HttpConnectionDispatcherOptions();
// Initial poll
var task = dispatcher.ExecuteAsync(context, options, app); var task = dispatcher.ExecuteAsync(context, options, app);
Assert.True(task.IsCompleted);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
// Real long running poll
task = dispatcher.ExecuteAsync(context, options, app);
var buffer = Encoding.UTF8.GetBytes("Hello World"); var buffer = Encoding.UTF8.GetBytes("Hello World");
@ -1166,7 +1184,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var options = new HttpConnectionDispatcherOptions(); var options = new HttpConnectionDispatcherOptions();
var context1 = MakeRequest("/foo", connection); var context1 = MakeRequest("/foo", connection);
// This is the initial poll to make sure things are setup
var task1 = dispatcher.ExecuteAsync(context1, options, app); var task1 = dispatcher.ExecuteAsync(context1, options, app);
Assert.True(task1.IsCompleted);
task1 = dispatcher.ExecuteAsync(context1, options, app);
var context2 = MakeRequest("/foo", connection); var context2 = MakeRequest("/foo", connection);
var task2 = dispatcher.ExecuteAsync(context2, options, app); var task2 = dispatcher.ExecuteAsync(context2, options, app);
@ -1363,8 +1384,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "name") })); context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "name") }));
var connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app); var connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app);
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).AsTask().OrTimeout(); await connectionHandlerTask.OrTimeout();
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app);
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).AsTask().OrTimeout();
await connectionHandlerTask.OrTimeout(); await connectionHandlerTask.OrTimeout();
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
@ -1444,7 +1468,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
new Claim(ClaimTypes.StreetAddress, "12345 123rd St. NW") new Claim(ClaimTypes.StreetAddress, "12345 123rd St. NW")
})); }));
// First poll
var connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app); var connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app);
Assert.True(connectionHandlerTask.IsCompleted);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app);
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).AsTask().OrTimeout(); await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).AsTask().OrTimeout();
await connectionHandlerTask.OrTimeout(); await connectionHandlerTask.OrTimeout();
@ -1502,7 +1531,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
// "authorize" user // "authorize" user
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "name") })); context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "name") }));
// Initial poll
var connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app); var connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app);
Assert.True(connectionHandlerTask.IsCompleted);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
connectionHandlerTask = dispatcher.ExecuteAsync(context, options, app);
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).AsTask().OrTimeout(); await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello, World")).AsTask().OrTimeout();
await connectionHandlerTask.OrTimeout(); await connectionHandlerTask.OrTimeout();
@ -1660,6 +1694,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var options = new HttpConnectionDispatcherOptions(); var options = new HttpConnectionDispatcherOptions();
var pollTask = dispatcher.ExecuteAsync(context, options, app); var pollTask = dispatcher.ExecuteAsync(context, options, app);
Assert.True(pollTask.IsCompleted);
// Now send the second poll
pollTask = dispatcher.ExecuteAsync(context, options, app);
// Issue the delete request and make sure the poll completes // Issue the delete request and make sure the poll completes
var deleteContext = new DefaultHttpContext(); var deleteContext = new DefaultHttpContext();

View File

@ -13,14 +13,20 @@ using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Http.Connections.Client; using Microsoft.AspNetCore.Http.Connections.Client;
using Microsoft.AspNetCore.Http.Connections.Client.Internal; using Microsoft.AspNetCore.Http.Connections.Client.Internal;
using Microsoft.AspNetCore.SignalR.Tests;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.SignalR.Client.Tests namespace Microsoft.AspNetCore.SignalR.Client.Tests
{ {
public partial class HttpConnectionTests public partial class HttpConnectionTests
{ {
public class Transport public class Transport : VerifiableLoggedTest
{ {
public Transport(ITestOutputHelper output) : base(output)
{
}
[Theory] [Theory]
[InlineData(HttpTransportType.LongPolling)] [InlineData(HttpTransportType.LongPolling)]
[InlineData(HttpTransportType.ServerSentEvents)] [InlineData(HttpTransportType.ServerSentEvents)]
@ -30,11 +36,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var requestsExecuted = false; var requestsExecuted = false;
var callCount = 0; var callCount = 0;
testHttpHandler.OnRequest((request, next, token) =>
{
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
});
testHttpHandler.OnNegotiate((_, cancellationToken) => testHttpHandler.OnNegotiate((_, cancellationToken) =>
{ {
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent()); return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent());
@ -52,6 +53,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return await next(); return await next();
}); });
testHttpHandler.OnRequest((request, next, token) =>
{
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
});
Task<string> AccessTokenProvider() Task<string> AccessTokenProvider()
{ {
callCount++; callCount++;
@ -75,28 +81,25 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
[InlineData(HttpTransportType.ServerSentEvents, false)] [InlineData(HttpTransportType.ServerSentEvents, false)]
public async Task HttpConnectionSetsInherentKeepAliveFeature(HttpTransportType transportType, bool expectedValue) public async Task HttpConnectionSetsInherentKeepAliveFeature(HttpTransportType transportType, bool expectedValue)
{ {
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false); using (StartVerifableLog(out var loggerFactory, testName: $"HttpConnectionSetsInherentKeepAliveFeature_{transportType}_{expectedValue}"))
testHttpHandler.OnRequest((request, next, token) =>
{ {
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent)); var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
});
testHttpHandler.OnNegotiate((_, cancellationToken) => testHttpHandler.OnNegotiate((_, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent()));
{
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent());
});
await WithConnectionAsync( testHttpHandler.OnRequest((request, next, token) => Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent)));
CreateConnection(testHttpHandler, transportType: transportType),
async (connection) =>
{
await connection.StartAsync(TransferFormat.Text).OrTimeout();
var feature = connection.Features.Get<IConnectionInherentKeepAliveFeature>(); await WithConnectionAsync(
Assert.NotNull(feature); CreateConnection(testHttpHandler, transportType: transportType, loggerFactory: loggerFactory),
Assert.Equal(expectedValue, feature.HasInherentKeepAlive); async (connection) =>
}); {
await connection.StartAsync(TransferFormat.Text).OrTimeout();
var feature = connection.Features.Get<IConnectionInherentKeepAliveFeature>();
Assert.NotNull(feature);
Assert.Equal(expectedValue, feature.HasInherentKeepAlive);
});
}
} }
[Theory] [Theory]
@ -107,10 +110,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false); var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
var requestsExecuted = false; var requestsExecuted = false;
testHttpHandler.OnRequest((request, next, token) =>
{
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
});
testHttpHandler.OnNegotiate((_, cancellationToken) => testHttpHandler.OnNegotiate((_, cancellationToken) =>
{ {
@ -135,6 +134,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return await next(); return await next();
}); });
testHttpHandler.OnRequest((request, next, token) =>
{
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
});
await WithConnectionAsync( await WithConnectionAsync(
CreateConnection(testHttpHandler, transportType: transportType), CreateConnection(testHttpHandler, transportType: transportType),
async (connection) => async (connection) =>
@ -154,11 +158,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false); var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
var requestsExecuted = false; var requestsExecuted = false;
testHttpHandler.OnRequest((request, next, token) =>
{
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
});
testHttpHandler.OnNegotiate((_, cancellationToken) => testHttpHandler.OnNegotiate((_, cancellationToken) =>
{ {
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent()); return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent());
@ -175,6 +174,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return await next(); return await next();
}); });
testHttpHandler.OnRequest((request, next, token) =>
{
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
});
await WithConnectionAsync( await WithConnectionAsync(
CreateConnection(testHttpHandler, transportType: transportType), CreateConnection(testHttpHandler, transportType: transportType),
async (connection) => async (connection) =>

View File

@ -109,15 +109,20 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
if (requests == 0) if (requests == 0)
{ {
requests++; requests++;
return ResponseUtils.CreateResponse(HttpStatusCode.OK, "Hello"); return ResponseUtils.CreateResponse(HttpStatusCode.OK);
} }
else if (requests == 1) else if (requests == 1)
{
requests++;
return ResponseUtils.CreateResponse(HttpStatusCode.OK, "Hello");
}
else if (requests == 2)
{ {
requests++; requests++;
// Time out // Time out
return ResponseUtils.CreateResponse(HttpStatusCode.OK); return ResponseUtils.CreateResponse(HttpStatusCode.OK);
} }
else if (requests == 2) else if (requests == 3)
{ {
requests++; requests++;
return ResponseUtils.CreateResponse(HttpStatusCode.OK, "World"); return ResponseUtils.CreateResponse(HttpStatusCode.OK, "World");
@ -147,7 +152,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
} }
[Fact] [Fact]
public async Task LongPollingTransportStopsWhenPollRequestFails() public async Task LongPollingTransportStartAsyncFailsIfFirstRequestFails()
{ {
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -158,6 +163,39 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError); return ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError);
}); });
using (var httpClient = new HttpClient(mockHttpHandler.Object))
{
var longPollingTransport = new LongPollingTransport(httpClient);
try
{
var exception = await Assert.ThrowsAsync<HttpRequestException>(() => longPollingTransport.StartAsync(TestUri, TransferFormat.Binary));
Assert.Contains(" 500 ", exception.Message);
}
finally
{
await longPollingTransport.StopAsync();
}
}
}
[Fact]
public async Task LongPollingTransportStopsWhenPollRequestFails()
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
var firstPoll = true;
mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
if (firstPoll)
{
firstPoll = false;
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
}
return ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object)) using (var httpClient = new HttpClient(mockHttpHandler.Object))
{ {
var longPollingTransport = new LongPollingTransport(httpClient); var longPollingTransport = new LongPollingTransport(httpClient);
@ -314,7 +352,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{ {
var message1Payload = new[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o' }; var message1Payload = new[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o' };
var firstCall = true; var requests = 0;
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
var sentRequests = new List<HttpRequestMessage>(); var sentRequests = new List<HttpRequestMessage>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -325,9 +363,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
await Task.Yield(); await Task.Yield();
if (firstCall) if (requests == 0)
{ {
firstCall = false; requests++;
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
}
else if (requests == 1)
{
requests++;
return ResponseUtils.CreateResponse(HttpStatusCode.OK, message1Payload); return ResponseUtils.CreateResponse(HttpStatusCode.OK, message1Payload);
} }
@ -349,7 +392,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var message = await longPollingTransport.Input.ReadAllAsync(); var message = await longPollingTransport.Input.ReadAllAsync();
// Check the provided request // Check the provided request
Assert.Equal(2, sentRequests.Count); Assert.Equal(3, sentRequests.Count);
// Check the messages received // Check the messages received
Assert.Equal(message1Payload, message); Assert.Equal(message1Payload, message);
@ -366,6 +409,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{ {
var sentRequests = new List<byte[]>(); var sentRequests = new List<byte[]>();
var tcs = new TaskCompletionSource<HttpResponseMessage>(); var tcs = new TaskCompletionSource<HttpResponseMessage>();
var firstPoll = true;
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -380,6 +424,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
} }
else if (request.Method == HttpMethod.Get) else if (request.Method == HttpMethod.Get)
{ {
// First poll completes immediately
if (firstPoll)
{
firstPoll = false;
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
}
cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken));
// This is the poll task // This is the poll task
return await tcs.Task; return await tcs.Task;
@ -426,6 +477,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var sentRequests = new List<byte[]>(); var sentRequests = new List<byte[]>();
var pollTcs = new TaskCompletionSource<HttpResponseMessage>(); var pollTcs = new TaskCompletionSource<HttpResponseMessage>();
var deleteTcs = new TaskCompletionSource<object>(); var deleteTcs = new TaskCompletionSource<object>();
var firstPoll = true;
var mockHttpHandler = new Mock<HttpMessageHandler>(); var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected() mockHttpHandler.Protected()
@ -440,6 +492,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
} }
else if (request.Method == HttpMethod.Get) else if (request.Method == HttpMethod.Get)
{ {
// First poll completes immediately
if (firstPoll)
{
firstPoll = false;
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
}
cancellationToken.Register(() => pollTcs.TrySetCanceled(cancellationToken)); cancellationToken.Register(() => pollTcs.TrySetCanceled(cancellationToken));
// This is the poll task // This is the poll task
return await pollTcs.Task; return await pollTcs.Task;
@ -538,7 +597,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
{ {
await Task.Yield(); await Task.Yield();
if (Interlocked.Increment(ref numPolls) < 3) if (numPolls == 0)
{
numPolls++;
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
}
if (numPolls++ < 3)
{ {
throw new OperationCanceledException(); throw new OperationCanceledException();
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
@ -39,6 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public static bool IsLongPollRequest(HttpRequestMessage request) public static bool IsLongPollRequest(HttpRequestMessage request)
{ {
return request.Method == HttpMethod.Get && return request.Method == HttpMethod.Get &&
!IsServerSentEventsRequest(request) &&
(request.RequestUri.PathAndQuery.Contains("?id=") || request.RequestUri.PathAndQuery.Contains("&id=")); (request.RequestUri.PathAndQuery.Contains("?id=") || request.RequestUri.PathAndQuery.Contains("&id="));
} }
@ -48,6 +50,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
(request.RequestUri.PathAndQuery.Contains("?id=") || request.RequestUri.PathAndQuery.Contains("&id=")); (request.RequestUri.PathAndQuery.Contains("?id=") || request.RequestUri.PathAndQuery.Contains("&id="));
} }
public static bool IsServerSentEventsRequest(HttpRequestMessage request)
{
return request.Method == HttpMethod.Get && request.Headers.Accept.Any(h => h.MediaType == "text/event-stream");
}
public static bool IsSocketSendRequest(HttpRequestMessage request) public static bool IsSocketSendRequest(HttpRequestMessage request)
{ {
return request.Method == HttpMethod.Post && return request.Method == HttpMethod.Post &&

View File

@ -7,10 +7,14 @@ using System.Threading.Tasks;
namespace Microsoft.AspNetCore.SignalR.Client.Tests namespace Microsoft.AspNetCore.SignalR.Client.Tests
{ {
delegate Task<HttpResponseMessage> RequestDelegate(HttpRequestMessage requestMessage, CancellationToken cancellationToken);
public class TestHttpMessageHandler : HttpMessageHandler public class TestHttpMessageHandler : HttpMessageHandler
{ {
private List<HttpRequestMessage> _receivedRequests = new List<HttpRequestMessage>(); private List<HttpRequestMessage> _receivedRequests = new List<HttpRequestMessage>();
private Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handler; private RequestDelegate _app;
private List<Func<RequestDelegate, RequestDelegate>> _middleware = new List<Func<RequestDelegate, RequestDelegate>>();
public IReadOnlyList<HttpRequestMessage> ReceivedRequests public IReadOnlyList<HttpRequestMessage> ReceivedRequests
{ {
@ -23,14 +27,29 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
} }
} }
public TestHttpMessageHandler(bool autoNegotiate = true) public TestHttpMessageHandler(bool autoNegotiate = true, bool handleFirstPoll = true)
{ {
_handler = BaseHandler;
if (autoNegotiate) if (autoNegotiate)
{ {
OnNegotiate((_, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent())); OnNegotiate((_, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent()));
} }
if (handleFirstPoll)
{
var firstPoll = true;
OnRequest(async (request, next, cancellationToken) =>
{
if (ResponseUtils.IsLongPollRequest(request) && firstPoll)
{
firstPoll = false;
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
}
else
{
return await next();
}
});
}
} }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
@ -40,9 +59,21 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
lock (_receivedRequests) lock (_receivedRequests)
{ {
_receivedRequests.Add(request); _receivedRequests.Add(request);
if (_app == null)
{
_middleware.Reverse();
RequestDelegate handler = BaseHandler;
foreach (var middleware in _middleware)
{
handler = middleware(handler);
}
_app = handler;
}
} }
return await _handler(request, cancellationToken); return await _app(request, cancellationToken);
} }
public static TestHttpMessageHandler CreateDefault() public static TestHttpMessageHandler CreateDefault()
@ -80,8 +111,18 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
public void OnRequest(Func<HttpRequestMessage, Func<Task<HttpResponseMessage>>, CancellationToken, Task<HttpResponseMessage>> handler) public void OnRequest(Func<HttpRequestMessage, Func<Task<HttpResponseMessage>>, CancellationToken, Task<HttpResponseMessage>> handler)
{ {
var nextHandler = _handler; void OnRequestCore(Func<RequestDelegate, RequestDelegate> middleware)
_handler = (request, cancellationToken) => handler(request, () => nextHandler(request, cancellationToken), cancellationToken); {
_middleware.Add(middleware);
}
OnRequestCore(next =>
{
return (request, cancellationToken) =>
{
return handler(request, () => next(request, cancellationToken), cancellationToken);
};
});
} }
public void OnGet(string pathAndQuery, Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) => OnRequest(HttpMethod.Get, pathAndQuery, handler); public void OnGet(string pathAndQuery, Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) => OnRequest(HttpMethod.Get, pathAndQuery, handler);

View File

@ -51,32 +51,44 @@ namespace Microsoft.AspNetCore.SignalR.Tests
[Fact] [Fact]
public async Task CanStartAndStopConnectionUsingDefaultTransport() public async Task CanStartAndStopConnectionUsingDefaultTransport()
{ {
var url = _serverFixture.Url + "/echo"; using (StartVerifableLog(out var loggerFactory))
// The test should connect to the server using WebSockets transport on Windows 8 and newer. {
// On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server. var url = _serverFixture.Url + "/echo";
var connection = new HttpConnection(new Uri(url)); // The test should connect to the server using WebSockets transport on Windows 8 and newer.
await connection.StartAsync(TransferFormat.Binary).OrTimeout(); // On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server.
await connection.DisposeAsync().OrTimeout(); var connection = new HttpConnection(new Uri(url), HttpTransports.All, loggerFactory);
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
await connection.DisposeAsync().OrTimeout();
}
} }
[Fact] [Fact]
public async Task TransportThatFallsbackCreatesNewConnection() public async Task TransportThatFallsbackCreatesNewConnection()
{ {
var url = _serverFixture.Url + "/echo"; bool ExpectedErrors(WriteContext writeContext)
// The test should connect to the server using WebSockets transport on Windows 8 and newer. {
// On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server. return writeContext.LoggerName == typeof(HttpConnection).FullName &&
writeContext.EventId.Name == "ErrorStartingTransport";
}
// The test logic lives in the TestTransportFactory and FakeTransport. using (StartVerifableLog(out var loggerFactory, expectedErrorsFilter: ExpectedErrors))
var connection = new HttpConnection(new HttpConnectionOptions { Url = new Uri(url) }, null, new TestTransportFactory()); {
await connection.StartAsync(TransferFormat.Text).OrTimeout(); var url = _serverFixture.Url + "/echo";
await connection.DisposeAsync().OrTimeout(); // The test should connect to the server using WebSockets transport on Windows 8 and newer.
// On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server.
// The test logic lives in the TestTransportFactory and FakeTransport.
var connection = new HttpConnection(new HttpConnectionOptions { Url = new Uri(url) }, loggerFactory, new TestTransportFactory());
await connection.StartAsync(TransferFormat.Text).OrTimeout();
await connection.DisposeAsync().OrTimeout();
}
} }
[Theory(Skip = "https://github.com/aspnet/SignalR/issues/2031")] [Theory]
[MemberData(nameof(TransportTypes))] [MemberData(nameof(TransportTypes))]
public async Task CanStartAndStopConnectionUsingGivenTransport(HttpTransportType transportType) public async Task CanStartAndStopConnectionUsingGivenTransport(HttpTransportType transportType)
{ {
using (StartVerifableLog(out var loggerFactory, testName: $"CanStartAndStopConnectionUsingGivenTransport_{transportType}")) using (StartVerifableLog(out var loggerFactory, minLogLevel: LogLevel.Trace, testName: $"CanStartAndStopConnectionUsingGivenTransport_{transportType}"))
{ {
var url = _serverFixture.Url + "/echo"; var url = _serverFixture.Url + "/echo";
var connection = new HttpConnection(new Uri(url), transportType, loggerFactory); var connection = new HttpConnection(new Uri(url), transportType, loggerFactory);
@ -532,7 +544,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
if (_tries < availableTransports) if (_tries < availableTransports)
{ {
throw new Exception(); return Task.FromException(new Exception());
} }
else else
{ {