diff --git a/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs b/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs index c5686be67b4ef9..fcd4f9faf743db 100644 --- a/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs +++ b/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs @@ -22,35 +22,13 @@ public static partial class WebSockets public static readonly Uri RemoteEchoHeadersServer = new Uri("ws://" + Host + "/" + EchoHeadersHandler); public static readonly Uri SecureRemoteEchoHeadersServer = new Uri("wss://" + SecureHost + "/" + EchoHeadersHandler); - public static object[][] GetEchoServers() - { - if (PlatformDetection.IsFirefox) - { - // https://github.com/dotnet/runtime/issues/101115 - return new object[][] { - new object[] { RemoteEchoServer }, - }; - } - return new object[][] { - new object[] { RemoteEchoServer }, - new object[] { SecureRemoteEchoServer }, - }; - } - - public static object[][] GetEchoHeadersServers() - { - if (PlatformDetection.IsFirefox) - { - // https://github.com/dotnet/runtime/issues/101115 - return new object[][] { - new object[] { RemoteEchoHeadersServer }, - }; - } - return new object[][] { - new object[] { RemoteEchoHeadersServer }, - new object[] { SecureRemoteEchoHeadersServer }, - }; - } + public static Uri[] GetEchoServers() => PlatformDetection.IsFirefox + ? [ RemoteEchoServer ] // https://github.com/dotnet/runtime/issues/101115 + : [ RemoteEchoServer, SecureRemoteEchoServer ]; + + public static Uri[] GetEchoHeadersServers() => PlatformDetection.IsFirefox + ? [ RemoteEchoHeadersServer ] // https://github.com/dotnet/runtime/issues/101115 + : [ RemoteEchoHeadersServer, SecureRemoteEchoHeadersServer ]; } } } diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index 6f19c5f21faf3b..58bc1a8ccc5141 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs @@ -28,6 +28,8 @@ public class Http2LoopbackConnection : GenericLoopbackConnection private readonly TimeSpan _timeout; private int _lastStreamId; private bool _expectClientDisconnect; + private readonly SemaphoreSlim? _readLock; + private readonly SemaphoreSlim? _writeLock; private readonly byte[] _prefix = new byte[24]; public string PrefixString => Encoding.UTF8.GetString(_prefix, 0, _prefix.Length); @@ -35,12 +37,59 @@ public class Http2LoopbackConnection : GenericLoopbackConnection public Stream Stream => _connectionStream; public Task SettingAckWaiter => _ignoredSettingsAckPromise?.Task; - private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan timeout, bool transparentPingResponse) + private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan timeout, Http2Options httpOptions) { _connectionSocket = socket; _connectionStream = stream; _timeout = timeout; - _transparentPingResponse = transparentPingResponse; + _transparentPingResponse = httpOptions.EnableTransparentPingResponse; + + if (httpOptions.EnsureThreadSafeIO) + { + _readLock = new SemaphoreSlim(1, 1); + _writeLock = new SemaphoreSlim(1, 1); + _connectionStream = CreateConcurrentConnectionStream(stream, _readLock, _writeLock); + } + + static Stream CreateConcurrentConnectionStream(Stream stream, SemaphoreSlim readLock, SemaphoreSlim writeLock) + { + return new DelegateStream( + canReadFunc: () => true, + canWriteFunc: () => true, + readAsyncFunc: async (buffer, offset, count, cancellationToken) => + { + await readLock.WaitAsync(cancellationToken); + try + { + return await stream.ReadAsync(buffer, offset, count, cancellationToken); + } + finally + { + readLock.Release(); + } + }, + writeAsyncFunc: async (buffer, offset, count, cancellationToken) => + { + await writeLock.WaitAsync(cancellationToken); + try + { + await stream.WriteAsync(buffer, offset, count, cancellationToken); + await stream.FlushAsync(cancellationToken); + } + finally + { + writeLock.Release(); + } + }, + disposeFunc: (disposing) => + { + if (disposing) + { + stream.Dispose(); + } + } + ); + } } public override string ToString() @@ -83,7 +132,7 @@ public static async Task CreateAsync(SocketWrapper sock stream = sslStream; } - var con = new Http2LoopbackConnection(socket, stream, timeout, httpOptions.EnableTransparentPingResponse); + var con = new Http2LoopbackConnection(socket, stream, timeout, httpOptions); await con.ReadPrefixAsync().ConfigureAwait(false); return con; diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs index 90929b70eec379..3e4c7584ad03ce 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs @@ -185,6 +185,7 @@ public class Http2Options : GenericLoopbackOptions public bool ClientCertificateRequired { get; set; } public bool EnableTransparentPingResponse { get; set; } = true; + public bool EnsureThreadSafeIO { get; set; } public Http2Options() { @@ -216,7 +217,12 @@ public override async Task CreateConnectionAsync(Sock private static Http2Options CreateOptions(GenericLoopbackOptions options) { - Http2Options http2Options = new Http2Options(); + if (options is Http2Options http2Options) + { + return http2Options; + } + + http2Options = new Http2Options(); if (options != null) { http2Options.Address = options.Address; diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index 8b9da85bdd13c5..0ac084afc4e3e2 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -1145,6 +1145,11 @@ public override async Task CreateConnectionAsync(Sock private static LoopbackServer.Options CreateOptions(GenericLoopbackOptions options) { + if (options is LoopbackServer.Options { } loopbackOptions) + { + return loopbackOptions; + } + LoopbackServer.Options newOptions = new LoopbackServer.Options(); if (options != null) { diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Directory.Build.targets b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Directory.Build.targets index 242d10ebfa0fac..a354006e837eae 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Directory.Build.targets +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Directory.Build.targets @@ -1,6 +1,7 @@ $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)../, global.json))/ + $([MSBuild]::NormalizeDirectory('$(RepositoryRoot)', 'src', 'libraries', 'Common', 'tests')) diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs index a290ce63bd4ffb..6cfaa4ff7109be 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs @@ -3,8 +3,7 @@ using System; using System.Net.WebSockets; -using System.Text; -using System.Threading; +using System.Net.Test.Common; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -12,177 +11,25 @@ namespace NetCoreServer { public class EchoWebSocketHandler { - private const int MaxBufferSize = 128 * 1024; - public static async Task InvokeAsync(HttpContext context) { - QueryString queryString = context.Request.QueryString; - bool replyWithPartialMessages = queryString.HasValue && queryString.Value.Contains("replyWithPartialMessages"); - bool replyWithEnhancedCloseMessage = queryString.HasValue && queryString.Value.Contains("replyWithEnhancedCloseMessage"); - - string subProtocol = context.Request.Query["subprotocol"]; - - if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay10sec")) - { - await Task.Delay(10000); - } - else if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay20sec")) - { - await Task.Delay(20000); - } - + var queryString = context.Request.QueryString.ToUriComponent(); // Returns empty string if request URI has no query + WebSocketEchoOptions options = await WebSocketEchoHelper.ProcessOptions(queryString); try { - if (!context.WebSockets.IsWebSocketRequest) + WebSocket socket = await WebSocketAcceptHelper.AcceptAsync(context, options.SubProtocol); + if (socket is null) { - context.Response.StatusCode = 200; - context.Response.ContentType = "text/plain"; - await context.Response.WriteAsync("Not a websocket request"); - return; } - WebSocket socket; - if (!string.IsNullOrEmpty(subProtocol)) - { - socket = await context.WebSockets.AcceptWebSocketAsync(subProtocol); - } - else - { - socket = await context.WebSockets.AcceptWebSocketAsync(); - } - - await ProcessWebSocketRequest(socket, replyWithPartialMessages, replyWithEnhancedCloseMessage); + await WebSocketEchoHelper.RunEchoAll( + socket, options.ReplyWithPartialMessages, options.ReplyWithEnhancedCloseMessage); } catch (Exception) { // We might want to log these exceptions. But for now we ignore them. } } - - private static async Task ProcessWebSocketRequest( - WebSocket socket, - bool replyWithPartialMessages, - bool replyWithEnhancedCloseMessage) - { - var receiveBuffer = new byte[MaxBufferSize]; - var throwAwayBuffer = new byte[MaxBufferSize]; - - // Stay in loop while websocket is open - while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) - { - var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); - if (receiveResult.MessageType == WebSocketMessageType.Close) - { - if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) - { - await socket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); - } - else - { - WebSocketCloseStatus closeStatus = receiveResult.CloseStatus.GetValueOrDefault(); - await socket.CloseAsync( - closeStatus, - replyWithEnhancedCloseMessage ? - ("Server received: " + (int)closeStatus + " " + receiveResult.CloseStatusDescription) : - receiveResult.CloseStatusDescription, - CancellationToken.None); - } - - continue; - } - - // Keep reading until we get an entire message. - int offset = receiveResult.Count; - while (receiveResult.EndOfMessage == false) - { - if (offset < MaxBufferSize) - { - receiveResult = await socket.ReceiveAsync( - new ArraySegment(receiveBuffer, offset, MaxBufferSize - offset), - CancellationToken.None); - } - else - { - receiveResult = await socket.ReceiveAsync( - new ArraySegment(throwAwayBuffer), - CancellationToken.None); - } - - offset += receiveResult.Count; - } - - // Close socket if the message was too big. - if (offset > MaxBufferSize) - { - await socket.CloseAsync( - WebSocketCloseStatus.MessageTooBig, - String.Format("{0}: {1} > {2}", WebSocketCloseStatus.MessageTooBig.ToString(), offset, MaxBufferSize), - CancellationToken.None); - - continue; - } - - bool sendMessage = false; - string receivedMessage = null; - if (receiveResult.MessageType == WebSocketMessageType.Text) - { - receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); - if (receivedMessage == ".close") - { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); - } - else if (receivedMessage == ".shutdown") - { - await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); - } - else if (receivedMessage == ".abort") - { - socket.Abort(); - } - else if (receivedMessage == ".delay5sec") - { - await Task.Delay(5000); - } - else if (receivedMessage == ".receiveMessageAfterClose") - { - byte[] buffer = new byte[1024]; - string message = $"{receivedMessage} {DateTime.Now.ToString("HH:mm:ss")}"; - buffer = System.Text.Encoding.UTF8.GetBytes(message); - await socket.SendAsync( - new ArraySegment(buffer, 0, message.Length), - WebSocketMessageType.Text, - true, - CancellationToken.None); - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); - } - else if (socket.State == WebSocketState.Open) - { - sendMessage = true; - } - } - else - { - sendMessage = true; - } - - if (sendMessage) - { - await socket.SendAsync( - new ArraySegment(receiveBuffer, 0, offset), - receiveResult.MessageType, - !replyWithPartialMessages, - CancellationToken.None); - } - if (receivedMessage == ".closeafter") - { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); - } - else if (receivedMessage == ".shutdownafter") - { - await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); - } - } - } } } diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHeadersHandler.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHeadersHandler.cs index c5ebca53b63a94..0df2a3089c1238 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHeadersHandler.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHeadersHandler.cs @@ -3,81 +3,35 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.WebSockets; -using System.Text; -using System.Threading; +using System.Net.Test.Common; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; namespace NetCoreServer { public class EchoWebSocketHeadersHandler { - private const int MaxBufferSize = 1024; - public static async Task InvokeAsync(HttpContext context) { try { - if (!context.WebSockets.IsWebSocketRequest) + WebSocket socket = await WebSocketAcceptHelper.AcceptAsync(context); + if (socket is null) { - context.Response.StatusCode = 200; - context.Response.ContentType = "text/plain"; - await context.Response.WriteAsync("Not a websocket request"); - return; } - WebSocket socket = await context.WebSockets.AcceptWebSocketAsync(); - await ProcessWebSocketRequest(socket, context.Request.Headers); + var headers = context.Request.Headers.Select( + h => new KeyValuePair(h.Key, h.Value.ToString())); + await WebSocketEchoHelper.RunEchoHeaders(socket, headers); } catch (Exception) { // We might want to log these exceptions. But for now we ignore them. } } - - private static async Task ProcessWebSocketRequest(WebSocket socket, IHeaderDictionary headers) - { - var receiveBuffer = new byte[MaxBufferSize]; - - // Reflect all headers and cookies - var sb = new StringBuilder(); - sb.AppendLine("Headers:"); - - foreach (KeyValuePair pair in headers) - { - sb.Append(pair.Key); - sb.Append(":"); - sb.AppendLine(pair.Value.ToString()); - } - - byte[] sendBuffer = Encoding.UTF8.GetBytes(sb.ToString()); - await socket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, true, new CancellationToken()); - - // Stay in loop while websocket is open - while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) - { - var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); - if (receiveResult.MessageType == WebSocketMessageType.Close) - { - if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) - { - await socket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); - } - else - { - await socket.CloseAsync( - receiveResult.CloseStatus.GetValueOrDefault(), - receiveResult.CloseStatusDescription, - CancellationToken.None); - } - - continue; - } - } - } } } diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketAcceptHelper.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketAcceptHelper.cs new file mode 100644 index 00000000000000..4476a2a928ac28 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketAcceptHelper.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.WebSockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace NetCoreServer +{ + public static class WebSocketAcceptHelper + { + public static async Task AcceptAsync(HttpContext context, string subProtocol = null) + { + if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = 200; + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Not a websocket request"); + return null; + } + + if (!string.IsNullOrEmpty(subProtocol)) + { + return await context.WebSockets.AcceptWebSocketAsync(subProtocol); + } + + return await context.WebSockets.AcceptWebSocketAsync(); + } + } +} diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/NetCoreServer.csproj b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/NetCoreServer.csproj index 057ce2b8fda562..04ae3c5c19b116 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/NetCoreServer.csproj +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/NetCoreServer.csproj @@ -2,7 +2,7 @@ $(_TargetFrameworkForXHarness) - @@ -33,6 +33,8 @@ + + @@ -51,6 +53,7 @@ + diff --git a/src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoHelper.cs b/src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoHelper.cs new file mode 100644 index 00000000000000..4b1e44b6b223a6 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoHelper.cs @@ -0,0 +1,201 @@ +// 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.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Test.Common +{ + public static class WebSocketEchoHelper + { + public static class EchoControlMessage + { + public const string Close = ".close"; + public const string Shutdown = ".shutdown"; + public const string Abort = ".abort"; + public const string Delay5Sec = ".delay5sec"; + public const string ReceiveMessageAfterClose = ".receiveMessageAfterClose"; + public const string CloseAfter = ".closeafter"; + public const string ShutdownAfter = ".shutdownafter"; + } + + public static async Task RunEchoAll(WebSocket socket, bool replyWithPartialMessages, bool replyWithEnhancedCloseMessage, CancellationToken cancellationToken = default) + { + const int MaxBufferSize = 128 * 1024; + + var receiveBuffer = new byte[MaxBufferSize]; + var throwAwayBuffer = new byte[MaxBufferSize]; + + // Stay in loop while websocket is open + while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) + { + var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), cancellationToken); + + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) + { + await socket.CloseAsync(WebSocketCloseStatus.Empty, null, cancellationToken); + } + else + { + WebSocketCloseStatus closeStatus = receiveResult.CloseStatus.GetValueOrDefault(); + await socket.CloseAsync( + closeStatus, + replyWithEnhancedCloseMessage ? + ("Server received: " + (int)closeStatus + " " + receiveResult.CloseStatusDescription) : + receiveResult.CloseStatusDescription, + cancellationToken); + } + + continue; + } + + // Keep reading until we get an entire message. + int offset = receiveResult.Count; + while (receiveResult.EndOfMessage == false) + { + if (offset < MaxBufferSize) + { + receiveResult = await socket.ReceiveAsync( + new ArraySegment(receiveBuffer, offset, MaxBufferSize - offset), + cancellationToken); + } + else + { + receiveResult = await socket.ReceiveAsync( + new ArraySegment(throwAwayBuffer), + cancellationToken); + } + + offset += receiveResult.Count; + } + + // Close socket if the message was too big. + if (offset > MaxBufferSize) + { + await socket.CloseAsync( + WebSocketCloseStatus.MessageTooBig, + string.Format("{0}: {1} > {2}", WebSocketCloseStatus.MessageTooBig.ToString(), offset, MaxBufferSize), + cancellationToken); + + continue; + } + + bool sendMessage = false; + string receivedMessage = null; + if (receiveResult.MessageType == WebSocketMessageType.Text) + { + receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); + if (receivedMessage == EchoControlMessage.Close) + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + } + else if (receivedMessage == EchoControlMessage.Shutdown) + { + await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + } + else if (receivedMessage == EchoControlMessage.Abort) + { + socket.Abort(); + } + else if (receivedMessage == EchoControlMessage.Delay5Sec) + { + await Task.Delay(5000); + } + else if (receivedMessage == EchoControlMessage.ReceiveMessageAfterClose) + { + string message = $"{receivedMessage} {DateTime.Now:HH:mm:ss}"; + byte[] buffer = Encoding.UTF8.GetBytes(message); + await socket.SendAsync( + new ArraySegment(buffer, 0, message.Length), + WebSocketMessageType.Text, + true, + cancellationToken); + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + } + else if (socket.State == WebSocketState.Open) + { + sendMessage = true; + } + } + else + { + sendMessage = true; + } + + if (sendMessage) + { + await socket.SendAsync( + new ArraySegment(receiveBuffer, 0, offset), + receiveResult.MessageType, + !replyWithPartialMessages, + cancellationToken); + } + if (receivedMessage == EchoControlMessage.CloseAfter) + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + } + else if (receivedMessage == EchoControlMessage.ShutdownAfter) + { + await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + } + } + } + + public static async Task RunEchoHeaders(WebSocket socket, IEnumerable> headers, CancellationToken cancellationToken = default) + { + const int MaxBufferSize = 1024; + var receiveBuffer = new byte[MaxBufferSize]; + + // Reflect all headers and cookies + var sb = new StringBuilder(); + sb.AppendLine("Headers:"); + + foreach (KeyValuePair pair in headers) + { + sb.Append(pair.Key); + sb.Append(":"); + sb.AppendLine(pair.Value); + } + + byte[] sendBuffer = Encoding.UTF8.GetBytes(sb.ToString()); + await socket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, true, cancellationToken); + + // Stay in loop while websocket is open + while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) + { + var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), cancellationToken); + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) + { + await socket.CloseAsync(WebSocketCloseStatus.Empty, null, cancellationToken); + } + else + { + await socket.CloseAsync( + receiveResult.CloseStatus.GetValueOrDefault(), + receiveResult.CloseStatusDescription, + cancellationToken); + } + + continue; + } + } + } + + public static async ValueTask ProcessOptions(string queryString, CancellationToken cancellationToken = default) + { + WebSocketEchoOptions options = WebSocketEchoOptions.Parse(queryString); + if (options.Delay is TimeSpan d) + { + await Task.Delay(d, cancellationToken).ConfigureAwait(false); + } + return options; + } + } +} diff --git a/src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoOptions.cs b/src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoOptions.cs new file mode 100644 index 00000000000000..9f8576e50bacea --- /dev/null +++ b/src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoOptions.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Test.Common +{ + public readonly struct WebSocketEchoOptions + { + public static class EchoQueryKey + { + public const string ReplyWithPartialMessages = "replyWithPartialMessages"; + public const string ReplyWithEnhancedCloseMessage = "replyWithEnhancedCloseMessage"; + public const string SubProtocol = "subprotocol"; + public const string Delay10Sec = "delay10sec"; + public const string Delay20Sec = "delay20sec"; + } + + public static readonly WebSocketEchoOptions Default = new(); + + public bool ReplyWithPartialMessages { get; init; } + public bool ReplyWithEnhancedCloseMessage { get; init; } + public string SubProtocol { get; init; } + public TimeSpan? Delay { get; init; } + + public static WebSocketEchoOptions Parse(string query) + { + if (query is null or "" or "?") + { + return Default; + } + + return new WebSocketEchoOptions + { + ReplyWithPartialMessages = query.Contains(EchoQueryKey.ReplyWithPartialMessages), + ReplyWithEnhancedCloseMessage = query.Contains(EchoQueryKey.ReplyWithEnhancedCloseMessage), + SubProtocol = ParseSubProtocol(query), + Delay = ParseDelay(query) + }; + } + + private static string ParseSubProtocol(string query) + { + const string subProtocolEquals = $"{EchoQueryKey.SubProtocol}="; + + var index = query.IndexOf(subProtocolEquals); + if (index == -1) + { + return null; + } + + var subProtocol = query.Substring(index + subProtocolEquals.Length); + return subProtocol.Contains("&") + ? subProtocol.Substring(0, subProtocol.IndexOf("&")) + : subProtocol; + } + + private static TimeSpan? ParseDelay(string query) + => query.Contains(EchoQueryKey.Delay10Sec) + ? TimeSpan.FromSeconds(10) + : query.Contains(EchoQueryKey.Delay20Sec) ? TimeSpan.FromSeconds(20) : null; + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs index 8d0a89b320d618..1c7823434eb74e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs @@ -1,10 +1,6 @@ // 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.Net.Sockets; -using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -14,16 +10,38 @@ namespace System.Net.WebSockets.Client.Tests { [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] - public abstract class AbortTest_Loopback : ClientWebSocketTestBase + public abstract class AbortTest_LoopbackBase(ITestOutputHelper output) : AbortTestBase(output) { - public AbortTest_Loopback(ITestOutputHelper output) : base(output) { } + #region Common (Echo Server) tests - protected virtual Version HttpVersion => Net.HttpVersion.Version11; + [Theory, MemberData(nameof(UseSsl))] + public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(bool useSsl) => RunEchoAsync( + RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage, useSsl); - public static object[][] AbortClient_MemberData = ToMemberData(Enum.GetValues(), UseSsl_Values, /* verifySendReceive */ Bool_Values); + [Theory, MemberData(nameof(UseSsl))] + public Task Abort_SendAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_SendAndAbort_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task Abort_ReceiveAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_ReceiveAndAbort_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task Abort_CloseAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_CloseAndAbort_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ClientWebSocket_Abort_CloseOutputAsync(bool useSsl) => RunEchoAsync( + RunClient_ClientWebSocket_Abort_CloseOutputAsync, useSsl); + + #endregion + + #region Loopback-only tests + + public static object[][] AbortTypeAndUseSslAndBoolean = ToMemberData(Enum.GetValues(), UseSsl_Values, Bool_Values); [Theory] - [MemberData(nameof(AbortClient_MemberData))] + [MemberData(nameof(AbortTypeAndUseSslAndBoolean))] public Task AbortClient_ServerGetsCorrectException(AbortType abortType, bool useSsl, bool verifySendReceive) { var clientMsg = new byte[] { 1, 2, 3, 4, 5, 6 }; @@ -34,11 +52,13 @@ public Task AbortClient_ServerGetsCorrectException(AbortType abortType, bool use var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); return LoopbackWebSocketServer.RunAsync( - async (clientWebSocket, token) => + async uri => { + ClientWebSocket clientWebSocket = await GetConnectedWebSocket(uri); + if (verifySendReceive) { - await VerifySendReceiveAsync(clientWebSocket, clientMsg, serverMsg, clientAckTcs, serverAckTcs.Task, token); + await VerifySendReceiveAsync(clientWebSocket, clientMsg, serverMsg, clientAckTcs, serverAckTcs.Task, timeoutCts.Token); } switch (abortType) @@ -65,14 +85,14 @@ public Task AbortClient_ServerGetsCorrectException(AbortType abortType, bool use Assert.Equal(WebSocketError.ConnectionClosedPrematurely, exception.WebSocketErrorCode); Assert.Equal(WebSocketState.Aborted, serverWebSocket.State); }, - new LoopbackWebSocketServer.Options(HttpVersion, useSsl, GetInvoker()), + new LoopbackWebSocketServer.Options(HttpVersion, useSsl) { DisposeServerWebSocket = true }, timeoutCts.Token); } - public static object[][] ServerPrematureEos_MemberData = ToMemberData(Enum.GetValues(), UseSsl_Values); + public static object[][] ServerEosTypeAndUseSsl = ToMemberData(Enum.GetValues(), UseSsl_Values); [Theory] - [MemberData(nameof(ServerPrematureEos_MemberData))] + [MemberData(nameof(ServerEosTypeAndUseSsl))] public Task ServerPrematureEos_ClientGetsCorrectException(ServerEosType serverEosType, bool useSsl) { var clientMsg = new byte[] { 1, 2, 3, 4, 5, 6 }; @@ -82,12 +102,6 @@ public Task ServerPrematureEos_ClientGetsCorrectException(ServerEosType serverEo var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var globalOptions = new LoopbackWebSocketServer.Options(HttpVersion, useSsl, HttpInvoker: null) - { - DisposeServerWebSocket = false, - ManualServerHandshakeResponse = true - }; - var serverReceivedEosTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var clientReceivedEosTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -95,8 +109,7 @@ public Task ServerPrematureEos_ClientGetsCorrectException(ServerEosType serverEo async uri => { var token = timeoutCts.Token; - var clientOptions = globalOptions with { HttpInvoker = GetInvoker() }; - var clientWebSocket = await LoopbackWebSocketServer.GetConnectedClientAsync(uri, clientOptions, token).ConfigureAwait(false); + ClientWebSocket clientWebSocket = await GetConnectedWebSocket(uri); if (serverEosType == ServerEosType.AfterSomeData) { @@ -121,7 +134,7 @@ await SendServerResponseAndEosAsync( (wsData, ct) => { var wsOptions = new WebSocketCreationOptions { IsServer = true }; - serverWebSocket = WebSocket.CreateFromStream(wsData.WebSocketStream, wsOptions); + serverWebSocket = WebSocket.CreateFromStream(wsData.TransportStream, wsOptions); return serverEosType == ServerEosType.AfterSomeData ? VerifySendReceiveAsync(serverWebSocket, serverMsg, clientMsg, serverAckTcs, clientAckTcs.Task, ct) @@ -146,12 +159,11 @@ await SendServerResponseAndEosAsync( serverWebSocket.Dispose(); }, - globalOptions, + new LoopbackWebSocketServer.Options(HttpVersion, useSsl) { SkipServerHandshakeResponse = true }, timeoutCts.Token); } - protected virtual Task SendServerResponseAndEosAsync(WebSocketRequestData requestData, ServerEosType serverEosType, Func serverFunc, CancellationToken cancellationToken) - => WebSocketHandshakeHelper.SendHttp11ServerResponseAndEosAsync(requestData, serverFunc, cancellationToken); // override for HTTP/2 + protected abstract Task SendServerResponseAndEosAsync(WebSocketRequestData data, ServerEosType eos, Func callback, CancellationToken ct); public enum AbortType { @@ -184,42 +196,51 @@ protected static async Task VerifySendReceiveAsync(WebSocket ws, byte[] localMsg await sendTask.ConfigureAwait(false); await remoteAck.WaitAsync(cancellationToken).ConfigureAwait(false); } + + #endregion } - // --- HTTP/1.1 WebSocket loopback tests --- + public abstract class AbortTest_Loopback(ITestOutputHelper output) : AbortTest_LoopbackBase(output) + { + protected override Task SendServerResponseAndEosAsync(WebSocketRequestData data, ServerEosType eos, Func callback, CancellationToken ct) + => WebSocketHandshakeHelper.SendHttp11ServerResponseAndEosAsync(data, callback, ct); + } - public class AbortTest_Invoker_Loopback : AbortTest_Loopback + public abstract class AbortTest_Http2Loopback(ITestOutputHelper output) : AbortTest_LoopbackBase(output) { - public AbortTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } - protected override bool UseCustomInvoker => true; + internal override Version HttpVersion => Net.HttpVersion.Version20; + + protected override Task SendServerResponseAndEosAsync(WebSocketRequestData data, ServerEosType eos, Func callback, CancellationToken ct) + => WebSocketHandshakeHelper.SendHttp2ServerResponseAndEosAsync(data, eosInHeadersFrame: eos == ServerEosType.WithHeaders, callback, ct); } - public class AbortTest_HttpClient_Loopback : AbortTest_Loopback + #region Runnable test classes: HTTP/1.1 Loopback + + public sealed class AbortTest_SharedHandler_Loopback(ITestOutputHelper output) : AbortTest_Loopback(output) { } + + public sealed class AbortTest_Invoker_Loopback(ITestOutputHelper output) : AbortTest_Loopback(output) { - public AbortTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } - protected override bool UseHttpClient => true; + protected override bool UseCustomInvoker => true; } - public class AbortTest_SharedHandler_Loopback : AbortTest_Loopback + public sealed class AbortTest_HttpClient_Loopback(ITestOutputHelper output) : AbortTest_Loopback(output) { - public AbortTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseHttpClient => true; } - // --- HTTP/2 WebSocket loopback tests --- + #endregion + + #region Runnable test classes: HTTP/2 Loopback - public class AbortTest_Invoker_Http2 : AbortTest_Invoker_Loopback + public sealed class AbortTest_Invoker_Http2Loopback(ITestOutputHelper output) : AbortTest_Http2Loopback(output) { - public AbortTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } - protected override Version HttpVersion => Net.HttpVersion.Version20; - protected override Task SendServerResponseAndEosAsync(WebSocketRequestData rd, ServerEosType eos, Func callback, CancellationToken ct) - => WebSocketHandshakeHelper.SendHttp2ServerResponseAndEosAsync(rd, eosInHeadersFrame: eos == ServerEosType.WithHeaders, callback, ct); + protected override bool UseCustomInvoker => true; } - public class AbortTest_HttpClient_Http2 : AbortTest_HttpClient_Loopback + public sealed class AbortTest_HttpClient_Http2Loopback(ITestOutputHelper output) : AbortTest_Http2Loopback(output) { - public AbortTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } - protected override Version HttpVersion => Net.HttpVersion.Version20; - protected override Task SendServerResponseAndEosAsync(WebSocketRequestData rd, ServerEosType eos, Func callback, CancellationToken ct) - => WebSocketHandshakeHelper.SendHttp2ServerResponseAndEosAsync(rd, eosInHeadersFrame: eos == ServerEosType.WithHeaders, callback, ct); + protected override bool UseHttpClient => true; } + + #endregion } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs index 85f6e875a824ca..703c7f2a1c477b 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs @@ -1,48 +1,52 @@ // 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.Net.Http; -using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; +using EchoControlMessage = System.Net.Test.Common.WebSocketEchoHelper.EchoControlMessage; +using EchoQueryKey = System.Net.Test.Common.WebSocketEchoOptions.EchoQueryKey; + namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerAbortTest : AbortTest - { - public InvokerAbortTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; - } - - public sealed class HttpClientAbortTest : AbortTest - { - public HttpClientAbortTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } - - public class AbortTest : ClientWebSocketTestBase + // + // Class hierarchy: + // + // - AbortTestBase → file:AbortTest.cs + // ├─ AbortTest_External + // │ ├─ [*]AbortTest_SharedHandler_External + // │ ├─ [*]AbortTest_Invoker_External + // │ └─ [*]AbortTest_HttpClient_External + // └─ AbortTest_LoopbackBase → file:AbortTest.Loopback.cs + // ├─ AbortTest_Loopback + // │ ├─ [*]AbortTest_SharedHandler_Loopback + // │ ├─ [*]AbortTest_Invoker_Loopback + // │ └─ [*]AbortTest_HttpClient_Loopback + // └─ AbortTest_Http2Loopback + // ├─ [*]AbortTest_Invoker_Http2Loopback + // └─ [*]AbortTest_HttpClient_Http2Loopback + // + // --- + // `[*]` - concrete runnable test classes + // `→ file:` - file containing the class and its concrete subclasses + + public abstract class AbortTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public AbortTest(ITestOutputHelper output) : base(output) { } - + #region Common (Echo Server) tests - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithmessage(Uri server) + protected async Task RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(Uri server) { using (var cws = new ClientWebSocket()) { var cts = new CancellationTokenSource(TimeOutMilliseconds); - var ub = new UriBuilder(server); - ub.Query = "delay10sec"; + var ub = new UriBuilder(server) { Query = EchoQueryKey.Delay10Sec }; Task t = ConnectAsync(cws, ub.Uri, cts.Token); + cws.Abort(); WebSocketException ex = await Assert.ThrowsAsync(() => t); @@ -53,16 +57,14 @@ public async Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithmessage(Uri } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task Abort_SendAndAbort_Success(Uri server) + protected async Task RunClient_Abort_SendAndAbort_Success(Uri server) { await TestCancellation(async (cws) => { var cts = new CancellationTokenSource(TimeOutMilliseconds); Task t = cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -73,16 +75,14 @@ await TestCancellation(async (cws) => }, server); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task Abort_ReceiveAndAbort_Success(Uri server) + protected async Task RunClient_Abort_ReceiveAndAbort_Success(Uri server) { await TestCancellation(async (cws) => { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -91,22 +91,21 @@ await cws.SendAsync( var segment = new ArraySegment(recvBuffer); Task t = cws.ReceiveAsync(segment, ctsDefault.Token); + cws.Abort(); await t; }, server); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task Abort_CloseAndAbort_Success(Uri server) + protected async Task RunClient_Abort_CloseAndAbort_Success(Uri server) { await TestCancellation(async (cws) => { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -121,16 +120,14 @@ await cws.SendAsync( }, server); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task ClientWebSocket_Abort_CloseOutputAsync(Uri server) + protected async Task RunClient_ClientWebSocket_Abort_CloseOutputAsync(Uri server) { await TestCancellation(async (cws) => { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -144,5 +141,52 @@ await cws.SendAsync( await t; }, server); } + + #endregion } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public abstract class AbortTest_External(ITestOutputHelper output) : AbortTestBase(output) + { + #region Common (Echo Server) tests + + [Theory, MemberData(nameof(EchoServers))] + public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(Uri server) + => RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task Abort_SendAndAbort_Success(Uri server) + => RunClient_Abort_SendAndAbort_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task Abort_ReceiveAndAbort_Success(Uri server) + => RunClient_Abort_ReceiveAndAbort_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task Abort_CloseAndAbort_Success(Uri server) + => RunClient_Abort_CloseAndAbort_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task ClientWebSocket_Abort_CloseOutputAsync(Uri server) + => RunClient_ClientWebSocket_Abort_CloseOutputAsync(server); + + #endregion + } + + #region Runnable test classes: External/Outerloop + + public sealed class AbortTest_SharedHandler_External(ITestOutputHelper output) : AbortTest_External(output) { } + + public sealed class AbortTest_Invoker_External(ITestOutputHelper output) : AbortTest_External(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class AbortTest_HttpClient_External(ITestOutputHelper output) : AbortTest_External(output) + { + protected override bool UseHttpClient => true; + } + + #endregion } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs new file mode 100644 index 00000000000000..0e713c566c0d2c --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.WebSockets.Client.Tests +{ + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] + public abstract class CancelTest_Loopback(ITestOutputHelper output) : CancelTestBase(output) + { + #region Common (Echo Server) tests + + [Theory, MemberData(nameof(UseSsl))] + public Task ConnectAsync_Cancel_ThrowsCancellationException(bool useSsl) => RunEchoAsync( + RunClient_ConnectAsync_Cancel_ThrowsCancellationException, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task SendAsync_Cancel_Success(bool useSsl) => RunEchoAsync( + RunClient_SendAsync_Cancel_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ReceiveAsync_Cancel_Success(bool useSsl) => RunEchoAsync( + RunClient_ReceiveAsync_Cancel_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseAsync_Cancel_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_Cancel_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseOutputAsync_Cancel_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_Cancel_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException(bool useSsl) => RunEchoAsync( + RunClient_ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(bool useSsl) => RunEchoAsync( + RunClient_ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(bool useSsl) => RunEchoAsync( + RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException, useSsl); + + #endregion + } + + public abstract class CancelTest_Http2Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; + } + + #region Runnable test classes: HTTP/1.1 Loopback + + public sealed class CancelTest_SharedHandler_Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) { } + + public sealed class CancelTest_Invoker_Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class CancelTest_HttpClient_Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) + { + protected override bool UseHttpClient => true; + } + + #endregion + + #region Runnable test classes: HTTP/2 Loopback + + public sealed class CancelTest_Invoker_Http2Loopback(ITestOutputHelper output) : CancelTest_Http2Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class CancelTest_HttpClient_Http2Loopback(ITestOutputHelper output) : CancelTest_Http2Loopback(output) + { + protected override bool UseHttpClient => true; + } + + #endregion +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs index a38b11d2321c87..20df47b2e6180b 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs @@ -1,68 +1,71 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; +using EchoControlMessage = System.Net.Test.Common.WebSocketEchoHelper.EchoControlMessage; +using EchoQueryKey = System.Net.Test.Common.WebSocketEchoOptions.EchoQueryKey; + namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerCancelTest : CancelTest - { - public InvokerCancelTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; - } - - public sealed class HttpClientCancelTest : CancelTest - { - public HttpClientCancelTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } - - public class CancelTest : ClientWebSocketTestBase + // + // Class hierarchy: + // + // - CancelTestBase → file:CancelTest.cs + // ├─ CancelTest_External + // │ ├─ [*]CancelTest_SharedHandler_External + // │ ├─ [*]CancelTest_Invoker_External + // │ └─ [*]CancelTest_HttpClient_External + // └─ CancelTest_Loopback → file:CancelTest.Loopback.cs + // ├─ [*]CancelTest_SharedHandler_Loopback + // ├─ [*]CancelTest_Invoker_Loopback + // ├─ [*]CancelTest_HttpClient_Loopback + // └─ CancelTest_Http2Loopback + // ├─ [*]CancelTest_Invoker_Http2Loopback + // └─ [*]CancelTest_HttpClient_Http2Loopback + // + // --- + // `[*]` - concrete runnable test classes + // `→ file:` - file containing the class and its concrete subclasses + + public abstract class CancelTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public CancelTest(ITestOutputHelper output) : base(output) { } + #region Common (Echo Server) tests - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/83579", typeof(PlatformDetection), nameof(PlatformDetection.IsNodeJS))] - public async Task ConnectAsync_Cancel_ThrowsCancellationException(Uri server) + protected async Task RunClient_ConnectAsync_Cancel_ThrowsCancellationException(Uri server) { using (var cws = new ClientWebSocket()) { var cts = new CancellationTokenSource(100); - var ub = new UriBuilder(server); - ub.Query = PlatformDetection.IsBrowser ? "delay20sec" : "delay10sec"; + var ub = new UriBuilder(server) + { + Query = PlatformDetection.IsBrowser ? EchoQueryKey.Delay20Sec : EchoQueryKey.Delay10Sec + }; var ex = await Assert.ThrowsAnyAsync(() => ConnectAsync(cws, ub.Uri, cts.Token)); Assert.True(WebSocketState.Closed == cws.State, $"Actual {cws.State} when {ex}"); } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task SendAsync_Cancel_Success(Uri server) + protected async Task RunClient_SendAsync_Cancel_Success(Uri server) { await TestCancellation((cws) => { var cts = new CancellationTokenSource(5); return cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); }, server); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task ReceiveAsync_Cancel_Success(Uri server) + protected async Task RunClient_ReceiveAsync_Cancel_Success(Uri server) { await TestCancellation(async (cws) => { @@ -70,7 +73,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(5); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -82,9 +85,7 @@ await cws.SendAsync( }, server); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseAsync_Cancel_Success(Uri server) + protected async Task RunClient_CloseAsync_Cancel_Success(Uri server) { await TestCancellation(async (cws) => { @@ -92,7 +93,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -104,9 +105,7 @@ await cws.SendAsync( }, server); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseOutputAsync_Cancel_Success(Uri server) + protected async Task RunClient_CloseOutputAsync_Cancel_Success(Uri server) { await TestCancellation(async (cws) => { @@ -115,7 +114,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -127,11 +126,9 @@ await cws.SendAsync( }, server); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException(Uri server) + protected async Task RunClient_ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var recvBuffer = new byte[100]; var segment = new ArraySegment(recvBuffer); @@ -143,11 +140,9 @@ public async Task ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledExceptio } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(Uri server) + protected async Task RunClient_ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var recvBuffer = new byte[100]; var segment = new ArraySegment(recvBuffer); @@ -159,11 +154,9 @@ public async Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledExceptio } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(Uri server) + protected async Task RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var recvBuffer = new byte[100]; var segment = new ArraySegment(recvBuffer); @@ -180,5 +173,63 @@ public async Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketEx ex.Message); } } + + #endregion + } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public abstract class CancelTest_External(ITestOutputHelper output) : CancelTestBase(output) + { + #region Common (Echo Server) tests + + [ActiveIssue("https://github.com/dotnet/runtime/issues/83579", typeof(PlatformDetection), nameof(PlatformDetection.IsNodeJS))] + [Theory, MemberData(nameof(EchoServers))] + public Task ConnectAsync_Cancel_ThrowsCancellationException(Uri server) + => RunClient_ConnectAsync_Cancel_ThrowsCancellationException(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task SendAsync_Cancel_Success(Uri server) + => RunClient_SendAsync_Cancel_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task ReceiveAsync_Cancel_Success(Uri server) + => RunClient_ReceiveAsync_Cancel_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_Cancel_Success(Uri server) + => RunClient_CloseAsync_Cancel_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_Cancel_Success(Uri server) + => RunClient_CloseOutputAsync_Cancel_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException(Uri server) + => RunClient_ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(Uri server) + => RunClient_ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(Uri server) + => RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(server); + + #endregion + } + + #region Runnable test classes: External/Outerloop + public sealed class CancelTest_SharedHandler_External(ITestOutputHelper output) : CancelTest_External(output) { } + + public sealed class CancelTest_Invoker_External(ITestOutputHelper output) : CancelTest_External(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class CancelTest_HttpClient_External(ITestOutputHelper output) : CancelTest_External(output) + { + protected override bool UseHttpClient => true; } + #endregion } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs index 7a39f2423cad86..7e64f6baa61246 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs @@ -7,16 +7,14 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; - +using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { - public class ClientWebSocketOptionsTests : ClientWebSocketTestBase + public class ClientWebSocketOptionsTests(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public ClientWebSocketOptionsTests(ITestOutputHelper output) : base(output) { } - [ConditionalFact(nameof(WebSocketsSupported))] [SkipOnPlatform(TestPlatforms.Browser, "Credentials not supported on browser")] public static void UseDefaultCredentials_Roundtrips() @@ -52,7 +50,7 @@ public async Task Proxy_SetNull_ConnectsSuccessfully(Uri server) { for (int i = 0; i < 3; i++) // Connect and disconnect multiple times to exercise shared handler on netcoreapp { - var ws = await WebSocketHelper.Retry(_output, async () => + var ws = await WebSocketHelper.Retry(async () => { var cws = new ClientWebSocket(); cws.Options.Proxy = null; @@ -72,19 +70,13 @@ public async Task Proxy_ConnectThruProxy_Success(Uri server) string proxyServerUri = System.Net.Test.Common.Configuration.WebSockets.ProxyServerUri; if (string.IsNullOrEmpty(proxyServerUri)) { - _output.WriteLine("Skipping test...no proxy server defined."); - return; + throw new SkipTestException("No proxy server defined."); } _output.WriteLine($"ProxyServer: {proxyServerUri}"); IWebProxy proxy = new WebProxy(new Uri(proxyServerUri)); - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket( - server, - TimeOutMilliseconds, - _output, - default(TimeSpan), - proxy)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server, o => o.Proxy = proxy)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); Assert.Equal(WebSocketState.Open, cws.State); @@ -119,7 +111,7 @@ public static void SetBuffer_InvalidArgs_Throws() AssertExtensions.Throws("receiveBufferSize", () => cws.Options.SetBuffer(0, 0, new ArraySegment(new byte[1]))); AssertExtensions.Throws("receiveBufferSize", () => cws.Options.SetBuffer(0, minSendBufferSize, new ArraySegment(new byte[1]))); AssertExtensions.Throws("sendBufferSize", () => cws.Options.SetBuffer(minReceiveBufferSize, 0, new ArraySegment(new byte[1]))); - AssertExtensions.Throws("buffer.Array", () => cws.Options.SetBuffer(minReceiveBufferSize, minSendBufferSize, default(ArraySegment))); + AssertExtensions.Throws("buffer.Array", () => cws.Options.SetBuffer(minReceiveBufferSize, minSendBufferSize, default)); AssertExtensions.Throws(bufferName, () => cws.Options.SetBuffer(minReceiveBufferSize, minSendBufferSize, new ArraySegment(new byte[0]))); } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.Unsupported.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.Unsupported.cs new file mode 100644 index 00000000000000..c6070e1e28af6e --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.Unsupported.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; + +namespace System.Net.WebSockets.Client.Tests +{ + public partial class ClientWebSocketTestBase + { + protected Task RunEchoAsync(Func clientFunc, bool useSsl) + => throw new PlatformNotSupportedException(); + + protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) + => throw new PlatformNotSupportedException(); + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs new file mode 100644 index 00000000000000..c29aa596710b32 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Test.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.WebSockets.Client.Tests +{ + public partial class ClientWebSocketTestBase + { + protected Task RunEchoAsync(Func clientFunc, bool useSsl) + { + var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); + var options = new LoopbackWebSocketServer.Options(HttpVersion, useSsl) + { + SkipServerHandshakeResponse = true, + IgnoreServerErrors = true, + AbortServerOnClientExit = true, + ParseEchoOptions = true + }; + + return LoopbackWebSocketServer.RunEchoAsync(clientFunc, options, timeoutCts.Token); + } + + protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) + { + var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); + var options = new LoopbackWebSocketServer.Options(HttpVersion, useSsl) + { + IgnoreServerErrors = true, + AbortServerOnClientExit = true + }; + + return LoopbackWebSocketServer.RunAsync( + clientFunc, + async (requestData, token) => + { + var serverWebSocket = WebSocket.CreateFromStream( + requestData.TransportStream, + new WebSocketCreationOptions { IsServer = true }); + + using var registration = token.Register(serverWebSocket.Abort); + await WebSocketEchoHelper.RunEchoHeaders(serverWebSocket, requestData.Headers, token); + }, + options, + timeoutCts.Token); + } + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index 4e4fb4b3d87c76..e1cd17fbaa3c4e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -5,29 +5,26 @@ using System.Diagnostics; using System.Linq; using System.Net.Http; -using System.Reflection; using System.Threading; using System.Threading.Tasks; -using TestUtilities; using Xunit; using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { - public class ClientWebSocketTestBase + public partial class ClientWebSocketTestBase(ITestOutputHelper output) { - public static readonly object[][] EchoServers = System.Net.Test.Common.Configuration.WebSockets.GetEchoServers(); - public static readonly object[][] EchoHeadersServers = System.Net.Test.Common.Configuration.WebSockets.GetEchoHeadersServers(); - public static readonly object[][] EchoServersAndBoolean = EchoServers.SelectMany(o => new object[][] - { - new object[] { o[0], false }, - new object[] { o[0], true } - }).ToArray(); + public static readonly Uri[] EchoServers_Values = System.Net.Test.Common.Configuration.WebSockets.GetEchoServers(); + public static readonly Uri[] EchoHeadersServers_Values = System.Net.Test.Common.Configuration.WebSockets.GetEchoHeadersServers(); + public static readonly bool[] Bool_Values = [ false, true ]; + public static readonly bool[] UseSsl_Values = PlatformDetection.SupportsAlpn ? Bool_Values : [ false ]; - public static readonly bool[] Bool_Values = new[] { false, true }; - public static readonly bool[] UseSsl_Values = PlatformDetection.SupportsAlpn ? Bool_Values : new[] { false }; - public static readonly object[][] UseSsl_MemberData = ToMemberData(UseSsl_Values); + public static readonly object[][] EchoServers = ToMemberData(EchoServers_Values); + public static readonly object[][] EchoHeadersServers = ToMemberData(EchoHeadersServers_Values); + public static readonly object[][] EchoServersAndBoolean = ToMemberData(EchoServers_Values, Bool_Values); + public static readonly object[][] UseSsl = ToMemberData(UseSsl_Values); + public static readonly object[][] UseSslAndBoolean = ToMemberData(UseSsl_Values, Bool_Values); public static object[][] ToMemberData(IEnumerable data) => data.Select(a => new object[] { a }).ToArray(); @@ -40,12 +37,7 @@ public static object[][] ToMemberData(IEnumerable dataA, IEnumer public const int TimeOutMilliseconds = 30000; public const int CloseDescriptionMaxLength = 123; - public readonly ITestOutputHelper _output; - - public ClientWebSocketTestBase(ITestOutputHelper output) - { - _output = output; - } + public readonly ITestOutputHelper _output = output; public static IEnumerable UnavailableWebSocketServers { @@ -66,7 +58,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 }; } @@ -75,27 +67,21 @@ 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 GetConnectedWebSocket(server)) { try { await action(cws); - // Operation finished before CTS expired. + _output.WriteLine($"Operation finished before CTS expired."); } - catch (OperationCanceledException exception) + catch (Exception e) when (e is OperationCanceledException or ObjectDisposedException or WebSocketException) { - // Expected exception - Assert.True(WebSocketState.Aborted == cws.State, $"Actual {cws.State} when {exception}"); - } - catch (ObjectDisposedException exception) - { - // Expected exception - Assert.True(WebSocketState.Aborted == cws.State, $"Actual {cws.State} when {exception}"); - } - catch (WebSocketException exception) - { - Assert.True(WebSocketError.InvalidState == exception.WebSocketErrorCode, $"Actual WebSocketErrorCode {exception.WebSocketErrorCode} when {exception}"); - Assert.True(WebSocketState.Aborted == cws.State, $"Actual {cws.State} when {exception}"); + Assert.True(WebSocketState.Aborted == cws.State, $"Actual {cws.State} when {e}"); + + if (e is WebSocketException wse) + { + Assert.True(WebSocketError.InvalidState == wse.WebSocketErrorCode, $"Actual WebSocketErrorCode {wse.WebSocketErrorCode} when {wse}"); + } } } } @@ -126,39 +112,101 @@ protected static async Task ReceiveEntireMessageAsync(We protected Action? ConfigureCustomHandler; + internal virtual Version HttpVersion => Net.HttpVersion.Version11; + internal HttpMessageInvoker? GetInvoker() { - var handler = new HttpClientHandler(); + if (UseSharedHandler) + { + return null; + } + HttpClientHandler handler = new HttpClientHandler(); if (PlatformDetection.IsNotBrowser) { - handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + ConfigureCustomHandler?.Invoke(handler); } - ConfigureCustomHandler?.Invoke(handler); - if (UseCustomInvoker) { Debug.Assert(!UseHttpClient); return new HttpMessageInvoker(handler); } - if (UseHttpClient) + Debug.Assert(UseHttpClient); + return new HttpClient(handler); + } + + public Task GetConnectedWebSocket(Uri uri, Action? configureOptions = null) + => WebSocketHelper.Retry( + async () => + { + var cws = new ClientWebSocket(); + configureOptions?.Invoke(cws.Options); + + using var cts = new CancellationTokenSource(TimeOutMilliseconds); + Task taskConnect = ConnectAsync(cws, uri, cts.Token); + + Assert.True( + (cws.State == WebSocketState.None) || + (cws.State == WebSocketState.Connecting) || + (cws.State == WebSocketState.Open) || + (cws.State == WebSocketState.Aborted), + "State immediately after ConnectAsync incorrect: " + cws.State); + await taskConnect; + + Assert.Equal(WebSocketState.Open, cws.State); + return cws; + }); + + protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) + { + if (PlatformDetection.IsNotBrowser) { - return new HttpClient(handler); + if (uri.Scheme == "wss" && UseSharedHandler) + { + cws.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true; + } + + cws.Options.HttpVersion = HttpVersion; + cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; + + if (HttpVersion == Net.HttpVersion.Version20 && uri.Query is not (null or "" or "?")) + { + // RFC 7540, section 8.3. The CONNECT Method: + // > The ":scheme" and ":path" pseudo-header fields MUST be omitted. + // + // HTTP/2 CONNECT requests must drop query (containing echo options) from the request URI. + // The information needs to be passed in a different way, e.g. in a custom header. + + cws.Options.SetRequestHeader(WebSocketHelper.OriginalQueryStringHeader, uri.Query); + } } - return null; + return UseSharedHandler + ? cws.ConnectAsync(uri, cancellationToken) // Ensure test coverage for both overloads + : cws.ConnectAsync(uri, GetInvoker(), cancellationToken); } - protected Task GetConnectedWebSocket(Uri uri, int TimeOutMilliseconds, ITestOutputHelper output) => - WebSocketHelper.GetConnectedWebSocket(uri, TimeOutMilliseconds, output, invoker: GetInvoker()); - - protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) => - cws.ConnectAsync(uri, GetInvoker(), cancellationToken); + protected Task RunClientAsync( + Uri uri, + Func clientWebSocketFunc, + Action? configureOptions = null) + { + var cts = new CancellationTokenSource(TimeOutMilliseconds); + return RunClientAsync(uri, clientWebSocketFunc, configureOptions, cts.Token); + } - protected Task TestEcho(Uri uri, WebSocketMessageType type, int timeOutMilliseconds, ITestOutputHelper output) => - WebSocketHelper.TestEcho(uri, WebSocketMessageType.Text, TimeOutMilliseconds, _output, GetInvoker()); + protected async Task RunClientAsync( + Uri uri, + Func clientWebSocketFunc, + Action? configureOptions, + CancellationToken cancellationToken) + { + using ClientWebSocket cws = await GetConnectedWebSocket(uri, configureOptions); + await clientWebSocketFunc(cws, cancellationToken); + } public static bool WebSocketsSupported { get { return WebSocketHelper.WebSocketsSupported; } } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs new file mode 100644 index 00000000000000..66eb8a53715a20 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs @@ -0,0 +1,217 @@ +// 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.Net.Test.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.WebSockets.Client.Tests +{ + + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] + public abstract class CloseTest_LoopbackBase(ITestOutputHelper output) : CloseTestBase(output) + { + #region Common (Echo Server) tests + + [Theory, MemberData(nameof(UseSslAndBoolean))] + public Task CloseAsync_ServerInitiatedClose_Success(bool useSsl, bool useCloseOutputAsync) => RunEchoAsync( + server => RunClient_CloseAsync_ServerInitiatedClose_Success(server, useCloseOutputAsync), useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseAsync_ClientInitiatedClose_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_ClientInitiatedClose_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseAsync_CloseDescriptionIsMaxLength_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseAsync_CloseDescriptionHasUnicode_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionHasUnicode_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseAsync_CloseDescriptionIsNull_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsNull_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseOutputAsync_ExpectedStates(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_ExpectedStates, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseAsync_CloseOutputAsync_Throws(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseOutputAsync_Throws, useSsl); + + [OuterLoop("Uses Task.Delay")] + [Theory, MemberData(nameof(UseSsl))] + public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseOutputAsync_ServerInitiated_CanReceive(bool useSsl) => RunEchoAsync( + server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving: false), useSsl); + + [OuterLoop("Uses Task.Delay")] + [Theory, MemberData(nameof(UseSsl))] + public Task CloseOutputAsync_ServerInitiated_DelayReceiving_CanReceive(bool useSsl) => RunEchoAsync( + server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving: true), useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseOutputAsync_ServerInitiated_CanSend(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_ServerInitiated_CanSend, useSsl); + + [OuterLoop("Uses Task.Delay")] + [Theory, MemberData(nameof(UseSslAndBoolean))] + public Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(bool useSsl, bool syncState) => RunEchoAsync( + server => RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(server, syncState), useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseOutputAsync_CloseDescriptionIsNull_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success, useSsl); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] + [Theory, MemberData(nameof(UseSsl))] + public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl); + + #endregion + } + + public abstract class CloseTest_Loopback(ITestOutputHelper output) : CloseTest_LoopbackBase(output) + { + #region HTTP/1.1-only loopback tests + + [Fact] + public async Task CloseAsync_CancelableEvenWhenPendingReceive_Throws() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + try + { + using (var cws = new ClientWebSocket()) + using (var testTimeoutCts = new CancellationTokenSource(TimeOutMilliseconds)) + { + await ConnectAsync(cws, uri, testTimeoutCts.Token); + + Task receiveTask = cws.ReceiveAsync(new byte[1], testTimeoutCts.Token); + + var cancelCloseCts = new CancellationTokenSource(); + await Assert.ThrowsAnyAsync(async () => + { + Task t = cws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, cancelCloseCts.Token); + cancelCloseCts.Cancel(); + await t; + }); + + Assert.True(cancelCloseCts.Token.IsCancellationRequested); + Assert.False(testTimeoutCts.Token.IsCancellationRequested); + + await Assert.ThrowsAnyAsync(() => receiveTask); + + Assert.False(testTimeoutCts.Token.IsCancellationRequested); + } + } + finally + { + tcs.SetResult(); + } + }, server => server.AcceptConnectionAsync(async connection => + { + Dictionary headers = await LoopbackHelper.WebSocketHandshakeAsync(connection); + Assert.NotNull(headers); + + await tcs.Task; + + }), new LoopbackServer.Options { WebSocketEndpoint = true }); + } + + // Regression test for https://github.com/dotnet/runtime/issues/80116. + [OuterLoop("Uses Task.Delay")] + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public async Task CloseHandshake_ExceptionsAreObserved() + { + await RemoteExecutor.Invoke(static (typeName) => + { + ClientWebSocketTestBase test = (ClientWebSocketTestBase)Activator.CreateInstance(typeof(ClientWebSocketTestBase).Assembly.GetType(typeName), new object[] { null }); + using CancellationTokenSource timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); + + Exception unobserved = null; + TaskScheduler.UnobservedTaskException += (obj, args) => + { + unobserved = args.Exception; + }; + + TaskCompletionSource clientCompleted = new TaskCompletionSource(); + + return LoopbackWebSocketServer.RunAsync(async uri => + { + var ct = timeoutCts.Token; + using var clientWs = await test.GetConnectedWebSocket(uri); + await clientWs.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", ct); + await clientWs.ReceiveAsync(new byte[16], ct); + await Task.Delay(1500); + GC.Collect(2); + GC.WaitForPendingFinalizers(); + clientCompleted.SetResult(); + Assert.Null(unobserved); + }, + async (serverWs, ct) => + { + await serverWs.ReceiveAsync(new byte[16], ct); + await serverWs.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", ct); + await clientCompleted.Task; + }, new LoopbackWebSocketServer.Options(Net.HttpVersion.Version11, UseSsl: PlatformDetection.SupportsAlpn), timeoutCts.Token); + }, GetType().FullName).DisposeAsync(); + } + + #endregion + } + + public abstract class CloseTest_Http2Loopback(ITestOutputHelper output) : CloseTest_LoopbackBase(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; + } + + #region Runnable test classes: HTTP/1.1 Loopback + + public sealed class CloseTest_SharedHandler_Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) { } + + public sealed class CloseTest_Invoker_Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class CloseTest_HttpClient_Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) + { + protected override bool UseHttpClient => true; + } + + #endregion + + #region Runnable test classes: HTTP/2 Loopback + + public sealed class CloseTest_Invoker_Http2Loopback(ITestOutputHelper output) : CloseTest_Http2Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class CloseTest_HttpClient_Http2Loopback(ITestOutputHelper output) : CloseTest_Http2Loopback(output) + { + protected override bool UseHttpClient => true; + } + #endregion +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index 063ee71169d17e..9a8f33299de93e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -1,95 +1,82 @@ // 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.Diagnostics; -using System.Net.Http; -using System.Net.Test.Common; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Linq; using Xunit; using Xunit.Abstractions; -using Microsoft.DotNet.RemoteExecutor; + +using EchoControlMessage = System.Net.Test.Common.WebSocketEchoHelper.EchoControlMessage; namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerCloseTest : CloseTest - { - public InvokerCloseTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; - } - - public sealed class HttpClientCloseTest : CloseTest + // + // Class hierarchy: + // + // - CloseTestBase → file:CloseTest.cs + // ├─ CloseTest_External + // │ ├─ [*]CloseTest_SharedHandler_External + // │ ├─ [*]CloseTest_Invoker_External + // │ └─ [*]CloseTest_HttpClient_External + // └─ CloseTest_Loopback → file:CloseTest.Loopback.cs + // ├─ [*]CloseTest_SharedHandler_Loopback + // ├─ [*]CloseTest_Invoker_Loopback + // ├─ [*]CloseTest_HttpClient_Loopback + // └─ CloseTest_Http2Loopback + // ├─ [*]CloseTest_Invoker_Http2Loopback + // └─ [*]CloseTest_HttpClient_Http2Loopback + // + // --- + // `[*]` - concrete runnable test classes + // `→ file:` - file containing the class and its concrete subclasses + + public abstract class CloseTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public HttpClientCloseTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } - - public class CloseTest : ClientWebSocketTestBase - { - public CloseTest(ITestOutputHelper output) : base(output) { } - + #region Common (Echo Server) tests - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServersAndBoolean))] - public async Task CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) + protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) { - const string shutdownWebSocketMetaCommand = ".shutdown"; - - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); - _output.WriteLine("SendAsync starting."); await cws.SendAsync( - WebSocketData.GetBufferFromText(shutdownWebSocketMetaCommand), + EchoControlMessage.Shutdown.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); - _output.WriteLine("SendAsync done."); var recvBuffer = new byte[256]; - _output.WriteLine("ReceiveAsync starting."); WebSocketReceiveResult recvResult = await cws.ReceiveAsync(new ArraySegment(recvBuffer), cts.Token); - _output.WriteLine("ReceiveAsync done."); // Verify received server-initiated close message. Assert.Equal(WebSocketCloseStatus.NormalClosure, recvResult.CloseStatus); - Assert.Equal(shutdownWebSocketMetaCommand, recvResult.CloseStatusDescription); + Assert.Equal(EchoControlMessage.Shutdown, recvResult.CloseStatusDescription); Assert.Equal(WebSocketMessageType.Close, recvResult.MessageType); // Verify current websocket state as CloseReceived which indicates only partial close. Assert.Equal(WebSocketState.CloseReceived, cws.State); Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); - Assert.Equal(shutdownWebSocketMetaCommand, cws.CloseStatusDescription); + Assert.Equal(EchoControlMessage.Shutdown, cws.CloseStatusDescription); // Send back close message to acknowledge server-initiated close. - _output.WriteLine("Close starting."); var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidMessageType : (WebSocketCloseStatus)3210; await (useCloseOutputAsync ? cws.CloseOutputAsync(closeStatus, string.Empty, cts.Token) : cws.CloseAsync(closeStatus, string.Empty, cts.Token)); - _output.WriteLine("Close done."); Assert.Equal(WebSocketState.Closed, cws.State); // Verify that there is no follow-up echo close message back from the server by // making sure the close code and message are the same as from the first server close message. Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); - Assert.Equal(shutdownWebSocketMetaCommand, cws.CloseStatusDescription); + Assert.Equal(EchoControlMessage.Shutdown, cws.CloseStatusDescription); } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseAsync_ClientInitiatedClose_Success(Uri server) + protected async Task RunClient_CloseAsync_ClientInitiatedClose_Success(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); Assert.Equal(WebSocketState.Open, cws.State); @@ -107,13 +94,11 @@ public async Task CloseAsync_ClientInitiatedClose_Success(Uri server) } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseAsync_CloseDescriptionIsMaxLength_Success(Uri server) + protected async Task RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success(Uri server) { string closeDescription = new string('C', CloseDescriptionMaxLength); - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -121,13 +106,11 @@ public async Task CloseAsync_CloseDescriptionIsMaxLength_Success(Uri server) } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(Uri server) + protected async Task RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(Uri server) { string closeDescription = new string('C', CloseDescriptionMaxLength + 1); - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -146,11 +129,9 @@ public async Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentEx } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseAsync_CloseDescriptionHasUnicode_Success(Uri server) + protected async Task RunClient_CloseAsync_CloseDescriptionHasUnicode_Success(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -165,11 +146,9 @@ public async Task CloseAsync_CloseDescriptionHasUnicode_Success(Uri server) } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseAsync_CloseDescriptionIsNull_Success(Uri server) + protected async Task RunClient_CloseAsync_CloseDescriptionIsNull_Success(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -181,11 +160,9 @@ public async Task CloseAsync_CloseDescriptionIsNull_Success(Uri server) } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseOutputAsync_ExpectedStates(Uri server) + protected async Task RunClient_CloseOutputAsync_ExpectedStates(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -200,11 +177,9 @@ public async Task CloseOutputAsync_ExpectedStates(Uri server) } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseAsync_CloseOutputAsync_Throws(Uri server) + protected async Task RunClient_CloseAsync_CloseOutputAsync_Throws(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -225,20 +200,18 @@ await Assert.ThrowsAnyAsync(async () => } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri server) + protected async Task RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri server) { string message = "Hello WebSockets!"; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidPayloadData : (WebSocketCloseStatus)3210; string closeDescription = "CloseOutputAsync_Client_InvalidPayloadData"; - await cws.SendAsync(WebSocketData.GetBufferFromText(message), WebSocketMessageType.Text, true, cts.Token); + await cws.SendAsync(message.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); // Need a short delay as per WebSocket rfc6455 section 5.5.1 there isn't a requirement to receive any // data fragments after a close has been sent. The delay allows the received data fragment to be // available before calling close. The WinRT MessageWebSocket implementation doesn't allow receiving @@ -252,7 +225,7 @@ public async Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri serve WebSocketReceiveResult recvResult = await cws.ReceiveAsync(segmentRecv, cts.Token); Assert.Equal(message.Length, recvResult.Count); segmentRecv = new ArraySegment(segmentRecv.Array, 0, recvResult.Count); - Assert.Equal(message, WebSocketData.GetTextFromBuffer(segmentRecv)); + Assert.Equal(message, segmentRecv.Utf8ToString()); Assert.Null(recvResult.CloseStatus); Assert.Null(recvResult.CloseStatusDescription); @@ -263,20 +236,17 @@ public async Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri serve } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServersAndBoolean))] - public async Task CloseOutputAsync_ServerInitiated_CanReceive(Uri server, bool delayReceiving) + protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceive(Uri server, bool delayReceiving) { var expectedCloseStatus = WebSocketCloseStatus.NormalClosure; - var expectedCloseDescription = ".shutdownafter"; + var expectedCloseDescription = EchoControlMessage.ShutdownAfter; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(expectedCloseDescription), + expectedCloseDescription.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -291,7 +261,7 @@ await cws.SendAsync( WebSocketReceiveResult recvResult = await cws.ReceiveAsync(segmentRecv, cts.Token); Assert.Equal(expectedCloseDescription.Length, recvResult.Count); segmentRecv = new ArraySegment(segmentRecv.Array, 0, recvResult.Count); - Assert.Equal(expectedCloseDescription, WebSocketData.GetTextFromBuffer(segmentRecv)); + Assert.Equal(expectedCloseDescription, segmentRecv.Utf8ToString()); Assert.Null(recvResult.CloseStatus); Assert.Null(recvResult.CloseStatusDescription); @@ -320,21 +290,18 @@ await cws.SendAsync( } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseOutputAsync_ServerInitiated_CanSend(Uri server) + protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanSend(Uri server) { string message = "Hello WebSockets!"; var expectedCloseStatus = WebSocketCloseStatus.NormalClosure; - var expectedCloseDescription = ".shutdown"; + var expectedCloseDescription = EchoControlMessage.Shutdown; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".shutdown"), + EchoControlMessage.Shutdown.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -354,7 +321,7 @@ await cws.SendAsync( Assert.Equal(WebSocketState.CloseReceived, cws.State); // Should be able to send. - await cws.SendAsync(WebSocketData.GetBufferFromText(message), WebSocketMessageType.Text, true, cts.Token); + await cws.SendAsync(message.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); // Cannot change the close status/description with the final close. var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidPayloadData : (WebSocketCloseStatus)3210; @@ -368,15 +335,13 @@ await cws.SendAsync( } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServersAndBoolean))] - public async Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(Uri server, bool syncState) + protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(Uri server, bool syncState) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".receiveMessageAfterClose"), + EchoControlMessage.ReceiveMessageAfterClose.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -392,17 +357,16 @@ await cws.SendAsync( var recvBuffer = new ArraySegment(new byte[1024]); WebSocketReceiveResult recvResult = await cws.ReceiveAsync(recvBuffer, cts.Token); - var message = Encoding.UTF8.GetString(recvBuffer.ToArray(), 0, recvResult.Count); + var recvSegment = new ArraySegment(recvBuffer.ToArray(), 0, recvResult.Count); + var message = recvSegment.Utf8ToString(); - Assert.Contains(".receiveMessageAfterClose", message); + Assert.Contains(EchoControlMessage.ReceiveMessageAfterClose, message); } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseOutputAsync_CloseDescriptionIsNull_Success(Uri server) + protected async Task RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -413,13 +377,10 @@ public async Task CloseOutputAsync_CloseDescriptionIsNull_Success(Uri server) } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) + protected async Task RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) { var receiveBuffer = new byte[1024]; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { // Issue a receive but don't wait for it. var t = cws.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); @@ -451,12 +412,10 @@ public async Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(U } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) + protected async Task RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) { var receiveBuffer = new byte[1024]; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var t = cws.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); Assert.False(t.IsCompleted); @@ -478,88 +437,96 @@ public async Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri ser } } - [ConditionalFact(nameof(WebSocketsSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/54153", TestPlatforms.Browser)] - public async Task CloseAsync_CancelableEvenWhenPendingReceive_Throws() - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + #endregion + } - await LoopbackServer.CreateClientAndServerAsync(async uri => - { - try - { - using (var cws = new ClientWebSocket()) - using (var testTimeoutCts = new CancellationTokenSource(TimeOutMilliseconds)) - { - await ConnectAsync(cws, uri, testTimeoutCts.Token); + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public abstract class CloseTest_External(ITestOutputHelper output) : CloseTestBase(output) + { + #region Common (Echo Server) tests - Task receiveTask = cws.ReceiveAsync(new byte[1], testTimeoutCts.Token); + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] // See https://github.com/dotnet/runtime/issues/28957 + [MemberData(nameof(EchoServersAndBoolean))] + public Task CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) + => RunClient_CloseAsync_ServerInitiatedClose_Success(server, useCloseOutputAsync); - var cancelCloseCts = new CancellationTokenSource(); - await Assert.ThrowsAnyAsync(async () => - { - Task t = cws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, cancelCloseCts.Token); - cancelCloseCts.Cancel(); - await t; - }); + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_ClientInitiatedClose_Success(Uri server) + => RunClient_CloseAsync_ClientInitiatedClose_Success(server); - Assert.True(cancelCloseCts.Token.IsCancellationRequested); - Assert.False(testTimeoutCts.Token.IsCancellationRequested); + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseDescriptionIsMaxLength_Success(Uri server) + => RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success(server); - await Assert.ThrowsAnyAsync(() => receiveTask); + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(Uri server) + => RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(server); - Assert.False(testTimeoutCts.Token.IsCancellationRequested); - } - } - finally - { - tcs.SetResult(); - } - }, server => server.AcceptConnectionAsync(async connection => - { - Dictionary headers = await LoopbackHelper.WebSocketHandshakeAsync(connection); - Assert.NotNull(headers); + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseDescriptionHasUnicode_Success(Uri server) + => RunClient_CloseAsync_CloseDescriptionHasUnicode_Success(server); - await tcs.Task; + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseDescriptionIsNull_Success(Uri server) + => RunClient_CloseAsync_CloseDescriptionIsNull_Success(server); - }), new LoopbackServer.Options { WebSocketEndpoint = true }); - } + [Theory, MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_ExpectedStates(Uri server) + => RunClient_CloseOutputAsync_ExpectedStates(server); - // Regression test for https://github.com/dotnet/runtime/issues/80116. - [OuterLoop("Uses Task.Delay")] - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public async Task CloseHandshake_ExceptionsAreObserved() - { - await RemoteExecutor.Invoke(static (typeName) => - { - CloseTest test = (CloseTest)Activator.CreateInstance(typeof(CloseTest).Assembly.GetType(typeName), new object[] { null }); - using CancellationTokenSource timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseOutputAsync_Throws(Uri server) + => RunClient_CloseAsync_CloseOutputAsync_Throws(server); - Exception unobserved = null; - TaskScheduler.UnobservedTaskException += (obj, args) => - { - unobserved = args.Exception; - }; + [Theory, MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri server) + => RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose(server); - TaskCompletionSource clientCompleted = new TaskCompletionSource(); + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] // See https://github.com/dotnet/runtime/issues/28957 + [MemberData(nameof(EchoServersAndBoolean))] + public Task CloseOutputAsync_ServerInitiated_CanReceive(Uri server, bool delayReceiving) + => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving); - return LoopbackWebSocketServer.RunAsync(async (clientWs, ct) => - { - await clientWs.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", ct); - await clientWs.ReceiveAsync(new byte[16], ct); - await Task.Delay(1500); - GC.Collect(2); - GC.WaitForPendingFinalizers(); - clientCompleted.SetResult(); - Assert.Null(unobserved); - }, - async (serverWs, ct) => - { - await serverWs.ReceiveAsync(new byte[16], ct); - await serverWs.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", ct); - await clientCompleted.Task; - }, new LoopbackWebSocketServer.Options(HttpVersion.Version11, true, test.GetInvoker()), timeoutCts.Token); - }, GetType().FullName).DisposeAsync(); - } + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] // See https://github.com/dotnet/runtime/issues/28957 + [MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_ServerInitiated_CanSend(Uri server) + => RunClient_CloseOutputAsync_ServerInitiated_CanSend(server); + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] // See https://github.com/dotnet/runtime/issues/28957 + [MemberData(nameof(EchoServersAndBoolean))] + public Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(Uri server, bool syncState) + => RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(server, syncState); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_CloseDescriptionIsNull_Success(Uri server) + => RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success(server); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] + [Theory, MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) + => RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) + => RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(server); + + #endregion + } + + #region Runnable test classes: External/Outerloop + + public sealed class CloseTest_SharedHandler_External(ITestOutputHelper output) : CloseTest_External(output) { } + + public sealed class CloseTest_Invoker_External(ITestOutputHelper output) : CloseTest_External(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class CloseTest_HttpClient_External(ITestOutputHelper output) : CloseTest_External(output) + { + protected override bool UseHttpClient => true; } + + #endregion } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs index 8b95714339fa95..814fae5132ac9e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs @@ -1,67 +1,20 @@ // 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.Net.Http; using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerConnectTest_Http2 : ConnectTest_Http2 + public abstract partial class ConnectTest_Http2Loopback { - public InvokerConnectTest_Http2(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; - } - - public sealed class HttpClientConnectTest_Http2 : ConnectTest_Http2 - { - public HttpClientConnectTest_Http2(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } - - public sealed class HttpClientConnectTest_Http2_NoInvoker : ClientWebSocketTestBase - { - public HttpClientConnectTest_Http2_NoInvoker(ITestOutputHelper output) : base(output) { } - - public static IEnumerable ConnectAsync_Http2WithNoInvoker_ThrowsArgumentException_MemberData() - { - yield return Options(options => options.HttpVersion = HttpVersion.Version20); - yield return Options(options => options.HttpVersion = HttpVersion.Version30); - yield return Options(options => options.HttpVersion = new Version(2, 1)); - yield return Options(options => options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher); - - static object[] Options(Action configureOptions) => - new object[] { configureOptions }; - } - - [Theory] - [MemberData(nameof(ConnectAsync_Http2WithNoInvoker_ThrowsArgumentException_MemberData))] - [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets aren't supported on Browser")] - public async Task ConnectAsync_Http2WithNoInvoker_ThrowsArgumentException(Action configureOptions) - { - using var ws = new ClientWebSocket(); - configureOptions(ws.Options); - - Task connectTask = ws.ConnectAsync(new Uri("wss://dummy"), CancellationToken.None); - - Assert.Equal(TaskStatus.Faulted, connectTask.Status); - await Assert.ThrowsAsync("options", () => connectTask); - } - } - - public abstract class ConnectTest_Http2 : ClientWebSocketTestBase - { - public ConnectTest_Http2(ITestOutputHelper output) : base(output) { } + #region HTTP/2-only loopback tests [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] public async Task ConnectAsync_VersionNotSupported_NoSsl_Throws() { await Http2LoopbackServer.CreateClientAndServerAsync(async uri => @@ -69,7 +22,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - cws.Options.HttpVersion = HttpVersion.Version20; + cws.Options.HttpVersion = Net.HttpVersion.Version20; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; Task t = cws.ConnectAsync(uri, GetInvoker(), cts.Token); @@ -87,7 +40,6 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => } [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] public async Task ConnectAsync_VersionNotSupported_WithSsl_Throws() { await Http2LoopbackServer.CreateClientAndServerAsync(async uri => @@ -95,7 +47,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - cws.Options.HttpVersion = HttpVersion.Version20; + cws.Options.HttpVersion = Net.HttpVersion.Version20; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; Task t = cws.ConnectAsync(uri, GetInvoker(), cts.Token); @@ -112,48 +64,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => }, new Http2Options() { WebSocketEndpoint = true }); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] - public async Task ConnectAsync_Http11Server_DowngradeFail() - { - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - cws.Options.HttpVersion = HttpVersion.Version20; - cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; - - Task t = cws.ConnectAsync(Test.Common.Configuration.WebSockets.SecureRemoteEchoServer, GetInvoker(), cts.Token); - - var ex = await Assert.ThrowsAnyAsync(() => t); - Assert.True(ex.InnerException.Data.Contains("HTTP2_ENABLED")); - HttpRequestException inner = Assert.IsType(ex.InnerException); - HttpRequestError expectedError = PlatformDetection.SupportsAlpn ? - HttpRequestError.SecureConnectionError : - HttpRequestError.VersionNegotiationError; - Assert.Equal(expectedError, inner.HttpRequestError); - Assert.Equal(WebSocketState.Closed, cws.State); - } - } - - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [Theory] - [MemberData(nameof(EchoServers))] - [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] - public async Task ConnectAsync_Http11Server_DowngradeSuccess(Uri server) - { - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - cws.Options.HttpVersion = HttpVersion.Version20; - cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; - await cws.ConnectAsync(server, GetInvoker(), cts.Token); - Assert.Equal(WebSocketState.Open, cws.State); - } - } - - [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] public async Task ConnectAsync_VersionSupported_NoSsl_Success() { await Http2LoopbackServer.CreateClientAndServerAsync(async uri => @@ -161,7 +72,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - cws.Options.HttpVersion = HttpVersion.Version20; + cws.Options.HttpVersion = Net.HttpVersion.Version20; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; await cws.ConnectAsync(uri, GetInvoker(), cts.Token); } @@ -175,7 +86,6 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => } [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] public async Task ConnectAsync_VersionSupported_WithSsl_Success() { await Http2LoopbackServer.CreateClientAndServerAsync(async uri => @@ -183,7 +93,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - cws.Options.HttpVersion = HttpVersion.Version20; + cws.Options.HttpVersion = Net.HttpVersion.Version20; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; await cws.ConnectAsync(uri, GetInvoker(), cts.Token); } @@ -197,17 +107,16 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => } [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets aren't supported on Browser")] public async Task ConnectAsync_SameHttp2ConnectionUsedForMultipleWebSocketConnection() { await Http2LoopbackServer.CreateClientAndServerAsync(async uri => { using var cws1 = new ClientWebSocket(); - cws1.Options.HttpVersion = HttpVersion.Version20; + cws1.Options.HttpVersion = Net.HttpVersion.Version20; cws1.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; using var cws2 = new ClientWebSocket(); - cws2.Options.HttpVersion = HttpVersion.Version20; + cws2.Options.HttpVersion = Net.HttpVersion.Version20; cws2.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; using var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -228,45 +137,6 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => }, new Http2Options() { WebSocketEndpoint = true, UseSsl = false }); } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [Theory] - [MemberData(nameof(EchoServers))] - [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] - public async Task ConnectAsync_Http11WithRequestVersionOrHigher_DowngradeSuccess(Uri server) - { - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - cws.Options.HttpVersion = HttpVersion.Version11; - cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; - await cws.ConnectAsync(server, GetInvoker(), cts.Token); - Assert.Equal(WebSocketState.Open, cws.State); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] - public async Task ConnectAsync_Http11WithRequestVersionOrHigher_Loopback_Success() - { - await LoopbackServer.CreateServerAsync(async (server, url) => - { - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - cws.Options.HttpVersion = HttpVersion.Version11; - cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; - - Task connectTask = cws.ConnectAsync(url, GetInvoker(), cts.Token); - - await server.AcceptConnectionAsync(async connection => - { - await LoopbackHelper.WebSocketHandshakeAsync(connection); - }); - - await connectTask; - Assert.Equal(WebSocketState.Open, cws.State); - } - }, new LoopbackServer.Options { UseSsl = true, WebSocketEndpoint = true }); - } + #endregion } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Invoker.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Invoker.cs new file mode 100644 index 00000000000000..7ee76e9008be9e --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Invoker.cs @@ -0,0 +1,90 @@ +// 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.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.WebSockets.Client.Tests +{ + public partial class ConnectTest_Invoker_Loopback + { + #region Invoker-only HTTP/1.1 loopback tests + + public static IEnumerable ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_ThrowsArgumentException_MemberData() + { + yield return Throw(options => options.UseDefaultCredentials = true); + yield return NoThrow(options => options.UseDefaultCredentials = false); + yield return Throw(options => options.Credentials = new NetworkCredential()); + yield return Throw(options => options.Proxy = new WebProxy()); + + // Will result in an exception on apple mobile platforms + // and crash the test. + if (PlatformDetection.IsNotAppleMobile) + { + yield return Throw(options => options.ClientCertificates.Add(Test.Common.Configuration.Certificates.GetClientCertificate())); + } + + yield return NoThrow(options => options.ClientCertificates = new X509CertificateCollection()); + yield return Throw(options => options.RemoteCertificateValidationCallback = delegate { return true; }); + yield return Throw(options => options.Cookies = new CookieContainer()); + + // We allow no proxy or the default proxy to be used + yield return NoThrow(options => { }); + yield return NoThrow(options => options.Proxy = null); + + // These options don't conflict with the custom invoker + yield return NoThrow(options => options.HttpVersion = new Version(2, 0)); + yield return NoThrow(options => options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher); + yield return NoThrow(options => options.SetRequestHeader("foo", "bar")); + yield return NoThrow(options => options.AddSubProtocol("foo")); + yield return NoThrow(options => options.KeepAliveInterval = TimeSpan.FromSeconds(42)); + yield return NoThrow(options => options.DangerousDeflateOptions = new WebSocketDeflateOptions()); + yield return NoThrow(options => options.CollectHttpResponseDetails = true); + + static object[] Throw(Action configureOptions) => + new object[] { configureOptions, true }; + + static object[] NoThrow(Action configureOptions) => + new object[] { configureOptions, false }; + } + + [Theory] + [MemberData(nameof(ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_ThrowsArgumentException_MemberData))] + [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] + public async Task ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_ThrowsArgumentException(Action configureOptions, bool shouldThrow) + { + using var invoker = new HttpMessageInvoker(new SocketsHttpHandler + { + ConnectCallback = (_, _) => ValueTask.FromException(new Exception("ConnectCallback")) + }); + + using var ws = new ClientWebSocket(); + configureOptions(ws.Options); + + Task connectTask = ws.ConnectAsync(new Uri("wss://dummy"), invoker, CancellationToken.None); + if (shouldThrow) + { + Assert.Equal(TaskStatus.Faulted, connectTask.Status); + await Assert.ThrowsAsync("options", () => connectTask); + } + else + { + WebSocketException ex = await Assert.ThrowsAsync(() => connectTask); + Assert.NotNull(ex.InnerException); + Assert.Contains("ConnectCallback", ex.InnerException.Message); + } + + foreach (X509Certificate cert in ws.Options.ClientCertificates) + { + cert.Dispose(); + } + } + + #endregion + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs new file mode 100644 index 00000000000000..3002ff8a8a37e6 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Net.Test.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.WebSockets.Client.Tests +{ + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] + public abstract class ConnectTest_LoopbackBase(ITestOutputHelper output) : ConnectTestBase(output) + { + #region Common (Echo Server) tests + + [Theory, MemberData(nameof(UseSsl))] + public Task EchoBinaryMessage_Success(bool useSsl) => RunEchoAsync( + RunClient_EchoBinaryMessage_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task EchoTextMessage_Success(bool useSsl) => RunEchoAsync( + RunClient_EchoTextMessage_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ConnectAsync_AddCustomHeaders_Success(bool useSsl) => RunEchoHeadersAsync( + RunClient_ConnectAsync_AddCustomHeaders_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ConnectAsync_CookieHeaders_Success(bool useSsl) => RunEchoHeadersAsync( + RunClient_ConnectAsync_CookieHeaders_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(bool useSsl) => RunEchoAsync( + RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl); + + [Theory, MemberData(nameof(UseSsl))] + public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(bool useSsl) => RunEchoAsync( + RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl); + + [ConditionalTheory] // Uses SkipTestException + [MemberData(nameof(UseSsl))] + public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(bool useSsl) => RunEchoAsync( + RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl); + + #endregion + } + + public abstract class ConnectTest_Loopback(ITestOutputHelper output) : ConnectTest_LoopbackBase(output) + { + #region HTTP/1.1-only loopback tests + + [ConditionalTheory] // Uses SkipTestException + [MemberData(nameof(UseSsl))] + public async Task ConnectAsync_Http11WithRequestVersionOrHigher_Loopback_DowngradeSuccess(bool useSsl) + { + if (UseSharedHandler) + { + throw new SkipTestException("HTTP/2 is not supported with SharedHandler"); + } + + await LoopbackServer.CreateServerAsync(async (server, url) => + { + using (var cws = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + cws.Options.HttpVersion = Net.HttpVersion.Version11; + cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; + + Task connectTask = cws.ConnectAsync(url, GetInvoker(), cts.Token); + + await server.AcceptConnectionAsync(async connection => + { + await LoopbackHelper.WebSocketHandshakeAsync(connection); + }); + + await connectTask; + Assert.Equal(WebSocketState.Open, cws.State); + } + }, new LoopbackServer.Options { UseSsl = useSsl, WebSocketEndpoint = true }); + } + + #endregion + } + + public abstract partial class ConnectTest_Http2Loopback(ITestOutputHelper output) : ConnectTest_LoopbackBase(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; + + // #region HTTP/2-only loopback tests -> extracted to ConnectTest.Http2.cs + } + + #region Runnable test classes: HTTP/1.1 Loopback + + public sealed partial class ConnectTest_SharedHandler_Loopback(ITestOutputHelper output) : ConnectTest_Loopback(output) + { + // #region SharedHandler-only HTTP/1.1 loopback tests -> extracted to ConnectTest.SharedHandler.cs + } + + public sealed partial class ConnectTest_Invoker_Loopback(ITestOutputHelper output) : ConnectTest_Loopback(output) + { + protected override bool UseCustomInvoker => true; + + // #region Invoker-only HTTP/1.1 loopback tests -> extracted to ConnectTest.Invoker.cs + } + + public sealed class ConnectTest_HttpClient_Loopback(ITestOutputHelper output) : ConnectTest_Loopback(output) + { + protected override bool UseHttpClient => true; + } + + #endregion + + #region Runnable test classes: HTTP/2 Loopback + + public sealed class ConnectTest_Invoker_Http2Loopback(ITestOutputHelper output) : ConnectTest_Http2Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class ConnectTest_HttpClient_Http2Loopback(ITestOutputHelper output) : ConnectTest_Http2Loopback(output) + { + protected override bool UseHttpClient => true; + } + + #endregion +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.SharedHandler.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.SharedHandler.cs new file mode 100644 index 00000000000000..193d0706dd9629 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.SharedHandler.cs @@ -0,0 +1,205 @@ +// 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.Net.Http; +using System.Net.Test.Common; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.WebSockets.Client.Tests +{ + public partial class ConnectTest_SharedHandler_Loopback + { + #region SharedHandler-only HTTP/1.1 loopback tests + + [Fact] + public async Task ConnectAsync_CancellationRequestedBeforeConnect_ThrowsOperationCanceledException() + { + using (var clientSocket = new ClientWebSocket()) + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + Task t = ConnectAsync(clientSocket, new Uri($"ws://{Guid.NewGuid():N}"), cts.Token); + await Assert.ThrowsAnyAsync(() => t); + } + } + + [Fact] + public async Task ConnectAsync_CancellationRequestedInflightConnect_ThrowsOperationCanceledException() + { + using (var clientSocket = new ClientWebSocket()) + { + var cts = new CancellationTokenSource(); + Task t = ConnectAsync(clientSocket, new Uri($"ws://{Guid.NewGuid():N}"), cts.Token); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => t); + } + } + + [Fact] + public async Task ConnectAsync_NonStandardRequestHeaders_HeadersAddedWithoutValidation() + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var clientSocket = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + clientSocket.Options.SetRequestHeader("Authorization", "AWS4-HMAC-SHA256 Credential=PLACEHOLDER /20190301/us-east-2/neptune-db/aws4_request, SignedHeaders=host;x-amz-date, Signature=b8155de54d9faab00000000000000000000000000a07e0d7dda49902e4d9202"); + await ConnectAsync(clientSocket, uri, cts.Token); + } + }, server => server.AcceptConnectionAsync(async connection => + { + Assert.NotNull(await LoopbackHelper.WebSocketHandshakeAsync(connection)); + }), new LoopbackServer.Options { WebSocketEndpoint = true }); + } + + [Fact] + public async Task ConnectAsync_CancellationRequestedAfterConnect_ThrowsOperationCanceledException() + { + var releaseServer = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + var clientSocket = new ClientWebSocket(); + try + { + var cts = new CancellationTokenSource(); + Task t = ConnectAsync(clientSocket, uri, cts.Token); + Assert.False(t.IsCompleted); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => t); + } + finally + { + releaseServer.SetResult(); + clientSocket.Dispose(); + } + }, async server => + { + try + { + await server.AcceptConnectionAsync(async connection => + { + await releaseServer.Task; + }); + } + // Ignore IO exception on server as there are race conditions when client is cancelling. + catch (IOException) { } + }, new LoopbackServer.Options { WebSocketEndpoint = true }); + } + + [Fact] + public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure() + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var clientWebSocket = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + clientWebSocket.Options.CollectHttpResponseDetails = true; + Task t = ConnectAsync(clientWebSocket, uri, cts.Token); + await Assert.ThrowsAnyAsync(() => t); + + Assert.Equal(HttpStatusCode.Unauthorized, clientWebSocket.HttpStatusCode); + Assert.NotEmpty(clientWebSocket.HttpResponseHeaders); + } + }, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Unauthorized), new LoopbackServer.Options { WebSocketEndpoint = true }); + } + + [Fact] + public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure_CustomHeader() + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var clientWebSocket = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + clientWebSocket.Options.CollectHttpResponseDetails = true; + Task t = ConnectAsync(clientWebSocket, uri, cts.Token); + await Assert.ThrowsAnyAsync(() => t); + + Assert.Equal(HttpStatusCode.Unauthorized, clientWebSocket.HttpStatusCode); + Assert.NotEmpty(clientWebSocket.HttpResponseHeaders); + Assert.Contains("X-CustomHeader1", clientWebSocket.HttpResponseHeaders); + Assert.Contains("X-CustomHeader2", clientWebSocket.HttpResponseHeaders); + Assert.NotNull(clientWebSocket.HttpResponseHeaders.Values); + } + }, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Unauthorized, "X-CustomHeader1: Value1\r\nX-CustomHeader2: Value2\r\n"), new LoopbackServer.Options { WebSocketEndpoint = true }); + } + + [Fact] + public async Task ConnectAsync_HttpResponseDetailsCollectedOnSuccess_Extensions() + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var clientWebSocket = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + clientWebSocket.Options.CollectHttpResponseDetails = true; + await ConnectAsync(clientWebSocket, uri, cts.Token); + + Assert.Equal(HttpStatusCode.SwitchingProtocols, clientWebSocket.HttpStatusCode); + Assert.NotEmpty(clientWebSocket.HttpResponseHeaders); + Assert.Contains("Sec-WebSocket-Extensions", clientWebSocket.HttpResponseHeaders); + } + }, server => server.AcceptConnectionAsync(async connection => + { + Dictionary headers = await LoopbackHelper.WebSocketHandshakeAsync(connection, "X-CustomHeader1"); + }), new LoopbackServer.Options { WebSocketEndpoint = true }); + } + + [Fact] + public async Task ConnectAsync_AddHostHeader_Success() + { + string expectedHost = null; + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + expectedHost = "subdomain." + uri.Host; + using (var cws = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + cws.Options.SetRequestHeader("Host", expectedHost); + await ConnectAsync(cws, uri, cts.Token); + } + }, server => server.AcceptConnectionAsync(async connection => + { + Dictionary headers = await LoopbackHelper.WebSocketHandshakeAsync(connection); + Assert.NotNull(headers); + Assert.True(headers.TryGetValue("Host", out string host)); + Assert.Equal(expectedHost, host); + }), new LoopbackServer.Options { WebSocketEndpoint = true }); + } + + #endregion + + #region SharedHandler-only unsupported HTTP version tests + public static IEnumerable ConnectAsync_Http2WithNoInvoker_ThrowsArgumentException_MemberData() + { + yield return Options(options => options.HttpVersion = Net.HttpVersion.Version20); + yield return Options(options => options.HttpVersion = Net.HttpVersion.Version30); + yield return Options(options => options.HttpVersion = new Version(2, 1)); + yield return Options(options => options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher); + + static object[] Options(Action configureOptions) => + new object[] { configureOptions }; + } + + [Theory] + [MemberData(nameof(ConnectAsync_Http2WithNoInvoker_ThrowsArgumentException_MemberData))] + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets aren't supported on Browser")] + public async Task ConnectAsync_Http2WithNoInvoker_ThrowsArgumentException(Action configureOptions) + { + using var ws = new ClientWebSocket(); + configureOptions(ws.Options); + + Task connectTask = ws.ConnectAsync(new Uri("wss://dummy"), CancellationToken.None); + + Assert.Equal(TaskStatus.Faulted, connectTask.Status); + await Assert.ThrowsAsync("options", () => connectTask); + } + + #endregion + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index cb5f0f978f761e..01f0fc01138de1 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -1,150 +1,53 @@ // 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.Net.Http; using System.Net.Test.Common; -using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; +using EchoQueryKey = System.Net.Test.Common.WebSocketEchoOptions.EchoQueryKey; + namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerConnectTest : ConnectTest + // + // Class hierarchy: + // + // - ConnectTestBase → file:ConnectTest.cs + // ├─ ConnectTest_External + // │ ├─ [*]ConnectTest_SharedHandler_External + // │ ├─ [*]ConnectTest_Invoker_External + // │ └─ [*]ConnectTest_HttpClient_External + // └─ ConnectTest_LoopbackBase → file:ConnectTest.Loopback.cs + // ├─ ConnectTest_Loopback + // │ ├─ [*]ConnectTest_SharedHandler_Loopback → file:ConnectTest.Loopback.cs, ConnectTest.SharedHandler.cs + // │ ├─ [*]ConnectTest_Invoker_Loopback → file:ConnectTest.Loopback.cs, ConnectTest.Invoker.cs + // │ └─ [*]ConnectTest_HttpClient_Loopback + // └─ ConnectTest_Http2Loopback → file:ConnectTest.Loopback.cs, ConnectTest.Http2.cs + // ├─ [*]ConnectTest_Invoker_Http2Loopback + // └─ [*]ConnectTest_HttpClient_Http2Loopback + // + // --- + // `[*]` - concrete runnable test classes + // `→ file:` - file containing the class and its concrete subclasses + + public abstract class ConnectTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public InvokerConnectTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; - - public static IEnumerable ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_ThrowsArgumentException_MemberData() - { - yield return Throw(options => options.UseDefaultCredentials = true); - yield return NoThrow(options => options.UseDefaultCredentials = false); - yield return Throw(options => options.Credentials = new NetworkCredential()); - yield return Throw(options => options.Proxy = new WebProxy()); - - // Will result in an exception on apple mobile platforms - // and crash the test. - if (PlatformDetection.IsNotAppleMobile) - { - yield return Throw(options => options.ClientCertificates.Add(Test.Common.Configuration.Certificates.GetClientCertificate())); - } - - yield return NoThrow(options => options.ClientCertificates = new X509CertificateCollection()); - yield return Throw(options => options.RemoteCertificateValidationCallback = delegate { return true; }); - yield return Throw(options => options.Cookies = new CookieContainer()); - - // We allow no proxy or the default proxy to be used - yield return NoThrow(options => { }); - yield return NoThrow(options => options.Proxy = null); + #region Common (Echo Server) tests - // These options don't conflict with the custom invoker - yield return NoThrow(options => options.HttpVersion = new Version(2, 0)); - yield return NoThrow(options => options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher); - yield return NoThrow(options => options.SetRequestHeader("foo", "bar")); - yield return NoThrow(options => options.AddSubProtocol("foo")); - yield return NoThrow(options => options.KeepAliveInterval = TimeSpan.FromSeconds(42)); - yield return NoThrow(options => options.DangerousDeflateOptions = new WebSocketDeflateOptions()); - yield return NoThrow(options => options.CollectHttpResponseDetails = true); + protected Task RunClient_EchoBinaryMessage_Success(Uri server) + => RunClientAsync(server, + (cws, ct) => WebSocketHelper.TestEcho(cws, WebSocketMessageType.Binary, ct)); - static object[] Throw(Action configureOptions) => - new object[] { configureOptions, true }; - static object[] NoThrow(Action configureOptions) => - new object[] { configureOptions, false }; - } + protected Task RunClient_EchoTextMessage_Success(Uri server) + => RunClientAsync(server, + (cws, ct) => WebSocketHelper.TestEcho(cws, WebSocketMessageType.Text, ct)); - [Theory] - [MemberData(nameof(ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_ThrowsArgumentException_MemberData))] - [SkipOnPlatform(TestPlatforms.Browser, "Custom invoker is ignored on Browser")] - public async Task ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_ThrowsArgumentException(Action configureOptions, bool shouldThrow) - { - using var invoker = new HttpMessageInvoker(new SocketsHttpHandler - { - ConnectCallback = (_, _) => ValueTask.FromException(new Exception("ConnectCallback")) - }); - - using var ws = new ClientWebSocket(); - configureOptions(ws.Options); - - Task connectTask = ws.ConnectAsync(new Uri("wss://dummy"), invoker, CancellationToken.None); - if (shouldThrow) - { - Assert.Equal(TaskStatus.Faulted, connectTask.Status); - await Assert.ThrowsAsync("options", () => connectTask); - } - else - { - WebSocketException ex = await Assert.ThrowsAsync(() => connectTask); - Assert.NotNull(ex.InnerException); - Assert.Contains("ConnectCallback", ex.InnerException.Message); - } - - foreach (X509Certificate cert in ws.Options.ClientCertificates) - { - cert.Dispose(); - } - } - } - - public sealed class HttpClientConnectTest : ConnectTest - { - public HttpClientConnectTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } - - public class ConnectTest : ClientWebSocketTestBase - { - public ConnectTest(ITestOutputHelper output) : base(output) { } - - [ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(UnavailableWebSocketServers))] - public async Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) - { - using (var cws = new ClientWebSocket()) - { - var cts = new CancellationTokenSource(TimeOutMilliseconds); - WebSocketException ex = await Assert.ThrowsAsync(() => - ConnectAsync(cws, server, cts.Token)); - - if (!PlatformDetection.IsInAppContainer) // bug fix in netcoreapp: https://github.com/dotnet/corefx/pull/35960 - { - Assert.Equal(errorCode, ex.WebSocketErrorCode); - } - Assert.Equal(WebSocketState.Closed, cws.State); - Assert.Equal(exceptionMessage, ex.Message); - - // Other operations throw after failed connect - await Assert.ThrowsAsync(() => cws.ReceiveAsync(new byte[1], default)); - await Assert.ThrowsAsync(() => cws.SendAsync(new byte[1], WebSocketMessageType.Binary, true, default)); - await Assert.ThrowsAsync(() => cws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, default)); - await Assert.ThrowsAsync(() => cws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, default)); - } - } - - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task EchoBinaryMessage_Success(Uri server) - { - await TestEcho(server, WebSocketMessageType.Binary, TimeOutMilliseconds, _output); - } - - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task EchoTextMessage_Success(Uri server) - { - await TestEcho(server, WebSocketMessageType.Text, TimeOutMilliseconds, _output); - } - - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoHeadersServers))] - [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on browser")] - public async Task ConnectAsync_AddCustomHeaders_Success(Uri server) + protected async Task RunClient_ConnectAsync_AddCustomHeaders_Success(Uri server) { using (var cws = new ClientWebSocket()) { @@ -171,59 +74,42 @@ public async Task ConnectAsync_AddCustomHeaders_Success(Uri server) } Assert.Equal(WebSocketMessageType.Text, recvResult.MessageType); - string headers = WebSocketData.GetTextFromBuffer(new ArraySegment(buffer, 0, recvResult.Count)); - Assert.Contains("X-CustomHeader1:Value1", headers); - Assert.Contains("X-CustomHeader2:Value2", headers); + string headers = new ArraySegment(buffer, 0, recvResult.Count).Utf8ToString(); + Assert.Contains("X-CustomHeader1:Value1", headers, StringComparison.OrdinalIgnoreCase); + Assert.Contains("X-CustomHeader2:Value2", headers, StringComparison.OrdinalIgnoreCase); await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } } - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on browser")] - public async Task ConnectAsync_AddHostHeader_Success() - { - string expectedHost = null; - await LoopbackServer.CreateClientAndServerAsync(async uri => - { - expectedHost = "subdomain." + uri.Host; - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - cws.Options.SetRequestHeader("Host", expectedHost); - await ConnectAsync(cws, uri, cts.Token); - } - }, server => server.AcceptConnectionAsync(async connection => - { - Dictionary headers = await LoopbackHelper.WebSocketHandshakeAsync(connection); - Assert.NotNull(headers); - Assert.True(headers.TryGetValue("Host", out string host)); - Assert.Equal(expectedHost, host); - }), new LoopbackServer.Options { WebSocketEndpoint = true }); - } - - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoHeadersServers))] - [SkipOnPlatform(TestPlatforms.Browser, "Cookies not supported on browser")] - public async Task ConnectAsync_CookieHeaders_Success(Uri server) + protected async Task RunClient_ConnectAsync_CookieHeaders_Success(Uri server) { using (var cws = new ClientWebSocket()) { Assert.Null(cws.Options.Cookies); - cws.Options.Cookies = new CookieContainer(); + + var cookies = new CookieContainer(); Cookie cookie1 = new Cookie("Cookies", "Are Yummy"); Cookie cookie2 = new Cookie("Especially", "Chocolate Chip"); - Cookie secureCookie = new Cookie("Occasionally", "Raisin"); - secureCookie.Secure = true; + Cookie secureCookie = new Cookie("Occasionally", "Raisin") { Secure = true }; + + cookies.Add(server, cookie1); + cookies.Add(server, cookie2); + cookies.Add(server, secureCookie); - cws.Options.Cookies.Add(server, cookie1); - cws.Options.Cookies.Add(server, cookie2); - cws.Options.Cookies.Add(server, secureCookie); + if (UseSharedHandler) + { + cws.Options.Cookies = cookies; + } + else + { + ConfigureCustomHandler = handler => handler.CookieContainer = cookies; + } using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - Task taskConnect = cws.ConnectAsync(server, cts.Token); + Task taskConnect = ConnectAsync(cws, server, cts.Token); Assert.True( cws.State == WebSocketState.None || cws.State == WebSocketState.Connecting || @@ -242,7 +128,7 @@ public async Task ConnectAsync_CookieHeaders_Success(Uri server) } Assert.Equal(WebSocketMessageType.Text, recvResult.MessageType); - string headers = WebSocketData.GetTextFromBuffer(new ArraySegment(buffer, 0, recvResult.Count)); + string headers = new ArraySegment(buffer, 0, recvResult.Count).Utf8ToString(); Assert.Contains("Cookies=Are Yummy", headers); Assert.Contains("Especially=Chocolate Chip", headers); @@ -252,10 +138,7 @@ public async Task ConnectAsync_CookieHeaders_Success(Uri server) } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/101115", typeof(PlatformDetection), nameof(PlatformDetection.IsFirefox))] - public async Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(Uri server) + protected async Task RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(Uri server) { const string AcceptedProtocol = "CustomProtocol"; @@ -263,21 +146,19 @@ public async Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketE { var cts = new CancellationTokenSource(TimeOutMilliseconds); - var ub = new UriBuilder(server); - ub.Query = "subprotocol=" + AcceptedProtocol; + var ub = new UriBuilder(server) { Query = $"{EchoQueryKey.SubProtocol}={AcceptedProtocol}" }; WebSocketException ex = await Assert.ThrowsAsync(() => ConnectAsync(cws, ub.Uri, cts.Token)); _output.WriteLine(ex.Message); - Assert.True(ex.WebSocketErrorCode == WebSocketError.Faulted || + Assert.True(ex.WebSocketErrorCode == WebSocketError.UnsupportedProtocol || + ex.WebSocketErrorCode == WebSocketError.Faulted || ex.WebSocketErrorCode == WebSocketError.NotAWebSocket, $"Actual WebSocketErrorCode {ex.WebSocketErrorCode} {ex.InnerException?.Message} \n {ex}"); Assert.Equal(WebSocketState.Closed, cws.State); } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(Uri server) + protected async Task RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(Uri server) { const string AcceptedProtocol = "AcceptedProtocol"; const string OtherProtocol = "OtherProtocol"; @@ -288,8 +169,7 @@ public async Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_Connectio cws.Options.AddSubProtocol(OtherProtocol); var cts = new CancellationTokenSource(TimeOutMilliseconds); - var ub = new UriBuilder(server); - ub.Query = "subprotocol=" + AcceptedProtocol; + var ub = new UriBuilder(server) { Query = $"{EchoQueryKey.SubProtocol}={AcceptedProtocol}" }; await ConnectAsync(cws, ub.Uri, cts.Token); Assert.Equal(WebSocketState.Open, cws.State); @@ -297,39 +177,25 @@ public async Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_Connectio } } - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on Browser")] - public async Task ConnectAsync_NonStandardRequestHeaders_HeadersAddedWithoutValidation() + protected async Task RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri server) { - await LoopbackServer.CreateClientAndServerAsync(async uri => + if (HttpVersion != Net.HttpVersion.Version11) { - using (var clientSocket = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - clientSocket.Options.SetRequestHeader("Authorization", "AWS4-HMAC-SHA256 Credential=PLACEHOLDER /20190301/us-east-2/neptune-db/aws4_request, SignedHeaders=host;x-amz-date, Signature=b8155de54d9faab00000000000000000000000000a07e0d7dda49902e4d9202"); - await ConnectAsync(clientSocket, uri, cts.Token); - } - }, server => server.AcceptConnectionAsync(async connection => - { - Assert.NotNull(await LoopbackHelper.WebSocketHandshakeAsync(connection)); - }), new LoopbackServer.Options { WebSocketEndpoint = true }); - } + throw new SkipTestException("LoopbackProxyServer is HTTP/1.1 only"); + } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - [SkipOnPlatform(TestPlatforms.Browser, "Proxy not supported on Browser")] - public async Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri server) - { using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) using (LoopbackProxyServer proxyServer = LoopbackProxyServer.Create()) { - ConfigureCustomHandler = handler => handler.Proxy = new WebProxy(proxyServer.Uri); - if (UseSharedHandler) { cws.Options.Proxy = new WebProxy(proxyServer.Uri); } + else + { + ConfigureCustomHandler = handler => handler.Proxy = new WebProxy(proxyServer.Uri); + } await ConnectAsync(cws, server, cts.Token); @@ -343,125 +209,161 @@ public async Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri se } } - [ConditionalFact(nameof(WebSocketsSupported))] - public async Task ConnectAsync_CancellationRequestedBeforeConnect_ThrowsOperationCanceledException() + #endregion + } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public abstract class ConnectTest_External(ITestOutputHelper output) : ConnectTestBase(output) + { + #region Common (Echo Server) tests + + [Theory, MemberData(nameof(EchoServers))] + public Task EchoBinaryMessage_Success(Uri server) + => RunClient_EchoBinaryMessage_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task EchoTextMessage_Success(Uri server) + => RunClient_EchoTextMessage_Success(server); + + [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on browser")] + [Theory, MemberData(nameof(EchoHeadersServers))] + public Task ConnectAsync_AddCustomHeaders_Success(Uri server) + => RunClient_ConnectAsync_AddCustomHeaders_Success(server); + + [SkipOnPlatform(TestPlatforms.Browser, "Cookies not supported on browser")] + [Theory, MemberData(nameof(EchoHeadersServers))] + public Task ConnectAsync_CookieHeaders_Success(Uri server) + => RunClient_ConnectAsync_CookieHeaders_Success(server); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/101115", typeof(PlatformDetection), nameof(PlatformDetection.IsFirefox))] + [Theory, MemberData(nameof(EchoServers))] + public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(Uri server) + => RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(Uri server) + => RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(server); + + [SkipOnPlatform(TestPlatforms.Browser, "Proxy not supported on Browser")] + [Theory, MemberData(nameof(EchoServers))] + public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri server) + => RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(server); + + #endregion + + #region External-only tests + + [ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] + [Theory] + [MemberData(nameof(UnavailableWebSocketServers))] + public async Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) { - using (var clientSocket = new ClientWebSocket()) + using (var cws = new ClientWebSocket()) { - var cts = new CancellationTokenSource(); - cts.Cancel(); - Task t = ConnectAsync(clientSocket, new Uri($"ws://{Guid.NewGuid():N}"), cts.Token); - await Assert.ThrowsAnyAsync(() => t); + var cts = new CancellationTokenSource(TimeOutMilliseconds); + WebSocketException ex = await Assert.ThrowsAsync(() => + ConnectAsync(cws, server, cts.Token)); + + if (!PlatformDetection.IsInAppContainer) // bug fix in netcoreapp: https://github.com/dotnet/corefx/pull/35960 + { + Assert.Equal(errorCode, ex.WebSocketErrorCode); + } + Assert.Equal(WebSocketState.Closed, cws.State); + Assert.Equal(exceptionMessage, ex.Message); + + // Other operations throw after failed connect + await Assert.ThrowsAsync(() => cws.ReceiveAsync(new byte[1], default)); + await Assert.ThrowsAsync(() => cws.SendAsync(new byte[1], WebSocketMessageType.Binary, true, default)); + await Assert.ThrowsAsync(() => cws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, default)); + await Assert.ThrowsAsync(() => cws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, default)); } } - [ConditionalFact(nameof(WebSocketsSupported))] - public async Task ConnectAsync_CancellationRequestedInflightConnect_ThrowsOperationCanceledException() + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets are not supported on this platform")] + [ConditionalFact] // Uses SkipTestException + public async Task ConnectAsync_Http11Server_DowngradeFail() { - using (var clientSocket = new ClientWebSocket()) + if (UseSharedHandler) { - var cts = new CancellationTokenSource(); - Task t = ConnectAsync(clientSocket, new Uri($"ws://{Guid.NewGuid():N}"), cts.Token); - cts.Cancel(); - await Assert.ThrowsAnyAsync(() => t); + throw new SkipTestException("HTTP/2 is not supported with SharedHandler"); } - } - [ConditionalFact(nameof(WebSocketsSupported))] - public async Task ConnectAsync_CancellationRequestedAfterConnect_ThrowsOperationCanceledException() - { - var releaseServer = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await LoopbackServer.CreateClientAndServerAsync(async uri => - { - var clientSocket = new ClientWebSocket(); - try - { - var cts = new CancellationTokenSource(); - Task t = ConnectAsync(clientSocket, uri, cts.Token); - Assert.False(t.IsCompleted); - cts.Cancel(); - await Assert.ThrowsAnyAsync(() => t); - } - finally - { - releaseServer.SetResult(); - clientSocket.Dispose(); - } - }, async server => + using (var cws = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - try - { - await server.AcceptConnectionAsync(async connection => - { - await releaseServer.Task; - }); - } - // Ignore IO exception on server as there are race conditions when client is cancelling. - catch (IOException) { } - }, new LoopbackServer.Options { WebSocketEndpoint = true }); + cws.Options.HttpVersion = Net.HttpVersion.Version20; + cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; + + Task t = cws.ConnectAsync(Test.Common.Configuration.WebSockets.SecureRemoteEchoServer, GetInvoker(), cts.Token); + + var ex = await Assert.ThrowsAnyAsync(() => t); + Assert.True(ex.InnerException.Data.Contains("HTTP2_ENABLED")); + HttpRequestException inner = Assert.IsType(ex.InnerException); + HttpRequestError expectedError = PlatformDetection.SupportsAlpn ? + HttpRequestError.SecureConnectionError : + HttpRequestError.VersionNegotiationError; + Assert.Equal(expectedError, inner.HttpRequestError); + Assert.Equal(WebSocketState.Closed, cws.State); + } } - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")] - public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure() + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets are not supported on this platform")] + [ConditionalTheory] // Uses SkipTestException + [MemberData(nameof(EchoServers))] + public async Task ConnectAsync_Http11Server_DowngradeSuccess(Uri server) { - await LoopbackServer.CreateClientAndServerAsync(async uri => + if (UseSharedHandler) { - using (var clientWebSocket = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - clientWebSocket.Options.CollectHttpResponseDetails = true; - Task t = ConnectAsync(clientWebSocket, uri, cts.Token); - await Assert.ThrowsAnyAsync(() => t); - - Assert.Equal(HttpStatusCode.Unauthorized, clientWebSocket.HttpStatusCode); - Assert.NotEmpty(clientWebSocket.HttpResponseHeaders); - } - }, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Unauthorized), new LoopbackServer.Options { WebSocketEndpoint = true }); - } + throw new SkipTestException("HTTP/2 is not supported with SharedHandler"); + } - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")] - public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure_CustomHeader() - { - await LoopbackServer.CreateClientAndServerAsync(async uri => + using (var cws = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - using (var clientWebSocket = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - clientWebSocket.Options.CollectHttpResponseDetails = true; - Task t = ConnectAsync(clientWebSocket, uri, cts.Token); - await Assert.ThrowsAnyAsync(() => t); - - Assert.Equal(HttpStatusCode.Unauthorized, clientWebSocket.HttpStatusCode); - Assert.NotEmpty(clientWebSocket.HttpResponseHeaders); - Assert.Contains("X-CustomHeader1", clientWebSocket.HttpResponseHeaders); - Assert.Contains("X-CustomHeader2", clientWebSocket.HttpResponseHeaders); - Assert.NotNull(clientWebSocket.HttpResponseHeaders.Values); - } - }, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Unauthorized, "X-CustomHeader1: Value1\r\nX-CustomHeader2: Value2\r\n"), new LoopbackServer.Options { WebSocketEndpoint = true }); + cws.Options.HttpVersion = Net.HttpVersion.Version20; + cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; + await cws.ConnectAsync(server, GetInvoker(), cts.Token); + Assert.Equal(WebSocketState.Open, cws.State); + } } - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")] - public async Task ConnectAsync_HttpResponseDetailsCollectedOnSuccess_Extensions() + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets are not supported on this platform")] + [ConditionalTheory] // Uses SkipTestException + [MemberData(nameof(EchoServers))] + public async Task ConnectAsync_Http11WithRequestVersionOrHigher_DowngradeSuccess(Uri server) { - await LoopbackServer.CreateClientAndServerAsync(async uri => + if (UseSharedHandler) { - using (var clientWebSocket = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - clientWebSocket.Options.CollectHttpResponseDetails = true; - await ConnectAsync(clientWebSocket, uri, cts.Token); + throw new SkipTestException("HTTP/2 is not supported with SharedHandler"); + } - Assert.Equal(HttpStatusCode.SwitchingProtocols, clientWebSocket.HttpStatusCode); - Assert.NotEmpty(clientWebSocket.HttpResponseHeaders); - Assert.Contains("Sec-WebSocket-Extensions", clientWebSocket.HttpResponseHeaders); - } - }, server => server.AcceptConnectionAsync(async connection => + using (var cws = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - Dictionary headers = await LoopbackHelper.WebSocketHandshakeAsync(connection, "X-CustomHeader1"); - }), new LoopbackServer.Options { WebSocketEndpoint = true }); + cws.Options.HttpVersion = Net.HttpVersion.Version11; + cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; + await cws.ConnectAsync(server, GetInvoker(), cts.Token); + Assert.Equal(WebSocketState.Open, cws.State); + } } + + #endregion } + +#region Runnable test classes: External/Outerloop + + public sealed class ConnectTest_SharedHandler_External(ITestOutputHelper output) : ConnectTest_External(output) { } + + public sealed class ConnectTest_Invoker_External(ITestOutputHelper output) : ConnectTest_External(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class ConnectTest_HttpClient_External(ITestOutputHelper output) : ConnectTest_External(output) + { + protected override bool UseHttpClient => true; + } + +#endregion } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs index 7c8f68ed1f5d99..9394bb0c20b27d 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Net.Http; using System.Net.Test.Common; using System.Reflection; using System.Text; @@ -14,27 +13,21 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerDeflateTests : DeflateTests - { - public InvokerDeflateTests(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; - } - - public sealed class HttpClientDeflateTests : DeflateTests - { - public HttpClientDeflateTests(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } + // + // Class hierarchy: + // + // - DeflateTests → file:DeflateTests.cs + // ├─ [*]DeflateTests_SharedHandler_Loopback + // ├─ [*]DeflateTests_Invoker_Loopback + // └─ [*]DeflateTests_HttpClient_Loopback + // + // --- + // `[*]` - concrete runnable test classes + // `→ file:` - file containing the class and its concrete subclasses [PlatformSpecific(~TestPlatforms.Browser)] - public class DeflateTests : ClientWebSocketTestBase + public abstract class DeflateTests(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public DeflateTests(ITestOutputHelper output) : base(output) - { - } - [ConditionalTheory(nameof(WebSocketsSupported))] [InlineData(15, true, 15, true, "permessage-deflate; client_max_window_bits")] [InlineData(14, true, 15, true, "permessage-deflate; client_max_window_bits=14")] @@ -199,4 +192,20 @@ private static string CreateDeflateOptionsHeader(WebSocketDeflateOptions options return builder.ToString(); } } + + #region Runnable test classes: HTTP/1.1 Loopback + + public sealed class DeflateTests_SharedHandler_Loopback(ITestOutputHelper output) : DeflateTests(output) { } + + public sealed class DeflateTests_Invoker_Loopback(ITestOutputHelper output) : DeflateTests(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class DeflateTests_HttpClient_Loopback(ITestOutputHelper output) : DeflateTests(output) + { + protected override bool UseHttpClient => true; + } + + #endregion } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs index 08306c0804ee48..767b4ddf8effae 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -1,27 +1,37 @@ // 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.Net.Test.Common; using System.Threading; using System.Threading.Tasks; -using System.Threading.Channels; using Xunit; using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { + // + // Class hierarchy: + // + // - KeepAliveTest_Loopback → file:KeepAliveTest.Loopback.cs + // ├─ [*]KeepAliveTest_SharedHandler_Loopback + // ├─ [*]KeepAliveTest_Invoker_Loopback + // ├─ [*]KeepAliveTest_HttpClient_Loopback + // └─ KeepAliveTest_Http2Loopback + // ├─ [*]KeepAliveTest_Invoker_Http2Loopback + // └─ [*]KeepAliveTest_HttpClient_Http2Loopback + // + // --- + // `[*]` - concrete runnable test classes + // `→ file:` - file containing the class and its concrete subclasses + [SkipOnPlatform(TestPlatforms.Browser, "KeepAlive not supported on browser")] - public abstract class KeepAliveTest_Loopback : ClientWebSocketTestBase + public abstract class KeepAliveTest_Loopback(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public KeepAliveTest_Loopback(ITestOutputHelper output) : base(output) { } - - protected virtual Version HttpVersion => Net.HttpVersion.Version11; + #region Loopback-only tests [OuterLoop("Uses Task.Delay")] [Theory] - [MemberData(nameof(UseSsl_MemberData))] + [MemberData(nameof(UseSsl))] public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) { var clientMsg = new byte[] { 1, 2, 3, 4, 5, 6 }; @@ -33,21 +43,17 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var options = new LoopbackWebSocketServer.Options(HttpVersion, useSsl, GetInvoker()) - { - DisposeServerWebSocket = true, - DisposeClientWebSocket = true, - ConfigureClientOptions = clientOptions => - { - clientOptions.KeepAliveInterval = TimeSpan.FromMilliseconds(100); - clientOptions.KeepAliveTimeout = TimeSpan.FromSeconds(1); - } - }; - return LoopbackWebSocketServer.RunAsync( - async (cws, token) => + async uri => { - ReadAheadWebSocket clientWebSocket = new(cws); + var token = timeoutCts.Token; + ClientWebSocket rawCws = await GetConnectedWebSocket(uri, + o => + { + o.KeepAliveInterval = TimeSpan.FromMilliseconds(100); + o.KeepAliveTimeout = TimeSpan.FromSeconds(1); + }); + ReadAheadWebSocket clientWebSocket = new(rawCws); await VerifySendReceiveAsync(clientWebSocket, clientMsg, serverMsg, clientAckTcs, serverAckTcs.Task, token).ConfigureAwait(false); @@ -85,7 +91,7 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) await serverWebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", token).ConfigureAwait(false); Assert.Equal(WebSocketState.Closed, serverWebSocket.State); }, - options, + new LoopbackWebSocketServer.Options(HttpVersion, useSsl), timeoutCts.Token); } @@ -107,38 +113,42 @@ private static async Task VerifySendReceiveAsync(WebSocket ws, byte[] localMsg, await sendTask.ConfigureAwait(false); await remoteAck.WaitAsync(cancellationToken).ConfigureAwait(false); } + + #endregion + } + + public abstract class KeepAliveTest_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; } - // --- HTTP/1.1 WebSocket loopback tests --- + #region Runnable test classes: HTTP/1.1 Loopback + + public sealed class KeepAliveTest_SharedHandler_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { } - public class KeepAliveTest_Invoker_Loopback : KeepAliveTest_Loopback + public sealed class KeepAliveTest_Invoker_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { - public KeepAliveTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; } - public class KeepAliveTest_HttpClient_Loopback : KeepAliveTest_Loopback + public sealed class KeepAliveTest_HttpClient_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { - public KeepAliveTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - public class KeepAliveTest_SharedHandler_Loopback : KeepAliveTest_Loopback - { - public KeepAliveTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } - } + #endregion - // --- HTTP/2 WebSocket loopback tests --- + #region Runnable test classes: HTTP/2 Loopback - public class KeepAliveTest_Invoker_Http2 : KeepAliveTest_Invoker_Loopback + public sealed class KeepAliveTest_Invoker_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Http2Loopback(output) { - public KeepAliveTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } - protected override Version HttpVersion => Net.HttpVersion.Version20; + protected override bool UseCustomInvoker => true; } - public class KeepAliveTest_HttpClient_Http2 : KeepAliveTest_HttpClient_Loopback + public sealed class KeepAliveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Http2Loopback(output) { - public KeepAliveTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } - protected override Version HttpVersion => Net.HttpVersion.Version20; + protected override bool UseHttpClient => true; } + + #endregion } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs index 5ff9c51e56a6af..f9bea58936cdf5 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs @@ -1,8 +1,6 @@ // 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.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -14,15 +12,13 @@ namespace System.Net.WebSockets.Client.Tests { [SkipOnPlatform(TestPlatforms.Browser, "KeepAlive not supported on browser")] - public class KeepAliveTest : ClientWebSocketTestBase + public class KeepAliveTest(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public KeepAliveTest(ITestOutputHelper output) : base(output) { } - [ConditionalFact(nameof(WebSocketsSupported))] [OuterLoop("Uses Task.Delay")] public async Task KeepAlive_LongDelayBetweenSendReceives_Succeeds() { - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(RemoteEchoServer, TimeOutMilliseconds, _output, TimeSpan.FromSeconds(1))) + using (ClientWebSocket cws = await GetConnectedWebSocket(RemoteEchoServer, o => o.KeepAliveInterval = TimeSpan.FromSeconds(1))) { await cws.SendAsync(new ArraySegment(new byte[1] { 42 }), WebSocketMessageType.Binary, true, CancellationToken.None); @@ -42,10 +38,8 @@ public async Task KeepAlive_LongDelayBetweenSendReceives_Succeeds() [InlineData(1, 2)] // ping/pong public async Task KeepAlive_LongDelayBetweenReceiveSends_Succeeds(int keepAliveIntervalSec, int keepAliveTimeoutSec) { - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket( + using (ClientWebSocket cws = await GetConnectedWebSocket( RemoteEchoServer, - TimeOutMilliseconds, - _output, options => { options.KeepAliveInterval = TimeSpan.FromSeconds(keepAliveIntervalSec); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackHelper.cs index cee509ee068467..8632a07236d011 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackHelper.cs @@ -43,7 +43,7 @@ public static async Task> WebSocketHandshakeAsync(Loo return null; } - public static string GetServerResponseString(string secWebSocketKey, string? extensions = null) + public static string GetServerResponseString(string secWebSocketKey, string? extensions = null, string? subProtocol = null) { var responseSecurityAcceptValue = ComputeWebSocketHandshakeSecurityAcceptValue(secWebSocketKey); return @@ -52,6 +52,7 @@ public static string GetServerResponseString(string secWebSocketKey, string? ext "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + (extensions is null ? null : $"Sec-WebSocket-Extensions: {extensions}\r\n") + + (subProtocol is null ? null : $"Sec-WebSocket-Protocol: {subProtocol}\r\n") + "Sec-WebSocket-Accept: " + responseSecurityAcceptValue + "\r\n\r\n"; } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs new file mode 100644 index 00000000000000..d20e70cf1b8055 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Test.Common; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.WebSockets.Client.Tests +{ + public static partial class LoopbackWebSocketServer + { + public static Task RunEchoAsync(Func loopbackClientFunc, Options options, CancellationToken cancellationToken) + { + Assert.True(options.IgnoreServerErrors); + Assert.True(options.AbortServerOnClientExit); + + return RunAsync( + loopbackClientFunc, + (data, token) => RunEchoServerWebSocketAsync(data, options, token), + options, + cancellationToken); + } + + private static async Task RunEchoServerWebSocketAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) + { + WebSocketEchoOptions echoOptions = data.EchoOptions ?? WebSocketEchoOptions.Default; + + if (echoOptions.SubProtocol is not null) + { + Assert.True(options.SkipServerHandshakeResponse, "SkipServerHandshakeResponse must be true to negotiate subprotocols"); + Assert.Null(options.ServerSubProtocol); + options = options with { ServerSubProtocol = echoOptions.SubProtocol }; + } + + if (options.SkipServerHandshakeResponse) + { + await SendNegotiatedServerResponseAsync(data, options, cancellationToken).ConfigureAwait(false); + } + + await RunServerWebSocketAsync( + data, + (serverWebSocket, token) => WebSocketEchoHelper.RunEchoAll( + serverWebSocket, + echoOptions.ReplyWithPartialMessages, + echoOptions.ReplyWithEnhancedCloseMessage, + token), + options, + cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs new file mode 100644 index 00000000000000..42a2d5fbc7e33a --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Net.Sockets; +using System.Net.Test.Common; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Net.WebSockets.Client.Tests +{ + public static partial class LoopbackWebSocketServer + { + abstract class HttpRunner + { + protected abstract LoopbackServerFactory ServerFactory { get; } + protected abstract GenericLoopbackOptions CreateOptions(bool useSsl); + + public async Task RunAsync( + Func clientFunc, + Func wsServerFunc, + Options options, + CancellationToken clientExitCt, + CancellationToken globalCt) + { + using (var server = ServerFactory.CreateServer(CreateOptions(options.UseSsl))) + { + Task clientTask = clientFunc(server.Address); + Task serverTask = ProcessWebSocketRequest(server, wsServerFunc, options, clientExitCt, globalCt); + + await new Task[] { clientTask, serverTask }.WhenAllOrAnyFailed(LoopbackServerFactory.LoopbackServerTimeoutMilliseconds); + } + } + + private async Task ProcessWebSocketRequest( + GenericLoopbackServer httpServer, + Func wsServerFunc, + Options options, + CancellationToken clientExitCt, + CancellationToken globalCt) + { + try + { + using CancellationTokenSource linkedCts = + CancellationTokenSource.CreateLinkedTokenSource(globalCt, clientExitCt); + await ProcessWebSocketRequestCore(httpServer, wsServerFunc, options, linkedCts.Token); + } + catch (Exception e) when (options.IgnoreServerErrors) + { + if (e is OperationCanceledException && clientExitCt.IsCancellationRequested) + { + return; // expected + } + + if (e is WebSocketException or IOException or SocketException) + { + return; // ignore + } + + throw; // don't swallow Assert failures and unexpected exceptions + } + } + + protected abstract Task ProcessWebSocketRequestCore( + GenericLoopbackServer httpServer, + Func loopbackServerFunc, + Options options, + CancellationToken cancellationToken); + } + + class Http11Runner : HttpRunner + { + public static HttpRunner Singleton { get; } = new Http11Runner(); + protected override LoopbackServerFactory ServerFactory => Http11LoopbackServerFactory.Singleton; + + protected override GenericLoopbackOptions CreateOptions(bool useSsl) => new LoopbackServer.Options + { + UseSsl = useSsl, + WebSocketEndpoint = true + }; + + protected override Task ProcessWebSocketRequestCore(GenericLoopbackServer s, Func func, Options o, CancellationToken ct) + => ProcessHttp11WebSocketRequest((LoopbackServer)s, func, o, ct); + } + + class Http2Runner : HttpRunner + { + public static HttpRunner Singleton { get; } = new Http2Runner(); + protected override LoopbackServerFactory ServerFactory => Http2LoopbackServerFactory.Singleton; + + protected override GenericLoopbackOptions CreateOptions(bool useSsl) => new Http2Options + { + UseSsl = useSsl, + WebSocketEndpoint = true, + EnsureThreadSafeIO = true + }; + + protected override Task ProcessWebSocketRequestCore(GenericLoopbackServer s, Func func, Options o, CancellationToken ct) + => ProcessHttp2WebSocketRequest((Http2LoopbackServer)s, func, o, ct); + } + + private static Task ProcessHttp11WebSocketRequest( + LoopbackServer http11server, + Func loopbackServerFunc, + Options options, + CancellationToken cancellationToken) + => http11server.AcceptConnectionAsync( + async connection => + { + var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync( + connection, + options.SkipServerHandshakeResponse, + options.ParseEchoOptions, + cancellationToken).ConfigureAwait(false); + + await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); + }); + + private static async Task ProcessHttp2WebSocketRequest( + Http2LoopbackServer http2Server, + Func loopbackServerFunc, + Options options, + CancellationToken cancellationToken) + { + var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync( + http2Server, + options.SkipServerHandshakeResponse, + options.ParseEchoOptions, + cancellationToken).ConfigureAwait(false); + + await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); + var http2Connection = requestData.Http2Connection!; + + if (options.AbortServerOnClientExit) // we need to wait for the client to disconnect + { + // Due to the way Extended CONNECT is implemented, we might receive both EOS and RST_STREAM frames, + // so we might need to drain more than 1 frame before shutting down the connection + while (true) + { + var frame = await http2Connection.ReadFrameAsync(cancellationToken).ConfigureAwait(false); + if (frame is null) + { + // No more frames to read + break; + } + + if (!options.IgnoreServerErrors) + { + Assert.False(frame.Type == FrameType.Data && !((DataFrame)frame).EndStreamFlag, $"Unexpected DATA frame: {frame}"); + } + } + + await http2Connection.WaitForConnectionShutdownAsync(options.IgnoreServerErrors).ConfigureAwait(false); + } + else + { + await http2Connection.ShutdownIgnoringErrorsAsync(requestData.Http2StreamId.Value).ConfigureAwait(false); + } + } + + private static Task SendNegotiatedServerResponseAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) + { + Assert.True(options.SkipServerHandshakeResponse); + + if (data.HttpVersion == HttpVersion.Version11) + { + return WebSocketHandshakeHelper.SendHttp11ServerResponseAsync( + data.Http11Connection!, + data.Headers["Sec-WebSocket-Key"], + options.ServerSubProtocol, + options.ServerExtensions, + cancellationToken); + } + + if (data.HttpVersion == HttpVersion.Version20) + { + return WebSocketHandshakeHelper.SendHttp2ServerResponseAsync( + data.Http2Connection!, + data.Http2StreamId!.Value, + options.ServerSubProtocol, + options.ServerExtensions, + cancellationToken: cancellationToken); + } + + throw new NotSupportedException($"HTTP version {data.HttpVersion} is not supported."); + } + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs index ec530201848025..a7384616cfbd51 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -1,151 +1,90 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; -using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; -using Xunit; namespace System.Net.WebSockets.Client.Tests { - public static class LoopbackWebSocketServer + public static partial class LoopbackWebSocketServer { public static Task RunAsync( - Func clientWebSocketFunc, - Func serverWebSocketFunc, + Func runClient, + Func runServer, Options options, CancellationToken cancellationToken) - { - Assert.False(options.ManualServerHandshakeResponse, "Not supported in this overload"); - - return RunAsyncPrivate( - uri => RunClientAsync(uri, clientWebSocketFunc, options, cancellationToken), - (requestData, token) => RunServerAsync(requestData, serverWebSocketFunc, options, token), - options, - cancellationToken); - } + => RunAsync( + runClient, + (rd, ct) => RunServerWebSocketAsync(rd, runServer, options, ct), + options, + cancellationToken); public static Task RunAsync( - Func loopbackClientFunc, - Func loopbackServerFunc, + Func runClient, + Func runServer, Options options, CancellationToken cancellationToken) { - Assert.False(options.DisposeClientWebSocket, "Not supported in this overload"); - Assert.False(options.DisposeServerWebSocket, "Not supported in this overload"); - Assert.False(options.DisposeHttpInvoker, "Not supported in this overload"); - Assert.Null(options.HttpInvoker); // Not supported in this overload - - return RunAsyncPrivate(loopbackClientFunc, loopbackServerFunc, options, cancellationToken); - } + CancellationToken clientExitCt = CancellationToken.None; - private static Task RunAsyncPrivate( - Func loopbackClientFunc, - Func loopbackServerFunc, - Options options, - CancellationToken cancellationToken) - { - bool sendDefaultServerHandshakeResponse = !options.ManualServerHandshakeResponse; - if (options.HttpVersion == HttpVersion.Version11) + if (options.AbortServerOnClientExit) { - return LoopbackServer.CreateClientAndServerAsync( - loopbackClientFunc, - async server => + CancellationTokenSource clientExitCts = new CancellationTokenSource(); + clientExitCt = clientExitCts.Token; + + var runClientCore = runClient; + runClient = async uri => + { + try { - await server.AcceptConnectionAsync(async connection => - { - var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync(connection, sendDefaultServerHandshakeResponse, cancellationToken).ConfigureAwait(false); - await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - }); - }, - new LoopbackServer.Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }); - } - else if (options.HttpVersion == HttpVersion.Version20) - { - return Http2LoopbackServer.CreateClientAndServerAsync( - loopbackClientFunc, - async server => + await runClientCore(uri); + } + finally { - var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync(server, sendDefaultServerHandshakeResponse, cancellationToken).ConfigureAwait(false); - var http2Connection = requestData.Http2Connection!; - var http2StreamId = requestData.Http2StreamId.Value; - - await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - - await http2Connection.ShutdownIgnoringErrorsAsync(http2StreamId).ConfigureAwait(false); - }, - new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }); + clientExitCts.Cancel(); + } + }; } - else + + var httpRunner = options.HttpVersion.Major switch { - throw new ArgumentException(nameof(options.HttpVersion)); - } + 1 => Http11Runner.Singleton, + 2 => Http2Runner.Singleton, + _ => throw new NotSupportedException($"HTTP version {options.HttpVersion} is not supported.") + }; + + return httpRunner.RunAsync(runClient, runServer, options, clientExitCt, cancellationToken); } - private static async Task RunServerAsync( + private static async Task RunServerWebSocketAsync( WebSocketRequestData requestData, Func serverWebSocketFunc, Options options, CancellationToken cancellationToken) { - var wsOptions = new WebSocketCreationOptions { IsServer = true }; - var serverWebSocket = WebSocket.CreateFromStream(requestData.WebSocketStream, wsOptions); - - await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); - - if (options.DisposeServerWebSocket) - { - serverWebSocket.Dispose(); - } - } - - private static async Task RunClientAsync( - Uri uri, - Func clientWebSocketFunc, - Options options, - CancellationToken cancellationToken) - { - var clientWebSocket = await GetConnectedClientAsync(uri, options, cancellationToken).ConfigureAwait(false); - - await clientWebSocketFunc(clientWebSocket, cancellationToken).ConfigureAwait(false); - - if (options.DisposeClientWebSocket) - { - clientWebSocket.Dispose(); - } + var wsOptions = new WebSocketCreationOptions { IsServer = true, SubProtocol = options.ServerSubProtocol }; - if (options.DisposeHttpInvoker) + var serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); + using (var registration = cancellationToken.Register(serverWebSocket.Abort)) { - options.HttpInvoker?.Dispose(); + await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); } - } - - public static async Task GetConnectedClientAsync(Uri uri, Options options, CancellationToken cancellationToken) - { - var clientWebSocket = new ClientWebSocket(); - clientWebSocket.Options.HttpVersion = options.HttpVersion; - clientWebSocket.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; - if (options.UseSsl && options.HttpInvoker is null) + if (options.DisposeServerWebSocket) { - clientWebSocket.Options.RemoteCertificateValidationCallback = delegate { return true; }; + serverWebSocket.Dispose(); } - - options.ConfigureClientOptions?.Invoke(clientWebSocket.Options); - - await clientWebSocket.ConnectAsync(uri, options.HttpInvoker, cancellationToken).ConfigureAwait(false); - - return clientWebSocket; } - public record class Options(Version HttpVersion, bool UseSsl, HttpMessageInvoker? HttpInvoker) + public record class Options(Version HttpVersion, bool UseSsl) { - public bool DisposeServerWebSocket { get; set; } = true; - public bool DisposeClientWebSocket { get; set; } - public bool DisposeHttpInvoker { get; set; } - public bool ManualServerHandshakeResponse { get; set; } - public Action? ConfigureClientOptions { get; set; } + public bool DisposeServerWebSocket { get; set; } + public bool SkipServerHandshakeResponse { get; set; } + public bool ParseEchoOptions { get; set; } + public bool IgnoreServerErrors { get; set; } + public bool AbortServerOnClientExit { get; set; } + public string? ServerSubProtocol { get; set; } + public string? ServerExtensions { get; set; } } } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs index 06e62d4a17e48f..e97d2feaf6f3b3 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Net.Sockets; using System.Net.Test.Common; @@ -14,7 +13,11 @@ namespace System.Net.WebSockets.Client.Tests { public static class WebSocketHandshakeHelper { - public static async Task ProcessHttp11RequestAsync(LoopbackServer.Connection connection, bool sendServerResponse = true, CancellationToken cancellationToken = default) + public static async Task ProcessHttp11RequestAsync( + LoopbackServer.Connection connection, + bool skipServerHandshakeResponse = false, + bool parseEchoOptions = false, + CancellationToken cancellationToken = default) { List headers = await connection.ReadRequestHeaderAsync().WaitAsync(cancellationToken).ConfigureAwait(false); @@ -24,9 +27,30 @@ public static async Task ProcessHttp11RequestAsync(Loopbac Http11Connection = connection }; - foreach (string header in headers.Skip(1)) + if (parseEchoOptions) { - string[] tokens = header.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + // extract query with leading '?' from request line + // e.g. GET /echo?query=string HTTP/1.1 => "?query=string" + int queryIndex = headers[0].IndexOf('?'); + if (queryIndex != -1) + { + int spaceIndex = headers[0].IndexOf(' ', queryIndex); + string query = headers[0].Substring(queryIndex, spaceIndex - queryIndex); + + // NOTE: ProcessOptions needs to be called before sending the server response + // because it may be configured to delay the response. + + data.EchoOptions = await WebSocketEchoHelper.ProcessOptions(query, cancellationToken).ConfigureAwait(false); + } + else + { + data.EchoOptions = WebSocketEchoOptions.Default; + } + } + + for (int i = 1; i < headers.Count; ++i) + { + string[] tokens = headers[i].Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); if (tokens.Length is 1 or 2) { data.Headers.Add( @@ -38,22 +62,39 @@ public static async Task ProcessHttp11RequestAsync(Loopbac var isValidOpeningHandshake = data.Headers.TryGetValue("Sec-WebSocket-Key", out var secWebSocketKey); Assert.True(isValidOpeningHandshake); - if (sendServerResponse) + if (!skipServerHandshakeResponse) { - await SendHttp11ServerResponseAsync(connection, secWebSocketKey, cancellationToken).ConfigureAwait(false); + foreach (string headerName in new[] { "Sec-WebSocket-Extensions", "Sec-WebSocket-Protocol" }) + { + if (data.Headers.TryGetValue(headerName, out var headerValue)) + { + Assert.Fail($"Header `{headerName}: {headerValue}` requires a custom server response, use skipServerHandshakeResponse=true"); + } + } + + await SendHttp11ServerResponseAsync(connection, secWebSocketKey, cancellationToken: cancellationToken).ConfigureAwait(false); } - data.WebSocketStream = connection.Stream; + data.TransportStream = connection.Stream; return data; } - private static async Task SendHttp11ServerResponseAsync(LoopbackServer.Connection connection, string secWebSocketKey, CancellationToken cancellationToken) + public static async Task SendHttp11ServerResponseAsync( + LoopbackServer.Connection connection, + string secWebSocketKey, + string? negotiatedSubProtocol = null, + string? negotiatedExtensions = null, + CancellationToken cancellationToken = default) { - var serverResponse = LoopbackHelper.GetServerResponseString(secWebSocketKey); + var serverResponse = LoopbackHelper.GetServerResponseString(secWebSocketKey, negotiatedExtensions, negotiatedSubProtocol); await connection.WriteStringAsync(serverResponse).WaitAsync(cancellationToken).ConfigureAwait(false); } - public static async Task ProcessHttp2RequestAsync(Http2LoopbackServer server, bool sendServerResponse = true, CancellationToken cancellationToken = default) + public static async Task ProcessHttp2RequestAsync( + Http2LoopbackServer server, + bool skipServerHandshakeResponse = false, + bool parseEchoOptions = false, + CancellationToken cancellationToken = default) { var connection = await server.EstablishConnectionAsync(new SettingsEntry { SettingId = SettingId.EnableConnect, Value = 1 }) .WaitAsync(cancellationToken).ConfigureAwait(false); @@ -78,21 +119,65 @@ public static async Task ProcessHttp2RequestAsync(Http2Loo var isValidOpeningHandshake = httpRequestData.Method == HttpMethod.Connect.ToString() && data.Headers.ContainsKey(":protocol"); Assert.True(isValidOpeningHandshake); - if (sendServerResponse) + if (parseEchoOptions) + { + // RFC 7540, section 8.3. The CONNECT Method: + // > The ":scheme" and ":path" pseudo-header fields MUST be omitted. + // + // HTTP/2 CONNECT requests must drop query (containing echo options) from the request URI. + // The information needs to be passed in a different way, e.g. in a custom header. + + if (data.Headers.TryGetValue(WebSocketHelper.OriginalQueryStringHeader, out var query)) + { + // NOTE: ProcessOptions needs to be called before sending the server response + // because it may be configured to delay the response. + data.EchoOptions = await WebSocketEchoHelper.ProcessOptions(query, cancellationToken).ConfigureAwait(false); + } + else + { + data.EchoOptions = WebSocketEchoOptions.Default; + } + } + + if (!skipServerHandshakeResponse) { + foreach (string headerName in new[] { "Sec-WebSocket-Extensions", "Sec-WebSocket-Protocol" }) + { + if (data.Headers.TryGetValue(headerName, out var headerValue)) + { + Assert.Fail($"Header `{headerName}: {headerValue}` requires a custom server response, use skipServerHandshakeResponse=true"); + } + } + await SendHttp2ServerResponseAsync(connection, streamId, cancellationToken: cancellationToken).ConfigureAwait(false); } - data.WebSocketStream = new Http2LoopbackStream(connection, streamId, sendResetOnDispose: false); + data.TransportStream = new Http2LoopbackStream(connection, streamId, sendResetOnDispose: false); return data; } - private static async Task SendHttp2ServerResponseAsync(Http2LoopbackConnection connection, int streamId, bool endStream = false, CancellationToken cancellationToken = default) + public static async Task SendHttp2ServerResponseAsync( + Http2LoopbackConnection connection, + int streamId, + string? negotiatedSubProtocol = null, + string? negotiatedExtensions = null, + bool endStream = false, + CancellationToken cancellationToken = default) { + var negotiatedValues = new List(); + if (negotiatedExtensions is not null) + { + negotiatedValues.Add(new HttpHeaderData("Sec-WebSocket-Extensions", negotiatedExtensions)); + } + if (negotiatedSubProtocol is not null) + { + negotiatedValues.Add(new HttpHeaderData("Sec-WebSocket-Protocol", negotiatedSubProtocol)); + } + // send status 200 OK to establish websocket - // we don't need to send anything additional as Sec-WebSocket-Key is not used for HTTP/2 + // we don't need to send Sec-WebSocket-Accept as Sec-WebSocket-Key is not used for HTTP/2 // note: endStream=true is abnormal and used for testing premature EOS scenarios only - await connection.SendResponseHeadersAsync(streamId, endStream: endStream).WaitAsync(cancellationToken).ConfigureAwait(false); + await connection.SendResponseHeadersAsync(streamId, endStream: endStream, headers: negotiatedValues).WaitAsync(cancellationToken).ConfigureAwait(false); } public static async Task SendHttp11ServerResponseAndEosAsync(WebSocketRequestData requestData, Func? requestDataCallback, CancellationToken cancellationToken) @@ -100,7 +185,7 @@ public static async Task SendHttp11ServerResponseAndEosAsync(WebSocketRequestDat Assert.Equal(HttpVersion.Version11, requestData.HttpVersion); // sending default handshake response - await SendHttp11ServerResponseAsync(requestData.Http11Connection!, requestData.Headers["Sec-WebSocket-Key"], cancellationToken).ConfigureAwait(false); + await SendHttp11ServerResponseAsync(requestData.Http11Connection!, requestData.Headers["Sec-WebSocket-Key"], cancellationToken: cancellationToken).ConfigureAwait(false); if (requestDataCallback is not null) { @@ -118,7 +203,7 @@ public static async Task SendHttp2ServerResponseAndEosAsync(WebSocketRequestData var connection = requestData.Http2Connection!; var streamId = requestData.Http2StreamId!.Value; - await SendHttp2ServerResponseAsync(connection, streamId, endStream: eosInHeadersFrame, cancellationToken).ConfigureAwait(false); + await SendHttp2ServerResponseAsync(connection, streamId, endStream: eosInHeadersFrame, cancellationToken: cancellationToken).ConfigureAwait(false); if (requestDataCallback is not null) { diff --git a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs index 799157a370f073..190f7af2682e2f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs @@ -9,8 +9,10 @@ namespace System.Net.WebSockets.Client.Tests { public class WebSocketRequestData { - public Dictionary Headers { get; set; } = new Dictionary(); - public Stream? WebSocketStream { get; set; } + public Dictionary Headers { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public WebSocketEchoOptions? EchoOptions { get; set; } + + public Stream? TransportStream { get; set; } public Version HttpVersion { get; set; } public LoopbackServer.Connection? Http11Connection { get; set; } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ResourceHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/ResourceHelper.cs index 67eb753f475410..fbaba8fa6a57ec 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ResourceHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ResourceHelper.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Linq; using System.Reflection; 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..cefdb72ccdd52a 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs @@ -8,30 +8,14 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { - public sealed class HttpClientSendReceiveTest_Http2 : SendReceiveTest_Http2 + public abstract partial class SendReceiveTest_Http2Loopback { - public HttpClientSendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } - - public sealed class InvokerSendReceiveTest_Http2 : SendReceiveTest_Http2 - { - public InvokerSendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; - } - - public abstract class SendReceiveTest_Http2 : ClientWebSocketTestBase - { - public SendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } + #region HTTP/2-only loopback tests [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 }; @@ -40,7 +24,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - cws.Options.HttpVersion = HttpVersion.Version20; + cws.Options.HttpVersion = Net.HttpVersion.Version20; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; await cws.ConnectAsync(uri, GetInvoker(), cts.Token); @@ -69,7 +53,6 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => } [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "Self-signed certificates are not supported on browser")] public async Task ReceiveNoThrowAfterSend_WithSsl() { var serverMessage = new byte[] { 4, 5, 6 }; @@ -78,7 +61,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - cws.Options.HttpVersion = HttpVersion.Version20; + cws.Options.HttpVersion = Net.HttpVersion.Version20; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; await cws.ConnectAsync(uri, GetInvoker(), cts.Token); @@ -105,5 +88,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => }, new Http2Options() { WebSocketEndpoint = true }); } + + #endregion } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs new file mode 100644 index 00000000000000..f54a4202d099d1 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using System.Net.Test.Common; +using System.Threading; +using System.Threading.Tasks; + +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.WebSockets.Client.Tests +{ + + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] + public abstract class SendReceiveTest_LoopbackBase(ITestOutputHelper output) : SendReceiveTestBase(output) + { + #region Common (Echo Server) tests + + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success, server, type), useSsl); + + [ConditionalTheory, MemberData(nameof(UseSslAndSendReceiveType))] // Uses SkipTestException + public Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success, server, type), useSsl); + + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage, server, type), useSsl); + + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendAsync_MultipleOutstandingSendOperations_Throws(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendAsync_MultipleOutstandingSendOperations_Throws, server, type), useSsl); + + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations_Throws, server, type), useSsl); + + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success, server, type), useSsl); + + [ConditionalTheory, MemberData(nameof(UseSslAndSendReceiveType))] // Uses SkipTestException + public Task SendReceive_VaryingLengthBuffers_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendReceive_VaryingLengthBuffers_Success, server, type), useSsl); + + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendReceive_Concurrent_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendReceive_Concurrent_Success, server, type), useSsl); + + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task ZeroByteReceive_CompletesWhenDataAvailable(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_ZeroByteReceive_CompletesWhenDataAvailable, server, type), useSsl); + + #endregion + } + + public abstract class SendReceiveTest_Loopback(ITestOutputHelper output) : SendReceiveTest_LoopbackBase(output) + { + #region HTTP/1.1-only loopback tests + + [Theory, MemberData(nameof(SendReceiveTypes))] + public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(SendReceiveType type) => RunSendReceive( + RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated, type); + + private async Task RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() + { + var options = new LoopbackServer.Options { WebSocketEndpoint = true }; + + Func connectToServerThatAbortsConnection = async (clientSocket, server, url) => + { + var pendingReceiveAsyncPosted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // 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)); + + // Wait for client-side ConnectAsync to complete and for a pending ReceiveAsync to be posted. + await pendingReceiveAsyncPosted.Task.WaitAsync(TimeSpan.FromMilliseconds(TimeOutMilliseconds)); + + // Close the underlying connection prematurely (without sending a WebSocket Close frame). + connection.Socket.Shutdown(SocketShutdown.Both); + connection.Socket.Close(); + }); + + // Initiate a connection attempt. + var cts = new CancellationTokenSource(TimeOutMilliseconds); + await ConnectAsync(clientSocket, url, 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(); + + // Wait for the server to close the underlying connection. + await acceptTask.WaitAsync(cts.Token); + + WebSocketException pendingReceiveException = await Assert.ThrowsAsync(() => pendingReceiveAsync); + + Assert.Equal(WebSocketError.ConnectionClosedPrematurely, pendingReceiveException.WebSocketErrorCode); + + if (PlatformDetection.IsInAppContainer) + { + const uint WININET_E_CONNECTION_ABORTED = 0x80072EFE; + + Assert.NotNull(pendingReceiveException.InnerException); + Assert.Equal(WININET_E_CONNECTION_ABORTED, (uint)pendingReceiveException.InnerException.HResult); + } + + WebSocketException newReceiveException = + await Assert.ThrowsAsync(() => ReceiveAsync(clientSocket, recvSegment, cts.Token)); + + Assert.Equal( + ResourceHelper.GetExceptionMessage("net_WebSockets_InvalidState", "Aborted", "Open, CloseSent"), + newReceiveException.Message); + + Assert.Equal(WebSocketState.Aborted, clientSocket.State); + Assert.Null(clientSocket.CloseStatus); + }; + + await LoopbackServer.CreateServerAsync(async (server, url) => + { + using (ClientWebSocket clientSocket = new ClientWebSocket()) + { + await connectToServerThatAbortsConnection(clientSocket, server, url); + } + }, options); + } + + #endregion + } + + public abstract partial class SendReceiveTest_Http2Loopback(ITestOutputHelper output) : SendReceiveTest_LoopbackBase(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; + + // #region HTTP/2-only loopback tests -> extracted to SendReceiveTest.Http2.cs + } + + #region Runnable test classes: HTTP/1.1 Loopback + + public sealed class SendReceiveTest_SharedHandler_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) { } + + public sealed class SendReceiveTest_Invoker_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class SendReceiveTest_HttpClient_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) + { + protected override bool UseHttpClient => true; + } + + #endregion + + #region Runnable test classes: HTTP/2 Loopback + + public sealed class SendReceiveTest_Invoker_Http2Loopback(ITestOutputHelper output) : SendReceiveTest_Http2Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class SendReceiveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : SendReceiveTest_Http2Loopback(output) + { + protected override bool UseHttpClient => true; + } + + #endregion +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 357dcb0945d665..f74e1efc848f48 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -1,87 +1,96 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; -using System.Net.Sockets; -using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; - +using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; +using EchoControlMessage = System.Net.Test.Common.WebSocketEchoHelper.EchoControlMessage; +using EchoQueryKey = System.Net.Test.Common.WebSocketEchoOptions.EchoQueryKey; + namespace System.Net.WebSockets.Client.Tests { - - public sealed class InvokerMemorySendReceiveTest : MemorySendReceiveTest + // + // Class hierarchy: + // + // - SendReceiveTestBase → file:SendReceiveTest.cs + // ├─ SendReceiveTest_External + // │ ├─ [*]SendReceiveTest_SharedHandler_External + // │ ├─ [*]SendReceiveTest_Invoker_External + // │ └─ [*]SendReceiveTest_HttpClient_External + // └─ SendReceiveTest_LoopbackBase → file:SendReceiveTest.Loopback.cs + // ├─ SendReceiveTest_Loopback + // │ ├─ [*]SendReceiveTest_SharedHandler_Loopback + // │ ├─ [*]SendReceiveTest_Invoker_Loopback + // │ └─ [*]SendReceiveTest_HttpClient_Loopback + // └─ SendReceiveTest_Http2Loopback → file:SendReceiveTest.Loopback.cs, SendReceiveTest.Http2.cs + // ├─ [*]SendReceiveTest_Invoker_Http2Loopback + // └─ [*]SendReceiveTest_HttpClient_Http2Loopback + // --- + // `[*]` - concrete runnable test classes + // `→ file:` - file containing the class and its concrete subclasses + + public abstract class SendReceiveTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public InvokerMemorySendReceiveTest(ITestOutputHelper output) : base(output) { } + #region Send-receive type setup - protected override bool UseCustomInvoker => true; - } - - public sealed class HttpClientMemorySendReceiveTest : MemorySendReceiveTest - { - public HttpClientMemorySendReceiveTest(ITestOutputHelper output) : base(output) { } + public static readonly object[][] EchoServersAndSendReceiveType = ToMemberData(EchoServers_Values, Enum.GetValues()); + public static readonly object[][] UseSslAndSendReceiveType = ToMemberData(UseSsl_Values, Enum.GetValues()); + public static readonly object[][] SendReceiveTypes = ToMemberData(Enum.GetValues()); - protected override bool UseHttpClient => true; - } + public enum SendReceiveType + { + ArraySegment = 1, + Memory = 2 + } - public sealed class InvokerArraySegmentSendReceiveTest : ArraySegmentSendReceiveTest - { - public InvokerArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } + protected SendReceiveType TestType { get; private set; } - protected override bool UseCustomInvoker => true; - } + protected Task SendAsync(WebSocket webSocket, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return TestType switch + { + SendReceiveType.ArraySegment => webSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken), + SendReceiveType.Memory => SendAsMemoryAsync(webSocket, buffer, messageType, endOfMessage, cancellationToken), + _ => throw new ArgumentException(nameof(TestType)) + }; - public sealed class HttpClientArraySegmentSendReceiveTest : ArraySegmentSendReceiveTest - { - public HttpClientArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } + static Task SendAsMemoryAsync(WebSocket ws, ArraySegment buf, WebSocketMessageType mt, bool eom, CancellationToken ct) + => ws.SendAsync((ReadOnlyMemory)buf, mt, eom, ct).AsTask(); + } - protected override bool UseHttpClient => true; - } + protected Task ReceiveAsync(WebSocket webSocket, ArraySegment buffer, CancellationToken cancellationToken) + { + return TestType switch + { + SendReceiveType.ArraySegment => webSocket.ReceiveAsync(buffer, cancellationToken), + SendReceiveType.Memory => ReceiveAsMemoryAsync(webSocket, buffer, cancellationToken), + _ => throw new ArgumentException(nameof(TestType)) + }; - public class MemorySendReceiveTest : SendReceiveTest - { - public MemorySendReceiveTest(ITestOutputHelper output) : base(output) { } + static async Task ReceiveAsMemoryAsync(WebSocket ws, ArraySegment buf, CancellationToken ct) + { + ValueWebSocketReceiveResult result = await ws.ReceiveAsync((Memory)buf, ct); + return new WebSocketReceiveResult(result.Count, result.MessageType, result.EndOfMessage, ws.CloseStatus, ws.CloseStatusDescription); + } + } - protected override async Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) + protected Task RunSendReceive(Func sendReceiveFunc, SendReceiveType sendReceiveTestType) { - ValueWebSocketReceiveResult r = await ws.ReceiveAsync( - (Memory)arraySegment, - cancellationToken).ConfigureAwait(false); - return new WebSocketReceiveResult(r.Count, r.MessageType, r.EndOfMessage, ws.CloseStatus, ws.CloseStatusDescription); + TestType = sendReceiveTestType; + return sendReceiveFunc(); } - protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => - ws.SendAsync( - (ReadOnlyMemory)arraySegment, - messageType, - endOfMessage, - cancellationToken).AsTask(); - } + protected Task RunSendReceive(Func sendReceiveFunc, Uri uri, SendReceiveType sendReceiveTestType) + => RunSendReceive(() => sendReceiveFunc(uri), sendReceiveTestType); - public class ArraySegmentSendReceiveTest : SendReceiveTest - { - public ArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } + #endregion - protected override Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) => - ws.ReceiveAsync(arraySegment, cancellationToken); + #region Common (Echo Server) tests - protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => - ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken); - } - - public abstract class SendReceiveTest : ClientWebSocketTestBase - { - protected abstract Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken); - protected abstract Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken); - - public SendReceiveTest(ITestOutputHelper output) : base(output) { } - - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) + protected async Task RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) { const int SendBufferSize = 10; var sendBuffer = new byte[SendBufferSize]; @@ -90,7 +99,7 @@ public async Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri var receiveBuffer = new byte[SendBufferSize / 2]; var receiveSegment = new ArraySegment(receiveBuffer); - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); @@ -116,20 +125,21 @@ public async Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - [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) + protected async Task RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(Uri server) { + if (HttpVersion == Net.HttpVersion.Version20) + { + throw new SkipTestException("[ActiveIssue] -- temporarily skipping on HTTP/2"); + } + 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"; + var ub = new UriBuilder(server) { Query = EchoQueryKey.ReplyWithPartialMessages }; - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(ub.Uri, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(ub.Uri)) { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); @@ -158,11 +168,9 @@ public async Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server) + protected async Task RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -186,12 +194,9 @@ public async Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMess } } - [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 SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) + protected async Task RunClient_SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -203,7 +208,7 @@ public async Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) { tasks[i] = SendAsync( cws, - WebSocketData.GetBufferFromText("hello"), + "hello".ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -246,21 +251,20 @@ 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. - [ActiveIssue("https://github.com/dotnet/runtime/issues/83517", typeof(PlatformDetection), nameof(PlatformDetection.IsNodeJS))] - public async Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) + protected const int SmallTimeoutMs = 200; + protected virtual int MultipleOutstandingReceiveOperations_TimeoutMs => SmallTimeoutMs; + + protected async Task RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { - var cts = new CancellationTokenSource(PlatformDetection.LocalEchoServerIsNotAvailable ? TimeOutMilliseconds : 200); + var cts = new CancellationTokenSource(MultipleOutstandingReceiveOperations_TimeoutMs); Task[] tasks = new Task[2]; await SendAsync( cws, - WebSocketData.GetBufferFromText(".delay5sec"), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -288,7 +292,7 @@ await SendAsync( "ReceiveAsync"), ex.Message); - Assert.True(WebSocketState.Aborted == cws.State, cws.State+" state when InvalidOperationException"); + Assert.True(WebSocketState.Aborted == cws.State, cws.State + " state when InvalidOperationException"); } else if (ex is WebSocketException) { @@ -312,17 +316,15 @@ await SendAsync( } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server) + protected async Task RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); string message = "hello"; await SendAsync( cws, - WebSocketData.GetBufferFromText(message), + message.ToUtf8(), WebSocketMessageType.Text, false, cts.Token); @@ -347,15 +349,18 @@ await SendAsync( Assert.Null(recvRet.CloseStatusDescription); var recvSegment = new ArraySegment(receiveSegment.Array, receiveSegment.Offset, recvRet.Count); - Assert.Equal(message, WebSocketData.GetTextFromBuffer(recvSegment)); + Assert.Equal(message, recvSegment.Utf8ToString()); } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task SendReceive_VaryingLengthBuffers_Success(Uri server) + protected async Task RunClient_SendReceive_VaryingLengthBuffers_Success(Uri server) { - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + if (HttpVersion == Net.HttpVersion.Version20) + { + throw new SkipTestException("[ActiveIssue] -- temporarily skipping on HTTP/2"); + } + + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var rand = new Random(); var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); @@ -391,11 +396,9 @@ public async Task SendReceive_VaryingLengthBuffers_Success(Uri server) } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task SendReceive_Concurrent_Success(Uri server) + protected async Task RunClient_SendReceive_Concurrent_Success(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { CancellationTokenSource ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); @@ -420,81 +423,9 @@ public async Task SendReceive_Concurrent_Success(Uri server) } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalFact(nameof(WebSocketsSupported))] - [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) => - { - var pendingReceiveAsyncPosted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - // 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)); - - // Wait for client-side ConnectAsync to complete and for a pending ReceiveAsync to be posted. - await pendingReceiveAsyncPosted.Task.WaitAsync(TimeSpan.FromMilliseconds(TimeOutMilliseconds)); - - // Close the underlying connection prematurely (without sending a WebSocket Close frame). - connection.Socket.Shutdown(SocketShutdown.Both); - connection.Socket.Close(); - }); - - // Initiate a connection attempt. - var cts = new CancellationTokenSource(TimeOutMilliseconds); - await ConnectAsync(clientSocket, url, 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(); - - // Wait for the server to close the underlying connection. - await acceptTask.WaitAsync(cts.Token); - - WebSocketException pendingReceiveException = await Assert.ThrowsAsync(() => pendingReceiveAsync); - - Assert.Equal(WebSocketError.ConnectionClosedPrematurely, pendingReceiveException.WebSocketErrorCode); - - if (PlatformDetection.IsInAppContainer) - { - const uint WININET_E_CONNECTION_ABORTED = 0x80072EFE; - - Assert.NotNull(pendingReceiveException.InnerException); - Assert.Equal(WININET_E_CONNECTION_ABORTED, (uint)pendingReceiveException.InnerException.HResult); - } - - WebSocketException newReceiveException = - await Assert.ThrowsAsync(() => ReceiveAsync(clientSocket, recvSegment, cts.Token)); - - Assert.Equal( - ResourceHelper.GetExceptionMessage("net_WebSockets_InvalidState", "Aborted", "Open, CloseSent"), - newReceiveException.Message); - - Assert.Equal(WebSocketState.Aborted, clientSocket.State); - Assert.Null(clientSocket.CloseStatus); - }; - - await LoopbackServer.CreateServerAsync(async (server, url) => - { - 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) + protected async Task RunClient_ZeroByteReceive_CompletesWhenDataAvailable(Uri server) { - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); @@ -515,7 +446,7 @@ public async Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) var receiveBuffer = new byte[1]; t = ReceiveAsync(cws, new ArraySegment(receiveBuffer), ctsDefault.Token); // this is not synchronously possible when the WS client is on another WebWorker - if(!PlatformDetection.IsWasmThreadingSupported) + if (!PlatformDetection.IsWasmThreadingSupported) { Assert.Equal(TaskStatus.RanToCompletion, t.Status); } @@ -527,8 +458,75 @@ public async Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) Assert.Equal(42, receiveBuffer[0]); // Clean up. - await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, nameof(ZeroByteReceive_CompletesWhenDataAvailable), ctsDefault.Token); + await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, nameof(RunClient_ZeroByteReceive_CompletesWhenDataAvailable), ctsDefault.Token); } } + + #endregion } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public abstract class SendReceiveTest_External(ITestOutputHelper output) : SendReceiveTestBase(output) + { + #region Common (Echo Server) tests + + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success, server, type); + + [SkipOnPlatform(TestPlatforms.Browser, "JS Websocket does not support see issue https://github.com/dotnet/runtime/issues/46983")] + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success, server, type); + + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage, server, type); + + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendAsync_MultipleOutstandingSendOperations_Throws, server, type); + + protected override int MultipleOutstandingReceiveOperations_TimeoutMs => PlatformDetection.LocalEchoServerIsNotAvailable ? TimeOutMilliseconds : SmallTimeoutMs; + + [ActiveIssue("https://github.com/dotnet/runtime/issues/83517", typeof(PlatformDetection), nameof(PlatformDetection.IsNodeJS))] + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations_Throws, server, type); + + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success, server, type); + + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendReceive_VaryingLengthBuffers_Success(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendReceive_VaryingLengthBuffers_Success, server, type); + + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendReceive_Concurrent_Success(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendReceive_Concurrent_Success, server, type); + + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_ZeroByteReceive_CompletesWhenDataAvailable, server, type); + + #endregion + } + + #region Runnable test classes: External/Outerloop + + public sealed class SendReceiveTest_SharedHandler_External(ITestOutputHelper output) : SendReceiveTest_External(output) { } + + public sealed class SendReceiveTest_Invoker_External(ITestOutputHelper output) : SendReceiveTest_External(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class SendReceiveTest_HttpClient_External(ITestOutputHelper output) : SendReceiveTest_External(output) + { + protected override bool UseHttpClient => true; + } + + #endregion } 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 d5e28ff2f552cb..de52d1f916203d 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 @@ -6,6 +6,8 @@ true $(NetCoreAppCurrent);$(NetCoreAppCurrent)-browser $(DefineConstants);NETSTANDARD + + $(NoWarn);xUnit1015 @@ -47,32 +49,50 @@ + + + + + + - + + + + + + + + - + + + + + + diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketData.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketData.cs deleted file mode 100644 index 320ce788379c76..00000000000000 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketData.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text; - -namespace System.Net.WebSockets.Client.Tests -{ - public static class WebSocketData - { - public static ArraySegment GetBufferFromText(string text) - { - byte[] buffer = Encoding.UTF8.GetBytes(text); - return new ArraySegment(buffer); - } - - public static string GetTextFromBuffer(ArraySegment buffer) - { - return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); - } - } -} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index df29e843590e9b..5616c0d9fe154d 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -1,122 +1,57 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; -using System.Net.Test.Common; +using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { public static class WebSocketHelper { + public const string OriginalQueryStringHeader = "x-original-query-string"; + private static readonly Lazy s_WebSocketSupported = new Lazy(InitWebSocketSupported); public static bool WebSocketsSupported { get { return s_WebSocketSupported.Value; } } public static async Task TestEcho( - Uri server, + ClientWebSocket cws, WebSocketMessageType type, - int timeOutMilliseconds, - ITestOutputHelper output, - HttpMessageInvoker? invoker = null) + CancellationToken cancellationToken) { - var cts = new CancellationTokenSource(timeOutMilliseconds); string message = "Hello WebSockets!"; string closeMessage = "Good bye!"; var receiveBuffer = new byte[100]; var receiveSegment = new ArraySegment(receiveBuffer); - using (ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, invoker: invoker)) - { - output.WriteLine("TestEcho: SendAsync starting."); - await cws.SendAsync(WebSocketData.GetBufferFromText(message), type, true, cts.Token); - output.WriteLine("TestEcho: SendAsync done."); - Assert.Equal(WebSocketState.Open, cws.State); - - output.WriteLine("TestEcho: ReceiveAsync starting."); - WebSocketReceiveResult recvRet = await cws.ReceiveAsync(receiveSegment, cts.Token); - output.WriteLine("TestEcho: ReceiveAsync done."); - Assert.Equal(WebSocketState.Open, cws.State); - Assert.Equal(message.Length, recvRet.Count); - Assert.Equal(type, 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)); - - output.WriteLine("TestEcho: CloseAsync starting."); - Task taskClose = cws.CloseAsync(WebSocketCloseStatus.NormalClosure, closeMessage, cts.Token); - Assert.True( - (cws.State == WebSocketState.Open) || (cws.State == WebSocketState.CloseSent) || - (cws.State == WebSocketState.CloseReceived) || (cws.State == WebSocketState.Closed), - "State immediately after CloseAsync : " + cws.State); - await taskClose; - output.WriteLine("TestEcho: CloseAsync done."); - Assert.Equal(WebSocketState.Closed, cws.State); - Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); - Assert.Equal(closeMessage, cws.CloseStatusDescription); - } + await cws.SendAsync(message.ToUtf8(), type, true, cancellationToken); + Assert.Equal(WebSocketState.Open, cws.State); + + WebSocketReceiveResult recvRet = await cws.ReceiveAsync(receiveSegment, cancellationToken); + Assert.Equal(WebSocketState.Open, cws.State); + Assert.Equal(message.Length, recvRet.Count); + Assert.Equal(type, 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, recvSegment.Utf8ToString()); + + Task taskClose = cws.CloseAsync(WebSocketCloseStatus.NormalClosure, closeMessage, cancellationToken); + Assert.True( + (cws.State == WebSocketState.Open) || (cws.State == WebSocketState.CloseSent) || + (cws.State == WebSocketState.CloseReceived) || (cws.State == WebSocketState.Closed), + "State immediately after CloseAsync : " + cws.State); + await taskClose; + Assert.Equal(WebSocketState.Closed, cws.State); + Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); + Assert.Equal(closeMessage, cws.CloseStatusDescription); } - public static Task GetConnectedWebSocket( - Uri server, - int timeOutMilliseconds, - ITestOutputHelper output, - TimeSpan keepAliveInterval = default, - IWebProxy proxy = null, - HttpMessageInvoker? invoker = null) => - GetConnectedWebSocket( - server, - timeOutMilliseconds, - output, - options => - { - if (proxy != null) - { - options.Proxy = proxy; - } - if (keepAliveInterval.TotalSeconds > 0) - { - options.KeepAliveInterval = keepAliveInterval; - } - }, - invoker - ); - - public static Task GetConnectedWebSocket( - Uri server, - int timeOutMilliseconds, - ITestOutputHelper output, - Action configureOptions, - HttpMessageInvoker? invoker = null) => - Retry(output, async () => - { - var cws = new ClientWebSocket(); - configureOptions(cws.Options); - - using (var cts = new CancellationTokenSource(timeOutMilliseconds)) - { - output.WriteLine("GetConnectedWebSocket: ConnectAsync starting."); - Task taskConnect = invoker == null ? cws.ConnectAsync(server, cts.Token) : cws.ConnectAsync(server, invoker, cts.Token); - Assert.True( - (cws.State == WebSocketState.None) || - (cws.State == WebSocketState.Connecting) || - (cws.State == WebSocketState.Open) || - (cws.State == WebSocketState.Aborted), - "State immediately after ConnectAsync incorrect: " + cws.State); - await taskConnect; - output.WriteLine("GetConnectedWebSocket: ConnectAsync done."); - Assert.Equal(WebSocketState.Open, cws.State); - } - return cws; - }); - - public static async Task Retry(ITestOutputHelper output, Func> func) + public static async Task Retry(Func> func) { const int MaxTries = 5; int betweenTryDelayMilliseconds = 1000; @@ -129,10 +64,9 @@ public static async Task Retry(ITestOutputHelper output, Func> fun } catch (WebSocketException exc) { - output.WriteLine($"Retry after attempt #{i} failed with {exc}"); if (i == MaxTries) { - throw; + Assert.Fail($"Failed after {MaxTries} attempts with exception: {exc}"); } await Task.Delay(betweenTryDelayMilliseconds); @@ -160,11 +94,14 @@ private static bool InitWebSocketSupported() } finally { - if (cws != null) - { - cws.Dispose(); - } + cws?.Dispose(); } } + + public static ArraySegment ToUtf8(this string text) + => new ArraySegment(Encoding.UTF8.GetBytes(text)); + + public static string Utf8ToString(this ArraySegment buffer) + => Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); } } 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..9db52deb2a9f3f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/wasm/BrowserTimerThrottlingTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/wasm/BrowserTimerThrottlingTest.cs @@ -22,7 +22,7 @@ namespace System.Net.WebSockets.Client.Wasm.Tests // requires --enable-features=IntensiveWakeUpThrottling:grace_period_seconds/1 chromeDriver flags // doesn't work with --disable-background-timer-throttling [TestCaseOrderer("System.Net.WebSockets.Client.Wasm.Tests.AlphabeticalOrderer", "System.Net.WebSockets.Client.Wasm.Tests")] - public class BrowserTimerThrottlingTest : ClientWebSocketTestBase + public class BrowserTimerThrottlingTest(ITestOutputHelper output) : ClientWebSocketTestBase(output) { public static bool IsBrowser => RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")); const double moreThanLightThrottlingThreshold = 1900; @@ -30,8 +30,6 @@ public class BrowserTimerThrottlingTest : ClientWebSocketTestBase const double webSocketMessageFrequency = 45000; const double fastTimeoutFrequency = 100; - public BrowserTimerThrottlingTest(ITestOutputHelper output) : base(output) { } - [ConditionalFact(nameof(PlatformDetection.IsBrowser))] [OuterLoop] // involves long delay // this test is influenced by usage of WS on the same browser tab in previous unit tests. we may need to wait long time for it to fizzle down @@ -90,7 +88,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 GetConnectedWebSocket(Test.Common.Configuration.WebSockets.RemoteEchoServer)) { 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 cd8f38fab54b42..024c780ec7c7cb 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 @@ -48,7 +48,6 @@ - diff --git a/src/libraries/System.Net.WebSockets/tests/WebSocketCreateTest.cs b/src/libraries/System.Net.WebSockets/tests/WebSocketCreateTest.cs index e13d46fc708db8..6dfc67c6a3510c 100644 --- a/src/libraries/System.Net.WebSockets/tests/WebSocketCreateTest.cs +++ b/src/libraries/System.Net.WebSockets/tests/WebSocketCreateTest.cs @@ -344,11 +344,12 @@ private static async Task CreateWebSocketStream(Uri echoUri, Socket clie return stream; } - public static readonly object[][] EchoServers = System.Net.Test.Common.Configuration.WebSockets.GetEchoServers(); - public static readonly object[][] EchoServersAndBoolean = EchoServers.SelectMany(o => new object[][] + public static readonly Uri[] EchoServers_Values = System.Net.Test.Common.Configuration.WebSockets.GetEchoServers(); + public static readonly object[][] EchoServers = EchoServers_Values.Select(uri => new object[] { uri }).ToArray(); + public static readonly object[][] EchoServersAndBoolean = EchoServers_Values.SelectMany(uri => new object[][] { - new object[] { o[0], false }, - new object[] { o[0], true } + new object[] { uri, false }, + new object[] { uri, true } }).ToArray(); protected sealed class UnreadableStream : Stream