diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index 6263483807c76a..ea280fa0f59934 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs @@ -1024,4 +1024,75 @@ public override Task WaitForCloseAsync(CancellationToken cancellationToken) throw new NotImplementedException(); } } + +#if !NETFRAMEWORK + public sealed class Http2Stream : Stream + { + private readonly Http2LoopbackConnection _connection; + private readonly int _streamId; + private bool _readEnded; + private ReadOnlyMemory _leftoverReadData; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public Http2Stream(Http2LoopbackConnection connection, int streamId) + { + _connection = connection; + _streamId = streamId; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (!_leftoverReadData.IsEmpty) + { + int read = Math.Min(buffer.Length, _leftoverReadData.Length); + _leftoverReadData.Span.Slice(0, read).CopyTo(buffer.Span); + _leftoverReadData = _leftoverReadData.Slice(read); + return read; + } + + if (_readEnded) + { + return 0; + } + + DataFrame dataFrame = (DataFrame)await _connection.ReadDataFrameAsync(); + Assert.Equal(_streamId, dataFrame.StreamId); + _leftoverReadData = dataFrame.Data; + _readEnded = dataFrame.EndStreamFlag; + + return await ReadAsync(buffer, cancellationToken); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + await _connection.SendResponseDataAsync(_streamId, buffer, endStream: false); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + + protected override void Dispose(bool disposing) => DisposeAsync().GetAwaiter().GetResult(); + + public override async ValueTask DisposeAsync() + { + await _connection.SendResponseDataAsync(_streamId, Memory.Empty, endStream: true); + } + + public override void Flush() { } + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override long Length => throw new NotImplementedException(); + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + } +#endif } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs index b2137a7faa7a20..38de3f5dd51964 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs @@ -83,6 +83,7 @@ public async Task Proxy_ConnectThruProxy_Success(Uri server) server, TimeOutMilliseconds, _output, + UseVersion, default(TimeSpan), proxy)) { diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index 0c8b94778a58ea..86344e35b00f19 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -10,6 +10,7 @@ using Xunit.Abstractions; using System.Net.Http; using System.Diagnostics; +using System.Net.Test.Common; namespace System.Net.WebSockets.Client.Tests { @@ -56,7 +57,7 @@ public static IEnumerable UnavailableWebSocketServers { server = System.Net.Test.Common.Configuration.Http.RemoteEchoServer; var ub = new UriBuilder("ws", server.Host, server.Port, server.PathAndQuery); - exceptionMessage = ResourceHelper.GetExceptionMessage("net_WebSockets_ConnectStatusExpected", (int) HttpStatusCode.OK, (int) HttpStatusCode.SwitchingProtocols); + exceptionMessage = ResourceHelper.GetExceptionMessage("net_WebSockets_ConnectStatusExpected", (int)HttpStatusCode.OK, (int)HttpStatusCode.SwitchingProtocols); yield return new object[] { ub.Uri, exceptionMessage, WebSocketError.NotAWebSocket }; } @@ -65,7 +66,7 @@ public static IEnumerable UnavailableWebSocketServers public async Task TestCancellation(Func action, Uri server) { - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output, UseVersion)) { try { @@ -111,6 +112,9 @@ protected static async Task ReceiveEntireMessageAsync(We protected virtual bool UseCustomInvoker => false; protected virtual bool UseHttpClient => false; + protected virtual Version UseVersion => HttpVersion.Version11; + protected virtual bool UseRemoteServer => false; + protected virtual bool UseSsl => false; protected bool UseSharedHandler => !UseCustomInvoker && !UseHttpClient; @@ -141,14 +145,45 @@ protected static async Task ReceiveEntireMessageAsync(We return null; } + protected Task CreateEchoServerAsync(Func clientFunc) + { + if (UseRemoteServer) + { + if (UseVersion.Major == 2) + { + throw new NotSupportedException("Remote servers don't support WebSockets over HTTP/2 yet"); + } + + if (UseSsl) + { + return clientFunc(Test.Common.Configuration.WebSockets.SecureRemoteEchoServer); + } + else + { + return clientFunc(Test.Common.Configuration.WebSockets.RemoteEchoServer); + } + } + else + { + if (UseVersion.Major == 2) + { + return WebSocketHelper.GetEchoHttp2LoopbackServer(clientFunc, new Http2Options() { WebSocketEndpoint = true, UseSsl = UseSsl }); + } + else + { + return WebSocketHelper.GetEchoLoopbackServer(clientFunc, new LoopbackServer.Options() { WebSocketEndpoint = true, UseSsl = UseSsl }); ; + } + } + } + protected Task GetConnectedWebSocket(Uri uri, int TimeOutMilliseconds, ITestOutputHelper output) => - WebSocketHelper.GetConnectedWebSocket(uri, TimeOutMilliseconds, output, invoker: GetInvoker()); + WebSocketHelper.GetConnectedWebSocket(uri, TimeOutMilliseconds, output, version: UseVersion, invoker: GetInvoker()); protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) => cws.ConnectAsync(uri, GetInvoker(), cancellationToken); protected Task TestEcho(Uri uri, WebSocketMessageType type, int timeOutMilliseconds, ITestOutputHelper output) => - WebSocketHelper.TestEcho(uri, WebSocketMessageType.Text, TimeOutMilliseconds, _output, GetInvoker()); + WebSocketHelper.TestEcho(uri, WebSocketMessageType.Text, TimeOutMilliseconds, _output, UseVersion, GetInvoker()); public static bool WebSocketsSupported { get { return WebSocketHelper.WebSocketsSupported; } } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs index 56acccdc05590a..0505898c5c8ccc 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs @@ -20,7 +20,7 @@ public KeepAliveTest(ITestOutputHelper output) : base(output) { } [OuterLoop] // involves long delay public async Task KeepAlive_LongDelayBetweenSendReceives_Succeeds() { - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(System.Net.Test.Common.Configuration.WebSockets.RemoteEchoServer, TimeOutMilliseconds, _output, TimeSpan.FromSeconds(1))) + using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(System.Net.Test.Common.Configuration.WebSockets.RemoteEchoServer, TimeOutMilliseconds, _output, UseVersion, TimeSpan.FromSeconds(1))) { await cws.SendAsync(new ArraySegment(new byte[1] { 42 }), WebSocketMessageType.Binary, true, CancellationToken.None); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs index ef21a36e44fa8f..4b1da16bff9ac2 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs @@ -12,98 +12,67 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class HttpClientSendReceiveTest_Http2 : SendReceiveTest_Http2 - { - public HttpClientSendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } + // Memory segment - protected override bool UseHttpClient => true; + // Invoker + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets aren't supported on Browser")] + public sealed class InvokerMemorySendReceiveLocalTest_Http2 : InvokerMemorySendReceiveLocalTest + { + public InvokerMemorySendReceiveLocalTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; } - public sealed class InvokerSendReceiveTest_Http2 : SendReceiveTest_Http2 + [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] + public sealed class InvokerMemorySendReceiveLocalSslTest_Http2 : InvokerMemorySendReceiveLocalSslTest { - public InvokerSendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; + public InvokerMemorySendReceiveLocalSslTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; } - public abstract class SendReceiveTest_Http2 : ClientWebSocketTestBase + //HttpClient + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets aren't supported on Browser")] + public sealed class HttpClientMemorySendReceiveLocalTest_Http2 : HttpClientMemorySendReceiveLocalTest { - public SendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } - - [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] - public async Task ReceiveNoThrowAfterSend_NoSsl() - { - var serverMessage = new byte[] { 4, 5, 6 }; - await Http2LoopbackServer.CreateClientAndServerAsync(async uri => - { - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - cws.Options.HttpVersion = HttpVersion.Version20; - cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; - - await cws.ConnectAsync(uri, GetInvoker(), cts.Token); - - await cws.SendAsync(new byte[] { 2, 3, 4 }, WebSocketMessageType.Binary, true, cts.Token); - - var readBuffer = new byte[serverMessage.Length]; - await cws.ReceiveAsync(readBuffer, cts.Token); - Assert.Equal(serverMessage, readBuffer); - } - }, - async server => - { - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(new SettingsEntry { SettingId = SettingId.EnableConnect, Value = 1 }); - (int streamId, HttpRequestData requestData) = await connection.ReadAndParseRequestHeaderAsync(readBody: false); - // send status 200 OK to establish websocket - await connection.SendResponseHeadersAsync(streamId, endStream: false).ConfigureAwait(false); - - // send reply - byte binaryMessageType = 2; - var prefix = new byte[] { binaryMessageType, (byte)serverMessage.Length }; - byte[] constructMessage = prefix.Concat(serverMessage).ToArray(); - await connection.SendResponseDataAsync(streamId, constructMessage, endStream: false); - - }, new Http2Options() { WebSocketEndpoint = true, UseSsl = false }); - } + public HttpClientMemorySendReceiveLocalTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } - [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] - public async Task ReceiveNoThrowAfterSend_WithSsl() - { - var serverMessage = new byte[] { 4, 5, 6 }; - await Http2LoopbackServer.CreateClientAndServerAsync(async uri => - { - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - cws.Options.HttpVersion = HttpVersion.Version20; - cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; + [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] + public sealed class HttpClientMemorySendReceiveLocalSslTest_Http2 : HttpClientMemorySendReceiveLocalSslTest + { + public HttpClientMemorySendReceiveLocalSslTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } - await cws.ConnectAsync(uri, GetInvoker(), cts.Token); + // Array segment - await cws.SendAsync(new byte[] { 2, 3, 4 }, WebSocketMessageType.Binary, true, cts.Token); + //Invoker + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets aren't supported on Browser")] + public sealed class InvokerArraySegmentSendReceiveLocalTest_Http2 : InvokerArraySegmentSendReceiveLocalTest + { + public InvokerArraySegmentSendReceiveLocalTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } - var readBuffer = new byte[serverMessage.Length]; - await cws.ReceiveAsync(readBuffer, cts.Token); - Assert.Equal(serverMessage, readBuffer); - } - }, - async server => - { - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(new SettingsEntry { SettingId = SettingId.EnableConnect, Value = 1 }); - (int streamId, HttpRequestData requestData) = await connection.ReadAndParseRequestHeaderAsync(readBody: false); - // send status 200 OK to establish websocket - await connection.SendResponseHeadersAsync(streamId, endStream: false).ConfigureAwait(false); + [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] + public sealed class InvokerArraySegmentSendReceiveLocalSslTest_Http2 : InvokerArraySegmentSendReceiveLocalSslTest + { + public InvokerArraySegmentSendReceiveLocalSslTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } - // send reply - byte binaryMessageType = 2; - var prefix = new byte[] { binaryMessageType, (byte)serverMessage.Length }; - byte[] constructMessage = prefix.Concat(serverMessage).ToArray(); - await connection.SendResponseDataAsync(streamId, constructMessage, endStream: false); + //HttpClient + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets aren't supported on Browser")] + public sealed class HttpClientArraySegmentSendReceiveLocalTest_Http2 : HttpClientArraySegmentSendReceiveLocalTest + { + public HttpClientArraySegmentSendReceiveLocalTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } - }, new Http2Options() { WebSocketEndpoint = true }); - } + [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] + public sealed class HttpClientArraySegmentSendReceiveLocalSslTest_Http2 : HttpClientArraySegmentSendReceiveLocalSslTest + { + public HttpClientArraySegmentSendReceiveLocalSslTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index ec3913c02c16c9..902cf21e73781c 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -12,36 +12,130 @@ namespace System.Net.WebSockets.Client.Tests { + // Memory array segment---------------- - public sealed class InvokerMemorySendReceiveTest : MemorySendReceiveTest + // Invoker-------- + + // Local---- + + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class InvokerMemorySendReceiveLocalTest : InvokerMemorySendReceiveTest + { + public InvokerMemorySendReceiveLocalTest(ITestOutputHelper output) : base(output) { } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class InvokerMemorySendReceiveLocalSslTest : InvokerMemorySendReceiveTest + { + public InvokerMemorySendReceiveLocalSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + // External server -- HTTP/1.1 only---- + // TODO: add HTTP/2 when https://corefx-net-http2.azurewebsites.net will support extended connect + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public sealed class InvokerMemorySendReceiveRemoteSslTest : InvokerMemorySendReceiveRemoteTest + { + public InvokerMemorySendReceiveRemoteSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class InvokerMemorySendReceiveRemoteTest : InvokerMemorySendReceiveTest + { + public InvokerMemorySendReceiveRemoteTest(ITestOutputHelper output) : base(output) { } + + protected override bool UseRemoteServer => true; + } + + public abstract class InvokerMemorySendReceiveTest : MemorySendReceiveTest { public InvokerMemorySendReceiveTest(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; } - public sealed class HttpClientMemorySendReceiveTest : MemorySendReceiveTest + //HttpClient-------- + + // Local---- + + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class HttpClientMemorySendReceiveLocalTest : HttpClientMemorySendReceiveTest + { + public HttpClientMemorySendReceiveLocalTest(ITestOutputHelper output) : base(output) { } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class HttpClientMemorySendReceiveLocalSslTest : HttpClientMemorySendReceiveTest + { + public HttpClientMemorySendReceiveLocalSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + // External server -- HTTP/1.1 only---- + // TODO: add HTTP/2 when https://corefx-net-http2.azurewebsites.net will support extended connect + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public sealed class HttpClientMemorySendReceiveRemoteSslTest : HttpClientMemorySendReceiveRemoteTest + { + public HttpClientMemorySendReceiveRemoteSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class HttpClientMemorySendReceiveRemoteTest : HttpClientMemorySendReceiveTest + { + public HttpClientMemorySendReceiveRemoteTest(ITestOutputHelper output) : base(output) { } + + protected override bool UseRemoteServer => true; + } + + public abstract class HttpClientMemorySendReceiveTest : MemorySendReceiveTest { public HttpClientMemorySendReceiveTest(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - public sealed class InvokerArraySegmentSendReceiveTest : ArraySegmentSendReceiveTest + // No invoker -- HTTP/1.1 only -------- + + // Local + + public class NoInvokerMemorySendReceiveLocalTest : MemorySendReceiveTest { - public InvokerArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } + public NoInvokerMemorySendReceiveLocalTest(ITestOutputHelper output) : base(output) { } + } - protected override bool UseCustomInvoker => true; + [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] + public class NoInvokerMemorySendReceiveLocalSslTest : MemorySendReceiveTest + { + public NoInvokerMemorySendReceiveLocalSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; } - public sealed class HttpClientArraySegmentSendReceiveTest : ArraySegmentSendReceiveTest + // External server---- + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] + public sealed class NoInvokerMemorySendReceiveRemoteSslTest : NoInvokerMemorySendReceiveRemoteTest { - public HttpClientArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } + public NoInvokerMemorySendReceiveRemoteSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } - protected override bool UseHttpClient => true; + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + public class NoInvokerMemorySendReceiveRemoteTest : MemorySendReceiveTest + { + public NoInvokerMemorySendReceiveRemoteTest(ITestOutputHelper output) : base(output) { } + + protected override bool UseRemoteServer => true; } - public class MemorySendReceiveTest : SendReceiveTest + public abstract class MemorySendReceiveTest : SendReceiveTest { public MemorySendReceiveTest(ITestOutputHelper output) : base(output) { } @@ -61,7 +155,130 @@ protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, cancellationToken).AsTask(); } - public class ArraySegmentSendReceiveTest : SendReceiveTest + // Array segment---------------- + + // Invoker-------- + + // Local---- + + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class InvokerArraySegmentSendReceiveLocalTest : InvokerArraySegmentSendReceiveTest + { + public InvokerArraySegmentSendReceiveLocalTest(ITestOutputHelper output) : base(output) { } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class InvokerArraySegmentSendReceiveLocalSslTest : InvokerArraySegmentSendReceiveTest + { + public InvokerArraySegmentSendReceiveLocalSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + // External server -- HTTP/1.1 only---- + // TODO: add HTTP/2 when https://corefx-net-http2.azurewebsites.net will support extended connect + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public sealed class InvokerArraySegmentSendReceiveRemoteSslTest : InvokerArraySegmentSendReceiveRemoteTest + { + public InvokerArraySegmentSendReceiveRemoteSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class InvokerArraySegmentSendReceiveRemoteTest : InvokerArraySegmentSendReceiveTest + { + public InvokerArraySegmentSendReceiveRemoteTest(ITestOutputHelper output) : base(output) { } + + protected override bool UseRemoteServer => true; + } + + public abstract class InvokerArraySegmentSendReceiveTest : ArraySegmentSendReceiveTest + { + public InvokerArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } + + protected override bool UseCustomInvoker => true; + } + + //HttpClient-------- + + // Local---- + + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class HttpClientArraySegmentSendReceiveLocalTest : HttpClientArraySegmentSendReceiveTest + { + public HttpClientArraySegmentSendReceiveLocalTest(ITestOutputHelper output) : base(output) { } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class HttpClientArraySegmentSendReceiveLocalSslTest : HttpClientArraySegmentSendReceiveTest + { + public HttpClientArraySegmentSendReceiveLocalSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + // External server -- HTTP/1.1 only---- + // TODO: add HTTP/2 when https://corefx-net-http2.azurewebsites.net will support extended connect + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public sealed class HttpClientArraySegmentSendReceiveRemoteSslTest : HttpClientArraySegmentSendReceiveRemoteTest + { + public HttpClientArraySegmentSendReceiveRemoteSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public class HttpClientArraySegmentSendReceiveRemoteTest : HttpClientArraySegmentSendReceiveTest + { + public HttpClientArraySegmentSendReceiveRemoteTest(ITestOutputHelper output) : base(output) { } + + protected override bool UseRemoteServer => true; + } + + public abstract class HttpClientArraySegmentSendReceiveTest : ArraySegmentSendReceiveTest + { + public HttpClientArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } + + protected override bool UseHttpClient => true; + } + + // No invoker -- HTTP/1.1 only -------- + + // Local + public class NoInvokerArraySegmentSendReceiveLocalTest : ArraySegmentSendReceiveTest + { + public NoInvokerArraySegmentSendReceiveLocalTest(ITestOutputHelper output) : base(output) { } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] + public class NoInvokerArraySegmentSendReceiveLocalSslTest : ArraySegmentSendReceiveTest + { + public NoInvokerArraySegmentSendReceiveLocalSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + // External server---- + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] + public sealed class NoInvokerArraySegmentSendReceiveRemoteSslTest : NoInvokerArraySegmentSendReceiveRemoteTest + { + public NoInvokerArraySegmentSendReceiveRemoteSslTest(ITestOutputHelper output) : base(output) { } + protected override bool UseSsl => true; + } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + public class NoInvokerArraySegmentSendReceiveRemoteTest : ArraySegmentSendReceiveTest + { + public NoInvokerArraySegmentSendReceiveRemoteTest(ITestOutputHelper output) : base(output) { } + + protected override bool UseRemoteServer => true; + } + + public abstract class ArraySegmentSendReceiveTest : SendReceiveTest { public ArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } @@ -83,36 +300,40 @@ public SendReceiveTest(ITestOutputHelper output) : base(output) { } [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] public async Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) { - const int SendBufferSize = 10; - var sendBuffer = new byte[SendBufferSize]; - var sendSegment = new ArraySegment(sendBuffer); - - var receiveBuffer = new byte[SendBufferSize / 2]; - var receiveSegment = new ArraySegment(receiveBuffer); - - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) { - var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); + const int SendBufferSize = 10; + var sendBuffer = new byte[SendBufferSize]; + var sendSegment = new ArraySegment(sendBuffer); - // The server will read buffers and aggregate it before echoing back a complete message. - // But since this test uses a receive buffer that is smaller than the complete message, we will get - // back partial message fragments as we read them until we read the complete message payload. - for (int i = 0; i < SendBufferSize * 5; i++) + var receiveBuffer = new byte[SendBufferSize / 2]; + var receiveSegment = new ArraySegment(receiveBuffer); + + using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) { - await SendAsync(cws, sendSegment, WebSocketMessageType.Binary, false, ctsDefault.Token); - } + var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); - await SendAsync(cws, sendSegment, WebSocketMessageType.Binary, true, ctsDefault.Token); + // The server will read buffers and aggregate it before echoing back a complete message. + // But since this test uses a receive buffer that is smaller than the complete message, we will get + // back partial message fragments as we read them until we read the complete message payload. + for (int i = 0; i < SendBufferSize * 5; i++) + { + await SendAsync(cws, sendSegment, WebSocketMessageType.Binary, false, ctsDefault.Token); + } - WebSocketReceiveResult recvResult = await ReceiveAsync(cws, receiveSegment, ctsDefault.Token); - Assert.False(recvResult.EndOfMessage); + await SendAsync(cws, sendSegment, WebSocketMessageType.Binary, true, ctsDefault.Token); - while (recvResult.EndOfMessage == false) - { - recvResult = await ReceiveAsync(cws, receiveSegment, ctsDefault.Token); - } + WebSocketReceiveResult recvResult = await ReceiveAsync(cws, receiveSegment, ctsDefault.Token); + Assert.False(recvResult.EndOfMessage); - await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "PartialMessageDueToSmallReceiveBufferTest", ctsDefault.Token); + while (recvResult.EndOfMessage == false) + { + recvResult = await ReceiveAsync(cws, receiveSegment, ctsDefault.Token); + } + + await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "PartialMessageDueToSmallReceiveBufferTest", ctsDefault.Token); + } } } @@ -121,40 +342,44 @@ public async Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri [SkipOnPlatform(TestPlatforms.Browser, "JS Websocket does not support see issue https://github.com/dotnet/runtime/issues/46983")] public async Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(Uri server) { - var sendBuffer = new byte[ushort.MaxValue + 1]; - Random.Shared.NextBytes(sendBuffer); - var sendSegment = new ArraySegment(sendBuffer); - - // Ask the remote server to echo back received messages without ever signaling "end of message". - var ub = new UriBuilder(server); - ub.Query = "replyWithPartialMessages"; - - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(ub.Uri, TimeOutMilliseconds, _output)) + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) { - var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); + var sendBuffer = new byte[ushort.MaxValue + 1]; + Random.Shared.NextBytes(sendBuffer); + var sendSegment = new ArraySegment(sendBuffer); - // Send data to the server; the server will reply back with one or more partial messages. We should be - // able to consume that data as it arrives, without having to wait for "end of message" to be signaled. - await SendAsync(cws, sendSegment, WebSocketMessageType.Binary, true, ctsDefault.Token); + // Ask the remote server to echo back received messages without ever signaling "end of message". + var ub = new UriBuilder(server); + ub.Query = "replyWithPartialMessages"; - int totalBytesReceived = 0; - var receiveBuffer = new byte[sendBuffer.Length]; - while (totalBytesReceived < receiveBuffer.Length) + using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(ub.Uri, TimeOutMilliseconds, _output, UseVersion)) { - WebSocketReceiveResult recvResult = await ReceiveAsync( - cws, - new ArraySegment(receiveBuffer, totalBytesReceived, receiveBuffer.Length - totalBytesReceived), - ctsDefault.Token); + var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); - Assert.False(recvResult.EndOfMessage); - Assert.InRange(recvResult.Count, 0, receiveBuffer.Length - totalBytesReceived); - totalBytesReceived += recvResult.Count; - } + // Send data to the server; the server will reply back with one or more partial messages. We should be + // able to consume that data as it arrives, without having to wait for "end of message" to be signaled. + await SendAsync(cws, sendSegment, WebSocketMessageType.Binary, true, ctsDefault.Token); + + int totalBytesReceived = 0; + var receiveBuffer = new byte[sendBuffer.Length]; + while (totalBytesReceived < receiveBuffer.Length) + { + WebSocketReceiveResult recvResult = await ReceiveAsync( + cws, + new ArraySegment(receiveBuffer, totalBytesReceived, receiveBuffer.Length - totalBytesReceived), + ctsDefault.Token); - Assert.Equal(receiveBuffer.Length, totalBytesReceived); - Assert.Equal(sendBuffer, receiveBuffer); + Assert.False(recvResult.EndOfMessage); + Assert.InRange(recvResult.Count, 0, receiveBuffer.Length - totalBytesReceived); + totalBytesReceived += recvResult.Count; + } - await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "PartialMessageBeforeCompleteMessageArrives", ctsDefault.Token); + Assert.Equal(receiveBuffer.Length, totalBytesReceived); + Assert.Equal(sendBuffer, receiveBuffer); + + await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "PartialMessageBeforeCompleteMessageArrives", ctsDefault.Token); + } } } @@ -162,27 +387,31 @@ public async Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] public async Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) { - var cts = new CancellationTokenSource(TimeOutMilliseconds); + using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + { + var cts = new CancellationTokenSource(TimeOutMilliseconds); - string expectedInnerMessage = ResourceHelper.GetExceptionMessage( - "net_WebSockets_Argument_InvalidMessageType", - "Close", - "SendAsync", - "Binary", - "Text", - "CloseOutputAsync"); + string expectedInnerMessage = ResourceHelper.GetExceptionMessage( + "net_WebSockets_Argument_InvalidMessageType", + "Close", + "SendAsync", + "Binary", + "Text", + "CloseOutputAsync"); - var expectedException = new ArgumentException(expectedInnerMessage, "messageType"); - string expectedMessage = expectedException.Message; + var expectedException = new ArgumentException(expectedInnerMessage, "messageType"); + string expectedMessage = expectedException.Message; - AssertExtensions.Throws("messageType", () => - { - Task t = SendAsync(cws, new ArraySegment(), WebSocketMessageType.Close, true, cts.Token); - }); + AssertExtensions.Throws("messageType", () => + { + Task t = SendAsync(cws, new ArraySegment(), WebSocketMessageType.Close, true, cts.Token); + }); - Assert.Equal(WebSocketState.Open, cws.State); + Assert.Equal(WebSocketState.Open, cws.State); + } } } @@ -191,55 +420,128 @@ public async Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMess // This will also pass when no exception is thrown. Current implementation doesn't throw. public async Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) { - var cts = new CancellationTokenSource(TimeOutMilliseconds); + using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + { + var cts = new CancellationTokenSource(TimeOutMilliseconds); - Task[] tasks = new Task[10]; + Task[] tasks = new Task[10]; - try - { - for (int i = 0; i < tasks.Length; i++) + try { - tasks[i] = SendAsync( - cws, - WebSocketData.GetBufferFromText("hello"), - WebSocketMessageType.Text, - true, - cts.Token); - } + for (int i = 0; i < tasks.Length; i++) + { + tasks[i] = SendAsync( + cws, + WebSocketData.GetBufferFromText("hello"), + WebSocketMessageType.Text, + true, + cts.Token); + } - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); - Assert.Equal(WebSocketState.Open, cws.State); + Assert.Equal(WebSocketState.Open, cws.State); + } + catch (AggregateException ag) + { + foreach (var ex in ag.InnerExceptions) + { + if (ex is InvalidOperationException) + { + Assert.Equal( + ResourceHelper.GetExceptionMessage( + "net_Websockets_AlreadyOneOutstandingOperation", + "SendAsync"), + ex.Message); + + Assert.Equal(WebSocketState.Aborted, cws.State); + } + else if (ex is WebSocketException) + { + // Multiple cases. + Assert.Equal(WebSocketState.Aborted, cws.State); + + WebSocketError errCode = (ex as WebSocketException).WebSocketErrorCode; + Assert.True( + (errCode == WebSocketError.InvalidState) || (errCode == WebSocketError.Success), + "WebSocketErrorCode"); + } + else + { + Assert.IsAssignableFrom(ex); + } + } + } } - catch (AggregateException ag) + } + } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] + // This will also pass when no exception is thrown. Current implementation doesn't throw. + public async Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) + { + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) + { + using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) { - foreach (var ex in ag.InnerExceptions) + var cts = new CancellationTokenSource(TimeOutMilliseconds); + + Task[] tasks = new Task[2]; + + await SendAsync( + cws, + WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketMessageType.Text, + true, + cts.Token); + + var recvBuffer = new byte[100]; + var recvSegment = new ArraySegment(recvBuffer); + + try + { + for (int i = 0; i < tasks.Length; i++) + { + tasks[i] = ReceiveAsync(cws, recvSegment, cts.Token); + } + + await Task.WhenAll(tasks); + Assert.Equal(WebSocketState.Open, cws.State); + } + catch (Exception ex) { if (ex is InvalidOperationException) { Assert.Equal( ResourceHelper.GetExceptionMessage( "net_Websockets_AlreadyOneOutstandingOperation", - "SendAsync"), + "ReceiveAsync"), ex.Message); - Assert.Equal(WebSocketState.Aborted, cws.State); + Assert.True(WebSocketState.Aborted == cws.State, cws.State + " state when InvalidOperationException"); } else if (ex is WebSocketException) { // Multiple cases. - Assert.Equal(WebSocketState.Aborted, cws.State); + Assert.True(WebSocketState.Aborted == cws.State, cws.State + " state when WebSocketException"); WebSocketError errCode = (ex as WebSocketException).WebSocketErrorCode; Assert.True( (errCode == WebSocketError.InvalidState) || (errCode == WebSocketError.Success), "WebSocketErrorCode"); } + else if (ex is OperationCanceledException) + { + Assert.True(WebSocketState.Aborted == cws.State, cws.State + " state when OperationCanceledException"); + } else { - Assert.IsAssignableFrom(ex); + Assert.True(false, "Unexpected exception: " + ex.Message); } } } @@ -248,175 +550,119 @@ public async Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - // This will also pass when no exception is thrown. Current implementation doesn't throw. - public async Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) + public async Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) { - var cts = new CancellationTokenSource(TimeOutMilliseconds); - - Task[] tasks = new Task[2]; - - await SendAsync( - cws, - WebSocketData.GetBufferFromText(".delay5sec"), - WebSocketMessageType.Text, - true, - cts.Token); - - var recvBuffer = new byte[100]; - var recvSegment = new ArraySegment(recvBuffer); - - try + using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) { - for (int i = 0; i < tasks.Length; i++) - { - tasks[i] = ReceiveAsync(cws, recvSegment, cts.Token); - } - - await Task.WhenAll(tasks); + var cts = new CancellationTokenSource(TimeOutMilliseconds); + string message = "hello"; + await SendAsync( + cws, + WebSocketData.GetBufferFromText(message), + WebSocketMessageType.Text, + false, + cts.Token); + Assert.Equal(WebSocketState.Open, cws.State); + await SendAsync( + cws, + new ArraySegment(new byte[0]), + WebSocketMessageType.Text, + true, + cts.Token); Assert.Equal(WebSocketState.Open, cws.State); - } - catch (Exception ex) - { - if (ex is InvalidOperationException) - { - Assert.Equal( - ResourceHelper.GetExceptionMessage( - "net_Websockets_AlreadyOneOutstandingOperation", - "ReceiveAsync"), - ex.Message); - Assert.True(WebSocketState.Aborted == cws.State, cws.State+" state when InvalidOperationException"); - } - else if (ex is WebSocketException) - { - // Multiple cases. - Assert.True(WebSocketState.Aborted == cws.State, cws.State + " state when WebSocketException"); + var recvBuffer = new byte[100]; + var receiveSegment = new ArraySegment(recvBuffer); + WebSocketReceiveResult recvRet = await ReceiveAsync(cws, receiveSegment, cts.Token); - WebSocketError errCode = (ex as WebSocketException).WebSocketErrorCode; - Assert.True( - (errCode == WebSocketError.InvalidState) || (errCode == WebSocketError.Success), - "WebSocketErrorCode"); - } - else if (ex is OperationCanceledException) - { - Assert.True(WebSocketState.Aborted == cws.State, cws.State + " state when OperationCanceledException"); - } - else - { - Assert.True(false, "Unexpected exception: " + ex.Message); - } + Assert.Equal(WebSocketState.Open, cws.State); + Assert.Equal(message.Length, recvRet.Count); + Assert.Equal(WebSocketMessageType.Text, recvRet.MessageType); + Assert.True(recvRet.EndOfMessage); + Assert.Null(recvRet.CloseStatus); + Assert.Null(recvRet.CloseStatusDescription); + + var recvSegment = new ArraySegment(receiveSegment.Array, receiveSegment.Offset, recvRet.Count); + Assert.Equal(message, WebSocketData.GetTextFromBuffer(recvSegment)); } } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server) - { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) - { - var cts = new CancellationTokenSource(TimeOutMilliseconds); - string message = "hello"; - await SendAsync( - cws, - WebSocketData.GetBufferFromText(message), - WebSocketMessageType.Text, - false, - cts.Token); - Assert.Equal(WebSocketState.Open, cws.State); - await SendAsync( - cws, - new ArraySegment(new byte[0]), - WebSocketMessageType.Text, - true, - cts.Token); - Assert.Equal(WebSocketState.Open, cws.State); - - var recvBuffer = new byte[100]; - var receiveSegment = new ArraySegment(recvBuffer); - WebSocketReceiveResult recvRet = await ReceiveAsync(cws, receiveSegment, cts.Token); - - Assert.Equal(WebSocketState.Open, cws.State); - Assert.Equal(message.Length, recvRet.Count); - Assert.Equal(WebSocketMessageType.Text, recvRet.MessageType); - Assert.True(recvRet.EndOfMessage); - Assert.Null(recvRet.CloseStatus); - Assert.Null(recvRet.CloseStatusDescription); - - var recvSegment = new ArraySegment(receiveSegment.Array, receiveSegment.Offset, recvRet.Count); - Assert.Equal(message, WebSocketData.GetTextFromBuffer(recvSegment)); - } - } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] public async Task SendReceive_VaryingLengthBuffers_Success(Uri server) { - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) { - var rand = new Random(); - var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); - - // Values chosen close to boundaries in websockets message length handling as well - // as in vectors used in mask application. - foreach (int bufferSize in new int[] { 1, 3, 4, 5, 31, 32, 33, 125, 126, 127, 128, ushort.MaxValue - 1, ushort.MaxValue, ushort.MaxValue + 1, ushort.MaxValue * 2 }) + using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output, UseVersion)) { - byte[] sendBuffer = new byte[bufferSize]; - rand.NextBytes(sendBuffer); - await SendAsync(cws, new ArraySegment(sendBuffer), WebSocketMessageType.Binary, true, ctsDefault.Token); + var rand = new Random(); + var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); - byte[] receiveBuffer = new byte[bufferSize]; - int totalReceived = 0; - while (true) + // Values chosen close to boundaries in websockets message length handling as well + // as in vectors used in mask application. + foreach (int bufferSize in new int[] { 1, 3, 4, 5, 31, 32, 33, 125, 126, 127, 128, ushort.MaxValue - 1, ushort.MaxValue, ushort.MaxValue + 1, ushort.MaxValue * 2 }) { - WebSocketReceiveResult recvResult = await ReceiveAsync( - cws, - new ArraySegment(receiveBuffer, totalReceived, receiveBuffer.Length - totalReceived), - ctsDefault.Token); + byte[] sendBuffer = new byte[bufferSize]; + rand.NextBytes(sendBuffer); + await SendAsync(cws, new ArraySegment(sendBuffer), WebSocketMessageType.Binary, true, ctsDefault.Token); + + byte[] receiveBuffer = new byte[bufferSize]; + int totalReceived = 0; + while (true) + { + WebSocketReceiveResult recvResult = await ReceiveAsync( + cws, + new ArraySegment(receiveBuffer, totalReceived, receiveBuffer.Length - totalReceived), + ctsDefault.Token); - Assert.InRange(recvResult.Count, 0, receiveBuffer.Length - totalReceived); - totalReceived += recvResult.Count; + Assert.InRange(recvResult.Count, 0, receiveBuffer.Length - totalReceived); + totalReceived += recvResult.Count; - if (recvResult.EndOfMessage) break; + if (recvResult.EndOfMessage) break; + } + + Assert.Equal(receiveBuffer.Length, totalReceived); + Assert.Equal(sendBuffer, receiveBuffer); } - Assert.Equal(receiveBuffer.Length, totalReceived); - Assert.Equal(sendBuffer, receiveBuffer); + await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "SendReceive_VaryingLengthBuffers_Success", ctsDefault.Token); } - - await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "SendReceive_VaryingLengthBuffers_Success", ctsDefault.Token); } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task SendReceive_Concurrent_Success(Uri server) + [ConditionalFact(nameof(WebSocketsSupported))] + public async Task SendReceive_Concurrent_Success() { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + await CreateEchoServerAsync(async uri => { - CancellationTokenSource ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); - - byte[] receiveBuffer = new byte[10]; - byte[] sendBuffer = new byte[10]; - for (int i = 0; i < sendBuffer.Length; i++) + using ClientWebSocket cws = await GetConnectedWebSocket(uri, TimeOutMilliseconds, _output); { - sendBuffer[i] = (byte)i; - } + CancellationTokenSource ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); - for (int i = 0; i < sendBuffer.Length; i++) - { - Task receive = ReceiveAsync(cws, new ArraySegment(receiveBuffer, receiveBuffer.Length - i - 1, 1), ctsDefault.Token); - Task send = SendAsync(cws, new ArraySegment(sendBuffer, i, 1), WebSocketMessageType.Binary, true, ctsDefault.Token); - await Task.WhenAll(receive, send); - Assert.Equal(1, receive.Result.Count); - } - await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "SendReceive_Concurrent_Success", ctsDefault.Token); + byte[] receiveBuffer = new byte[10]; + byte[] sendBuffer = new byte[10]; + for (int i = 0; i < sendBuffer.Length; i++) + { + sendBuffer[i] = (byte)i; + } - Array.Reverse(receiveBuffer); - Assert.Equal(sendBuffer, receiveBuffer); - } + for (int i = 0; i < sendBuffer.Length; i++) + { + Task receive = ReceiveAsync(cws, new ArraySegment(receiveBuffer, i, 1), ctsDefault.Token); + Task send = SendAsync(cws, new ArraySegment(sendBuffer, i, 1), WebSocketMessageType.Binary, true, ctsDefault.Token); + await Task.WhenAll(receive, send); + Assert.Equal(1, receive.Result.Count); + } + await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "SendReceive_Concurrent_Success", ctsDefault.Token); + + Assert.Equal(sendBuffer, receiveBuffer); + } + }); } [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] @@ -424,105 +670,113 @@ public async Task SendReceive_Concurrent_Success(Uri server) [ActiveIssue("https://github.com/dotnet/runtime/issues/54153", TestPlatforms.Browser)] public async Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() { - var options = new LoopbackServer.Options { WebSocketEndpoint = true }; - - Func connectToServerThatAbortsConnection = async (clientSocket, server, url) => + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) { - var pendingReceiveAsyncPosted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var options = new LoopbackServer.Options { WebSocketEndpoint = true }; - // Start listening for incoming connections on the server side. - Task acceptTask = server.AcceptConnectionAsync(async connection => + Func connectToServerThatAbortsConnection = async (clientSocket, server, url) => { - // Complete the WebSocket upgrade. After this is done, the client-side ConnectAsync should complete. - Assert.NotNull(await LoopbackHelper.WebSocketHandshakeAsync(connection)); + var pendingReceiveAsyncPosted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // Wait for client-side ConnectAsync to complete and for a pending ReceiveAsync to be posted. - await pendingReceiveAsyncPosted.Task.WaitAsync(TimeSpan.FromMilliseconds(TimeOutMilliseconds)); + // Start listening for incoming connections on the server side. + Task acceptTask = server.AcceptConnectionAsync(async connection => + { + // Complete the WebSocket upgrade. After this is done, the client-side ConnectAsync should complete. + Assert.NotNull(await LoopbackHelper.WebSocketHandshakeAsync(connection)); - // Close the underlying connection prematurely (without sending a WebSocket Close frame). - connection.Socket.Shutdown(SocketShutdown.Both); - connection.Socket.Close(); - }); + // Wait for client-side ConnectAsync to complete and for a pending ReceiveAsync to be posted. + await pendingReceiveAsyncPosted.Task.WaitAsync(TimeSpan.FromMilliseconds(TimeOutMilliseconds)); - // Initiate a connection attempt. - var cts = new CancellationTokenSource(TimeOutMilliseconds); - await ConnectAsync(clientSocket, url, cts.Token); + // Close the underlying connection prematurely (without sending a WebSocket Close frame). + connection.Socket.Shutdown(SocketShutdown.Both); + connection.Socket.Close(); + }); - // Post a pending ReceiveAsync before the TCP connection is torn down. - var recvBuffer = new byte[100]; - var recvSegment = new ArraySegment(recvBuffer); - Task pendingReceiveAsync = ReceiveAsync(clientSocket, recvSegment, cts.Token); - pendingReceiveAsyncPosted.SetResult(); + // Initiate a connection attempt. + var cts = new CancellationTokenSource(TimeOutMilliseconds); + await ConnectAsync(clientSocket, url, cts.Token); - // Wait for the server to close the underlying connection. - await acceptTask.WaitAsync(cts.Token); + // Post a pending ReceiveAsync before the TCP connection is torn down. + var recvBuffer = new byte[100]; + var recvSegment = new ArraySegment(recvBuffer); + Task pendingReceiveAsync = ReceiveAsync(clientSocket, recvSegment, cts.Token); + pendingReceiveAsyncPosted.SetResult(); - WebSocketException pendingReceiveException = await Assert.ThrowsAsync(() => pendingReceiveAsync); + // Wait for the server to close the underlying connection. + await acceptTask.WaitAsync(cts.Token); - Assert.Equal(WebSocketError.ConnectionClosedPrematurely, pendingReceiveException.WebSocketErrorCode); + WebSocketException pendingReceiveException = await Assert.ThrowsAsync(() => pendingReceiveAsync); - if (PlatformDetection.IsInAppContainer) - { - const uint WININET_E_CONNECTION_ABORTED = 0x80072EFE; + Assert.Equal(WebSocketError.ConnectionClosedPrematurely, pendingReceiveException.WebSocketErrorCode); - Assert.NotNull(pendingReceiveException.InnerException); - Assert.Equal(WININET_E_CONNECTION_ABORTED, (uint)pendingReceiveException.InnerException.HResult); - } + if (PlatformDetection.IsInAppContainer) + { + const uint WININET_E_CONNECTION_ABORTED = 0x80072EFE; - WebSocketException newReceiveException = - await Assert.ThrowsAsync(() => ReceiveAsync(clientSocket, recvSegment, cts.Token)); + Assert.NotNull(pendingReceiveException.InnerException); + Assert.Equal(WININET_E_CONNECTION_ABORTED, (uint)pendingReceiveException.InnerException.HResult); + } - Assert.Equal( - ResourceHelper.GetExceptionMessage("net_WebSockets_InvalidState", "Aborted", "Open, CloseSent"), - newReceiveException.Message); + WebSocketException newReceiveException = + await Assert.ThrowsAsync(() => ReceiveAsync(clientSocket, recvSegment, cts.Token)); - Assert.Equal(WebSocketState.Aborted, clientSocket.State); - Assert.Null(clientSocket.CloseStatus); - }; + Assert.Equal( + ResourceHelper.GetExceptionMessage("net_WebSockets_InvalidState", "Aborted", "Open, CloseSent"), + newReceiveException.Message); - await LoopbackServer.CreateServerAsync(async (server, url) => - { - using (ClientWebSocket clientSocket = new ClientWebSocket()) + Assert.Equal(WebSocketState.Aborted, clientSocket.State); + Assert.Null(clientSocket.CloseStatus); + }; + + await LoopbackServer.CreateServerAsync(async (server, url) => { - await connectToServerThatAbortsConnection(clientSocket, server, url); - } - }, options); + using (ClientWebSocket clientSocket = new ClientWebSocket()) + { + await connectToServerThatAbortsConnection(clientSocket, server, url); + } + }, options); + } } [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] public async Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + // TODO remove condition when this test is updated to use inherited test classes + if (this is NoInvokerMemorySendReceiveRemoteTest || this is NoInvokerArraySegmentSendReceiveRemoteTest) { - var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); - - // Do a 0-byte receive. It shouldn't complete yet. - Task t = ReceiveAsync(cws, new ArraySegment(Array.Empty()), ctsDefault.Token); - Assert.False(t.IsCompleted); - - // Send a packet to the echo server. - await SendAsync(cws, new ArraySegment(new byte[1] { 42 }), WebSocketMessageType.Binary, true, ctsDefault.Token); - - // Now the 0-byte receive should complete, but without reading any data. - WebSocketReceiveResult r = await t; - Assert.Equal(WebSocketMessageType.Binary, r.MessageType); - Assert.Equal(0, r.Count); - Assert.False(r.EndOfMessage); - - // Now do a receive to get the payload. - var receiveBuffer = new byte[1]; - t = ReceiveAsync(cws, new ArraySegment(receiveBuffer), ctsDefault.Token); - Assert.Equal(TaskStatus.RanToCompletion, t.Status); - - r = await t; - Assert.Equal(WebSocketMessageType.Binary, r.MessageType); - Assert.Equal(1, r.Count); - Assert.True(r.EndOfMessage); - Assert.Equal(42, receiveBuffer[0]); - - // Clean up. - await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, nameof(ZeroByteReceive_CompletesWhenDataAvailable), ctsDefault.Token); + using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + { + var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); + + // Do a 0-byte receive. It shouldn't complete yet. + Task t = ReceiveAsync(cws, new ArraySegment(Array.Empty()), ctsDefault.Token); + Assert.False(t.IsCompleted); + + // Send a packet to the echo server. + await SendAsync(cws, new ArraySegment(new byte[1] { 42 }), WebSocketMessageType.Binary, true, ctsDefault.Token); + + // Now the 0-byte receive should complete, but without reading any data. + WebSocketReceiveResult r = await t; + Assert.Equal(WebSocketMessageType.Binary, r.MessageType); + Assert.Equal(0, r.Count); + Assert.False(r.EndOfMessage); + + // Now do a receive to get the payload. + var receiveBuffer = new byte[1]; + t = ReceiveAsync(cws, new ArraySegment(receiveBuffer), ctsDefault.Token); + Assert.Equal(TaskStatus.RanToCompletion, t.Status); + + r = await t; + Assert.Equal(WebSocketMessageType.Binary, r.MessageType); + Assert.Equal(1, r.Count); + Assert.True(r.EndOfMessage); + Assert.Equal(42, receiveBuffer[0]); + + // Clean up. + await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, nameof(ZeroByteReceive_CompletesWhenDataAvailable), ctsDefault.Token); + } } } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj b/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj index f0ba87aa3c6c51..4bbcce4864ddec 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj +++ b/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj @@ -71,4 +71,8 @@ + + + + diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index d409007f9995d0..a8f95a292a3011 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -1,7 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net.Http; +using System.Net.Sockets; using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -11,9 +15,12 @@ namespace System.Net.WebSockets.Client.Tests { + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public static class WebSocketHelper { private static readonly Lazy s_WebSocketSupported = new Lazy(InitWebSocketSupported); + + public const int TimeOutMilliseconds = 30000; public static bool WebSocketsSupported { get { return s_WebSocketSupported.Value; } } public static async Task TestEcho( @@ -21,6 +28,7 @@ public static async Task TestEcho( WebSocketMessageType type, int timeOutMilliseconds, ITestOutputHelper output, + Version version, HttpMessageInvoker? invoker = null) { var cts = new CancellationTokenSource(timeOutMilliseconds); @@ -29,7 +37,7 @@ public static async Task TestEcho( var receiveBuffer = new byte[100]; var receiveSegment = new ArraySegment(receiveBuffer); - using (ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, invoker: invoker)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, version, invoker: invoker)) { output.WriteLine("TestEcho: SendAsync starting."); await cws.SendAsync(WebSocketData.GetBufferFromText(message), type, true, cts.Token); @@ -67,6 +75,7 @@ public static Task GetConnectedWebSocket( Uri server, int timeOutMilliseconds, ITestOutputHelper output, + Version version, TimeSpan keepAliveInterval = default, IWebProxy proxy = null, HttpMessageInvoker? invoker = null) => @@ -83,6 +92,17 @@ public static Task GetConnectedWebSocket( cws.Options.KeepAliveInterval = keepAliveInterval; } + if (!PlatformDetection.IsBrowser) + { + if (invoker == null) + { + cws.Options.ClientCertificates.Add(Test.Common.Configuration.Certificates.GetClientCertificate()); + cws.Options.RemoteCertificateValidationCallback = delegate { return true; }; + } + cws.Options.HttpVersion = version; + cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; + } + using (var cts = new CancellationTokenSource(timeOutMilliseconds)) { output.WriteLine("GetConnectedWebSocket: ConnectAsync starting."); @@ -125,6 +145,69 @@ public static async Task Retry(ITestOutputHelper output, Func> fun } } + public static Task GetEchoHttp2LoopbackServer(Func clientFunc) + { + return GetEchoHttp2LoopbackServer(clientFunc, new Http2Options()); + } + + public static async Task GetEchoHttp2LoopbackServer(Func clientFunc, Http2Options options) + { + using Http2LoopbackServer server = Http2LoopbackServer.CreateServer(options); + + Task serverTask = Task.Run(async () => + { + Http2LoopbackConnection connection = await server.EstablishConnectionAsync(new SettingsEntry { SettingId = SettingId.EnableConnect, Value = 1 }); + (int streamId, HttpRequestData requestData) = await connection.ReadAndParseRequestHeaderAsync(readBody: false); + // send status 200 OK to establish websocket + await connection.SendResponseHeadersAsync(streamId, endStream: false).ConfigureAwait(false); + + var webSocketStream = new Http2Stream(connection, streamId); + await WebSocketSendReceive(webSocketStream); + + await connection.DisposeAsync(); + }); + + await new Task[] { serverTask, clientFunc(server.Address) }.WhenAllOrAnyFailed(TimeOutMilliseconds * 2); + } + + public static async Task GetEchoLoopbackServer(Func clientFunc, LoopbackServer.Options options = null) + { + using LoopbackServer server = new LoopbackServer(options); + await server.ListenAsync(); + + Task serverTask = server.AcceptConnectionAsync(async connection => + { + Dictionary headers = await LoopbackHelper.WebSocketHandshakeAsync(connection); + await WebSocketSendReceive(connection.Stream); + + await connection.DisposeAsync(); + }); + + await new Task[] { serverTask, clientFunc(server.Address) }.WhenAllOrAnyFailed(TimeOutMilliseconds * 2); + } + + private static async Task WebSocketSendReceive(Stream stream) + { + var buffer = new byte[128 * 1024]; + using WebSocket websocket = WebSocket.CreateFromStream(stream, true, null, TimeSpan.FromMilliseconds(TimeOutMilliseconds)); + while (websocket.State == WebSocketState.Open || websocket.State == WebSocketState.CloseSent) + { + var result = await websocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + if (result.MessageType == WebSocketMessageType.Close) + { + await websocket.CloseAsync( + result.CloseStatus ?? WebSocketCloseStatus.Empty, + result.CloseStatusDescription, + CancellationToken.None); + + continue; + } + + int offset = result.Count; + await websocket.SendAsync(new ArraySegment(buffer, 0, offset), result.MessageType, result.EndOfMessage, CancellationToken.None); + } + } + private static bool InitWebSocketSupported() { ClientWebSocket cws = null; diff --git a/src/libraries/System.Net.WebSockets.Client/tests/wasm/BrowserTimerThrottlingTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/wasm/BrowserTimerThrottlingTest.cs index bd44afd52fab56..e9b82824e65e3b 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/wasm/BrowserTimerThrottlingTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/wasm/BrowserTimerThrottlingTest.cs @@ -90,7 +90,7 @@ public async Task WebSocketKeepsDotnetTimersOnlyLightlyThrottled() DateTime start = DateTime.Now; CancellationTokenSource cts = new CancellationTokenSource(); - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(Test.Common.Configuration.WebSockets.RemoteEchoServer, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(Test.Common.Configuration.WebSockets.RemoteEchoServer, TimeOutMilliseconds, _output, UseVersion)) { await SendAndReceive(cws, "test"); using (var timer = new Timers.Timer(fastTimeoutFrequency)) diff --git a/src/libraries/System.Net.WebSockets.Client/tests/wasm/System.Net.WebSockets.Client.Wasm.Tests.csproj b/src/libraries/System.Net.WebSockets.Client/tests/wasm/System.Net.WebSockets.Client.Wasm.Tests.csproj index 0a27428b1d7fb5..c3fd60025e9e9f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/wasm/System.Net.WebSockets.Client.Wasm.Tests.csproj +++ b/src/libraries/System.Net.WebSockets.Client/tests/wasm/System.Net.WebSockets.Client.Wasm.Tests.csproj @@ -20,30 +20,26 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -55,4 +51,4 @@ - + \ No newline at end of file