Adding structured negotiate

This commit is contained in:
Pawel Kadluczka 2017-06-05 13:00:04 -07:00
parent 4c4be7ed6f
commit a14a0ab039
18 changed files with 468 additions and 78 deletions

View File

@ -6,6 +6,6 @@
<Target Name="RunTSClientNodeTests" AfterTargets="Test">
<Message Text="Running TypeScript client Node tests" Importance="high" />
<Exec Command="npm test" WorkingDirectory="$(RepositoryRoot)client-ts" />
<Exec Command="npm test" WorkingDirectory="$(RepositoryRoot)client-ts" IgnoreStandardErrorWarningFormat="true" />
</Target>
</Project>

View File

@ -0,0 +1,9 @@
import { ITransport, TransportType } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports"
export function eachTransport(action: (transport: TransportType) => void) {
let transportTypes = [
TransportType.WebSockets,
TransportType.ServerSentEvents,
TransportType.LongPolling ];
transportTypes.forEach(t => action(t));
};

View File

@ -2,7 +2,8 @@ import { IHttpClient } from "../Microsoft.AspNetCore.SignalR.Client.TS/HttpClien
import { Connection } from "../Microsoft.AspNetCore.SignalR.Client.TS/Connection"
import { ISignalROptions } from "../Microsoft.AspNetCore.SignalR.Client.TS/ISignalROptions"
import { DataReceived, TransportClosed } from "../Microsoft.AspNetCore.SignalR.Client.TS/Common"
import { ITransport } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports"
import { ITransport, TransportType } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports"
import { eachTransport } from "./Common";
describe("Connection", () => {
@ -102,7 +103,7 @@ describe("Connection", () => {
httpClient: <IHttpClient>{
options(url: string): Promise<string> {
connection.stop();
return Promise.resolve("");
return Promise.resolve("{}");
},
get(url: string): Promise<string> {
connection.stop();
@ -133,7 +134,7 @@ describe("Connection", () => {
let options: ISignalROptions = {
httpClient: <IHttpClient>{
options(url: string): Promise<string> {
return Promise.resolve("42");
return Promise.resolve("{ \"connectionId\": \"42\" }");
},
get(url: string): Promise<string> {
return Promise.resolve("");
@ -168,4 +169,54 @@ describe("Connection", () => {
expect(connectUrl).toBe("http://tempuri.org?q=myData&id=42");
done();
});
eachTransport((requestedTransport: TransportType) => {
it(`Connection cannot be started if requested ${TransportType[requestedTransport]} transport not available on server`, async done => {
let options: ISignalROptions = {
httpClient: <IHttpClient>{
options(url: string): Promise<string> {
return Promise.resolve("{ \"connectionId\": \"42\", \"availableTransports\": [] }");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
}
} as ISignalROptions;
var connection = new Connection("http://tempuri.org", options);
try {
await connection.start(requestedTransport);
fail();
done();
}
catch (e) {
expect(e.message).toBe("No available transports found.");
done();
}
});
});
it(`Connection cannot be started if no transport available on server and no transport requested`, async done => {
let options: ISignalROptions = {
httpClient: <IHttpClient>{
options(url: string): Promise<string> {
return Promise.resolve("{ \"connectionId\": \"42\", \"availableTransports\": [] }");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
}
} as ISignalROptions;
var connection = new Connection("http://tempuri.org", options);
try {
await connection.start();
fail();
done();
}
catch (e) {
expect(e.message).toBe("No available transports found.");
done();
}
});
});

View File

@ -11,6 +11,11 @@ enum ConnectionState {
Disconnected
}
interface INegotiateResponse {
connectionId: string
availableTransports: string[]
}
export class Connection implements IConnection {
private connectionState: ConnectionState;
private url: string;
@ -25,7 +30,7 @@ export class Connection implements IConnection {
this.connectionState = ConnectionState.Initial;
}
async start(transport: TransportType | ITransport = TransportType.WebSockets): Promise<void> {
async start(transport?: TransportType | ITransport): Promise<void> {
if (this.connectionState != ConnectionState.Initial) {
return Promise.reject(new Error("Cannot start a connection that is not in the 'Initial' state."));
}
@ -38,7 +43,9 @@ export class Connection implements IConnection {
private async startInternal(transportType: TransportType | ITransport): Promise<void> {
try {
this.connectionId = await this.httpClient.options(this.url);
let negotiatePayload = await this.httpClient.options(this.url);
let negotiateResponse: INegotiateResponse = JSON.parse(negotiatePayload);
this.connectionId = negotiateResponse.connectionId;
// the user tries to stop the the connection when it is being started
if (this.connectionState == ConnectionState.Disconnected) {
@ -47,7 +54,7 @@ export class Connection implements IConnection {
this.url += (this.url.indexOf("?") == -1 ? "?" : "&") + `id=${this.connectionId}`;
this.transport = this.createTransport(transportType);
this.transport = this.createTransport(transportType, negotiateResponse.availableTransports);
this.transport.onDataReceived = this.onDataReceived;
this.transport.onClosed = e => this.stopConnection(true, e);
await this.transport.connect(this.url);
@ -56,21 +63,24 @@ export class Connection implements IConnection {
this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
}
catch (e) {
console.log("Failed to start the connection. " + e)
console.log("Failed to start the connection. " + e);
this.connectionState = ConnectionState.Disconnected;
this.transport = null;
throw e;
};
}
private createTransport(transport: TransportType | ITransport): ITransport {
if (transport === TransportType.WebSockets) {
private createTransport(transport: TransportType | ITransport, availableTransports: string[]): ITransport {
if (!transport && availableTransports.length > 0) {
transport = TransportType[availableTransports[0]];
}
if (transport === TransportType.WebSockets && availableTransports.indexOf(TransportType[transport]) >= 0) {
return new WebSocketTransport();
}
if (transport === TransportType.ServerSentEvents) {
if (transport === TransportType.ServerSentEvents && availableTransports.indexOf(TransportType[transport]) >= 0) {
return new ServerSentEventsTransport(this.httpClient);
}
if (transport === TransportType.LongPolling) {
if (transport === TransportType.LongPolling && availableTransports.indexOf(TransportType[transport]) >= 0) {
return new LongPollingTransport(this.httpClient);
}
@ -78,11 +88,11 @@ export class Connection implements IConnection {
return transport;
}
throw new Error("No valid transports requested.");
throw new Error("No available transports found.");
}
private isITransport(transport: any): transport is ITransport {
return "connect" in transport;
return typeof(transport) === "object" && "connect" in transport;
}
private changeState(from: ConnectionState, to: ConnectionState): Boolean {

View File

@ -1,4 +1,31 @@
describe('connection', () => {
it(`can connect to the server without specifying transport explicitly`, done => {
const message = "Hello World!";
let connection = new signalR.Connection(ECHOENDPOINT_URL);
let received = "";
connection.onDataReceived = data => {
received += data;
if (data == message) {
connection.stop();
}
}
connection.onClosed = error => {
expect(error).toBeUndefined();
done();
}
connection.start()
.then(() => {
connection.send(message);
})
.catch(e => {
fail();
done();
});
});
eachTransport(transportType => {
it(`over ${signalR.TransportType[transportType]} can send and receive messages`, done => {
const message = "Hello World!";

View File

@ -47,11 +47,11 @@ describe('hubConnection', () => {
},
error: (err) => {
fail(err);
done();
hubConnection.stop();
},
complete: () => {
expect(received).toEqual(["a", "b", "c"]);
done();
hubConnection.stop();
}
});
})

View File

@ -16,6 +16,17 @@ Throughout this document, the term `[endpoint-base]` is used to refer to the rou
**NOTE on errors:** In all error cases, by default, the detailed exception message is **never** provided; a short description string may be provided. However, an application developer may elect to allow detailed exception messages to be emitted, which should only be used in the `Development` environment. Unexpected errors are communicated by HTTP `500 Server Error` status codes or WebSockets `1008 Policy Violation` close frames; in these cases the connection should be considered to be terminated.
## `OPTIONS [endpoint-base]` request
The `OPTIONS [endpoint-base]` request is used to establish connection between the client and the server. The response to the `OPTIONS [endpoint-base]` request contains the `connectionId` which will be used to identify the connection on the server and the list of the transports supported by the server. The content type of the response is `application/json`. The following is a sample response to the `OPTIONS [endpoint-base]` request
```
{
"connectionId":"807809a5-31bf-470d-9e23-afaee35d8a0d",
"availableTransports":["WebSockets","ServerSentEvents","LongPolling"]
}
```
## WebSockets (Full Duplex)
The WebSockets transport is unique in that it is full duplex, and a persistent connection that can be established in a single operation. As a result, the client is not required to use the `OPTIONS [endpoint-base]` request to establish a connection in advance. It also includes all the necessary metadata in it's own frame metadata.

View File

@ -4,9 +4,6 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.IO.Pipelines.Text.Primitives;
using System.Linq;
using System.Reflection;
using System.Text;

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -10,6 +11,7 @@ using Microsoft.AspNetCore.Sockets.Client.Internal;
using Microsoft.AspNetCore.Sockets.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Sockets.Client
{
@ -103,7 +105,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
try
{
var connectUrl = await GetConnectUrl(Url, httpClient, _logger);
var negotiationResponse = await Negotiate(Url, httpClient, _logger);
// Connection is being stopped while start was in progress
if (_connectionState == ConnectionState.Disconnected)
@ -112,9 +114,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
return;
}
// TODO: Available server transports should be sent by the server in the negotiation response
_transport = transportFactory.CreateTransport(TransportType.All);
_transport = transportFactory.CreateTransport(GetAvailableServerTransports(negotiationResponse));
var connectUrl = CreateConnectUrl(Url, negotiationResponse);
_logger.LogDebug("Starting transport '{0}' with Url: {1}", _transport.GetType().Name, connectUrl);
await StartTransport(connectUrl);
}
@ -179,32 +181,75 @@ namespace Microsoft.AspNetCore.Sockets.Client
}
}
private static async Task<Uri> GetConnectUrl(Uri url, HttpClient httpClient, ILogger logger)
{
var connectionId = await GetConnectionId(url, httpClient, logger);
return Utils.AppendQueryString(url, "id=" + connectionId);
}
private static async Task<string> GetConnectionId(Uri url, HttpClient httpClient, ILogger logger)
private async static Task<NegotiationResponse> Negotiate(Uri url, HttpClient httpClient, ILogger logger)
{
try
{
// Get a connection ID from the server
logger.LogDebug("Establishing Connection at: {0}", url);
var request = new HttpRequestMessage(HttpMethod.Options, url);
var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var connectionId = await response.Content.ReadAsStringAsync();
logger.LogDebug("Connection Id: {0}", connectionId);
return connectionId;
using (var request = new HttpRequestMessage(HttpMethod.Options, url))
using (var response = await httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
return await ParseNegotiateResponse(response, logger);
}
}
catch (Exception ex)
{
logger.LogError("Failed to start connection. Error getting connection id from '{0}': {1}", url, ex);
logger.LogError("Failed to start connection. Error getting negotiation response from '{0}': {1}", url, ex);
throw;
}
}
private static async Task<NegotiationResponse> ParseNegotiateResponse(HttpResponseMessage response, ILogger logger)
{
NegotiationResponse negotiationResponse;
using (var reader = new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync())))
{
try
{
negotiationResponse = new JsonSerializer().Deserialize<NegotiationResponse>(reader);
}
catch (Exception ex)
{
throw new FormatException("Invalid negotiation response received.", ex);
}
}
if (negotiationResponse == null)
{
throw new FormatException("Invalid negotiation response received.");
}
return negotiationResponse;
}
private TransportType GetAvailableServerTransports(NegotiationResponse negotiationResponse)
{
if (negotiationResponse.AvailableTransports == null)
{
throw new FormatException("No transports returned in negotiation response.");
}
var availableServerTransports = (TransportType)0;
foreach (var t in negotiationResponse.AvailableTransports)
{
availableServerTransports |= t;
}
return availableServerTransports;
}
private static Uri CreateConnectUrl(Uri url, NegotiationResponse negotiationResponse)
{
if (string.IsNullOrWhiteSpace(negotiationResponse.ConnectionId))
{
throw new FormatException("Invalid connection id returned in negotiation response.");
}
return Utils.AppendQueryString(url, "id=" + negotiationResponse.ConnectionId);
}
private async Task StartTransport(Uri connectUrl)
{
var applicationToTransport = Channel.CreateUnbounded<SendMessage>();
@ -352,5 +397,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
public const int Connected = 2;
public const int Disconnected = 3;
}
private class NegotiationResponse
{
public string ConnectionId { get; set; }
public TransportType[] AvailableTransports { get; set; }
}
}
}

View File

@ -22,6 +22,7 @@
<PackageReference Include="System.IO.Pipelines" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="System.Threading.Tasks.Channels" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
</ItemGroup>
</Project>

View File

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Sockets.Internal;
using Microsoft.AspNetCore.Sockets.Transports;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Sockets
{
@ -314,17 +315,46 @@ namespace Microsoft.AspNetCore.Sockets
// Set the allowed headers for this resource
context.Response.Headers.AppendCommaSeparatedValues("Allow", "GET", "POST", "OPTIONS");
context.Response.ContentType = "text/plain";
context.Response.ContentType = "application/json";
// Establish the connection
var connection = _manager.CreateConnection();
// Get the bytes for the connection id
var connectionIdBuffer = Encoding.UTF8.GetBytes(connection.ConnectionId);
var negotiateResponseBuffer = Encoding.UTF8.GetBytes(GetNegotiatePayload(connection.ConnectionId, options));
// Write it out to the response with the right content length
context.Response.ContentLength = connectionIdBuffer.Length;
return context.Response.Body.WriteAsync(connectionIdBuffer, 0, connectionIdBuffer.Length);
context.Response.ContentLength = negotiateResponseBuffer.Length;
return context.Response.Body.WriteAsync(negotiateResponseBuffer, 0, negotiateResponseBuffer.Length);
}
private static string GetNegotiatePayload(string connectionId, HttpSocketOptions options)
{
var sb = new StringBuilder();
using (var jsonWriter = new JsonTextWriter(new StringWriter(sb)))
{
jsonWriter.WriteStartObject();
jsonWriter.WritePropertyName("connectionId");
jsonWriter.WriteValue(connectionId);
jsonWriter.WritePropertyName("availableTransports");
jsonWriter.WriteStartArray();
if ((options.Transports & TransportType.WebSockets) != 0)
{
jsonWriter.WriteValue(nameof(TransportType.WebSockets));
}
if ((options.Transports & TransportType.ServerSentEvents) != 0)
{
jsonWriter.WriteValue(nameof(TransportType.ServerSentEvents));
}
if ((options.Transports & TransportType.LongPolling) != 0)
{
jsonWriter.WriteValue(nameof(TransportType.LongPolling));
}
jsonWriter.WriteEndArray();
jsonWriter.WriteEndObject();
}
return sb.ToString();
}
private async Task ProcessSend(HttpContext context)

View File

@ -25,6 +25,7 @@
<PackageReference Include="Microsoft.Extensions.SecurityHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Channels" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="System.IO.Pipelines.Text.Primitives" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
</ItemGroup>
</Project>

View File

@ -51,7 +51,10 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -81,7 +84,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -132,7 +137,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
// allow DisposeAsync to continue once we know we are past the connection state check
allowDisposeTcs.SetResult(null);
await releaseNegotiateTcs.Task;
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -174,7 +181,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -199,7 +208,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -230,7 +241,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
var mockTransport = new Mock<ITransport>();
@ -267,7 +280,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -294,11 +309,12 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
if (request.Method == HttpMethod.Get)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(string.Empty) };
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Get
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
: request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -328,7 +344,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
var mockTransport = new Mock<ITransport>();
@ -370,7 +388,9 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
var mockTransport = new Mock<ITransport>();
@ -438,7 +458,10 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -477,7 +500,10 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
{
sendTcs.SetResult(await request.Content.ReadAsByteArrayAsync());
}
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -523,7 +549,10 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
{
content = "T2:T:42;";
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(content) };
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -549,11 +578,12 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
if (request.Method == HttpMethod.Post)
{
return ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError);
}
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
return request.Method == HttpMethod.Post
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
: request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -585,7 +615,10 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
{
content = "42";
}
return ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -627,11 +660,12 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
if (request.Method == HttpMethod.Get)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(string.Empty) };
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) };
return request.Method == HttpMethod.Get
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
: request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -658,5 +692,100 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
}
}
}
[Theory]
[InlineData("")]
[InlineData("Not Json")]
public async Task StartThrowsFormatExceptionIfNegotiationResponseIsInvalid(string negotiatePayload)
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(HttpStatusCode.OK, negotiatePayload);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
{
var connection = new Connection(new Uri("http://fakeuri.org/"));
var exception = await Assert.ThrowsAsync<FormatException>(
() => connection.StartAsync(TransportType.LongPolling, httpClient));
Assert.Equal("Invalid negotiation response received.", exception.Message);
}
}
[Fact]
public async Task StartThrowsFormatExceptionIfNegotiationResponseHasNoConnectionId()
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
ResponseUtils.CreateNegotiationResponse(connectionId: null));
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
{
var connection = new Connection(new Uri("http://fakeuri.org/"));
var exception = await Assert.ThrowsAsync<FormatException>(
() => connection.StartAsync(TransportType.LongPolling, httpClient));
Assert.Equal("Invalid connection id returned in negotiation response.", exception.Message);
}
}
[Fact]
public async Task StartThrowsFormatExceptionIfNegotiationResponseHasNoTransports()
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
ResponseUtils.CreateNegotiationResponse(transportTypes: null));
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
{
var connection = new Connection(new Uri("http://fakeuri.org/"));
var exception = await Assert.ThrowsAsync<FormatException>(
() => connection.StartAsync(TransportType.LongPolling, httpClient));
Assert.Equal("No transports returned in negotiation response.", exception.Message);
}
}
[Theory]
[InlineData((TransportType)0)]
[InlineData(TransportType.ServerSentEvents)]
public async Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer(TransportType serverTransports)
{
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
ResponseUtils.CreateNegotiationResponse(transportTypes: serverTransports));
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
{
var connection = new Connection(new Uri("http://fakeuri.org/"));
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => connection.StartAsync(TransportType.LongPolling, httpClient));
Assert.Equal("No requested transports available on the server.", exception.Message);
}
}
}
}

View File

@ -47,7 +47,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK);
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -78,7 +80,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK);
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -139,7 +143,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK);
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))
@ -170,7 +176,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
{
await Task.Yield();
return ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK);
return request.Method == HttpMethod.Options
? ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
: ResponseUtils.CreateResponse(System.Net.HttpStatusCode.OK);
});
using (var httpClient = new HttpClient(mockHttpHandler.Object))

View File

@ -3,7 +3,9 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using SocketsTransportType = Microsoft.AspNetCore.Sockets.TransportType;
namespace Microsoft.AspNetCore.Client.Tests
{
@ -25,5 +27,37 @@ namespace Microsoft.AspNetCore.Client.Tests
Content = payload
};
}
public static string CreateNegotiationResponse(string connectionId = "00000000-0000-0000-0000-000000000000",
SocketsTransportType? transportTypes = SocketsTransportType.All)
{
var sb = new StringBuilder("{ ");
if (connectionId != null)
{
sb.Append($"\"connectionId\": \"{connectionId}\",");
}
if (transportTypes != null)
{
sb.Append($"\"availableTransports\": [ ");
if ((transportTypes & SocketsTransportType.WebSockets) == SocketsTransportType.WebSockets)
{
sb.Append($"\"{nameof(SocketsTransportType.WebSockets)}\",");
}
if ((transportTypes & SocketsTransportType.ServerSentEvents) == SocketsTransportType.ServerSentEvents)
{
sb.Append($"\"{nameof(SocketsTransportType.ServerSentEvents)}\",");
}
if ((transportTypes & SocketsTransportType.LongPolling) == SocketsTransportType.LongPolling)
{
sb.Append($"\"{nameof(SocketsTransportType.LongPolling)}\",");
}
sb.Length--;
sb.Append("],");
}
sb.Length--;
sb.Append("}");
return sb.ToString();
}
}
}

View File

@ -15,7 +15,6 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
{
[Theory]
[InlineData("\r\n", "")]
[InlineData("\r\n", "")]
[InlineData("\r\n:\r\n", "")]
[InlineData("\r\n:comment\r\n", "")]
[InlineData("data: \r\r\n\r\n", "\r")]
@ -99,7 +98,6 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters
[InlineData(new[] { "dat", "a: Hello, World\r\n\r\n" }, "Hello, World")]
[InlineData(new[] { "data", ": Hello, World\r\n\r\n" }, "Hello, World")]
[InlineData(new[] { "data:", " Hello, World\r\n\r\n" }, "Hello, World")]
[InlineData(new[] { "data: ", "Hello, World\r\n\r\n" }, "Hello, World")]
[InlineData(new[] { "data: Hello, World", "\r\n\r\n" }, "Hello, World")]
[InlineData(new[] { "data: Hello, World\r\n", "\r\n" }, "Hello, World")]
[InlineData(new[] { "data: ", "Hello, World\r\n\r\n" }, "Hello, World")]

View File

@ -18,6 +18,8 @@ using Microsoft.AspNetCore.SignalR.Tests.Common;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Sockets.Tests
@ -45,11 +47,41 @@ namespace Microsoft.AspNetCore.Sockets.Tests
builder.UseEndPoint<TestEndPoint>();
var app = builder.Build();
await dispatcher.ExecuteAsync(context, new HttpSocketOptions(), app);
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
var connectionId = negotiateResponse.Value<string>("connectionId");
Assert.True(manager.TryGetConnection(connectionId, out var connectionContext));
Assert.Equal(connectionId, connectionContext.ConnectionId);
}
var id = Encoding.UTF8.GetString(ms.ToArray());
[Theory]
[InlineData(TransportType.All)]
[InlineData((TransportType)0)]
[InlineData(TransportType.LongPolling | TransportType.WebSockets)]
public async Task NegotiateReturnsAvailableTransports(TransportType transports)
{
var manager = CreateConnectionManager();
var dispatcher = new HttpConnectionDispatcher(manager, new LoggerFactory());
var context = new DefaultHttpContext();
var services = new ServiceCollection();
services.AddEndPoint<TestEndPoint>();
services.AddOptions();
var ms = new MemoryStream();
context.Request.Path = "/foo";
context.Request.Method = "OPTIONS";
context.Response.Body = ms;
var builder = new SocketBuilder(services.BuildServiceProvider());
builder.UseEndPoint<TestEndPoint>();
var app = builder.Build();
await dispatcher.ExecuteAsync(context, new HttpSocketOptions { Transports = transports }, app);
Assert.True(manager.TryGetConnection(id, out var connection));
Assert.Equal(id, connection.ConnectionId);
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
var availableTransports = (TransportType)0;
foreach (var transport in negotiateResponse["availableTransports"])
{
availableTransports |= (TransportType)Enum.Parse(typeof(TransportType), transport.Value<string>());
}
Assert.Equal(transports, availableTransports);
}
[Theory]

View File

@ -20,6 +20,7 @@
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="System.IO.Pipelines.Extensions" Version="$(CoreFxLabsVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
</ItemGroup>