diff --git a/Microsoft.AspNetCore.Sockets.sln b/Microsoft.AspNetCore.Sockets.sln index 1834ba9c07..61f6f31626 100644 --- a/Microsoft.AspNetCore.Sockets.sln +++ b/Microsoft.AspNetCore.Sockets.sln @@ -19,6 +19,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Socket EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ClientSample", "samples\ClientSample\ClientSample.xproj", "{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6A35B453-52EC-48AF-89CA-D4A69800F131}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Sockets.Tests", "test\Microsoft.AspNetCore.Sockets.Tests\Microsoft.AspNetCore.Sockets.Tests.xproj", "{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,6 +41,10 @@ Global {BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Release|Any CPU.Build.0 = Release|Any CPU + {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -45,5 +53,6 @@ Global {C4AEAB04-F341-4539-B6C0-52368FB4BF9E} = {C4BC9889-B49F-41B6-806B-F84941B2549B} {1715EA8D-8E13-4ACF-8BCA-57D048E55ED8} = {DA69F624-5398-4884-87E4-B816698CDE65} {BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9} = {C4BC9889-B49F-41B6-806B-F84941B2549B} + {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9} = {6A35B453-52EC-48AF-89CA-D4A69800F131} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNetCore.Sockets/EndPoint.cs b/src/Microsoft.AspNetCore.Sockets/EndPoint.cs index 41afc43841..507db7862f 100644 --- a/src/Microsoft.AspNetCore.Sockets/EndPoint.cs +++ b/src/Microsoft.AspNetCore.Sockets/EndPoint.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Sockets /// /// Represents an end point that multiple connections connect to. For HTTP, endpoints are URLs, for non HTTP it can be a TCP listener (or similar) /// - public class EndPoint + public abstract class EndPoint { /// /// Live list of connections for this @@ -17,9 +17,6 @@ namespace Microsoft.AspNetCore.Sockets /// /// The new /// A that represents the connection lifetime. When the task completes, the connection is complete. - public virtual Task OnConnected(Connection connection) - { - return Task.CompletedTask; - } + public abstract Task OnConnected(Connection connection); } } diff --git a/src/Microsoft.AspNetCore.Sockets/HttpConnectionDispatcher.cs b/src/Microsoft.AspNetCore.Sockets/HttpConnectionDispatcher.cs index af9ff72a65..663fab8c73 100644 --- a/src/Microsoft.AspNetCore.Sockets/HttpConnectionDispatcher.cs +++ b/src/Microsoft.AspNetCore.Sockets/HttpConnectionDispatcher.cs @@ -10,8 +10,14 @@ namespace Microsoft.AspNetCore.Sockets { public class HttpConnectionDispatcher { - private readonly ConnectionManager _manager = new ConnectionManager(); - private readonly ChannelFactory _channelFactory = new ChannelFactory(); + private readonly ConnectionManager _manager; + private readonly ChannelFactory _channelFactory; + + public HttpConnectionDispatcher(ConnectionManager manager, ChannelFactory factory) + { + _manager = manager; + _channelFactory = factory; + } public async Task Execute(string path, HttpContext context) where TEndPoint : EndPoint { @@ -201,7 +207,7 @@ namespace Microsoft.AspNetCore.Sockets return context.Request.Body.CopyToAsync(httpChannel.Input); } - return Task.CompletedTask; + throw new InvalidOperationException("Unknown connection id"); } private ConnectionState GetOrCreateConnection(HttpContext context) diff --git a/src/Microsoft.AspNetCore.Sockets/HttpDispatcherAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Sockets/HttpDispatcherAppBuilderExtensions.cs index 51204bd5e8..97a87f5cc4 100644 --- a/src/Microsoft.AspNetCore.Sockets/HttpDispatcherAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Sockets/HttpDispatcherAppBuilderExtensions.cs @@ -1,4 +1,5 @@ using System; +using Channels; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Routing; @@ -9,7 +10,9 @@ namespace Microsoft.AspNetCore.Builder { public static IApplicationBuilder UseSockets(this IApplicationBuilder app, Action callback) { - var dispatcher = new HttpConnectionDispatcher(); + var manager = new ConnectionManager(); + var factory = new ChannelFactory(); + var dispatcher = new HttpConnectionDispatcher(manager, factory); var routes = new RouteBuilder(app); callback(new SocketRouteBuilder(routes, dispatcher)); diff --git a/src/Microsoft.AspNetCore.Sockets/project.json b/src/Microsoft.AspNetCore.Sockets/project.json index 971f8b6547..d3d5bc09a2 100644 --- a/src/Microsoft.AspNetCore.Sockets/project.json +++ b/src/Microsoft.AspNetCore.Sockets/project.json @@ -7,6 +7,7 @@ }, "frameworks": { "netstandard1.3": { - } + }, + "net46": { } } } diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/ConnectionManagerTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/ConnectionManagerTests.cs new file mode 100644 index 0000000000..9eec53d06f --- /dev/null +++ b/test/Microsoft.AspNetCore.Sockets.Tests/ConnectionManagerTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Channels; +using Xunit; + +namespace Microsoft.AspNetCore.Sockets.Tests +{ + public class ConnectionManagerTests + { + [Fact] + public void ReservedConnectionsHaveConnectionId() + { + var connectionManager = new ConnectionManager(); + var state = connectionManager.ReserveConnection(); + + Assert.NotNull(state.Connection); + Assert.NotNull(state.Connection.ConnectionId); + Assert.True(state.Active); + Assert.Null(state.Close); + Assert.Null(state.Connection.Channel); + } + + [Fact] + public void ReservedConnectionsCanBeRetrieved() + { + var connectionManager = new ConnectionManager(); + var state = connectionManager.ReserveConnection(); + + Assert.NotNull(state.Connection); + Assert.NotNull(state.Connection.ConnectionId); + + ConnectionState newState; + Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState)); + Assert.Same(newState, state); + } + + [Fact] + public void AddNewConnection() + { + using (var factory = new ChannelFactory()) + using (var channel = new HttpChannel(factory)) + { + var connectionManager = new ConnectionManager(); + var state = connectionManager.AddNewConnection(channel); + + Assert.NotNull(state.Connection); + Assert.NotNull(state.Connection.ConnectionId); + Assert.NotNull(state.Connection.Channel); + + ConnectionState newState; + Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState)); + Assert.Same(newState, state); + Assert.Same(channel, newState.Connection.Channel); + } + } + + [Fact] + public void RemoveConnection() + { + using (var factory = new ChannelFactory()) + using (var channel = new HttpChannel(factory)) + { + var connectionManager = new ConnectionManager(); + var state = connectionManager.AddNewConnection(channel); + + Assert.NotNull(state.Connection); + Assert.NotNull(state.Connection.ConnectionId); + Assert.NotNull(state.Connection.Channel); + + ConnectionState newState; + Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState)); + Assert.Same(newState, state); + Assert.Same(channel, newState.Connection.Channel); + + connectionManager.RemoveConnection(state.Connection.ConnectionId); + Assert.False(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState)); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs new file mode 100644 index 0000000000..7048970d5d --- /dev/null +++ b/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Sockets.Tests +{ + public class HttpConnectionDispatcherTests + { + [Fact] + public async Task GetIdReservesConnectionIdAndReturnsIt() + { + var manager = new ConnectionManager(); + using (var factory = new ChannelFactory()) + { + var dispatcher = new HttpConnectionDispatcher(manager, factory); + var context = new DefaultHttpContext(); + var ms = new MemoryStream(); + context.Request.Path = "/getid"; + context.Response.Body = ms; + await dispatcher.Execute("", context); + + var id = Encoding.UTF8.GetString(ms.ToArray()); + + ConnectionState state; + Assert.True(manager.TryGetConnection(id, out state)); + Assert.Equal(id, state.Connection.ConnectionId); + } + } + + [Fact] + public async Task SendingToReservedConnectionsThatHaveNotConnectedThrows() + { + var manager = new ConnectionManager(); + var state = manager.ReserveConnection(); + + using (var factory = new ChannelFactory()) + { + var dispatcher = new HttpConnectionDispatcher(manager, factory); + var context = new DefaultHttpContext(); + context.Request.Path = "/send"; + var values = new Dictionary(); + values["id"] = state.Connection.ConnectionId; + var qs = new QueryCollection(values); + context.Request.Query = qs; + await Assert.ThrowsAsync(async () => + { + await dispatcher.Execute("", context); + }); + } + } + + [Fact] + public async Task SendingToUnknownConnectionIdThrows() + { + var manager = new ConnectionManager(); + using (var factory = new ChannelFactory()) + { + var dispatcher = new HttpConnectionDispatcher(manager, factory); + var context = new DefaultHttpContext(); + context.Request.Path = "/send"; + var values = new Dictionary(); + values["id"] = "unknown"; + var qs = new QueryCollection(values); + context.Request.Query = qs; + await Assert.ThrowsAsync(async () => + { + await dispatcher.Execute("", context); + }); + } + } + + [Fact] + public async Task SendingWithoutConnectionIdThrows() + { + var manager = new ConnectionManager(); + using (var factory = new ChannelFactory()) + { + var dispatcher = new HttpConnectionDispatcher(manager, factory); + var context = new DefaultHttpContext(); + context.Request.Path = "/send"; + await Assert.ThrowsAsync(async () => + { + await dispatcher.Execute("", context); + }); + } + } + } + + public class TestEndPoint : EndPoint + { + public override Task OnConnected(Connection connection) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/LongPollingTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/LongPollingTests.cs new file mode 100644 index 0000000000..d9442b3f9e --- /dev/null +++ b/test/Microsoft.AspNetCore.Sockets.Tests/LongPollingTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Sockets.Tests +{ + public class LongPollingTests + { + [Fact] + public async Task Set204StatusCodeWhenChannelComplete() + { + using (var factory = new ChannelFactory()) + { + var connection = new Connection(); + connection.ConnectionId = Guid.NewGuid().ToString(); + var channel = new HttpChannel(factory); + connection.Channel = channel; + var context = new DefaultHttpContext(); + var poll = new LongPolling(connection); + + channel.Output.CompleteWriter(); + + await poll.ProcessRequest(context); + + Assert.Equal(204, context.Response.StatusCode); + } + } + + [Fact] + public async Task NoFramingAddedWhenDataSent() + { + using (var factory = new ChannelFactory()) + { + var connection = new Connection(); + connection.ConnectionId = Guid.NewGuid().ToString(); + var channel = new HttpChannel(factory); + connection.Channel = channel; + var context = new DefaultHttpContext(); + var ms = new MemoryStream(); + context.Response.Body = ms; + var poll = new LongPolling(connection); + + await channel.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello World")); + + channel.Output.CompleteWriter(); + + await poll.ProcessRequest(context); + + Assert.Equal("Hello World", Encoding.UTF8.GetString(ms.ToArray())); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/Microsoft.AspNetCore.Sockets.Tests.xproj b/test/Microsoft.AspNetCore.Sockets.Tests/Microsoft.AspNetCore.Sockets.Tests.xproj new file mode 100644 index 0000000000..eb7ad5a0bd --- /dev/null +++ b/test/Microsoft.AspNetCore.Sockets.Tests/Microsoft.AspNetCore.Sockets.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + aad719d5-5e31-4ed1-a60f-6eb92efa66d9 + Microsoft.AspNetCore.Sockets.Tests + .\obj + .\bin\ + v4.5.2 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.Sockets.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b3149d65f7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Sockets.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.AspNetCore.Sockets.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("aad719d5-5e31-4ed1-a60f-6eb92efa66d9")] diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs new file mode 100644 index 0000000000..3d1d9868fc --- /dev/null +++ b/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Sockets.Tests +{ + public class ServerSentEventsTests + { + [Fact] + public async Task SSESetsContentType() + { + using (var factory = new ChannelFactory()) + { + var connection = new Connection(); + connection.ConnectionId = Guid.NewGuid().ToString(); + var channel = new HttpChannel(factory); + connection.Channel = channel; + var sse = new ServerSentEvents(connection); + var context = new DefaultHttpContext(); + + channel.Output.CompleteWriter(); + + await sse.ProcessRequest(context); + + Assert.Equal("text/event-stream", context.Response.ContentType); + Assert.Equal("no-cache", context.Response.Headers["Cache-Control"]); + } + } + + [Fact] + public async Task SSEAddsAppropriateFraming() + { + using (var factory = new ChannelFactory()) + { + var connection = new Connection(); + connection.ConnectionId = Guid.NewGuid().ToString(); + var channel = new HttpChannel(factory); + connection.Channel = channel; + var sse = new ServerSentEvents(connection); + var context = new DefaultHttpContext(); + var ms = new MemoryStream(); + context.Response.Body = ms; + + await channel.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello World")); + + channel.Output.CompleteWriter(); + + await sse.ProcessRequest(context); + + var expected = "data: Hello World\n\n"; + Assert.Equal(expected, Encoding.UTF8.GetString(ms.ToArray())); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/project.json b/test/Microsoft.AspNetCore.Sockets.Tests/project.json new file mode 100644 index 0000000000..c6ef6c82e4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Sockets.Tests/project.json @@ -0,0 +1,25 @@ +{ + "buildOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "dotnet-test-xunit": "2.2.0-*", + "Microsoft.AspNetCore.Http": "1.1.0-*", + "Microsoft.AspNetCore.Sockets": { + "target": "project" + }, + "xunit": "2.2.0-*" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0", + "type": "platform" + } + } + }, + "net46": {} + }, + "testRunner": "xunit" +} \ No newline at end of file