Skip to content

Commit 54449b3

Browse files
Add IHttpUpgradeFeature to TestServer for SignalR WebSocket support (#33595) (#33648)
1 parent fd19f92 commit 54449b3

8 files changed

+252
-1
lines changed

src/Hosting/Hosting.slnf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
"src\\Hosting\\test\\FunctionalTests\\Microsoft.AspNetCore.Hosting.FunctionalTests.csproj",
1818
"src\\Hosting\\test\\testassets\\IStartupInjectionAssemblyName\\IStartupInjectionAssemblyName.csproj",
1919
"src\\Hosting\\test\\testassets\\TestStartupAssembly1\\TestStartupAssembly1.csproj",
20+
"src\\Http\\Features\\src\\Microsoft.Extensions.Features.csproj",
2021
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
2122
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
2223
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
23-
"src\\Http\\Features\\src\\Microsoft.Extensions.Features.csproj",
2424
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
2525
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
2626
"src\\Http\\Owin\\src\\Microsoft.AspNetCore.Owin.csproj",
2727
"src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
28+
"src\\Middleware\\WebSockets\\src\\Microsoft.AspNetCore.WebSockets.csproj",
2829
"src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj",
2930
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
3031
"src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",

src/Hosting/TestHost/src/HttpContextBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ internal HttpContextBuilder(ApplicationWrapper application, bool allowSynchronou
5757
_httpContext.Features.Set<IHttpResponseBodyFeature>(_responseFeature);
5858
_httpContext.Features.Set<IHttpRequestLifetimeFeature>(_requestLifetimeFeature);
5959
_httpContext.Features.Set<IHttpResponseTrailersFeature>(_responseTrailersFeature);
60+
_httpContext.Features.Set<IHttpUpgradeFeature>(new UpgradeFeature());
6061
}
6162

6263
public bool AllowSynchronousIO { get; set; }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Http.Features;
8+
9+
namespace Microsoft.AspNetCore.TestHost
10+
{
11+
internal class UpgradeFeature : IHttpUpgradeFeature
12+
{
13+
public bool IsUpgradableRequest => false;
14+
15+
// TestHost provides an IHttpWebSocketFeature so it wont call UpgradeAsync()
16+
public Task<Stream> UpgradeAsync()
17+
{
18+
throw new NotSupportedException();
19+
}
20+
}
21+
}

src/Hosting/TestHost/test/Microsoft.AspNetCore.TestHost.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<Reference Include="Microsoft.AspNetCore.TestHost" />
1313
<Reference Include="Microsoft.Extensions.DiagnosticAdapter" />
1414
<Reference Include="Microsoft.Extensions.Hosting" />
15+
<Reference Include="Microsoft.AspNetCore.WebSockets" />
1516
</ItemGroup>
1617

1718
</Project>

src/Hosting/TestHost/test/TestClientTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.AspNetCore.Builder;
1414
using Microsoft.AspNetCore.Hosting;
1515
using Microsoft.AspNetCore.Http;
16+
using Microsoft.AspNetCore.Http.Features;
1617
using Microsoft.AspNetCore.Internal;
1718
using Microsoft.AspNetCore.Testing;
1819
using Microsoft.Extensions.DependencyInjection;
@@ -960,5 +961,35 @@ public async Task SendAsync_ExplicitlySet_Protocol20()
960961
Assert.Equal(expected, actual);
961962
Assert.Equal(new Version(2, 0), message.Version);
962963
}
964+
965+
[Fact]
966+
public async Task VerifyWebSocketAndUpgradeFeaturesForNonWebSocket()
967+
{
968+
using (var testServer = new TestServer(new WebHostBuilder()
969+
.Configure(app =>
970+
{
971+
app.UseWebSockets();
972+
app.Run(async c =>
973+
{
974+
var upgradeFeature = c.Features.Get<IHttpUpgradeFeature>();
975+
// Feature needs to exist for SignalR to verify that the server supports WebSockets
976+
Assert.NotNull(upgradeFeature);
977+
Assert.False(upgradeFeature.IsUpgradableRequest);
978+
await Assert.ThrowsAsync<NotSupportedException>(() => upgradeFeature.UpgradeAsync());
979+
980+
var webSocketFeature = c.Features.Get<IHttpWebSocketFeature>();
981+
Assert.NotNull(webSocketFeature);
982+
Assert.False(webSocketFeature.IsWebSocketRequest);
983+
984+
await c.Response.WriteAsync("test");
985+
});
986+
})))
987+
{
988+
var client = testServer.CreateClient();
989+
990+
var actual = await client.GetStringAsync("http://localhost:12345/");
991+
Assert.Equal("test", actual);
992+
}
993+
}
963994
}
964995
}

src/Hosting/TestHost/test/WebSocketClientTests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading.Tasks;
66
using Microsoft.AspNetCore.Builder;
77
using Microsoft.AspNetCore.Hosting;
8+
using Microsoft.AspNetCore.Http.Features;
89
using Xunit;
910

1011
namespace Microsoft.AspNetCore.TestHost.Tests
@@ -54,5 +55,77 @@ await client.ConnectAsync(
5455
Assert.Equal(expectedHost, capturedHost);
5556
Assert.Equal("/connect", capturedPath);
5657
}
58+
59+
[Fact]
60+
public async Task CanAcceptWebSocket()
61+
{
62+
using (var testServer = new TestServer(new WebHostBuilder()
63+
.Configure(app =>
64+
{
65+
app.UseWebSockets();
66+
app.Run(async ctx =>
67+
{
68+
if (ctx.Request.Path.StartsWithSegments("/connect"))
69+
{
70+
if (ctx.WebSockets.IsWebSocketRequest)
71+
{
72+
using var websocket = await ctx.WebSockets.AcceptWebSocketAsync();
73+
var buffer = new byte[1000];
74+
var res = await websocket.ReceiveAsync(buffer, default);
75+
await websocket.SendAsync(buffer.AsMemory(0, res.Count), System.Net.WebSockets.WebSocketMessageType.Binary, true, default);
76+
await websocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, null, default);
77+
}
78+
}
79+
});
80+
})))
81+
{
82+
var client = testServer.CreateWebSocketClient();
83+
84+
using var socket = await client.ConnectAsync(
85+
uri: new Uri("http://localhost/connect"),
86+
cancellationToken: default);
87+
88+
await socket.SendAsync(new byte[10], System.Net.WebSockets.WebSocketMessageType.Binary, true, default);
89+
var res = await socket.ReceiveAsync(new byte[100], default);
90+
Assert.Equal(10, res.Count);
91+
Assert.True(res.EndOfMessage);
92+
93+
await socket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, null, default);
94+
}
95+
}
96+
97+
[Fact]
98+
public async Task VerifyWebSocketAndUpgradeFeatures()
99+
{
100+
using (var testServer = new TestServer(new WebHostBuilder()
101+
.Configure(app =>
102+
{
103+
app.Run(async c =>
104+
{
105+
var upgradeFeature = c.Features.Get<IHttpUpgradeFeature>();
106+
Assert.NotNull(upgradeFeature);
107+
Assert.False(upgradeFeature.IsUpgradableRequest);
108+
await Assert.ThrowsAsync<NotSupportedException>(() => upgradeFeature.UpgradeAsync());
109+
110+
var webSocketFeature = c.Features.Get<IHttpWebSocketFeature>();
111+
Assert.NotNull(webSocketFeature);
112+
Assert.True(webSocketFeature.IsWebSocketRequest);
113+
});
114+
})))
115+
{
116+
var client = testServer.CreateWebSocketClient();
117+
118+
try
119+
{
120+
using var socket = await client.ConnectAsync(
121+
uri: new Uri("http://localhost/connect"),
122+
cancellationToken: default);
123+
}
124+
catch
125+
{
126+
// An exception will be thrown because our endpoint does not accept the websocket
127+
}
128+
}
129+
}
57130
}
58131
}

src/SignalR/clients/csharp/Client/test/UnitTests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<ItemGroup>
1919
<Reference Include="Microsoft.AspNetCore.SignalR.Client" />
2020
<Reference Include="Microsoft.Extensions.Logging" />
21+
<Reference Include="Microsoft.AspNetCore.TestHost" />
22+
<Reference Include="Microsoft.AspNetCore.SignalR" />
2123
</ItemGroup>
2224

2325
</Project>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.AspNetCore.SignalR.Tests;
8+
using Microsoft.AspNetCore.TestHost;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Logging;
11+
using Xunit;
12+
13+
namespace Microsoft.AspNetCore.SignalR.Client.Tests
14+
{
15+
public class TestServerTests : VerifiableLoggedTest
16+
{
17+
[Fact]
18+
public async Task WebSocketsWorks()
19+
{
20+
using (StartVerifiableLog())
21+
{
22+
var builder = new WebHostBuilder().ConfigureServices(s =>
23+
{
24+
s.AddLogging();
25+
s.AddSingleton(LoggerFactory);
26+
s.AddSignalR();
27+
}).Configure(app =>
28+
{
29+
app.UseRouting();
30+
app.UseEndpoints(endpoints =>
31+
{
32+
endpoints.MapHub<EchoHub>("/echo");
33+
});
34+
});
35+
var server = new TestServer(builder);
36+
37+
var webSocketFactoryCalled = false;
38+
var connectionBuilder = new HubConnectionBuilder()
39+
.WithUrl(server.BaseAddress + "echo", options =>
40+
{
41+
options.Transports = Http.Connections.HttpTransportType.WebSockets;
42+
options.HttpMessageHandlerFactory = _ =>
43+
{
44+
return server.CreateHandler();
45+
};
46+
options.WebSocketFactory = async (context, token) =>
47+
{
48+
webSocketFactoryCalled = true;
49+
var wsClient = server.CreateWebSocketClient();
50+
return await wsClient.ConnectAsync(context.Uri, default);
51+
};
52+
});
53+
connectionBuilder.Services.AddLogging();
54+
connectionBuilder.Services.AddSingleton(LoggerFactory);
55+
var connection = connectionBuilder.Build();
56+
57+
var originalMessage = "message";
58+
connection.On<string>("Echo", (receivedMessage) =>
59+
{
60+
Assert.Equal(originalMessage, receivedMessage);
61+
});
62+
63+
await connection.StartAsync();
64+
await connection.InvokeAsync("Echo", originalMessage);
65+
Assert.True(webSocketFactoryCalled);
66+
}
67+
}
68+
69+
[Fact]
70+
public async Task LongPollingWorks()
71+
{
72+
using (StartVerifiableLog())
73+
{
74+
var builder = new WebHostBuilder().ConfigureServices(s =>
75+
{
76+
s.AddLogging();
77+
s.AddSingleton(LoggerFactory);
78+
s.AddSignalR();
79+
}).Configure(app =>
80+
{
81+
app.UseRouting();
82+
app.UseEndpoints(endpoints =>
83+
{
84+
endpoints.MapHub<EchoHub>("/echo");
85+
});
86+
});
87+
var server = new TestServer(builder);
88+
89+
var connectionBuilder = new HubConnectionBuilder()
90+
.WithUrl(server.BaseAddress + "echo", options =>
91+
{
92+
options.Transports = Http.Connections.HttpTransportType.LongPolling;
93+
options.HttpMessageHandlerFactory = _ =>
94+
{
95+
return server.CreateHandler();
96+
};
97+
});
98+
connectionBuilder.Services.AddLogging();
99+
connectionBuilder.Services.AddSingleton(LoggerFactory);
100+
var connection = connectionBuilder.Build();
101+
102+
var originalMessage = "message";
103+
connection.On<string>("Echo", (receivedMessage) =>
104+
{
105+
Assert.Equal(originalMessage, receivedMessage);
106+
});
107+
108+
await connection.StartAsync();
109+
await connection.InvokeAsync("Echo", originalMessage);
110+
}
111+
}
112+
}
113+
114+
class EchoHub : Hub
115+
{
116+
public Task Echo(string message)
117+
{
118+
return Clients.All.SendAsync("Echo", message);
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)